固件 A16:风扇闭环接进任务体 + NTC / TACH 真实采样(已上板)

- ntc.{h,c}(宿主机可测):分压端 mV → Ω → NCP18XH103F03RB 的 R-T 表插值;
  开路 / 短路 / 表外一律「读不到」(t_thermal 因此满速)
- thermal_esp.{h,c}(仅 IDF):ADC1 oneshot + 曲线校准读两路 NTC;两个 PCNT 单元数 TACH 边沿
- app_main:task_thermal 任务体、t_thermal_init 放在上电恢复之前、status 带热管理快照
- test_ntc.c 4 例;全工程 429 例全绿;版本 0.1.0-a16
- 板 B:两路 NTC 38.2 / 34.0 °C;风扇两路 rpm 0 报停转(拉满电流不涨,疑未插风扇,待确认)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-27 13:31:32 +08:00
共同撰写人 Claude Opus 5.5
父节点 c85eec53e6
当前提交 c37c08aa43
共修改 12 个文件,包含 450 行新增和 47 行删除
+34 -6
查看文件
@@ -25,7 +25,9 @@
#include "slot.h"
#include "t_actuator.h"
#include "t_safety.h"
#include "t_thermal.h"
#include "tasks.h"
#include "thermal_esp.h"
#include "mlink_esp.h"
#include "nvs.h"
#include "nvs_flash.h"
@@ -136,16 +138,30 @@ static void task_safety(void *arg)
/* 下面四个是骨架:周期与优先级已经按 §10.3 定好,任务体等各自的包。
现在就把它们建起来,是为了让 A9~A16 只需要往里填,不用再动启动流程。 */
/* 热管理快照(A16)。写者只有本任务;`status` 命令跨任务只读(cmd.h:遥测无害)。 */
static thermal_snap_t s_therm;
/* A16:两路 NTC → 风扇 PWM 闭环 + TACH 采样窗口(TR-6)。判据全在 `t_thermal_step()`,
温度 / TACH 的来源在 thermal_esp.c。`t_thermal_init()` 在 app_main 里做(风扇要先于上电转起来)。 */
static void task_thermal(void *arg)
{
(void)arg;
sched_periodic_t tick;
sched_periodic_init(&tick, task_desc(TASK_THERMAL)->period_ms);
for (;;) {
/* A16:两路 NTC(IO4/IO5)→ 风扇 PWM 闭环(TR-6 的 TACH 采样窗口)。
⚠️ R2 没有 LM75 了(载板整个不存在,计划 §5.10)。
真正的任务体在 `main/t_thermal.c` 里,由 A16 那一包填;
**这里不 include 它**,免得两个人同时改到同一处。 */
(void)t_thermal_step(&s_therm); /* 写 PWM 失败只计数(pwm_errors),不在这里处理 */
/* 每 10 s 一行:温度 / 占空比 / 两路转速。上板定阈值(t_thermal.h 那四个「待实测」)就看它。 */
if ((s_therm.cycles % 10u) == 1u) {
ESP_LOGI(TAG, "t_thermal: %d/%d m°C (%d/%d mV) duty=%u%s rpm=%u/%u%s%s",
s_therm.temp_ok[0] ? (int)s_therm.temp_mc[0] : -1,
s_therm.temp_ok[1] ? (int)s_therm.temp_mc[1] : -1,
(int)thermal_esp_last_mv(0), (int)thermal_esp_last_mv(1),
(unsigned)s_therm.duty, s_therm.temp_valid ? "" : "(读不到温度→满速)",
(unsigned)s_therm.fan[0].rpm, (unsigned)s_therm.fan[1].rpm,
(s_therm.fan[0].stalled || s_therm.fan[1].stalled) ? " 停转!" : "",
s_therm.derate ? " 过温降载!" : "");
}
sched_periodic_wait(&tick);
}
}
@@ -368,15 +384,27 @@ void app_main(void)
}
log_report(&boot);
/* §10.9 第 7 步的前半:风扇闭环。**放在上电之前**:t_thermal_init 先满速、再拉 FAN_EN,
第一圈之前风扇就在转(t_thermal.h)。ADC / PCNT 起不来不 abort ——
温度读不到 t_thermal 会一直满速(§10.10),TACH 读不到就不判停转。 */
err = thermal_esp_init();
if (err != HAL_OK) {
ESP_LOGE(TAG, "温度 / 转速采样不完整:%s(温度读不到 ⇒ 风扇满速)", hal_err_str(err));
}
err = t_thermal_init(&s_therm, thermal_esp_temp, thermal_esp_tach, NULL);
if (err != HAL_OK) {
ESP_LOGE(TAG, "风扇起不来:%s —— 没有主动散热", hal_err_str(err));
}
/* 执行器:PG 从巡检的快照里取,本任务自己不碰 I²C(t_actuator.h 开头那一段)。
⚠️ 必须在 `spawn(TASK_SAFETY, ...)` 之前:判据要关路时会先 `t_actuator_abort()`,
而没 init 的执行器只会直接返回,那时序列还在跑,电关掉几秒又会被抬回来。 */
t_actuator_init(act_pg_from_safety, NULL);
/* 命令处理器(A12)。status 读巡检快照;热管理快照等 A16 把任务体接上再给。
/* 命令处理器(A12)。status 读巡检与热管理两份快照。
`power` 命令的意图经 `want_save` 存 NVS(A15)。
队列要在执行器任务起来之前建好 —— 那个任务第一件事就是等它。 */
const cmd_env_t cmd_env = { .safety = &s_snap, .thermal = NULL, .intent = want_save };
const cmd_env_t cmd_env = { .safety = &s_snap, .thermal = &s_therm, .intent = want_save };
cmd_init(&cmd_env);
s_cmd_q = xQueueCreate(CMD_QUEUE_DEPTH, sizeof(cmd_msg_t));
if (s_cmd_q == NULL) {