固件:R2 迁移 R-0 ~ R-6(底板 + 5 片同构刀片)

2026-09-16 完成、当时未提交,这里补提交(原样,不含 09-27 上板的修改)。

- 器件层 / 拓扑层整体从 R1 换到 R2:TCA9548A 分通道、五路同址 INA226 0x40 + 输入侧 0x43、
  刀片 PCA9555(VBUS_EN 挪到 P0.4)、CH224A 只读驱动、风扇闭环 t_thermal
- 新增 i2cbus(mux 原子事务)、slot(刀片 I/O 门面)、host/qtest.sh
- 74HC595 链退役(代码与测试保留)
- 判据阈值与时间常量未动;19 个 ctest 全绿,idf.py build 通过

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-27 10:26:24 +08:00
共同撰写人 Claude Opus 5.5
父节点 5a80d8a872
当前提交 b8ad69681f
共修改 51 个文件,包含 10882 行新增和 4068 行删除
+120 -12
查看文件
@@ -6,12 +6,17 @@
#include "evlog.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "hal_gpio.h"
#include "hal_i2c.h"
#include "hal_lock.h"
#include "hal_time.h"
#include "i2cbus.h"
#include "ina226.h"
#include "pca9555.h"
#include "phonerail.h"
#include "rail_cfg.h"
#include "sched.h"
#include "slot.h"
#include "t_actuator.h"
#include "t_safety.h"
#include "tasks.h"
@@ -64,10 +69,19 @@ static safety_snap_t s_snap;
static bool act_pg_from_safety(void *ctx, int rail, bool *pg)
{
(void)ctx;
if (!s_snap.io_ok) {
/* ⭐ R2:PCA9555 变成**每路一片**(五片同构、同地址 0x20,靠 mux 分开),
所以快照里的输入也是每路一份 —— R1 那时全板只有一片,只有一份。
⚠️ 这里的 `rail - 1` 是**数组下标**,不是由路号算出来的器件编号
(rail_cfg.h 禁的是后者)。器件是哪一片由 `slot_*` 那一层查表决定。 */
if (rail < 1 || rail > PHONERAIL_RAIL_COUNT) {
return false;
}
*pg = pca9555_rail_pg(&s_snap.io, rail); /* 极性只在驱动里出现一次 */
if (!s_snap.io_ok[rail - 1]) {
return false;
}
/* 极性只在驱动里出现一次。⚠️ R2 的判读函数**不带 rail** —— 五片逐格相同,
位号是板级常量(pca9555.h),传路号进去反而会让人以为五片位表不一样。 */
*pg = pca9555_efuse_pg(&s_snap.io[rail - 1]);
return true;
}
@@ -118,7 +132,10 @@ static void task_thermal(void *arg)
sched_periodic_t tick;
sched_periodic_init(&tick, task_desc(TASK_THERMAL)->period_ms);
for (;;) {
/* A16:两路 NTC + 载板 LM75 → 风扇 PWM 闭环(TR-6 的 TACH 采样窗口)。 */
/* A16:两路 NTC(IO4/IO5)→ 风扇 PWM 闭环(TR-6 的 TACH 采样窗口)。
⚠️ R2 没有 LM75 了(载板整个不存在,计划 §5.10)。
真正的任务体在 `main/t_thermal.c` 里,由 A16 那一包填;
**这里不 include 它**,免得两个人同时改到同一处。 */
sched_periodic_wait(&tick);
}
}
@@ -174,6 +191,33 @@ static void task_log(void *arg)
}
}
/* USB2517I 的复位脉冲 —— **整个固件里唯一碰 `HUB_RST#` 的地方,而且只在启动调一次**。
低有效(板上 R716 上拉到 +3V3_HUB),给 1 ms 足够:手册要的是几十微秒量级。
⛔⛔ **运行期一个字都不许再写它**(R2 D13 代价 2)。ESP32 自己的
USB-Serial-JTAG 就挂在这个 Hub 的第 6 个下行口上,所以复位 Hub =
**自己把自己的控制台拔了**,而且下一次 `idf.py flash` 也进不来
—— 到那时候唯一的退路是 J33 那个 5P 应急串口(UART0,IO43/IO44),
得拆机接线。写一行 "Hub 卡了就复位一下" 的自愈代码,代价是整机变砖。
⚠️ 这一下放在 `rail_en_init()` 之后、其它一切之前:Hub 复位会让 USB 重新枚举,
期间串口日志是丢的。越早做,后面越多的启动日志能被电脑收到。
失败**不 abort**:Hub 没起来只影响调试与手机的数据口,五路供电照常
(§10.2:失联 ≠ 断电)。 */
static void hub_reset_pulse_once(void)
{
hal_err_t err = hal_gpio_config(BOARD_PIN_HUB_RST_N, HAL_GPIO_OUTPUT);
if (err != HAL_OK) {
ESP_LOGE(TAG, "HUB_RST# 配置失败:%s —— 跳过复位脉冲", hal_err_str(err));
return;
}
(void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 0); /* 低 = 复位 */
hal_time_delay_ms(1);
(void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 1); /* 放开,之后再也不动 */
ESP_LOGI(TAG, "USB Hub 已复位一次(运行期不再动 HUB_RST#)");
}
static void spawn(task_id_t id, TaskFunction_t fn)
{
const task_desc_t *d = task_desc(id);
@@ -191,21 +235,32 @@ void app_main(void)
selftest_or_die();
/* 十根 EN 线配成输出并全部拉低。**先于一切外设**:DAC / eFuse 一上电就有
输出,这两根线这时必须已经确定是低的。 */
/* ⭐ R2:**五根** `EFUSE_EN` 配成输出并全部拉低(R1 是十根:另外五根 `VBUS_EN`
已经挪到刀片的 PCA9555 上了)。**先于一切外设**:DAC / eFuse 一上电就有
输出,这五根线这时必须已经确定是低的。 */
hal_err_t err = rail_en_init();
if (err != HAL_OK) {
ESP_LOGE(TAG, "EN 引脚初始化失败:%s", hal_err_str(err));
abort();
}
ESP_LOGI(TAG, "五路输出与 VBUS 全部关闭");
/* ⚠️ R2:这一步关掉的**只有五路直供**(`EFUSE_EN` 是 GPIO 直连)。
`VBUS_EN` 已经挪到刀片的 PCA9555 P0.4 上了(计划 §5.4),此刻还不受控 ——
它靠的是硬件:PCA9555 复位后全是输入(高阻)+ R701 100 kΩ 下拉 = VBUS 断。
真正"固件说了算"要等到下面的 `slot_init()`。 */
ESP_LOGI(TAG, "五路 eFuse 使能已全部拉低(VBUS 此刻靠下拉电阻关着)");
/* 两把锁必须早于任何一个用总线的任务。 */
/* USB Hub 复位脉冲 —— 只此一次,见函数头上那一段。 */
hub_reset_pulse_once();
/* 锁必须早于任何一个用总线的任务。
⚠️ R2 之后**全系统只剩一把** `HAL_LOCK_I2C`(595 那把随着移位链一起退役),
所以「两把锁嵌套」在物理上不可能了;代价是执行器与巡检开始争同一把锁
(计划 §5.6)。 */
(void)hal_lock_init();
const hal_i2c_cfg_t i2c = {
.sda_gpio = BOARD_PIN_I2C_SDA,
.scl_gpio = BOARD_PIN_I2C_SCL,
.sda_gpio = BOARD_PIN_I2C_SDA, /* ⭐ R2:IO41(R1 是 IO14) */
.scl_gpio = BOARD_PIN_I2C_SCL, /* ⭐ R2:IO42(R1 是 IO15) */
.freq_hz = 400000,
};
err = hal_i2c_init(&i2c);
@@ -214,9 +269,62 @@ void app_main(void)
abort();
}
/* ⚠️ 器件初始化(INA226 的 Config/Calibration、DAC、PCA9555 方向、595 安全值)
与**降级矩阵**是 A15 的启动顺序那一包。在那之前 t_safety 读回来的电流恒为 0
(Calibration 还没写),这是预期的,不是故障。 */
/* ⭐ R2 新增:带 mux 的总线门面。配 `MUX_RST#`、认 TCA9548A(0x70)、把通道全关。
**必须在 `hal_i2c_init()` 与 `hal_lock_init()` 之后**(i2cbus.h)。
⚠️ 失败**不 abort**:mux 认不到是**运行期**的降级情形(计划 §5.8 —— 判据会
先脉冲 `MUX_RST#` 再决定要不要五路全断),不是固件写错了。这与
`selftest_or_die()` 的分界线始终是同一条:常量表错 = abort,器件不在 = 降级。 */
err = i2cbus_init();
if (err != HAL_OK) {
ESP_LOGE(TAG, "TCA9548A(0x%02x) 初始化失败:%s —— 五路的电压电流这一刻全看不见",
(unsigned)TCA9548A_ADDR, hal_err_str(err));
}
/* ⭐ R2 新增:五根 `PRSNT#` 配成上拉输入并立刻读一遍(`slot_prsnt_init()` 自带
一次 poll)。**必须在 `slot_init()` 之前**:下面要拿它决定哪几槽值得初始化。
⚠️ 这一步失败就 abort:它只可能因为引脚号非法而失败,而那正是
`rail_selftest()` 的 `PINS` 位刚刚查过的东西 —— 走到这里还失败 = 固件写错了。
而且降级的方向是**不安全**的那一侧:`slot_present()` 读不到时返回"在位",
于是空槽也会被允许上电 = 金手指裸露带 12V(计划 §5.8)。 */
err = slot_prsnt_init();
if (err != HAL_OK) {
ESP_LOGE(TAG, "PRSNT# 引脚初始化失败:%s", hal_err_str(err));
abort();
}
/* ⭐⭐ R2 必须有的一行(计划 §5.9):底板输入侧那片 INA226(U13, 0x43)的
`IN+`/`IN−` **在板上是对调接的**(pin9 = `VIN_SW` 上游、pin10 = `+12V` 下游)。
漏了这一行,正常供电时它读回来的电流是负的,SW-8 会把「板子在好好工作」
判成**反灌**,于是一上电就断 VBUS —— 而电压、功率、温度全都正常,
日志里只会看到反复的 `LOG_EV_REVERSE`。
⚠️ 这是纯软件状态(驱动里一个标志位),不碰总线,所以放在认件之前也没关系;
但必须早于 `t_safety` 起来,那才是读它的人。 */
ina226_set_reversed(INA226_ADDR_INPUT, true);
/* ⭐ R2 新增:逐片刀片的 PCA9555 初始化(自带 `i2cbus_xact`,各走各的 mux 通道)。
驱动里那个框保证了顺序 **输出锁存器先清零、方向最后改** —— 反过来的那一刻
P0.0~P0.4 全输出 1 = 三个键一起按下 + EDL 短接 + VBUS 上电(计划 §5.3)。
这一步之后 `VBUS_EN` 才真的由固件说了算。
⚠️ 失败**不 abort**(同 `i2cbus_init()`:这是运行期降级)。代价写在 §5.4:
PCA9555 失联 ⇒ 该路 VBUS **关不掉**,兜底只剩关 `EFUSE_EN`。 */
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
if (!slot_present(rail)) {
/* 空槽不去初始化:那一片器件本来就不在,只会白等五次 NAK。
这一路也不许上电 —— 判据在 `t_safety` 里每圈查(计划 §5.8)。 */
ESP_LOGW(TAG, "槽 %d 没插刀片(PRSNT# 为高):跳过初始化,且不许上电", rail);
continue;
}
const hal_err_t e = slot_init(rail);
if (e != HAL_OK) {
ESP_LOGE(TAG, "槽 %d 的 PCA9555(0x%02x) 初始化失败:%s —— 这一路的 VBUS "
"关不掉了,兜底只剩 EFUSE_EN",
rail, (unsigned)PCA9555_ADDR, hal_err_str(e));
}
}
/* ⚠️ 还没做的:INA226 的 Config/Calibration 与 DAC 的上电值(A15 那一包)。
在那之前 t_safety 读回来的电流恒为 0(Calibration 还没写),
这是预期的,不是故障。 */
/* 执行器:PG 从巡检的快照里取,本任务自己不碰 I²C(t_actuator.h 开头那一段)。
⚠️ 必须在 `spawn(TASK_SAFETY, ...)` 之前:判据要关路时会先 `t_actuator_abort()`,