固件: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 行删除
+73
查看文件
@@ -0,0 +1,73 @@
/* CH224A —— 底板的 USB-PD 受电芯片(`U36`,R2 方案 D16)。
板上它只干一件事:用 `R7` 120 k 这一颗电阻**硬件**请求 20 V,
降压 `U37` 再把 20 V 变成 12 V 母线。固件**不参与协商**,只做一件事:
**读出当前这个充电器在 20 V 档最多能给多少电流**,换算成整板功率上限。
---- 为什么需要这个上限(计划 §5.9) ------------------------------------
R2 的输入从 DC 头改成了 USB-C PD。单口上限 20 V × 5 A = 100 W(5 A 还要
e-marker 线;普通 3 A 线只有 60 W),而整板最坏工况约 108 W。
常态 10~25 W 怎么都够,但**最坏工况要靠固件限功率**(R1 §8.3 的降载)。
充电器插的是哪一个,只有运行时才知道 —— 就是读这颗芯片。
┌─ ⛔ 一条禁令 ────────────────────────────────────────────────────────────┐
│ **本驱动不提供写 `0x0A` 的接口,一个都不提供。** │
│ `0x0A = 5` 是「请求 28 V EPR」。板上 `D3` 是 SMBJ22A(VRWM 22 V), │
│ `U37` 的 EN 分压按 20 V 设计 —— 28 V 会把 TVS 打到雪崩。 │
│ 这跟 `sr595.h` 不提供「抬 OE#」、`pca9555.h` 不提供「把接地脚配成输出」 │
│ 是同一条规矩:**能烧板子的动作不给接口,而不是给了再叮嘱别用。** │
└──────────────────────────────────────────────────────────────────────────┘
挂在 TCA9548A 的**通道 5**(`TCA9548A_CH_PD`)上,不在主干。 */
#ifndef CH224A_H
#define CH224A_H
#include <stdbool.h>
#include <stdint.h>
#include "hal_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* ⚠️⚠️ **这个地址还没有被实物证实**(计划 §11 的待定项):
R2 §36.2 只写了寄存器号,没写从机地址。所以:
· 所有接口都**收一个 addr 参数**,不写死;
· B1 上板扫描时把通道 5 上扫到的那个地址填回这里,并同步改计划 §5.2。
在那之前,用这个值跑出来的「扫不到」是**预期结果**,不是固件 bug。 */
#define CH224A_ADDR_DEFAULT 0x23
/* 寄存器(R2 §36.2)。 */
#define CH224A_REG_STATUS 0x09 /* 协商状态 / 当前档位 */
#define CH224A_REG_CURRENT 0x50 /* 当前档位的最大电流,**单位 50 mA** */
/* 板上硬件固定请求的档位。`R7` 120 k 一颗电阻定死,固件改不了也不该改。 */
#define CH224A_FIXED_MV 20000
/* 0x50 那个字节 → mA。 */
uint32_t ch224a_ma_from_reg(uint8_t reg);
/* mA → 整板可用功率(mW),按 20 V 档算。
60 W / 65 W / 100 W 三档在这里分别是 3000 / 3250 / 5000 mA。 */
uint32_t ch224a_power_limit_mw(uint32_t ma);
/* 地址不与主干上的器件撞车。⚠️ 它挂在通道 5 上,与刀片支路的 0x40 / 0x20
本来就不冲突,这里查的是「万一它和主干某个器件同址」——
那种情况下选到通道 5 时主干那位会被它盖住。 */
bool ch224a_addr_ok(uint8_t addr);
/* 认件:读状态寄存器。不应答 → HAL_ERR_NAK。
⚠️ **不校验内容**:这颗芯片没有 ID 寄存器,状态字的每一个值都是合法的。 */
hal_err_t ch224a_probe(uint8_t addr);
hal_err_t ch224a_read_status(uint8_t addr, uint8_t *status);
/* 当前档位最大电流(mA)。读失败时 `*ma` 不动。 */
hal_err_t ch224a_read_max_ma(uint8_t addr, uint32_t *ma);
#ifdef __cplusplus
}
#endif
#endif /* CH224A_H */
+38 -10
查看文件
@@ -47,20 +47,48 @@ extern "C" {
不是任何一种错误。tasks.c 的表自检拿它和 20 ms 对一次。 */
#define INA226_CONV_US 8800
/* ---- 地址表(设计文档 §8.2) ------------------------------------------- */
/* ---- 地址表(**R2**:五片同址 + 输入侧一片,计划 §5.2) -----------------
#define INA226_RAIL_COUNT 5
#define INA226_COUNT 6
#define INA226_ADDR_INPUT 0x43 /* U13,输入侧 V+I,整板功率预算用 */
⚠️⚠️ **R1 的「六片六个地址」在 R2 不成立了。** R2 的五片 INA226 各在一片刀片
上(`U3`),五片刀片完全同构 ⇒ **地址全是 0x40**,靠底板 TCA9548A 的通道区分
(方案 D8)。板上出处:`.claude/r2/_blade_pcb_nets.json` 的 `U3` pin1/pin2
(A1/A0)都接 GND。
/* CH1~CH5 → U8~U12 的地址。rail 是 1~5,越界直接 abort。
⚠️ **`0x40 + ch` 是错的**,CH3/CH4/CH5 三路全错(0x44 / 0x45 / 0x42)。
地址按布线难度重排过(§8.2 那张 A1/A0 表),永远查表,不要算。 */
于是「查表不要算」这条规矩换了对象:**要查的不再是地址,而是 mux 通道**
(`rail_cfg.h` 的 `mux_ch`)。选错通道读到的是另一台手机的电压电流,
而量纲、范围、变化趋势全都正常 —— 比 R1 的「地址算错」更难发现。 */
#define INA226_RAIL_COUNT 5
#define INA226_COUNT 6 /* 物理片数:五片刀片 + 一片输入侧 */
/* 五片刀片上那一片的地址(**五片相同**)。 */
#define INA226_ADDR_RAIL 0x40
/* U13,底板输入侧 V+I,整板功率预算与 PD 限功率用(计划 §5.9)。
挂**主干**,不在任何 mux 通道后面。R2 底板上 Kelvin 对调接,见 ina226_set_reversed()。 */
#define INA226_ADDR_INPUT 0x43
/* 扫描用:总线上出现过的 INA226 地址只有这两个(不是六个)。 */
#define INA226_ADDR_COUNT 2
/* 登记「这片的 IN+ / IN− 在板上对调接了」(IN+ 接分流电阻下游):之后驱动对它的
分流电压 / 电流取反,调用方看到的永远是「正 = 正常供电方向」。母线电压 / 功率不受影响。
默认全部正接(R1)。R2 底板的 U13(0x43)为了让两条 Kelvin 在 PCB 上不交叉而对调
(2026-09-11),R2 固件启动时必须调 ina226_set_reversed(INA226_ADDR_INPUT, true),
否则 SW-8 反灌判据会把正常供电当成反灌。地址不在 0x40~0x4F 的调用被忽略。 */
void ina226_set_reversed(uint8_t addr, bool reversed);
bool ina226_is_reversed(uint8_t addr);
/* CH1~CH5 那一片的地址。rail 是 1~5,越界直接 abort。
⚠️ **R2 里它对五路返回同一个值(0x40)**,这不是 bug。
真正区分是哪一路的是 `rail_cfg(rail)->mux_ch` —— 光有地址是选不中的,
所以任何「拿到地址就直接读」的代码在 R2 上都读的是**上一次选中的那一路**。 */
uint8_t ina226_rail_addr(int rail);
extern const uint8_t ina226_all_addr[INA226_COUNT]; /* CH1..CH5 + 输入侧,扫描用 */
extern const uint8_t ina226_addr_list[INA226_ADDR_COUNT]; /* {0x40, 0x43},扫描用 */
/* 六个地址互不重复、且不与 DAC63204(0x4A/0x4B) / PCA9555(0x20) / LM75(0x48) 撞车。
/* 两个地址不重复、都合法、且不与主干上的 DAC63204(0x4A/0x4B) / TCA9548A(0x70)
撞车,也不与刀片支路上的 PCA9555(0x20) 撞车。
启动自检调一次;返回 false 说明表被改坏了。 */
bool ina226_addr_table_ok(void);
@@ -68,7 +96,7 @@ bool ina226_addr_table_ok(void);
typedef struct {
int32_t bus_mv; /* 母线电压,1.25 mV/LSB */
int32_t current_ua; /* **有符号** —— 反灌时为负(SW-8 靠这个判据) */
int32_t current_ua; /* **有符号** —— 反灌时为负(SW-8 靠这个判据);对调接的片子已由驱动取反 */
} ina226_vi_t;
typedef struct {
+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
+145 -133
查看文件
@@ -1,133 +1,145 @@
/* 74HC595 ×3 —— 一条 24 位移位链,带全固件唯一的影子寄存器(需求 §10.6)。
链上挂着 **15 路侧键 + 5 路 EDL_EN + HUB_RST**,共 21 位有效、3 位空脚。
写任何一位都要把整条链重发一遍,所以:
⚠️ **内存里只有一份 24 位影子值,只有本文件的 `sr595_flush()` 会碰移位链。**
任何任务改按键 / EDL 都是「`sr595_modify()` 改影子 → `sr595_flush()` 请求刷新」。
多处直接操作移位链的症状是**另一台手机的按键被顺手清掉**,而且要两个动作
正好撞在一起才复现 —— 属于会拖很久的那类 bug。
`RCLK` 打点之前输出不变 ⇒ 整帧刷新是原子的,组合键(VOL− + PWR)必须
**一次 `sr595_write()` 两位一起给**,不能先后两次。
⚠️ **不许用拉高 `OE#` 的方式「临时全松开」**(需求 §10.6):那会让 15 个栅极
同时进高阻,靠的是每个栅极自己的 100 kΩ 下拉 —— 那是**复位路径**,不是正常
控制路径。所以本头文件根本不提供「抬 OE#」的接口。
本驱动只经 hal_gpio_*,宿主机上由 sim 的移位链观测器(`sim_sr595_*`)全部测完。 */
#ifndef SR595_H
#define SR595_H
#include <stdbool.h>
#include <stdint.h>
#include "hal_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* ---- 四根线(ESP32-S3 引脚,设计文档 §8.1 的表,§26.18 重排后) --------- */
#define SR595_PIN_SER 13 /* IO13 模组脚 21 → U21.DS(14) */
#define SR595_PIN_SRCLK 12 /* IO12 模组脚 20 → 三片的 SRCLK(11) */
#define SR595_PIN_RCLK 9 /* IO9 模组脚 17 → 三片的 RCLK(12) */
#define SR595_PIN_OE_N 21 /* IO21 模组脚 23 → 三片的 OE#(13),板上另有 R609 100 kΩ 上拉 */
/* `MR#`(10) 三片都直接接 +3V3 —— 没有复位线,上电后移位寄存器内容是**不确定**的。
所以 `sr595_init()` 必须先把安全影子锁存进去、**再**驱动 `OE#`。 */
/* ---- 24 位的编号 --------------------------------------------------------- */
/* 位号 = **链上的物理位置**:0 = U21.QA(离 ESP32 最近的第一级),23 = U33.QH(链尾)。
一次移位 = 每一级往编号大的方向走一格,所以**先移入的位停在编号最大处**。 */
#define SR595_CHIP_COUNT 3
#define SR595_BITS 24
#define SR595_MASK_ALL ((uint32_t)0x00FFFFFFu)
#define SR595_CHIP_U21 0 /* 影子的 bit0~7 ;P3B 页,板上 y≈50(中)*/
#define SR595_CHIP_U22 1 /* 影子的 bit8~15 ;P3B 页,板上 y≈35(南)*/
#define SR595_CHIP_U33 2 /* 影子的 bit16~23;**P6 页**,板上 y≈70(北)*/
/* 位分配(**2026-09-06 按 PCB 几何重排**,设计文档 §26.18 C;
此处的值是从 `.claude/pcb1-parts.json` 的真实焊盘网络逐脚核对出来的):
U21 QA=EDL_EN4 QB=K1_PWR QC=K1_VUP QD=K1_VDN QE=空 QF=EDL_EN5 QG=EDL_EN3 QH=EDL_EN2
U22 QA=空 QB=K4_PWR QC=K4_VUP QD=K4_VDN QE=空 QF=K5_VDN QG=K5_VUP QH=K5_PWR
U33 QA=EDL_EN1 QB=K2_VUP QC=K2_VDN QD=K2_PWR QE=K3_VDN QF=K3_VUP QG=K3_PWR QH=HUB_RST
⚠️ **「K1/K2/K3 顺排在 U21、U33 是 EDL 专用片」是旧版**(设计文档 §17.2 的
09-04 那张表),15 路里有 12 路对不上。三片本来就是一条链,谁在哪一位只是移位
的字节序 ⇒ **搞错了板子照样正常工作,只是按 CH1 的电源键按到了 CH4 的音量键**,
功能测试完全测不出来。查表,不要算。 */
#define SR595_BIT_HUB_RST 23
/* 空脚(U21.QE / U22.QA / U22.QE)。写它们没有意义,`sr595_modify()` 直接 abort。 */
#define SR595_MASK_UNUSED (((uint32_t)1u << 4) | ((uint32_t)1u << 8) | ((uint32_t)1u << 12))
#define SR595_MASK_VALID (SR595_MASK_ALL & ~SR595_MASK_UNUSED)
typedef enum {
SR595_KEY_PWR = 0,
SR595_KEY_VUP,
SR595_KEY_VDN,
SR595_KEY_COUNT,
} sr595_key_t;
/* rail 是 **1~5**,key 越界或 rail 越界一律 abort(那是调用方算错了位号)。 */
int sr595_key_bit(int rail, sr595_key_t key);
uint32_t sr595_key_mask(int rail); /* 该路三键的掩码,「一路全松开」用得上 */
int sr595_edl_bit(int rail);
/* 15 路侧键的总掩码。安全巡检与「全松开」用。 */
uint32_t sr595_all_keys_mask(void);
/* 21 个有效位互不重复、都落在 SR595_MASK_VALID 里、空脚一个都没被占。
启动自检调一次(A7);返回 false = 位表被改坏了。 */
bool sr595_map_ok(void);
/* ---- 上电安全影子 -------------------------------------------------------- */
/* ⚠️ **`HUB_RST` 是低有效**(USB2517I 的 `RESET_N`(43),设计文档 §21.7,
板上 R716 10 kΩ 上拉)。影子位 = 1 才是「Hub 正常跑」,= 0 是**按住复位**。
全 0 的影子会把 Hub 一直摁在复位里 —— 现象是「插上枚举不出来」,
而五路手机的电压电流全都正常,看起来一点毛病没有。
所以上电安全值不是 0,是这个: */
#define SR595_SHADOW_SAFE ((uint32_t)1u << SR595_BIT_HUB_RST)
/* 复位是低有效,反相只在 `sr595_set_hub_reset()` 里出现这一次
(和 `rail_mv_to_code()` 里那个减法同一个道理:多一处就多一处写反的机会)。 */
void sr595_set_hub_reset(bool in_reset);
/* ---- 器件操作 ------------------------------------------------------------ */
/* 三步,顺序不能换(上电时移位寄存器内容不确定,`MR#` 又接死在 +3V3):
① SER / SRCLK / RCLK 配成输出并置低;**`OE#` 这时还是输入**,
靠板上 R609 上拉着 ⇒ 三片输出保持高阻,15 个栅极被各自的 100 kΩ 拉低
② 把 `SR595_SHADOW_SAFE` 移进去并锁存
③ 这才把 `OE#` 配成输出并拉低,输出使能
②③ 换个个儿 = 上电瞬间把移位寄存器里的随机值放到 15 个栅极上,
也就是**五台手机的电源键可能一起被按下去**。 */
hal_err_t sr595_init(void);
uint32_t sr595_shadow(void); /* 当前影子(未必已经刷下去,看 sr595_dirty()) */
bool sr595_dirty(void); /* 影子和链上的值不一致 */
/* 影子 = (影子 & ~mask) | (value & mask),并置脏。**不碰总线**。
mask 越出 SR595_MASK_VALID(含碰到空脚)直接 abort。 */
void sr595_modify(uint32_t mask, uint32_t value);
/* 把影子整帧发下去:U33 字节 → U22 字节 → U21 字节,每个字节 QH 先发,
最后一个 RCLK 上升沿统一生效。不脏就直接返回 HAL_OK(不空发一帧)。
没 init 过 → HAL_ERR_STATE。 */
hal_err_t sr595_flush(void);
/* `sr595_modify()` + `sr595_flush()`。**组合键必须走它**——
VOL− + PWR 要落在同一帧里,分两次写会先单按一个键。 */
hal_err_t sr595_write(uint32_t mask, uint32_t value);
#ifdef __cplusplus
}
#endif
#endif /* SR595_H */
/* ⛔⛔ **R2 已退役:这条 24 位链在现在的板子上不存在。**
(2026-09-16。R2 方案 D9 用「每片刀片一颗 PCA9555」取代了它,
少 4 根跨板信号,而且复位态天然安全 —— PCA9555 上电是高阻输入,
配栅极 100 k 下拉 ⇒ 按键必然松开;595 那套要靠 `KEY_OE#` 上拉才做得到同一件事。)
文件和测试**保留**(它自洽、测试全绿、不碰任何板级事实),但:
· 没有任何生产代码 include 它,`rail_cfg` / `t_actuator` 都已经改走 `pca9555.h`;
· 下面那些 `SR595_PIN_*` 的 GPIO 号是 **R1 底板**的,与 R2 的 `board_pins.h` 冲突
(IO9/12/13/21 在 R2 上分别是 EFUSE_EN1/EN4/EN5 与 FAN_TACH_IN)——
**别拿它们去查重、也别照着改 board_pins.h**;
· 三个逻辑键位(PWR/VUP/VDN)已经搬到 `pca9555.h` 的 `pca9555_key_t`。
要改 R2 的按键,去 `pca9555.h` 与 `main/slot.h`,不要动这个文件。 */
/* 74HC595 ×3 —— 一条 24 位移位链,带全固件唯一的影子寄存器(需求 §10.6)。
链上挂着 **15 路侧键 + 5 路 EDL_EN + HUB_RST**,共 21 位有效、3 位空脚。
写任何一位都要把整条链重发一遍,所以:
⚠️ **内存里只有一份 24 位影子值,只有本文件的 `sr595_flush()` 会碰移位链。**
任何任务改按键 / EDL 都是「`sr595_modify()` 改影子 → `sr595_flush()` 请求刷新」。
多处直接操作移位链的症状是**另一台手机的按键被顺手清掉**,而且要两个动作
正好撞在一起才复现 —— 属于会拖很久的那类 bug。
`RCLK` 打点之前输出不变 ⇒ 整帧刷新是原子的,组合键(VOL− + PWR)必须
**一次 `sr595_write()` 两位一起给**,不能先后两次。
⚠️ **不许用拉高 `OE#` 的方式「临时全松开」**(需求 §10.6):那会让 15 个栅极
同时进高阻,靠的是每个栅极自己的 100 kΩ 下拉 —— 那是**复位路径**,不是正常
控制路径。所以本头文件根本不提供「抬 OE#」的接口。
本驱动只经 hal_gpio_*,宿主机上由 sim 的移位链观测器(`sim_sr595_*`)全部测完。 */
#ifndef SR595_H
#define SR595_H
#include <stdbool.h>
#include <stdint.h>
#include "hal_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* ---- 四根线(ESP32-S3 引脚,设计文档 §8.1 的表,§26.18 重排后) --------- */
#define SR595_PIN_SER 13 /* IO13 模组脚 21 → U21.DS(14) */
#define SR595_PIN_SRCLK 12 /* IO12 模组脚 20 → 三片的 SRCLK(11) */
#define SR595_PIN_RCLK 9 /* IO9 模组脚 17 → 三片的 RCLK(12) */
#define SR595_PIN_OE_N 21 /* IO21 模组脚 23 → 三片的 OE#(13),板上另有 R609 100 kΩ 上拉 */
/* `MR#`(10) 三片都直接接 +3V3 —— 没有复位线,上电后移位寄存器内容是**不确定**的。
所以 `sr595_init()` 必须先把安全影子锁存进去、**再**驱动 `OE#`。 */
/* ---- 24 位的编号 --------------------------------------------------------- */
/* 位号 = **链上的物理位置**:0 = U21.QA(离 ESP32 最近的第一级),23 = U33.QH(链尾)。
一次移位 = 每一级往编号大的方向走一格,所以**先移入的位停在编号最大处**。 */
#define SR595_CHIP_COUNT 3
#define SR595_BITS 24
#define SR595_MASK_ALL ((uint32_t)0x00FFFFFFu)
#define SR595_CHIP_U21 0 /* 影子的 bit0~7 ;P3B 页,板上 y≈50(中)*/
#define SR595_CHIP_U22 1 /* 影子的 bit8~15 ;P3B 页,板上 y≈35(南)*/
#define SR595_CHIP_U33 2 /* 影子的 bit16~23;**P6 页**,板上 y≈70(北)*/
/* 位分配(**2026-09-06 按 PCB 几何重排**,设计文档 §26.18 C;
此处的值是从 `.claude/pcb1-parts.json` 的真实焊盘网络逐脚核对出来的):
U21 QA=EDL_EN4 QB=K1_PWR QC=K1_VUP QD=K1_VDN QE=空 QF=EDL_EN5 QG=EDL_EN3 QH=EDL_EN2
U22 QA=空 QB=K4_PWR QC=K4_VUP QD=K4_VDN QE=空 QF=K5_VDN QG=K5_VUP QH=K5_PWR
U33 QA=EDL_EN1 QB=K2_VUP QC=K2_VDN QD=K2_PWR QE=K3_VDN QF=K3_VUP QG=K3_PWR QH=HUB_RST
⚠️ **「K1/K2/K3 顺排在 U21、U33 是 EDL 专用片」是旧版**(设计文档 §17.2 的
09-04 那张表),15 路里有 12 路对不上。三片本来就是一条链,谁在哪一位只是移位
的字节序 ⇒ **搞错了板子照样正常工作,只是按 CH1 的电源键按到了 CH4 的音量键**,
功能测试完全测不出来。查表,不要算。 */
#define SR595_BIT_HUB_RST 23
/* 空脚(U21.QE / U22.QA / U22.QE)。写它们没有意义,`sr595_modify()` 直接 abort。 */
#define SR595_MASK_UNUSED (((uint32_t)1u << 4) | ((uint32_t)1u << 8) | ((uint32_t)1u << 12))
#define SR595_MASK_VALID (SR595_MASK_ALL & ~SR595_MASK_UNUSED)
typedef enum {
SR595_KEY_PWR = 0,
SR595_KEY_VUP,
SR595_KEY_VDN,
SR595_KEY_COUNT,
} sr595_key_t;
/* rail 是 **1~5**,key 越界或 rail 越界一律 abort(那是调用方算错了位号)。 */
int sr595_key_bit(int rail, sr595_key_t key);
uint32_t sr595_key_mask(int rail); /* 该路三键的掩码,「一路全松开」用得上 */
int sr595_edl_bit(int rail);
/* 15 路侧键的总掩码。安全巡检与「全松开」用。 */
uint32_t sr595_all_keys_mask(void);
/* 21 个有效位互不重复、都落在 SR595_MASK_VALID 里、空脚一个都没被占。
启动自检调一次(A7);返回 false = 位表被改坏了。 */
bool sr595_map_ok(void);
/* ---- 上电安全影子 -------------------------------------------------------- */
/* ⚠️ **`HUB_RST` 是低有效**(USB2517I 的 `RESET_N`(43),设计文档 §21.7,
板上 R716 10 kΩ 上拉)。影子位 = 1 才是「Hub 正常跑」,= 0 是**按住复位**。
全 0 的影子会把 Hub 一直摁在复位里 —— 现象是「插上枚举不出来」,
而五路手机的电压电流全都正常,看起来一点毛病没有。
所以上电安全值不是 0,是这个: */
#define SR595_SHADOW_SAFE ((uint32_t)1u << SR595_BIT_HUB_RST)
/* 复位是低有效,反相只在 `sr595_set_hub_reset()` 里出现这一次
(和 `rail_mv_to_code()` 里那个减法同一个道理:多一处就多一处写反的机会)。 */
void sr595_set_hub_reset(bool in_reset);
/* ---- 器件操作 ------------------------------------------------------------ */
/* 三步,顺序不能换(上电时移位寄存器内容不确定,`MR#` 又接死在 +3V3):
① SER / SRCLK / RCLK 配成输出并置低;**`OE#` 这时还是输入**,
靠板上 R609 上拉着 ⇒ 三片输出保持高阻,15 个栅极被各自的 100 kΩ 拉低
② 把 `SR595_SHADOW_SAFE` 移进去并锁存
③ 这才把 `OE#` 配成输出并拉低,输出使能
②③ 换个个儿 = 上电瞬间把移位寄存器里的随机值放到 15 个栅极上,
也就是**五台手机的电源键可能一起被按下去**。 */
hal_err_t sr595_init(void);
uint32_t sr595_shadow(void); /* 当前影子(未必已经刷下去,看 sr595_dirty()) */
bool sr595_dirty(void); /* 影子和链上的值不一致 */
/* 影子 = (影子 & ~mask) | (value & mask),并置脏。**不碰总线**。
mask 越出 SR595_MASK_VALID(含碰到空脚)直接 abort。 */
void sr595_modify(uint32_t mask, uint32_t value);
/* 把影子整帧发下去:U33 字节 → U22 字节 → U21 字节,每个字节 QH 先发,
最后一个 RCLK 上升沿统一生效。不脏就直接返回 HAL_OK(不空发一帧)。
没 init 过 → HAL_ERR_STATE。 */
hal_err_t sr595_flush(void);
/* `sr595_modify()` + `sr595_flush()`。**组合键必须走它**——
VOL− + PWR 要落在同一帧里,分两次写会先单按一个键。 */
hal_err_t sr595_write(uint32_t mask, uint32_t value);
#ifdef __cplusplus
}
#endif
#endif /* SR595_H */
@@ -0,0 +1,79 @@
/* TCA9548A —— 底板上的 I²C 八路多路器(U35,0x70),R2 新增(方案 D8)。
它的存在让五片刀片可以**完全同构**:每片上都是 0x40 的 INA226 + 0x20 的
PCA9555,零 strap 电阻、五片可互换。代价是**每一次访问刀片都先得选通道**。
---- 这颗器件的三件反直觉的事 --------------------------------------------
1. **它没有寄存器地址。** 整个器件只有一个 8 位控制寄存器,写就是「发一个
字节」、读就是「收一个字节」,**不带寄存器指针**。所以这里用的是
`hal_i2c_write(addr, &byte, 1)` 而不是常见的「写 reg + 写 val」。
按后者写会把通道选成 `reg` 那个字节的值 —— 而且**不会报错**。
2. **控制字是掩码不是编号。** bit n = 1 表示通道 n 接通,**可以同时开多路**。
同时开两路的后果是两片同地址的 INA226 一起应答,读回来的是两片的**线与**
(量纲正常、数值偏小、还很稳)。所以本驱动只接受「恰好一路」或「全关」,
多路那种用法在这块板上永远是 bug。
3. **`RESET#` 只复位它自己**(把通道全关),不影响挂在支路上的器件。
复位脉冲很短(datasheet 最小 6 ns),但复位之后**通道是全关的** ——
所以调用方的通道影子必须一起作废,见 `i2cbus.h`。
⚠️ 本驱动**不做加锁,也不管影子**。「选通道 + 事务」的原子性是 `i2cbus`
那一层的事(计划 §4.3)—— 混在这里会让驱动没法单独测。 */
#ifndef TCA9548A_H
#define TCA9548A_H
#include <stdbool.h>
#include <stdint.h>
#include "hal_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* A0 = A1 = A2 = GND(U35 的 pin1 / pin2 / pin21,已按 PCB 焊盘网络核过)。 */
#define TCA9548A_ADDR 0x70
#define TCA9548A_CH_COUNT 8
/* 全关。`tca9548a_select()` 传这个值 = 断开所有支路,只剩主干可见
(主干上是 DAC63204 ×2 + 输入侧 INA226 + CH224A 以外的东西)。 */
#define TCA9548A_CH_NONE (-1)
/* 通道 → 去哪(`.claude/r2/_base_pcb_nets.json` 的 `U35` 逐脚读回,2026-09-16)。
⚠️ 槽 1~5 这次**确实是顺排的**,但通道号仍然从 `rail_cfg` 那张表里查 ——
顺排是当下的事实,不是不变量。 */
#define TCA9548A_CH_SLOT1 0
#define TCA9548A_CH_SLOT5 4
#define TCA9548A_CH_PD 5 /* SDA6/SCL6 → U36 CH224A(PD 受电芯片) */
/* 通道 6 / 7 悬空。 */
/* 0x70 不与 INA226(0x40/0x43) / DAC63204(0x4A/0x4B) / PCA9555(0x20) 撞车。
启动自检调一次。 */
bool tca9548a_addr_ok(void);
/* 选通道。`ch` 是 0~7 或 `TCA9548A_CH_NONE`;其它值 → HAL_ERR_ARG。
**一次只开一路**(见文件头第 2 条)。 */
hal_err_t tca9548a_select(uint8_t addr, int ch);
/* 读回控制字(一个字节,不带寄存器指针)。
用来验证「选通道真的生效了」—— 写下去不报错但没生效是这颗器件最难查的失效
(比如上拉太弱、支路被某个器件拉死)。 */
hal_err_t tca9548a_read_mask(uint8_t addr, uint8_t *mask);
/* 认件:全关之后读回控制字,必须是 0。
不应答 → HAL_ERR_NAK;应答了但读回不是 0 → HAL_ERR_BUS(地址上有东西,
但它不是一颗听话的 TCA9548A)。 */
hal_err_t tca9548a_probe(uint8_t addr);
/* 掩码 → 通道号。恰好一位才返回 0~7;全 0 返回 `TCA9548A_CH_NONE`;
**多于一位返回 -2**(见文件头第 2 条:那是 bug,不是一种用法)。 */
int tca9548a_mask_to_ch(uint8_t mask);
#ifdef __cplusplus
}
#endif
#endif /* TCA9548A_H */