- A12 单一命令处理器(cmd*.c):一行 JSON 进出、seq 全局判重(32 条按值)、
限幅 / 空槽一律拒 / 拒绝记事件;命令号与错误码冻结(进 flash)。新增 power 命令,
fan 改为只抬不压的下限(t_thermal_set_floor)。rail_set_mv 注明不拿锁,调用方包 xact。
- A13 USB-Serial-JTAG 分帧(ulink*.c):@C / @R 前缀 + CRC-16,日志行首 @ 垫空格,
任意切块可切;关掉 USB 副控制台(它绕开驱动写 FIFO)。PC 端客户端 tools/prlink.py。
- A14 + C1(netcfg / mlink / cmd_net):topic phonerail/<mac>/{cmd,rsp,tele,state},
mqtts(evse-ca 编进固件)、1 s 遥测、遗嘱离线;wifi / mqtt 配置只收 USB、存 NVS,
net 永不回显密码。
- 协议定稿写进需求文档 §10.8(A12 / A13 / A14 三节);N7 撤销改 D7(机架管理前后端)。
宿主机 23 个 ctest / 400 例全绿;板 B 已烧 0.1.0-a14,经 WiFi → 公网 → 8883 TLS 真连网验过。
固件 0xE2220 / 1 MB 分区只剩 12%,C2 前要重排分区表。
Co-Authored-By: Claude Opus 5.5 <[email protected]>
250 行
9.4 KiB
C
250 行
9.4 KiB
C
/* `t_thermal` 的实现。判据与出处全部写在 `t_thermal.h` 的文件头,这里只放代码。 */
|
||
#include "t_thermal.h"
|
||
|
||
#include <string.h>
|
||
|
||
#include "board_pins.h"
|
||
#include "evlog.h"
|
||
#include "hal_gpio.h"
|
||
#include "hal_pwm.h"
|
||
#include "hal_time.h"
|
||
|
||
/* 占空比有下限 ⇒ 窗口的墙钟长度有上界 = `TACH_ON_MS × 1000 / DUTY_MIN`。
|
||
这两条断言钉的是同一件事:**下限为 0 时导通时间永远攒不满,停转判据会静默失效**
|
||
(不报错、不告警,只是再也不报停转)。所以下限只能在这里被改小一点点,
|
||
改到让窗口超过 30 s 就编译不过。 */
|
||
_Static_assert(T_THERMAL_DUTY_MIN > 0, "占空比下限为 0:TACH 窗口永远攒不满,停转判据会静默失效");
|
||
_Static_assert((uint64_t)T_THERMAL_TACH_ON_MS * 1000u / T_THERMAL_DUTY_MIN <= T_THERMAL_TACH_MAX_MS,
|
||
"占空比下限太低:TACH 窗口会拖过 T_THERMAL_TACH_MAX_MS,风扇停了很久才报得出来");
|
||
|
||
static therm_temp_fn s_temp_fn;
|
||
static therm_tach_fn s_tach_fn;
|
||
static void *s_ctx;
|
||
static bool s_inited;
|
||
|
||
/* 上位机 `fan` 命令给的下限(A12)。**跨任务**:命令处理器在执行器任务里写,
|
||
本模块在热管理任务里读。一个对齐的 16 位量,Xtensa 上读写是原子的,不加锁。 */
|
||
static volatile uint16_t s_floor;
|
||
|
||
/* 风扇侧占空比 → 真正写进 `hal_pwm` 的值。Q6 是低边管,栅极高 = 把风扇的
|
||
PWM 线拉低 ⇒ 反相(`t_thermal.h` 硬件那三条的第 2 条)。 */
|
||
static uint16_t gpio_duty_of(uint16_t fan_duty)
|
||
{
|
||
#if T_THERMAL_PWM_INVERTED
|
||
return (uint16_t)(1000u - fan_duty);
|
||
#else
|
||
return fan_duty;
|
||
#endif
|
||
}
|
||
|
||
uint16_t t_thermal_duty_for(int32_t milli_c, bool valid)
|
||
{
|
||
/* 读不到温度 = 满速。需求 §10.10:**不要按「读不到温度 = 不热」处理**。 */
|
||
if (!valid) {
|
||
return 1000;
|
||
}
|
||
if (milli_c <= T_THERMAL_TEMP_LO_MC) {
|
||
return T_THERMAL_DUTY_MIN;
|
||
}
|
||
if (milli_c >= T_THERMAL_TEMP_HI_MC) {
|
||
return 1000;
|
||
}
|
||
/* 分段线性。分子最大 = 25000 × 700 = 1.75e7,int32 装得下(不用 64 位)。 */
|
||
const int32_t span_mc = T_THERMAL_TEMP_HI_MC - T_THERMAL_TEMP_LO_MC;
|
||
const int32_t span_duty = 1000 - T_THERMAL_DUTY_MIN;
|
||
return (uint16_t)(T_THERMAL_DUTY_MIN + (milli_c - T_THERMAL_TEMP_LO_MC) * span_duty / span_mc);
|
||
}
|
||
|
||
static hal_err_t apply_duty(thermal_snap_t *s, uint16_t fan_duty)
|
||
{
|
||
const uint16_t g = gpio_duty_of(fan_duty);
|
||
const hal_err_t e = hal_pwm_set_permille(T_THERMAL_PWM_CH, g);
|
||
if (e != HAL_OK) {
|
||
s->pwm_errors++;
|
||
return e;
|
||
}
|
||
s->duty = fan_duty;
|
||
s->gpio_duty = g;
|
||
return HAL_OK;
|
||
}
|
||
|
||
hal_err_t t_thermal_init(thermal_snap_t *s, therm_temp_fn temp, therm_tach_fn tach, void *ctx)
|
||
{
|
||
if (s == NULL) {
|
||
return HAL_ERR_ARG;
|
||
}
|
||
memset(s, 0, sizeof(*s));
|
||
s_temp_fn = temp;
|
||
s_tach_fn = tach;
|
||
s_ctx = ctx;
|
||
|
||
/* ① `FAN_EN` 先配成输出、先写 0 —— 设计文档 §22.6 的上电顺序。
|
||
先关一下不是多余的:复位态是「不转」靠的是 R749 下拉,而**配成输出的那一刻**
|
||
引脚就开始驱动了,不先写就是让上一次跑飞留下的电平决定 12V 通断。 */
|
||
hal_err_t e = hal_gpio_config(BOARD_PIN_FAN_EN, HAL_GPIO_OUTPUT);
|
||
if (e != HAL_OK) {
|
||
return e;
|
||
}
|
||
e = hal_gpio_write(BOARD_PIN_FAN_EN, 0);
|
||
if (e != HAL_OK) {
|
||
return e;
|
||
}
|
||
|
||
/* ② 配 PWM,并先按**满速**输出:还没读到第一个温度之前,宁可吵一秒。 */
|
||
e = hal_pwm_config(T_THERMAL_PWM_CH, BOARD_PIN_FAN_PWM, T_THERMAL_PWM_HZ);
|
||
if (e != HAL_OK) {
|
||
return e;
|
||
}
|
||
e = apply_duty(s, 1000);
|
||
if (e != HAL_OK) {
|
||
return e;
|
||
}
|
||
|
||
/* ③ 最后才给 12V。⚠️ 这一行就是「固件必须主动开」那件事本身:
|
||
删掉它之后温度闭环、PWM、遥测全部照常工作,只是风扇一动不动。 */
|
||
e = hal_gpio_write(BOARD_PIN_FAN_EN, 1);
|
||
if (e != HAL_OK) {
|
||
return e;
|
||
}
|
||
s->fan_en = true;
|
||
s->en_ms = hal_time_now_ms();
|
||
s->last_ms = s->en_ms;
|
||
|
||
s_inited = true;
|
||
return HAL_OK;
|
||
}
|
||
|
||
/* 一路 TACH 的窗口累加 + 结算。`dt_ms` 是本圈实测的时间,`duty` 是**这段时间里
|
||
实际在输出的**占空比(也就是上一圈定下来的那个),不是刚算出来的新值。 */
|
||
static void tach_step(thermal_snap_t *s, int idx, uint32_t dt_ms, uint16_t duty)
|
||
{
|
||
therm_tach_state_t *t = &s->fan[idx];
|
||
|
||
uint32_t now_edges = 0;
|
||
if (s_tach_fn == NULL || !s_tach_fn(s_ctx, idx, &now_edges)) {
|
||
/* 计数器这一刻读不到:本圈不累加也不判。失联 ≠ 停转。 */
|
||
return;
|
||
}
|
||
if (!t->have_last) {
|
||
t->last_edges = now_edges;
|
||
t->have_last = true;
|
||
return;
|
||
}
|
||
const uint32_t delta = now_edges - t->last_edges; /* uint32 相减,回绕安全 */
|
||
t->last_edges = now_edges;
|
||
|
||
/* 启动宽限期内一直重置窗口:这时候风扇还在起转,脉冲本来就没有或很稀。 */
|
||
if (s->fan_en && hal_time_elapsed_ms(s->en_ms) < T_THERMAL_SPINUP_MS) {
|
||
t->win_edges = 0;
|
||
t->win_on_ms = 0;
|
||
t->win_ms = 0;
|
||
return;
|
||
}
|
||
|
||
t->win_edges += delta;
|
||
t->win_ms += dt_ms;
|
||
/* **导通时间**:脉冲只可能出现在这段时间里,判据看的就是它(TR-6,见头文件)。 */
|
||
t->win_on_ms += (uint32_t)((uint64_t)dt_ms * duty / 1000u);
|
||
|
||
if (t->win_on_ms < T_THERMAL_TACH_ON_MS) {
|
||
return; /* 还没攒够导通时间,接着攒 —— 这期间**不判**,包括不判停转 */
|
||
}
|
||
|
||
/* ---- 结算 ---- */
|
||
t->windows++;
|
||
/* RPM 按**墙钟**窗口算(四线风扇的 TACH 是连续的,12V 常供)。
|
||
⚠️ 万一最终装的是三线低边调速风扇,脉冲只在导通期出现,这个数会按占空比
|
||
偏低 —— 那时可信的只有下面那条停转判据,RPM 要改用 `win_on_ms` 当分母。
|
||
风扇定型(§22.6 的 MPN 待补)之后回来改这一行。 */
|
||
if (t->win_ms > 0) {
|
||
t->rpm = (uint32_t)((uint64_t)t->win_edges * 60000u / ((uint64_t)T_THERMAL_TACH_PPR * t->win_ms));
|
||
t->rpm_valid = true;
|
||
}
|
||
|
||
const bool stalled_now = (t->win_edges < T_THERMAL_TACH_MIN_EDGES);
|
||
if (stalled_now && !t->stalled) {
|
||
t->stalls++;
|
||
/* 只在**上升沿**记一条:停转是个持续状态,每个窗口记一条会把 256 条的
|
||
RAM 缓冲刷光,把过压/反灌那些真正救命的事件挤掉(`evlog.h` 的 LOST)。
|
||
`rail = 0` 是整板级;`arg` 是风扇号;`bus_mv` 借位放算出来的 RPM。 */
|
||
evlog_put(0, LOG_EV_FAN_STALL, (uint8_t)idx, (int32_t)t->rpm, 0);
|
||
}
|
||
t->stalled = stalled_now;
|
||
|
||
t->win_edges = 0;
|
||
t->win_on_ms = 0;
|
||
t->win_ms = 0;
|
||
}
|
||
|
||
hal_err_t t_thermal_step(thermal_snap_t *s)
|
||
{
|
||
if (s == NULL) {
|
||
return HAL_ERR_ARG;
|
||
}
|
||
if (!s_inited) {
|
||
return HAL_ERR_STATE;
|
||
}
|
||
|
||
const uint32_t now_ms = hal_time_now_ms();
|
||
const uint32_t dt_ms = hal_time_elapsed_ms(s->last_ms);
|
||
s->last_ms = now_ms;
|
||
s->cycles++;
|
||
|
||
/* 这段 dt 里真正在输出的是**上一圈**定下的占空比。先记下来再去改它 ——
|
||
用新值去算刚过去的那段导通时间,就是在拿未来的事实解释过去的脉冲数。 */
|
||
const uint16_t prev_duty = s->duty;
|
||
|
||
/* ---- 温度 → 占空比(先做:散热是这一包的活,事件与转速都在它后面)---- */
|
||
s->temp_valid = false;
|
||
s->temp_max_mc = 0;
|
||
for (int i = 0; i < T_THERMAL_NTC_COUNT; i++) {
|
||
int32_t mc = 0;
|
||
s->temp_ok[i] = (s_temp_fn != NULL) && s_temp_fn(s_ctx, i, &mc);
|
||
if (!s->temp_ok[i]) {
|
||
continue;
|
||
}
|
||
s->temp_mc[i] = mc;
|
||
/* 两路取**高**:一路在电感区、一路在 MOS 区(设计 §8.2),
|
||
凉的那一路完全可能是「那半边没在用」。取平均会把一个 90 °C 的热点
|
||
和一个 30 °C 的凉点平成 60 °C —— 一个哪儿都不存在、但看着很健康的数。 */
|
||
if (!s->temp_valid || mc > s->temp_max_mc) {
|
||
s->temp_max_mc = mc;
|
||
s->temp_valid = true;
|
||
}
|
||
}
|
||
if (!s->temp_valid) {
|
||
s->temp_errors++;
|
||
}
|
||
uint16_t duty = t_thermal_duty_for(s->temp_max_mc, s->temp_valid);
|
||
const uint16_t floor_ = s_floor;
|
||
if (floor_ > duty) {
|
||
duty = floor_; /* 手动值只抬不压:曲线要得更高时照曲线来 */
|
||
}
|
||
(void)apply_duty(s, duty);
|
||
|
||
/* ---- 过温降载:本模块只**报**,关路是限功率那一包的事 ---- */
|
||
if (s->temp_valid) {
|
||
if (!s->derate && s->temp_max_mc >= T_THERMAL_DERATE_MC) {
|
||
s->derate = true;
|
||
s->derates++;
|
||
/* `bus_mv` = 温度 m°C(`evlog.h` 对这个类型的约定),`arg` = 占空比 %。 */
|
||
evlog_put(0, LOG_EV_THERMAL_DERATE, (uint8_t)(s->duty / 10u), s->temp_max_mc, 0);
|
||
} else if (s->derate && s->temp_max_mc <= T_THERMAL_DERATE_CLEAR_MC) {
|
||
s->derate = false; /* 解除不记事件:解除不是故障 */
|
||
}
|
||
}
|
||
|
||
/* ---- 两路 TACH ---- */
|
||
for (int i = 0; i < T_THERMAL_FAN_COUNT; i++) {
|
||
tach_step(s, i, dt_ms, prev_duty);
|
||
}
|
||
return HAL_OK;
|
||
}
|
||
|
||
void t_thermal_set_floor(uint16_t permille)
|
||
{
|
||
s_floor = (permille > 1000u) ? 1000u : permille;
|
||
}
|
||
|
||
uint16_t t_thermal_floor(void) { return s_floor; }
|