/* PCA9555 —— **每片刀片上一颗**(`U4`,五片全是 0x20),R2 D9 取代 R1 的 74HC595 链。五片靠底板 TCA9548A 分通道区分(D8),所以从驱动看它们**完全一样**。 ⚠️⚠️ **R1 那版驱动「16 个脚全是输入」的前提在 R2 不成立了。** R2 的 P0.0~P0.4 是**输出**:三个侧键 + EDL + VBUS 开关。 拿 R1 的记忆改这个文件会在板子上冒烟,原因见下面那个框。 位分配从 `.claude/r2/_blade_pcb_nets.json` 的 `U4` 逐脚读回(2026-09-16): 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 **直接接地** ┌─ ⛔⛔ 初始化顺序是这块板子上最危险的一处 ──────────────────────────────┐ │ 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 #include #include #include "hal_err.h" #ifdef __cplusplus extern "C" { #endif /* ---- 地址与实例 ---------------------------------------------------------- */ /* A0 = A1 = A2 = GND(`U4` 的 pin21 / pin2 / pin3,已按 PCB 焊盘网络核过)。 **五片同址** —— 这是 D8 的全部意义,不是笔误。 */ #define PCA9555_ADDR 0x20 #define PCA9555_SLOT_COUNT 5 /* ---- 寄存器(全部 8 位) ------------------------------------------------- */ #define PCA9555_REG_INPUT0 0x00 #define PCA9555_REG_INPUT1 0x01 #define PCA9555_REG_OUTPUT0 0x02 #define PCA9555_REG_OUTPUT1 0x03 #define PCA9555_REG_POLINV0 0x04 #define PCA9555_REG_POLINV1 0x05 #define PCA9555_REG_CONFIG0 0x06 #define PCA9555_REG_CONFIG1 0x07 /* 方向寄存器:**1 = 输入**(复位值 0xFF)。 */ #define PCA9555_DIR_ALL_INPUT 0xFFu /* 本板的两个方向字。Port0 只有 bit0~4 是输出。 */ #define PCA9555_CONFIG0_INIT 0xE0u #define PCA9555_CONFIG1_INIT 0xFFu /* 极性反转必须是 0。写成非 0 的现象是「故障全都看不见」,而 I²C 一切正常。 */ #define PCA9555_POLINV_NONE 0x00u /* ---- Port0:五个输出位 --------------------------------------------------- */ /* 三个侧键的逻辑键位。**上层一律用这个枚举,不许写 `1 << 0`。** (R1 时这个枚举在 `sr595.h` 里,链退役之后搬到这里。) */ typedef enum { PCA9555_KEY_PWR = 0, PCA9555_KEY_VUP, PCA9555_KEY_VDN, PCA9555_KEY_COUNT, } pca9555_key_t; #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_P0_OUT_MASK 0x1Fu /* bit0~4:唯一允许配成输出的五位 */ #define PCA9555_P0_GND_MASK 0xE0u /* bit5~7 焊死在地上 */ /* ---- Port1:四个输入位(全是开漏 + 上拉) -------------------------------- */ #define PCA9555_BIT_EFUSE_FLT 0 #define PCA9555_BIT_BUCK_PG 1 #define PCA9555_BIT_INA_ALERT 2 #define PCA9555_BIT_EFUSE_PG 3 #define PCA9555_P1_IN_MASK 0x0Fu #define PCA9555_P1_GND_MASK 0xF0u /* bit4~7 焊死在地上 */ /* 键 → 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 不与主干上的 TCA9548A(0x70) / INA226(0x43) / DAC63204(0x4A/0x4B) 撞车。 ⚠️ 刀片上的 INA226 也是 0x40,与 0x20 本来就不同,两者同在一条支路上没问题。 */ bool pca9555_addr_ok(void); /* `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); /* 同上,但输出锁存器里 `keep_mask` 那几位**保留现值**(「重启不断电」时留 VBUS, 按键 / EDL 照样清零 —— 重启前正按着的键,起来就松开)。 **只在这片的方向寄存器已经是 PCA9555_CONFIG0_INIT 时才留**:否则它掉过电, 锁存器是复位值 0xFF,留下来 = 凭空打开 VBUS。`keep_mask` 只许是 Port0 的输出位。 */ hal_err_t pca9555_init_keep(int slot, uint8_t keep_mask); /* 巡检主路径:**一次事务**读两个输入端口(PCA9555 的读指针在 0x00 / 0x01 之间 toggle,这两个字节是 datasheet 保证的)。 */ typedef struct { uint8_t port0; uint8_t port1; } pca9555_in_t; hal_err_t pca9555_read_inputs(pca9555_in_t *out); /* ---- Port0 输出:影子 + 唯一写入点 --------------------------------------- */ /* 改 `mask` 指定的那几位为 `value` 里的值,其余位保持影子里的值,整字节写下去。 组合键的两位落在**同一次写**里(§10.6:分两次写会先单按一个键)。 ⚠️ **`slot` 必须与当前 mux 通道对应的那一路一致。** 驱动没法自己验证这件事, 传错的后果是影子记在了另一片上 —— 下一次写会把这一片的键"顺手"改掉。 这就是 `slot.c` 存在的理由:别在 `slot.c` 之外调这个函数。 `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`:开漏、低有效 ⇒ **低 = 在告警**。 ⚠️ R1 里是六片线或,R2 里刀片上的 ALERT 不跨板,只进本片的 P1.2。 */ bool pca9555_ina_alert(const pca9555_in_t *in); #ifdef __cplusplus } #endif #endif /* PCA9555_H */