固件 0.1.19~0.1.21-c5:配网热点(SoftAP + 网页)、风扇 40 ℃ 以下断电

- 0.1.19 配网热点:没配 WiFi / 连续 60 s 连不上时自己开 SoftAP,192.168.4.1 网页里加删 WiFi、改 MQTT;
  配置拼成 wifi / mqtt 命令走唯一的命令处理器(新链路号 3 CMD_LINK_AP),MQTT 仍不收配置命令
- 0.1.20 热点口令固定 88888888
- 0.1.21 风扇:板温 >=40 ℃ 上电、<38 ℃ 断电,40~65 ℃ 线性调速;fan 命令加可选 sec(下限到点自动交回)
- mqtt 命令加 keep_pass;net 回应加 ap;status 的 fan 加 left
- 新增 test_prov(13 例),test_t_thermal +3;29 ctest 全绿

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-10-06 14:35:44 +08:00
共同撰写人 Claude Opus 5.5
父节点 3bf4548c62
当前提交 ece65f7c19
共修改 24 个文件,包含 1925 行新增和 324 行删除
+60 -2
查看文件
@@ -25,6 +25,10 @@ static bool s_inited;
/* 上位机 `fan` 命令给的下限(A12)。**跨任务**:命令处理器在执行器任务里写,
本模块在热管理任务里读。一个对齐的 16 位量,Xtensa 上读写是原子的,不加锁。 */
static volatile uint16_t s_floor;
/* 下限的时限(`t_thermal_set_floor_for`)。写的顺序是「先 until、再 timed、最后 floor」,
读的一侧最坏只会晚一圈(1 s)才认到新值。 */
static volatile uint32_t s_floor_until;
static volatile bool s_floor_timed;
/* 风扇侧占空比 → 真正写进 `hal_pwm` 的值。Q6 是低边管,栅极高 = 把风扇的
PWM 线拉低 ⇒ 反相(`t_thermal.h` 硬件那三条的第 2 条)。 */
@@ -107,6 +111,7 @@ hal_err_t t_thermal_init(thermal_snap_t *s, therm_temp_fn temp, therm_tach_fn ta
return e;
}
s->fan_en = true;
s->fan_starts = 1;
s->en_ms = hal_time_now_ms();
s->last_ms = s->en_ms;
@@ -133,6 +138,17 @@ static void tach_step(thermal_snap_t *s, int idx, uint32_t dt_ms, uint16_t duty)
const uint32_t delta = now_edges - t->last_edges; /* uint32 相减,回绕安全 */
t->last_edges = now_edges;
/* 风扇断着电:不累加、不判(惯性滑行那几圈的脉冲也不算数),转速如实报 0。
`stalled` 不动 —— 见头文件「断电期间不判停转」。 */
if (!s->fan_en) {
t->win_edges = 0;
t->win_on_ms = 0;
t->win_ms = 0;
t->rpm = 0;
t->rpm_valid = true;
return;
}
/* 启动宽限期内一直重置窗口:这时候风扇还在起转,脉冲本来就没有或很稀。 */
if (s->fan_en && hal_time_elapsed_ms(s->en_ms) < T_THERMAL_SPINUP_MS) {
t->win_edges = 0;
@@ -216,11 +232,37 @@ hal_err_t t_thermal_step(thermal_snap_t *s)
s->temp_errors++;
}
uint16_t duty = t_thermal_duty_for(s->temp_max_mc, s->temp_valid);
if (s_floor_timed && (int32_t)(now_ms - s_floor_until) >= 0) {
s_floor = 0; /* 上位机给的时限到了:交回自动 */
s_floor_timed = false;
}
const uint16_t floor_ = s_floor;
if (floor_ > duty) {
duty = floor_; /* 手动值只抬不压:曲线要得更高时照曲线来 */
}
(void)apply_duty(s, duty);
/* ---- 开还是关(`T_THERMAL_FAN_OFF_MC` 上的说明)----
读不到温度、上位机要风:一律开。否则按迟滞:开着的掉到 OFF 以下才关,关着的到 ON 才开。 */
bool want_on = true;
if (s->temp_valid && floor_ == 0) {
want_on = s->fan_en ? (s->temp_max_mc >= T_THERMAL_FAN_OFF_MC)
: (s->temp_max_mc >= T_THERMAL_FAN_ON_MC);
}
if (want_on != s->fan_en) {
/* 开:先把占空比写好再给 12V(同 init 的顺序);关:先断 12V。写失败就维持原状,下一圈再来。 */
if (want_on) {
(void)apply_duty(s, duty);
}
if (hal_gpio_write(BOARD_PIN_FAN_EN, want_on ? 1 : 0) == HAL_OK) {
s->fan_en = want_on;
if (want_on) {
s->en_ms = now_ms; /* 启动宽限从这里重新算 */
s->fan_starts++;
}
}
}
/* 断着电时占空比报 0:遥测里「duty 300、en false」会让人以为它在转。 */
(void)apply_duty(s, s->fan_en ? duty : 0);
/* ---- 过温降载:本模块只**报**,关路是限功率那一包的事 ---- */
if (s->temp_valid) {
@@ -241,9 +283,25 @@ hal_err_t t_thermal_step(thermal_snap_t *s)
return HAL_OK;
}
void t_thermal_set_floor(uint16_t permille)
void t_thermal_set_floor(uint16_t permille) { t_thermal_set_floor_for(permille, 0); }
void t_thermal_set_floor_for(uint16_t permille, uint32_t ttl_ms)
{
const bool timed = ttl_ms > 0 && permille > 0;
if (timed) {
s_floor_until = hal_time_now_ms() + ttl_ms;
}
s_floor_timed = timed;
s_floor = (permille > 1000u) ? 1000u : permille;
}
uint16_t t_thermal_floor(void) { return s_floor; }
uint32_t t_thermal_floor_left_ms(void)
{
if (!s_floor_timed || s_floor == 0) {
return 0;
}
const int32_t left = (int32_t)(s_floor_until - hal_time_now_ms());
return left > 0 ? (uint32_t)left : 0;
}