固件 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]>
这个提交包含在:
+21
-37
@@ -1,49 +1,33 @@
|
||||
# 交接
|
||||
更新:2026-09-27 完成 **A15**(启动顺序 + 降级矩阵,需求 §10.9 / §10.10)
|
||||
更新:2026-09-27 完成 **A16**(风扇闭环接进任务体 + NTC / TACH 的真实采样)。**A1~A16 全部完成 ⇒ M1 的固件部分齐了**
|
||||
|
||||
## 一句话现状
|
||||
启动序列从 `app_main.c`(只进 IDF、一行测不到)拆进 `main/bringup.{h,c}`,宿主机 `test_bringup.c` **24 例**逐个「拔掉」器件对 §10.10 逐行。
|
||||
**24 个 ctest、425 个用例全绿**,`idf.py build` 通过(**0xE2C60,分区剩 11%**)。板 B(COM5)已烧 **0.1.0-a15**。
|
||||
`t_thermal` 的判据在 R-6 就写好测过了;A16 补的是它两个回调的真实来源(NTC 走 ADC1、TACH 走 PCNT)和任务体。
|
||||
**25 个 ctest、429 个用例全绿**,`idf.py build` 通过(**0xE9920,分区只剩 9%**)。板 B(COM5)已烧 **0.1.0-a16**。
|
||||
|
||||
## 下一步
|
||||
- **A16**:风扇闭环接进 `t_thermal` 任务体(app_main.c 的 `task_thermal` 还是空壳;`cmd_env.thermal` 仍是 NULL)。
|
||||
- 服务器:`rack/backend/prrack/protocol.py` 已加事件 17,已部署(HOST=10.5.5.13,26 例过)。
|
||||
- 待核:CH224A 读到 5000 mA(100 W),而 PROGRESS 早上记的是 20V/3A —— 换了充电器,还是 0x50 的理解有误?
|
||||
- ⚠️ **风扇待你确认**:板 B 上两路都报 `rpm 0` + 停转;拉到 100 % 时输入电流**没有上升**(130 → 110 mA,属波动)
|
||||
⇒ 多半是**风扇没插 / 没转**,而不是 TACH 没数到。插上风扇后看 `status.fan.rpm`;若在转却仍是 0,查 Q9/Q10 与 PCNT。
|
||||
- 风扇闭环四个阈值 + 占空比下限 + PPR 都是「待上板实测」(t_thermal.h),手机上架跑满载之后再定。
|
||||
- **分区只剩 9%**:C2(OTA 双分区)前必须重排分区表(模组 16 MB,sdkconfig 只按 8 MB 用)。
|
||||
- 仍欠(A15 留下的):I²C 总线挂死恢复(HAL 层);PD 上限的降载动作(§8.3);CH224A 读到 100 W 与早上记的 20V/3A 不一致,待核。
|
||||
|
||||
## 本次新增/修改的文件
|
||||
- `main/bringup.{h,c}` **新** — 两段:`bringup_devices()`(§10.9 第 1~4 步 + 降级)、`bringup_power_restore(want)`(第 5~6 步)
|
||||
- `main/app_main.c` — 启动段换成两段调用;**`t_safety` 在两段之间 spawn**(上电时巡检已在看);
|
||||
`evlog_init_ex(esp_reset_reason())` 挪到最前;NVS 键 `phonerail/rail_want` 存「意图」;`hub_reset_pulse_once()` 搬进 bringup.c
|
||||
- `main/cmd.h` / `cmd_exec.c` — `cmd_env_t` 加 `intent` 回调,只有 `power` 命令真的生效时调
|
||||
- `main/evlog.{h,c}` — `evlog_init_ex(boot_arg)`;新事件 **`LOG_EV_PG_TIMEOUT = 17`**
|
||||
- `main/rail.{h,c}` — `rail_volt_init_one(addr)`;`rail_volt_init()` 一片失败**不再放弃另一片**
|
||||
- sim:`sim_gpio_rise_us()`、`sim_gpio_set_hook()`;`sim_gpio_drive()` 可在配置前调、跨 config 保持(刀片把 PRSNT# 短到地与固件何时配脚无关)
|
||||
- `test/test_bringup.c` **新**(24 例);`test_cmd.c` +1(意图);`test_evlog.c` 冻结号 +17;版本 `0.1.0-a15`
|
||||
- `main/ntc.{h,c}` **新**(宿主机可测)— mV → Ω → 查 Murata NCP18XH103F03RB 的 R-T 表(−40~125 °C、5 °C 一格、格内线性插值);
|
||||
贴 +3V3 / 贴地 / 表外一律「读不到」(⇒ t_thermal 满速)
|
||||
- `main/thermal_esp.{h,c}` **新**(只进 IDF)— ADC1 oneshot + 曲线拟合校准(8 次平均,引脚反查通道);两个 PCNT 单元数 TACH 上升沿(累计计数、1 µs 毛刺滤波)
|
||||
- `main/app_main.c` — `task_thermal` 任务体(每 10 s 打一行温度 / mV / 占空比 / 转速);`thermal_esp_init()` + `t_thermal_init()` 放在**上电恢复之前**;`cmd_env.thermal = &s_therm`
|
||||
- `main/CMakeLists.txt` REQUIRES 加 `esp_adc esp_driver_pcnt`;`host/CMakeLists.txt` 加 `ntc.c`
|
||||
- `test/test_ntc.c` **新**(4 例:表点、⭐方向「越热电压越低」、⭐开路/短路不是温度、比例式);版本 `0.1.0-a16`
|
||||
|
||||
## §10.10 各行落在哪
|
||||
| 行 | 行为 | 用例 |
|
||||
|---|---|---|
|
||||
| 刀片 INA226 无应答 | 照常上电、`blind`、I2C_LOST(rail, 0x40) | `test_row_rail_ina_missing_keeps_power` |
|
||||
| DAC 无应答 | 不关输出、另一片照样初始化、影子回 3.90 | `test_row_dac_missing_…` |
|
||||
| PCA9555 无应答 | 照常上电,PG 改按 INA226 电压;两样都看不见 = 开着报 unverified | `test_row_pca_*`(3 例) |
|
||||
| mux 选不中 | 脉冲一次 MUX_RST#,仍不通 ⇒ 一路不开、MUX_LOST,不碰主干 | `test_row_mux_*`(2 例) |
|
||||
| PRSNT# 高 | 不发一个总线字节、不上电;想开的才记 SLOT_EMPTY | `test_row_empty_slot_*`(2 例) |
|
||||
| CH224A 读不到 / 偏低 | 按 60 W / 按读到的,PD_LIMIT 事件 | `test_row_pd_*` + 通道 5 兜底 |
|
||||
| 看门狗复位 | BOOT.arg = 复位原因 | `test_row_watchdog_reset_is_recorded_in_boot` |
|
||||
| **I²C 总线挂死** | ⛔ **没做**(9 个时钟恢复在 HAL 层) | — |
|
||||
|
||||
注错验证过:删掉 `ina226_set_reversed(0x43)` ⇒ 第一条红;错开改成 1 ms ⇒ 两条红。
|
||||
|
||||
## 上板实测(板 B,五槽空)
|
||||
## 上板实测(板 B,五槽空,风扇状态未知)
|
||||
```
|
||||
status: "fw":"0.1.0-a15","in":{"mv":12034,"ua":98500} ← 输入电流第一次是真值(以前 Calibration 没写,恒 0)
|
||||
events: [27, 51, 0, 1(BOOT), 11(ESP_RST_USB), …] [28, 155, 0, 16(PD_LIMIT), 0, 1000(=100 W), 5000 mA]
|
||||
status: "temp":[38249,34027] 分压端 1266 / 1384 mV ← 两路 NTC 读数合理(NTC1 在电感区更热)
|
||||
"fan":{"duty":300,"en":true,"rpm":[0,0],"stall":[true,true]}
|
||||
events: [31, 14318, 0, 11(FAN_STALL), 0] [32, …, 11, 1] ← 启动宽限 5 s + 一个窗口之后各报一次,不刷屏
|
||||
```
|
||||
没有 I2C_LOST ⇒ 两片 DAC / 输入侧 INA226 / CH224A 都在主干应答。复位原因是 USB ⇒ 没碰 HUB_RST#,COM5 没掉。
|
||||
**上电复位路径(Hub 脉冲、DAC NVM 核对)和有刀片时的逐路上电还没在实物上走过**——要等刀片 + 手机上架。
|
||||
|
||||
## 踩到的坑(只写会再犯的)
|
||||
- ⛔ **mux 死了主干也够不着**:`i2cbus_xact(TRUNK)` 先写 mux「全关」,失败就不跑回调。别以为「DAC 在主干上就不受 mux 影响」。
|
||||
- sim 里同一地址在主干和某通道各挂一个,主干那颗 `present=false` 时仍会先被匹配 ⇒ NAK。测「只在通道 5」要主干上干脆不挂。
|
||||
- 设定电压**不持久化**:热重启后五路回到 3.90 V(bringup 把 DAC 重写成安全值)。上位机要的话重下。
|
||||
- `python` 在这台机器的 Git Bash 里是 Windows Store 占位程序,会挂住;一律用 `/c/Python314/python`。
|
||||
- 比例式 NTC 的电源误差估算:+3V3 偏 1 % 在 25 °C 附近是 **~0.6 °C**(中点处电阻误差是电源误差的 2 倍、NTC 每度 ~4 %),不是 0.3 °C。
|
||||
- PowerShell 5.1 的 `Set-Content -Encoding utf8` 会写 BOM —— 改 `phonerail.h` 这种文件用 sed,别用它。
|
||||
- `python` 在 Git Bash 里是 Windows Store 占位程序,会挂住;一律用 `/c/Python314/python`。
|
||||
|
||||
@@ -41,6 +41,7 @@ set(PHONERAIL_CORE_SRCS
|
||||
${FW_ROOT}/main/netcfg.c
|
||||
${FW_ROOT}/main/mlink.c
|
||||
${FW_ROOT}/main/bringup.c
|
||||
${FW_ROOT}/main/ntc.c
|
||||
)
|
||||
|
||||
# HAL:接口在 include/,宿主机链 sim/ 的实现(esp/ 那份只有 IDF 目标编)。
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ESP-IDF 侧的 main 组件。
|
||||
# app_main.c / ulink_esp.c / mlink_esp.c 是 ESP 专用;其余 .c 必须同时在宿主机上编得过
|
||||
# app_main.c / ulink_esp.c / mlink_esp.c / thermal_esp.c 是 ESP 专用;其余 .c 必须同时在宿主机上编得过
|
||||
# (新增文件时两处都要加:这里 + host/CMakeLists.txt 的 PHONERAIL_CORE_SRCS)。
|
||||
idf_component_register(
|
||||
SRCS "app_main.c"
|
||||
@@ -25,7 +25,10 @@ idf_component_register(
|
||||
"netcfg.c"
|
||||
"mlink.c"
|
||||
"bringup.c"
|
||||
"ntc.c"
|
||||
"thermal_esp.c"
|
||||
INCLUDE_DIRS "."
|
||||
EMBED_TXTFILES "certs/evse_ca.pem"
|
||||
REQUIRES phonerail_hal drv esp_driver_usb_serial_jtag
|
||||
esp_wifi esp_netif esp_event esp_timer nvs_flash mqtt)
|
||||
esp_wifi esp_netif esp_event esp_timer nvs_flash mqtt
|
||||
esp_adc esp_driver_pcnt)
|
||||
|
||||
+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) {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/* NTC 换算。电路与出处见 ntc.h。 */
|
||||
#include "ntc.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* NCP18XH103F03RB 的 R-T 表(Murata 规格书,−40 ~ 125 °C,5 °C 一格),单位 Ω。
|
||||
⚠️ 单调**递减**是这张表唯一不能破的性质 —— `test_ntc.c` 逐格查。
|
||||
抽查:25 °C = 10000(标称);50 °C = 4161 与 B3380 公式算出的 4160 吻合。 */
|
||||
#define NTC_T0_MC (-40000)
|
||||
#define NTC_STEP_MC 5000
|
||||
static const uint32_t k_ohm[] = {
|
||||
195652, 148171, 113347, 87559, 68237, 53650, 42506, 33892, /* −40 … −5 */
|
||||
27219, 22021, 17926, 14674, 12081, 10000, 8315, 6948, /* 0 … 35 */
|
||||
5834, 4917, 4161, 3535, 3014, 2586, 2228, 1925, /* 40 … 75 */
|
||||
1669, 1452, 1268, 1110, 974, 858, 758, 672, /* 80 … 115 */
|
||||
596, 531, /* 120, 125 */
|
||||
};
|
||||
#define NTC_N ((int)(sizeof(k_ohm) / sizeof(k_ohm[0])))
|
||||
_Static_assert(NTC_N == 34, "表应当覆盖 −40 ~ 125 °C,5 °C 一格");
|
||||
|
||||
bool ntc_mc_from_ohm(uint32_t ohm, int32_t *milli_c)
|
||||
{
|
||||
if (milli_c == NULL || ohm > k_ohm[0] || ohm < k_ohm[NTC_N - 1]) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < NTC_N - 1; i++) {
|
||||
const uint32_t hi = k_ohm[i]; /* 较冷的一端,电阻大 */
|
||||
const uint32_t lo = k_ohm[i + 1]; /* 较热的一端 */
|
||||
if (ohm <= hi && ohm >= lo) {
|
||||
/* 格内按电阻线性插值。分子最大 190000 × 5000 ≈ 9.5e8,int64 稳妥。 */
|
||||
const int64_t frac = (int64_t)(hi - ohm) * NTC_STEP_MC / (int64_t)(hi - lo);
|
||||
*milli_c = NTC_T0_MC + i * NTC_STEP_MC + (int32_t)frac;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false; /* 走不到:上面的范围判断已经把表外挡掉了 */
|
||||
}
|
||||
|
||||
bool ntc_mc_from_mv(int32_t mv, int32_t vref_mv, int32_t *milli_c)
|
||||
{
|
||||
if (milli_c == NULL || vref_mv <= 2 * NTC_RAIL_MARGIN_MV) {
|
||||
return false;
|
||||
}
|
||||
if (mv < NTC_RAIL_MARGIN_MV || mv > vref_mv - NTC_RAIL_MARGIN_MV) {
|
||||
return false; /* 贴地 = 短路,贴 +3V3 = 开路:都不是温度 */
|
||||
}
|
||||
/* 下臂分压:mv / vref = R / (Rpu + R) ⇒ R = Rpu × mv / (vref − mv)。 */
|
||||
const uint64_t ohm = (uint64_t)NTC_PULLUP_OHM * (uint64_t)mv / (uint64_t)(vref_mv - mv);
|
||||
if (ohm > 0xFFFFFFFFull) {
|
||||
return false;
|
||||
}
|
||||
return ntc_mc_from_ohm((uint32_t)ohm, milli_c);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/* 两路板载 NTC 的换算:ADC 读回的毫伏 → 毫摄氏度(A16,需求 §10.3 `t_thermal`)。
|
||||
|
||||
---- 电路(已按 `.claude/r2/_base_pcb_nets.json` 逐脚核实)----------------
|
||||
|
||||
+3V3 ── R660 10k ──┬── NTC1(IO4, ADC1_CH3) ── C736 ── GND
|
||||
└── RT601 ── GND
|
||||
(NTC2 同形:R661 / RT602 / C739 → IO5, ADC1_CH4)
|
||||
|
||||
**NTC 在下臂**:越热电阻越小、电压越低。写反成上臂的现象是「越热读数越低」——
|
||||
风扇闭环照样算、照样有数,只是方向反了,要等机器满载才暴露。
|
||||
|
||||
RT601/RT602 = Murata **NCP18XH103F03RB**(10 kΩ @25 °C,B25/50 = 3380,立创 C13564;
|
||||
设计文档 BOM 那一行)。换算走厂家 R-T 表(5 °C 一格、格内线性插值),
|
||||
不用 B 值公式:B 公式在 25/50 °C 以外偏差 1~2 °C,而且要浮点和 log。
|
||||
|
||||
---- 这一层不做的事 ------------------------------------------------------
|
||||
· 不读 ADC(那是 `thermal_esp.c`,只进 IDF)。本文件是纯整数换算,宿主机可测;
|
||||
· 不做滤波:C736/C739 是硬件 RC,t_thermal 1 s 才读一次,再滤就是加延迟。 */
|
||||
#ifndef NTC_H
|
||||
#define NTC_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define NTC_PULLUP_OHM 10000 /* R660 / R661 */
|
||||
|
||||
/* 分压端离两条轨多近就判「读不到」(mV)。
|
||||
开路(NTC 没焊 / 断线)⇒ 电压贴 +3V3;短路 ⇒ 贴地。两者都不是温度。
|
||||
⚠️ 这不是精度门限,是「这还是不是一颗 NTC」的门限:
|
||||
表的两端(−40 °C / +125 °C)分别对应约 3.13 V / 0.16 V,离两条轨都还远。 */
|
||||
#define NTC_RAIL_MARGIN_MV 30
|
||||
|
||||
/* ADC 读回的分压端电压 + 当时的上臂电源电压 → 毫摄氏度。
|
||||
`vref_mv` 传 +3V3 的实际值(不知道就传 3300);比例式测量,电源偏 1 % 在 25 °C 附近带来 ~0.6 °C(分压中点处电阻误差是电源误差的 2 倍,NTC 每度约 4 %)。
|
||||
返回 false = 开路 / 短路 / 超出表的范围(−40~125 °C),`*milli_c` 不动。 */
|
||||
bool ntc_mc_from_mv(int32_t mv, int32_t vref_mv, int32_t *milli_c);
|
||||
|
||||
/* 电阻 → 毫摄氏度(查表 + 插值)。超出表的范围返回 false。给测试用,也给标定用。 */
|
||||
bool ntc_mc_from_ohm(uint32_t ohm, int32_t *milli_c);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NTC_H */
|
||||
@@ -3,7 +3,7 @@
|
||||
#define PHONERAIL_H
|
||||
|
||||
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
||||
#define PHONERAIL_FW_VERSION "0.1.0-a15"
|
||||
#define PHONERAIL_FW_VERSION "0.1.0-a16"
|
||||
|
||||
/* 通道数。五路直供,见设计文档 §2。 */
|
||||
#define PHONERAIL_RAIL_COUNT 5
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/* `t_thermal` 回调的 ESP 实现。契约见 thermal_esp.h;判据在 t_thermal.c(宿主机测过),
|
||||
换算在 ntc.c(同上)。**这里只放驱动胶水,不许出现判据。** */
|
||||
#include "thermal_esp.h"
|
||||
|
||||
#include "board_pins.h"
|
||||
#include "driver/pulse_cnt.h"
|
||||
#include "esp_adc/adc_cali.h"
|
||||
#include "esp_adc/adc_cali_scheme.h"
|
||||
#include "esp_adc/adc_oneshot.h"
|
||||
#include "esp_log.h"
|
||||
#include "ntc.h"
|
||||
#include "t_thermal.h"
|
||||
|
||||
static const char *TAG = "thermal";
|
||||
|
||||
/* 上臂电源的名义值。比例式测量,偏 1 % 只带来 ~0.6 °C(ntc.h),对风扇闭环无所谓,不值得多占一路 ADC 去量它。 */
|
||||
#define THERM_VREF_MV 3300
|
||||
|
||||
/* 一次读几次取平均。ADC 单次噪声 ±10 mV 量级 ≈ ±0.3 °C;8 次之后可以忽略。1 s 一回,开销无所谓。 */
|
||||
#define THERM_AVG 8
|
||||
|
||||
/* PCNT 计数器是 16 位有符号的;开 `accum_count` + 在上限放观察点之后,驱动会把溢出
|
||||
累进 `pcnt_unit_get_count()` 的返回值里(int,回绕后按 uint32 用即可)。 */
|
||||
#define TACH_LIMIT 30000
|
||||
|
||||
/* 毛刺滤波:TACH 最快 ~6000 RPM × 2 PPR = 200 Hz,周期 5 ms;1 µs 的滤波远在它下面,
|
||||
但能把 Q6 开关 25 kHz PWM 串进来的尖刺挡掉。 */
|
||||
#define TACH_GLITCH_NS 1000
|
||||
|
||||
static const int k_ntc_pin[T_THERMAL_NTC_COUNT] = { BOARD_PIN_NTC1, BOARD_PIN_NTC2 };
|
||||
static const int k_tach_pin[T_THERMAL_FAN_COUNT] = {
|
||||
[THERM_FAN_IN] = BOARD_PIN_FAN_TACH_IN,
|
||||
[THERM_FAN_OUT] = BOARD_PIN_FAN_TACH_OUT,
|
||||
};
|
||||
|
||||
static adc_oneshot_unit_handle_t s_adc;
|
||||
static adc_channel_t s_ch[T_THERMAL_NTC_COUNT];
|
||||
static adc_cali_handle_t s_cali[T_THERMAL_NTC_COUNT];
|
||||
static bool s_adc_ok[T_THERMAL_NTC_COUNT];
|
||||
static int32_t s_last_mv[T_THERMAL_NTC_COUNT] = { -1, -1 };
|
||||
|
||||
static pcnt_unit_handle_t s_pcnt[T_THERMAL_FAN_COUNT];
|
||||
static bool s_pcnt_ok[T_THERMAL_FAN_COUNT];
|
||||
|
||||
static hal_err_t adc_up(void)
|
||||
{
|
||||
const adc_oneshot_unit_init_cfg_t ucfg = { .unit_id = ADC_UNIT_1 };
|
||||
if (adc_oneshot_new_unit(&ucfg, &s_adc) != ESP_OK) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
hal_err_t first = HAL_OK;
|
||||
for (int i = 0; i < T_THERMAL_NTC_COUNT; i++) {
|
||||
adc_unit_t unit;
|
||||
/* 由引脚反查通道,不写死「CH3/CH4」:board_pins.h 是引脚的唯一出处,
|
||||
而且这一步顺带证明了它确实落在 ADC1 上(ADC2 在 WiFi 开着时读不了)。 */
|
||||
if (adc_oneshot_io_to_channel(k_ntc_pin[i], &unit, &s_ch[i]) != ESP_OK ||
|
||||
unit != ADC_UNIT_1) {
|
||||
ESP_LOGE(TAG, "IO%d 不在 ADC1 上", k_ntc_pin[i]);
|
||||
first = (first == HAL_OK) ? HAL_ERR_ARG : first;
|
||||
continue;
|
||||
}
|
||||
/* 12 dB 衰减:量程到 ~3.1 V,覆盖整张 R-T 表(−40 °C 时 3.14 V 会略微饱和,
|
||||
那一端本来就不关心 —— 低温不需要风扇)。 */
|
||||
const adc_oneshot_chan_cfg_t ccfg = { .atten = ADC_ATTEN_DB_12,
|
||||
.bitwidth = ADC_BITWIDTH_DEFAULT };
|
||||
if (adc_oneshot_config_channel(s_adc, s_ch[i], &ccfg) != ESP_OK) {
|
||||
first = (first == HAL_OK) ? HAL_ERR_STATE : first;
|
||||
continue;
|
||||
}
|
||||
const adc_cali_curve_fitting_config_t kcfg = {
|
||||
.unit_id = ADC_UNIT_1,
|
||||
.chan = s_ch[i],
|
||||
.atten = ADC_ATTEN_DB_12,
|
||||
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||||
};
|
||||
if (adc_cali_create_scheme_curve_fitting(&kcfg, &s_cali[i]) != ESP_OK) {
|
||||
/* eFuse 里没有校准值(不该发生在 WROOM-1 上):没校准的读数误差可达
|
||||
±100 mV ≈ ±3 °C,宁可报「读不到」让风扇满速,也不给一个看着正常的错数。 */
|
||||
ESP_LOGE(TAG, "NTC%d 的 ADC 校准不可用", i + 1);
|
||||
first = (first == HAL_OK) ? HAL_ERR_STATE : first;
|
||||
continue;
|
||||
}
|
||||
s_adc_ok[i] = true;
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
static hal_err_t pcnt_up(int idx)
|
||||
{
|
||||
const pcnt_unit_config_t ucfg = {
|
||||
.low_limit = -TACH_LIMIT,
|
||||
.high_limit = TACH_LIMIT,
|
||||
.flags.accum_count = 1,
|
||||
};
|
||||
pcnt_unit_handle_t u = NULL;
|
||||
if (pcnt_new_unit(&ucfg, &u) != ESP_OK) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
const pcnt_glitch_filter_config_t fcfg = { .max_glitch_ns = TACH_GLITCH_NS };
|
||||
(void)pcnt_unit_set_glitch_filter(u, &fcfg);
|
||||
|
||||
/* ⚠️ 只数边沿、不看电平:Q9/Q10 反相(t_thermal.h 硬件第 3 条),
|
||||
数上升沿还是下降沿都一样 —— 每个 TACH 脉冲正好一个。板上 R753/R757 上拉,不开内部上拉。 */
|
||||
const pcnt_chan_config_t ccfg = { .edge_gpio_num = k_tach_pin[idx], .level_gpio_num = -1 };
|
||||
pcnt_channel_handle_t c = NULL;
|
||||
if (pcnt_new_channel(u, &ccfg, &c) != ESP_OK ||
|
||||
pcnt_channel_set_edge_action(c, PCNT_CHANNEL_EDGE_ACTION_INCREASE,
|
||||
PCNT_CHANNEL_EDGE_ACTION_HOLD) != ESP_OK ||
|
||||
pcnt_unit_add_watch_point(u, TACH_LIMIT) != ESP_OK ||
|
||||
pcnt_unit_enable(u) != ESP_OK || pcnt_unit_clear_count(u) != ESP_OK ||
|
||||
pcnt_unit_start(u) != ESP_OK) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
s_pcnt[idx] = u;
|
||||
s_pcnt_ok[idx] = true;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t thermal_esp_init(void)
|
||||
{
|
||||
hal_err_t first = adc_up();
|
||||
for (int i = 0; i < T_THERMAL_FAN_COUNT; i++) {
|
||||
const hal_err_t e = pcnt_up(i);
|
||||
if (e != HAL_OK) {
|
||||
ESP_LOGE(TAG, "TACH%d(IO%d)的 PCNT 起不来", i, k_tach_pin[i]);
|
||||
if (first == HAL_OK) {
|
||||
first = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
bool thermal_esp_temp(void *ctx, int idx, int32_t *milli_c)
|
||||
{
|
||||
(void)ctx;
|
||||
if (idx < 0 || idx >= T_THERMAL_NTC_COUNT || !s_adc_ok[idx]) {
|
||||
return false;
|
||||
}
|
||||
int32_t sum = 0;
|
||||
for (int n = 0; n < THERM_AVG; n++) {
|
||||
int mv = 0;
|
||||
if (adc_oneshot_get_calibrated_result(s_adc, s_cali[idx], s_ch[idx], &mv) != ESP_OK) {
|
||||
s_last_mv[idx] = -1;
|
||||
return false;
|
||||
}
|
||||
sum += mv;
|
||||
}
|
||||
s_last_mv[idx] = sum / THERM_AVG;
|
||||
return ntc_mc_from_mv(s_last_mv[idx], THERM_VREF_MV, milli_c);
|
||||
}
|
||||
|
||||
bool thermal_esp_tach(void *ctx, int idx, uint32_t *edges)
|
||||
{
|
||||
(void)ctx;
|
||||
if (idx < 0 || idx >= T_THERMAL_FAN_COUNT || !s_pcnt_ok[idx]) {
|
||||
return false;
|
||||
}
|
||||
int v = 0;
|
||||
if (pcnt_unit_get_count(s_pcnt[idx], &v) != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
*edges = (uint32_t)v; /* 累计值;t_thermal 用 uint32 相减,回绕安全 */
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t thermal_esp_last_mv(int idx)
|
||||
{
|
||||
return (idx >= 0 && idx < T_THERMAL_NTC_COUNT) ? s_last_mv[idx] : -1;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/* `t_thermal` 的两个回调在 ESP32-S3 上的实现(A16)。**只进 IDF 目标。**
|
||||
温度:ADC1 oneshot + 曲线拟合校准 → mV → `ntc_mc_from_mv()`;
|
||||
TACH:两个 PCNT 单元数上升沿(累计计数,uint32 回绕安全)。 */
|
||||
#ifndef THERMAL_ESP_H
|
||||
#define THERMAL_ESP_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
|
||||
/* 起 ADC 与 PCNT。失败不 abort:温度读不到 t_thermal 会满速(§10.10),
|
||||
TACH 读不到就不判停转 —— 两者都是降级,不是固件写错了。
|
||||
返回第一个错误码;已经起来的那一半照常可用。 */
|
||||
hal_err_t thermal_esp_init(void);
|
||||
|
||||
bool thermal_esp_temp(void *ctx, int idx, int32_t *milli_c); /* therm_temp_fn */
|
||||
bool thermal_esp_tach(void *ctx, int idx, uint32_t *edges); /* therm_tach_fn */
|
||||
|
||||
/* 最近一次读到的两路原始毫伏(调试 / 标定用;读失败是 −1)。 */
|
||||
int32_t thermal_esp_last_mv(int idx);
|
||||
|
||||
#endif /* THERMAL_ESP_H */
|
||||
@@ -0,0 +1,86 @@
|
||||
/* A16:NTC 换算(ntc.h)。两条最要紧的:
|
||||
⭐ 方向 —— NTC 在下臂,**越热电压越低**;写反了闭环照样有数,只是反着转;
|
||||
⭐ 开路 / 短路判「读不到」—— 读不到时 t_thermal 会满速(§10.10),
|
||||
而把开路算成 −40 °C 的后果是「风扇按最低速转、机器在烤」。 */
|
||||
#include "ntc.h"
|
||||
#include "unity.h"
|
||||
|
||||
void setUp(void) {}
|
||||
void tearDown(void) {}
|
||||
|
||||
/* 分压端电压(与 ntc.c 的反算互为逆运算,单独写一遍免得自己证明自己)。 */
|
||||
static int32_t mv_at(uint32_t ohm) { return (int32_t)(3300ull * ohm / (NTC_PULLUP_OHM + ohm)); }
|
||||
|
||||
static void test_table_points(void)
|
||||
{
|
||||
int32_t mc = 0;
|
||||
TEST_ASSERT_TRUE(ntc_mc_from_ohm(10000, &mc));
|
||||
TEST_ASSERT_EQUAL_INT32(25000, mc);
|
||||
TEST_ASSERT_TRUE(ntc_mc_from_ohm(4161, &mc));
|
||||
TEST_ASSERT_EQUAL_INT32(50000, mc);
|
||||
TEST_ASSERT_TRUE(ntc_mc_from_ohm(195652, &mc));
|
||||
TEST_ASSERT_EQUAL_INT32(-40000, mc);
|
||||
TEST_ASSERT_TRUE(ntc_mc_from_ohm(531, &mc));
|
||||
TEST_ASSERT_EQUAL_INT32(125000, mc);
|
||||
/* 格内插值:10000 与 8315 的正中间 ≈ 27.5 °C。 */
|
||||
TEST_ASSERT_TRUE(ntc_mc_from_ohm((10000 + 8315) / 2, &mc));
|
||||
TEST_ASSERT_INT32_WITHIN(20, 27500, mc);
|
||||
}
|
||||
|
||||
/* ⭐ 方向:电压从高扫到低,温度必须一路**不降**;电阻从大到小同理。 */
|
||||
static void test_hotter_means_lower_voltage(void)
|
||||
{
|
||||
int32_t prev = -1000000, mc = 0;
|
||||
int n = 0;
|
||||
for (int32_t mv = 3300 - NTC_RAIL_MARGIN_MV - 1; mv > NTC_RAIL_MARGIN_MV; mv -= 7) {
|
||||
if (ntc_mc_from_mv(mv, 3300, &mc)) {
|
||||
TEST_ASSERT_TRUE_MESSAGE(mc >= prev, "电压降低时温度必须升高(NTC 在下臂)");
|
||||
prev = mc;
|
||||
n++;
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_TRUE(n > 300);
|
||||
/* 两个现实点:25 °C 正好一半(1650 mV);60 °C ≈ 0.76 V。 */
|
||||
TEST_ASSERT_TRUE(ntc_mc_from_mv(1650, 3300, &mc));
|
||||
TEST_ASSERT_INT32_WITHIN(100, 25000, mc);
|
||||
TEST_ASSERT_TRUE(ntc_mc_from_mv(mv_at(3014), 3300, &mc));
|
||||
TEST_ASSERT_INT32_WITHIN(200, 60000, mc);
|
||||
}
|
||||
|
||||
/* ⭐ 开路(贴 +3V3)/ 短路(贴地)/ 表外:一律「读不到」,而且不动输出。 */
|
||||
static void test_open_and_short_are_not_temperatures(void)
|
||||
{
|
||||
int32_t mc = 12345;
|
||||
TEST_ASSERT_FALSE(ntc_mc_from_mv(3300, 3300, &mc));
|
||||
TEST_ASSERT_FALSE(ntc_mc_from_mv(3290, 3300, &mc));
|
||||
TEST_ASSERT_FALSE(ntc_mc_from_mv(0, 3300, &mc));
|
||||
TEST_ASSERT_FALSE(ntc_mc_from_mv(10, 3300, &mc));
|
||||
TEST_ASSERT_FALSE(ntc_mc_from_mv(-5, 3300, &mc));
|
||||
TEST_ASSERT_FALSE(ntc_mc_from_mv(1650, 0, &mc));
|
||||
TEST_ASSERT_FALSE(ntc_mc_from_ohm(200000, &mc)); /* < −40 °C */
|
||||
TEST_ASSERT_FALSE(ntc_mc_from_ohm(500, &mc)); /* > 125 °C */
|
||||
TEST_ASSERT_EQUAL_INT32(12345, mc);
|
||||
TEST_ASSERT_FALSE(ntc_mc_from_mv(1650, 3300, NULL));
|
||||
}
|
||||
|
||||
/* 比例式:+3V3 偏 1 %(3.267 V),只要把实际值传进来,读数不变;
|
||||
传错成 3300 时误差约 0.6 °C(25 °C 附近)—— 对风扇闭环无所谓。 */
|
||||
static void test_ratiometric(void)
|
||||
{
|
||||
int32_t a = 0, b = 0;
|
||||
TEST_ASSERT_TRUE(ntc_mc_from_mv(1650, 3300, &a));
|
||||
TEST_ASSERT_TRUE(ntc_mc_from_mv(1634, 3267, &b));
|
||||
TEST_ASSERT_INT32_WITHIN(50, a, b);
|
||||
TEST_ASSERT_TRUE(ntc_mc_from_mv(1634, 3300, &b));
|
||||
TEST_ASSERT_INT32_WITHIN(700, a, b);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_table_points);
|
||||
RUN_TEST(test_hotter_means_lower_voltage);
|
||||
RUN_TEST(test_open_and_short_are_not_temperatures);
|
||||
RUN_TEST(test_ratiometric);
|
||||
return UNITY_END();
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户