固件: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 行删除
+117 -70
查看文件
@@ -1,20 +1,33 @@
/* PCA9555(U23,地址 0x20)—— 板上唯一的 I/O 扩展器,**16 个脚全部是输入**。
/* PCA9555 —— **每片刀片上一颗**(`U4`,五片全是 0x20),R2 D9 取代 R1 的
74HC595 链。五片靠底板 TCA9548A 分通道区分(D8),所以从驱动看它们**完全一样**。
挂的是五路 eFuse 的 FLT#/PGOOD、Buck 的线或 PWRGD、六片 INA226 的线或 ALERT,
共 12 个信号 + 4 个直接接地的备用脚(设计文档 §17.2)。
⚠️⚠️ **R1 那版驱动「16 个脚全是输入」的前提在 R2 不成立了。**
R2 的 P0.0~P0.4 是**输出**:三个侧键 + EDL + VBUS 开关。
拿 R1 的记忆改这个文件会在板子上冒烟,原因见下面那个框。
⚠️ **本驱动不提供「把某个脚配成输出」的接口,一个都不提供。**
四个备用脚 P0_7 / P1_5~7 在板上**直接接地、不串限流电阻**(§17.2 的注,
图纸上已标)。而 PCA9555 的 Output Port 寄存器**复位值是 0xFF**——
所以只要把其中一个配成输出,它下一刻就输出高电平,**直接对地短路**。
配错既不会报错也不会 NAK,只会发烫。所以方向字一律走 pca9555_dir_safe(),
不安全直接 abort。这跟 sr595.h 不提供「抬 OE#」是同一条规矩。
位分配从 `.claude/r2/_blade_pcb_nets.json` 的 `U4` 逐脚读回(2026-09-16):
§10.10 的降级行为:这片不应答 = 失去 FAULT / PG 可见性,**不关输出**,
改用 INA226 的电压判据兜底并持续告警。所以本驱动的读失败一律只是返回错误码,
不做任何动作。
Port0 bit0 KEY_PWR bit1 KEY_VUP bit2 KEY_VDN bit3 EDL_EN bit4 VBUS_EN
bit5~7 **直接接地**
Port1 bit0 EFUSE_FLT# bit1 BUCK_PG bit2 INA_ALERT bit3 EFUSE_PG
bit4~7 **直接接地**
本驱动只经 hal_i2c_*,宿主机上由 sim 的假 PCA9555(`sim_pca9555_*`)全部测完。 */
┌─ ⛔⛔ 初始化顺序是这块板子上最危险的一处 ──────────────────────────────┐
│ PCA9555 的 **Output Port 复位值是 0xFF**,Config 复位值也是 0xFF(全输入)。│
│ 所以**必须先把输出锁存器写成 0,最后才改方向**: │
│ OUTPUT0=0 → OUTPUT1=0 → POLINV=0 → CONFIG1=0xFF → CONFIG0=0xE0 │
│ 顺序反过来的那一刻,P0.0~P0.4 一起输出 1 │
│ = 三个键全按下 + EDL 短接 + VBUS 上电,五台手机一起被按住电源键。 │
│ 而 P0.5~7 / P1.4~7 七个脚在刀片上**直接接地、不串限流电阻** —— │
│ 方向写错一位就是对地短路:不报错、不 NAK,只会发烫。 │
│ ⇒ `pca9555_dir_safe()` 是硬闸,走到 false 直接 abort。 │
└──────────────────────────────────────────────────────────────────────────┘
§10.10 的降级行为:某片不应答 = 失去那一路的 FAULT / PG 可见性,**不关输出**,
改用 INA226 的电压判据兜底并持续告警。所以本驱动的读失败一律只返回错误码。
⚠️ R2 新增的代价:这片不应答时**那一路的 VBUS 也控制不了了**(计划 §5.4)。
本驱动只经 hal_i2c_*,**不碰 mux**:通道由调用方(`slot.c`)先选好。 */
#ifndef PCA9555_H
#define PCA9555_H
@@ -27,17 +40,13 @@
extern "C" {
#endif
/* ---- 地址与引脚 ---------------------------------------------------------- */
/* ---- 地址与实例 ---------------------------------------------------------- */
/* A0 = A1 = A2 = GND(U23 的 pin21 / pin2 / pin3,已按 PCB 焊盘网络核过)。 */
/* A0 = A1 = A2 = GND(`U4` 的 pin21 / pin2 / pin3,已按 PCB 焊盘网络核过)。
**五片同址** —— 这是 D8 的全部意义,不是笔误。 */
#define PCA9555_ADDR 0x20
/* `IOEXP_INT#`:开漏、低有效、板上 R608 10 kΩ 上拉,直连 ESP32 IO38(§8.1)。
⚠️ **中断只是加速路径,不是主路径**:§10.3 的 `t_safety` 每 20 ms 无条件轮询
读一次两个输入端口。原因是 PCA9555 的 INT 要**读走输入端口才会释放**,而且
输入自己变回原值时它也会释放 —— 只靠中断的话,漏掉一次读就再也等不到下一个沿。
A6 只留这个宏,读它是 A8/A10 的活。 */
#define PCA9555_PIN_INT_N 38
#define PCA9555_SLOT_COUNT 5
/* ---- 寄存器(全部 8 位) ------------------------------------------------- */
@@ -50,86 +59,124 @@ extern "C" {
#define PCA9555_REG_CONFIG0 0x06
#define PCA9555_REG_CONFIG1 0x07
/* 方向寄存器:**1 = 输入**(复位值 0xFF)。本板全输入,所以只会写这一个值。 */
/* 方向寄存器:**1 = 输入**(复位值 0xFF)。 */
#define PCA9555_DIR_ALL_INPUT 0xFFu
/* 极性反转寄存器必须是 0。写成非 0 的现象是「五路同时报故障」或者更糟的
「五路故障全都看不见」,而 I²C 一切正常 —— init 会写 0 并回读校验。 */
/* 本板的两个方向字。Port0 只有 bit0~4 是输出。 */
#define PCA9555_CONFIG0_INIT 0xE0u
#define PCA9555_CONFIG1_INIT 0xFFu
/* 极性反转必须是 0。写成非 0 的现象是「故障全都看不见」,而 I²C 一切正常。 */
#define PCA9555_POLINV_NONE 0x00u
/* ---- 位分配(从 `.claude/pcb1-parts.json` 的真实焊盘网络逐脚核对) -------
/* ---- Port0:五个输出位 --------------------------------------------------- */
Port0 bit0~4 = EFUSE_FLT1~5 bit5 = BUCK_PG bit6 = INA_ALERT bit7 = 接地
Port1 bit0~4 = EFUSE_PG1~5 bit5~7 = 接地
/* 三个侧键的逻辑键位。**上层一律用这个枚举,不许写 `1 << 0`。**
(R1 时这个枚举在 `sr595.h` 里,链退役之后搬到这里。) */
typedef enum {
PCA9555_KEY_PWR = 0,
PCA9555_KEY_VUP,
PCA9555_KEY_VDN,
PCA9555_KEY_COUNT,
} pca9555_key_t;
这一次两张表**确实是顺排的**(`bit = rail - 1`),和 INA226 地址、595 位号
那两张表不一样。所以这里最容易写出来的错不是「算式」,而是
**把 FLT 和 PG 的端口搞反** —— 两个端口的 bit0~4 长得一模一样,
搞反之后「有故障」会被读成「电源良好」,方向正好相反。
所以取位号一律走下面两个函数,不要在别处写 `1 << (rail - 1)`。 */
#define PCA9555_BIT_KEY_PWR 0
#define PCA9555_BIT_KEY_VUP 1
#define PCA9555_BIT_KEY_VDN 2
#define PCA9555_BIT_EDL_EN 3
#define PCA9555_BIT_VBUS_EN 4
#define PCA9555_RAIL_COUNT 5
#define PCA9555_P0_OUT_MASK 0x1Fu /* bit0~4:唯一允许配成输出的五位 */
#define PCA9555_P0_GND_MASK 0xE0u /* bit5~7 焊死在地上 */
#define PCA9555_BIT_BUCK_PG 5 /* Port0,五路 Buck 的 PWRGD 线或(都是开漏) */
#define PCA9555_BIT_INA_ALERT 6 /* Port0,六片 INA226 的 ALERT 线或 */
/* ---- Port1:四个输入位(全是开漏 + 上拉) -------------------------------- */
/* 接地的备用脚。**永远、永远是输入。** */
#define PCA9555_P0_GND_MASK 0x80u /* P0_7 */
#define PCA9555_P1_GND_MASK 0xE0u /* P1_5 P1_6 P1_7 */
#define PCA9555_BIT_EFUSE_FLT 0
#define PCA9555_BIT_BUCK_PG 1
#define PCA9555_BIT_INA_ALERT 2
#define PCA9555_BIT_EFUSE_PG 3
/* rail 是 **1~5**,越界 abort。返回的是**位号**(0~4),不是掩码。 */
int pca9555_flt_bit(int rail); /* Port0 */
int pca9555_pg_bit(int rail); /* Port1 */
#define PCA9555_P1_IN_MASK 0x0Fu
#define PCA9555_P1_GND_MASK 0xF0u /* bit4~7 焊死在地上 */
/* 12 个信号位互不重复、都没落到接地脚上、FLT 与 PG 分属两个端口。
启动自检调一次(A7,和 ina226_addr_table_ok() / sr595_map_ok() 一起)。 */
/* 键 → Port0 位号。**不带路号**:五片刀片同构,位号与路号无关(计划 §5.7)。
越界 abort。 */
int pca9555_key_bit(pca9555_key_t key);
/* 一路上三个键的掩码(Port0)。`t_actuator` 的「防御性松键」用它。 */
uint8_t pca9555_key_mask(void);
/* 位分配自检:五个输出位互不重复、四个输入位互不重复、
**一个信号都没落到接地脚上**、输出位与输入位分属两个端口。
启动自检调一次(和 `ina226_addr_table_ok()` 一起)。 */
bool pca9555_map_ok(void);
/* 0x20 不与 INA226(0x40/41/44/45/42/43) / DAC63204(0x4A/0x4B) / LM75(0x48) 撞车。 */
/* 0x20 不与主干上的 TCA9548A(0x70) / INA226(0x43) / DAC63204(0x4A/0x4B) 撞车。
⚠️ 刀片上的 INA226 也是 0x40,与 0x20 本来就不同,两者同在一条支路上没问题。 */
bool pca9555_addr_ok(void);
/* dir 里 1 = 输入。返回 false = 这个方向字会把某个**接地脚**配成输出。
/* `dir` 里 1 = 输入。返回 false = 这个方向字会把某个**接地脚**配成输出,
或者把 Port1 上那四个开漏信号源配成输出(与 eFuse 的 FLT# 打架)。
驱动内部走到 false 直接 abort:悄悄放过去的后果是板子发烫,不是功能不好用。 */
bool pca9555_dir_safe(int port, uint8_t dir);
/* ---- 采样 ---------------------------------------------------------------- */
/* ---- 器件操作(当前 mux 通道上那一片) ----------------------------------- */
/* 认件:读回两个方向寄存器,全 0xFF(复位值)才算是它。
不应答 → HAL_ERR_NAK;应答了但方向寄存器不是复位值 → HAL_ERR_BUS。
⚠️ 已经被 `pca9555_init()` 配过的片子会返回 HAL_ERR_BUS —— 这是对的:
probe 的语义是「这是一片刚上电的 PCA9555 吗」。 */
hal_err_t pca9555_probe(void);
/* 按文件头那个框里的顺序初始化,并**逐个回读校验**。
`slot` 是 0~`PCA9555_SLOT_COUNT-1`,**只用来记影子**,不参与总线操作 ——
通道必须由调用方先选好(本驱动不碰 mux)。 */
hal_err_t pca9555_init(int slot);
/* 巡检主路径:**一次事务**读两个输入端口(PCA9555 的读指针在 0x00 / 0x01 之间
toggle,这两个字节是 datasheet 保证的)。 */
typedef struct {
uint8_t port0;
uint8_t port1;
} pca9555_in_t;
/* 认件:读回两个方向寄存器,全 0xFF(复位值)才算是它。
不应答 → HAL_ERR_NAK;应答了但方向寄存器不是复位值 → HAL_ERR_BUS
(这个地址上有东西,但不是一片刚上电的 PCA9555)。 */
hal_err_t pca9555_probe(void);
/* 写方向(全输入)与极性反转(全 0)并**逐个回读校验**,回读不一致 → HAL_ERR_BUS。
⚠️ Output Port 一个字节都不写:写它没有意义(没有输出脚),而写错方向的
后果见文件头。 */
hal_err_t pca9555_init(void);
/* 巡检主路径:**一次事务**读两个输入端口(§10.3 的 0.12 ms 预算就是按一次算的;
PCA9555 的读指针在 0x00 / 0x01 之间 toggle,这两个字节是 datasheet 保证的)。 */
hal_err_t pca9555_read_inputs(pca9555_in_t *out);
/* ---- 语义判读 ------------------------------------------------------------
四个信号**三种极性**,这是本文件第二个容易写反的地方。极性只在下面四个函数里
各出现一次(和 sr595_set_hub_reset() 里那个反相同一个道理:多一处就多一处写反
的机会)。上层一律问「有没有故障」,不许自己去 & 位。 */
/* ---- Port0 输出:影子 + 唯一写入点 --------------------------------------- */
/* TPS25940A 的 `FLT#`:开漏、板上 R113 47 kΩ 上拉 ⇒ **低 = 有故障**。 */
bool pca9555_rail_fault(const pca9555_in_t *in, int rail);
/* 改 `mask` 指定的那几位为 `value` 里的值,其余位保持影子里的值,整字节写下去。
组合键的两位落在**同一次写**里(§10.6:分两次写会先单按一个键)。
/* TPS25940A 的 `PGOOD`:开漏、R115 47 kΩ 上拉,输出 2.97 V 以上才释放
⇒ **高 = 电源良好**。 */
bool pca9555_rail_pg(const pca9555_in_t *in, int rail);
⚠️ **`slot` 必须与当前 mux 通道对应的那一路一致。** 驱动没法自己验证这件事,
传错的后果是影子记在了另一片上 —— 下一次写会把这一片的键"顺手"改掉。
这就是 `slot.c` 存在的理由:别在 `slot.c` 之外调这个函数。
/* 五路 Buck 的 `PWRGD` 线或(开漏)⇒ **高 = 五路都正常**;低只说明「有某一路
不正常」,看不出是哪一路(§16.1)。 */
`mask` 里出现输出位以外的位 → HAL_ERR_ARG(不是 abort:那可能是上层算错了
掩码,返回错误码能让它记一条事件)。写失败时**影子不更新**。 */
hal_err_t pca9555_write_out0(int slot, uint8_t mask, uint8_t value);
uint8_t pca9555_shadow(int slot); /* 该片 Port0 输出锁存器的影子(init 后是 0) */
/* 把影子全部清成 0,**不碰总线**。给测试的 setUp() 与 A15 的降级路径用
(「现在生效的是上电默认值」)。 */
void pca9555_shadow_reset(void);
/* ---- 语义判读(Port1) ---------------------------------------------------
四个信号**两种极性**,这是本文件第二个容易写反的地方。极性只在下面四个函数里
各出现一次。上层一律问「有没有故障」,不许自己去 & 位。 */
/* TPS25940A 的 `FLT#`:开漏、上拉 ⇒ **低 = 有故障**。 */
bool pca9555_efuse_fault(const pca9555_in_t *in);
/* TPS25940A 的 `PGOOD`:开漏、上拉,输出过 2.97 V 才释放 ⇒ **高 = 电源良好**。 */
bool pca9555_efuse_pg(const pca9555_in_t *in);
/* 本路 Buck 的 `PWRGD` ⇒ 高 = 正常。
⚠️ R1 里这一位是**五路线或**,R2 里它只代表**本路**(一片刀片一个 Buck)。 */
bool pca9555_buck_pg(const pca9555_in_t *in);
/* 六片 INA226 的 `ALERT` 线或:开漏、低有效 ⇒ **低 = 有片子在告警**。 */
/* 本片 INA226 的 `ALERT`:开漏、低有效 ⇒ **低 = 在告警**。
⚠️ R1 里是六片线或,R2 里刀片上的 ALERT 不跨板,只进本片的 P1.2。 */
bool pca9555_ina_alert(const pca9555_in_t *in);
#ifdef __cplusplus