From eb3df9ff4995409fc3dbfa9c6e5bda154024ba7a Mon Sep 17 00:00:00 2001 From: zhou yang Date: Mon, 7 Sep 2026 06:28:11 +0800 Subject: [PATCH] =?UTF-8?q?A6=EF=BC=9APCA9555=20=E9=A9=B1=E5=8A=A8=20+=20?= =?UTF-8?q?=E3=80=8C=E6=8E=A5=E5=9C=B0=E8=84=9A=E6=B0=B8=E8=BF=9C=E6=98=AF?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E3=80=8D=E7=9A=84=E7=A1=AC=E7=BA=A6=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit U23 @ 0x20,16 个脚全部是输入:5×EFUSE_FLT# + 5×EFUSE_PGOOD + BUCK_PG + INA_ALERT + 4 个焊死在地上的备用脚(P0_7 / P1_5~7)。位表与地址从 .claude/pcb1-parts.json 的真实焊盘网络逐脚核回来,与设计文档 §17.2 一致。 驱动根本不提供「把某个脚配成输出」的接口(和 sr595.h 不提供「抬 OE#」同一条 规矩),方向字一律过 pca9555_dir_safe(),不安全直接 abort。原因是 PCA9555 的 Output Port 寄存器复位值是 0xFF —— 把接地脚配成输出的那一刻它就输出高电平, 而真器件既不报错也不 NAK,只是发烫。 三种极性(FLT# 低有效 / PGOOD 高有效 / INA_ALERT 低有效)各自只在一个函数里 出现一次;上层问「有没有故障」,不许自己去 & 位。 sim 侧新增按 datasheet 建模的假 PCA9555:Output 复位 0xFF、输入按位合成、 接地脚被驱动时记短路、INT# 只被「读输入端口」清除。为此给假 I²C 总线加了一个 窃听钩子(POST_WRITE / PRE_READ)—— 通用寄存器表模拟不了「配成输出就对地 短路」这类事,以后再有这种器件照这个挂。 验收:8 条 ctest(81 用例)全绿;idf.py build 通过,pca9555.c.obj 在。 12 个「一定会被写出来的错」逐个注进去跑过,全部变红。 Co-Authored-By: Claude Opus 5 --- firmware/HANDOFF.md | 114 +++--- firmware/components/drv/CMakeLists.txt | 1 + firmware/components/drv/include/pca9555.h | 139 +++++++ firmware/components/drv/pca9555.c | 194 ++++++++++ .../phonerail_hal/sim/hal_i2c_sim.c | 6 + .../components/phonerail_hal/sim/sim_hal.c | 2 + .../components/phonerail_hal/sim/sim_hal.h | 39 +- .../phonerail_hal/sim/sim_pca9555.c | 162 +++++++++ firmware/host/CMakeLists.txt | 2 + firmware/main/phonerail.h | 2 +- firmware/test/test_pca9555.c | 340 ++++++++++++++++++ 需求文档.md | 20 ++ 12 files changed, 962 insertions(+), 59 deletions(-) create mode 100644 firmware/components/drv/include/pca9555.h create mode 100644 firmware/components/drv/pca9555.c create mode 100644 firmware/components/phonerail_hal/sim/sim_pca9555.c create mode 100644 firmware/test/test_pca9555.c diff --git a/firmware/HANDOFF.md b/firmware/HANDOFF.md index f5082c2..9e38670 100644 --- a/firmware/HANDOFF.md +++ b/firmware/HANDOFF.md @@ -1,76 +1,76 @@ # 交接 -更新:2026-09-06 完成任务包 **A5**(74HC595 24 位链 + 影子寄存器单一写入点) +更新:2026-09-07 完成任务包 **A6**(PCA9555 驱动) ## 一句话现状 -15 路侧键 + 5 路 `EDL_EN` + `HUB_RST` 全在一条 24 位链上,内存里只有一份影子, -唯一写入点是 `sr595_flush()`。位表**从 PCB 的真实焊盘网络逐脚核回来**(不是照文档抄), -上电顺序按「先锁存安全值、再使能 `OE#`」写死。7 条 ctest(61 个用例)全绿, -`idf.py build` 通过。 +板上唯一的 I/O 扩展器(U23 @ 0x20)**16 个脚全部是输入**:5×FLT# + 5×PGOOD + +BUCK_PG + INA_ALERT + 4 个焊死在地上的备用脚。驱动**根本不提供「配成输出」的接口**, +方向字一律过 `pca9555_dir_safe()`。位表与地址从 PCB 焊盘网络逐脚核回来(与文档一致)。 +8 条 ctest(81 个用例)全绿,`idf.py build` 通过。 ## 下一步 -**A6**:PCA9555 驱动(需求 §10.4 末段)。验收断言:**方向寄存器永远不会把 -Port0[7] / Port1[7:5] 配成输出**(配错直接 abort)—— 那 4 个备用脚在板上**直接接地** -(设计文档 §17.2 的注),配成输出并输出高就是对地短路。 -A3/A4/A5/A6 四个驱动包彼此独立,做完 A6 就能进 A7(通道配置表)。 +**A7**:通道配置表 + 表驱动遍历(需求 §10.4 全节)。四个驱动包(A3/A4/A5/A6)已全部完成, +A7 的活是把 `rail_cfg_t` 那张静态表建起来,并在启动自检里**一起**调上四个自检谓词: +`ina226_addr_table_ok()` / `dac63204_map_ok()` / `sr595_map_ok()` / `pca9555_map_ok()` +(前三个至今没有任何调用点,A7 是它们的归宿)。 ## 本次新增/修改的文件 -- `components/drv/include/sr595.h` — 24 位链的位表、引脚、影子契约(唯一对外契约) -- `components/drv/sr595.c` — 驱动本体(≈200 行,只经 `hal_gpio_*`) -- `components/phonerail_hal/sim/sim_sr595.c` — **按 datasheet 建模的假移位链** - (SRCLK 上升沿移一格、RCLK 上升沿锁存),加上 `sim_hal.h` 的一节声明 -- `components/phonerail_hal/sim/hal_gpio_sim.c` — 加了一个窃听钩子给上面那个模型; - `hal_gpio_config(OUTPUT)` 也会触发(真芯片配成输出就开始驱动,`OE#` 靠这一条) -- `test/test_sr595.c`(15 个用例) -- 三处 CMakeLists + `main/phonerail.h`(版本 `0.1.0-a5`) -- `../需求文档.md` §10.6、`../五路直供电源板设计.md` §17.2 — 记落地 + **修掉过期的位表** - (§8.2 那行过期的 DAC 通道分配也顺手改了) +- `components/drv/include/pca9555.h` — 位表、三种极性、接地脚掩码(唯一对外契约) +- `components/drv/pca9555.c` — 驱动本体(≈195 行,只经 `hal_i2c_*`) +- `components/phonerail_hal/sim/sim_pca9555.c` — **按 datasheet 建模的假器件** + (Output 复位 0xFF、输入按位合成、接地脚短路检测、INT# 只被读输入端口清) +- `components/phonerail_hal/sim/hal_i2c_sim.c` + `sim_hal.h` — 假 I²C 加了一个 + **窃听钩子**(`SIM_I2C_EV_POST_WRITE` / `PRE_READ`),通用寄存器表模拟不了「配成 + 输出就对地短路」这种事;以后再有这类器件照这个挂 +- `test/test_pca9555.c`(20 个用例) +- 三处 CMakeLists + `sim_hal.c` 的 reset 汇总 + `main/phonerail.h`(版本 `0.1.0-a6`) +- `../需求文档.md` §10.4 — 记 A6 落地 + 四条实现约定 ## 验收命令与结果 ``` $ bash host/test.sh -100% tests passed, 0 tests failed out of 7 # +test_sr595(15) +100% tests passed, 0 tests failed out of 8 # +test_pca9555(20) $ .\build_idf.ps1 # PowerShell phonerail.bin binary size 0x32550 bytes. ... 0xcdab0 bytes (80%) free. -Project build complete. # sr595.c.obj 在;bin 与 A4 同样大是正常的(还没人调它) +Project build complete. # pca9555.c.obj 在(已单独确认) -# 四个「一定会被写出来的错」逐个改进去跑过,确认测试拦得住: -# 字节顺序发反(U21 先发) -> test_byte_order 红 -# 先使能 OE# 再锁存安全值 -> test_outputs_are_enabled_only_after_a_safe_latch 红 -# 安全影子写成 0(HUB_RST 摁死) -> 三条红,其中一条直接说「上电后 Hub 不能被摁在复位里」 -# 位表改成 3(rail−1)+key 的算式 -> sr595_modify 的 assert 当场 abort(碰到空脚 bit4) +# 12 个「一定会被写出来的错」逐个注进去跑过,**全部变红**(一次性脚本,没入库;做法见下面第 4、5 条坑): +# FLT#/PGOOD/INA_ALERT/BUCK_PG 四处极性各写反 -> 3~4 条红 +# FLT 去读 Port1(端口搞反) -> 只有 test_one_rail_at_a_time 红 +# 方向写成 0x7F(P0_7 配成输出) -> 4 条红,含 sim 的短路计数 +# 忘了清极性反转 / 写了 Output Port -> 各 1~2 条红 +# dir_safe 写成近似判据 dir != 0 -> 1 条红 +# 读输入拆成两次事务 -> 1 条红(事务计数) +# 读失败时把调用方缓冲清零 -> 1 条红 ``` ## 踩到的坑(只写会再犯的) -- **位表不能照 §17.2 抄** —— 那张表是 09-04 的两片版,§26.18 重排之后 15 路里 - **12 路对不上**。这次是从 `.claude/pcb1-parts.json` 的真实焊盘网络逐脚核出来的 - (顺手把 §17.2 和 §8.2 里过期的两张表都改了)。和 A4 的 DAC 通道分配同一类坑。 -- ⚠️ **CH4 的三个键碰巧就是 bit 9/10/11**,和「顺排公式 `3(rail−1)+key`」一模一样。 - 所以「断言位表不是公式」只能**整体**断言(15 位里只有 3 位重合), - 逐路写 `NOT_EQUAL` 到 CH4 就自己红了 —— 第一版正是这么写的。 -- **`HUB_RST` 是低有效**(USB2517I 的 `RESET_N`,§21.7)。全 0 的上电影子会把 Hub - 一直摁在复位里,而五路手机的电压电流全都正常 —— 看起来跟固件一点关系都没有。 -- **上电顺序不能换**:三片 `MR#` 接死在 +3V3,没有复位线 ⇒ 上电时移位寄存器内容 - 不确定。所以 `KEY_OE#` 在安全值锁存完之前**根本不配成输出**(ESP-IDF 一配成输出 - 就开始驱动,输出寄存器复位值 0,等于当场使能)。板上 R609 100 kΩ 上拉扛着这一段。 -- **假移位链值得写**:A5 的两条验收断言如果只对着驱动自己的影子变量断言,那是自己 - 证明自己 —— 字节顺序发反、每帧多打一个时钟、RCLK 打早了,影子变量一律看不出来。 - `sim_sr595_*` 按 datasheet 建模,attach 时把移位寄存器塞成 `0xA5A5A5`(真板子上电 - 就是不确定值),四个注错进去的 bug 全被它照出来了。 -- **cygwin 的偶发 `Subprocess aborted` 又来了一次**:这次是三条测试同时在 0.03 s 里挂, - 其中两条根本不碰 595。重跑就正常。**别去查代码。** +- ⚠️ **PCA9555 的 Output Port 寄存器复位值是 0xFF。** 所以把一个接地脚配成输出的 + 那一刻它就输出高电平,中间没有「先写个 0 再说」的余地 —— 这比「配错方向」本身 + 更急。和 A5 那条「ESP-IDF 一配成输出就开始驱动」是同一类坑,只是这里默认值是**高**。 +- **FLT 与 PG 这次确实是顺排的**(`bit = rail - 1`),和 INA226 地址、595 位号那两张表 + 不一样。所以这一片最容易写出来的错不是算式,而是**把 Port0 / Port1 搞反**: + 两个端口的 bit0~4 长得一模一样,**静息态两个端口读回来都是全 1**, + 单看一帧完全看不出来。注错实验证实:搞反之后 20 条测试里只有 + `test_one_rail_at_a_time` 一条红 —— 那条是专门为它写的,别删。 +- **极性反转寄存器要无条件清零,不能赌复位值**:看门狗只复位 ESP32,PCA9555 不掉电。 +- **`-Werror` 会替测试「假装」拦住错**:注错实验里「读输入拆成两次事务」第一版是 + 编译期红(`reg` 变量没人用),根本没跑到断言。补了一版能编过的才算数。 + 以后做注错实验,**编译期红的那一条一定要重写成能编过的**。 +- **注错脚本必须 try/finally 恢复原文**:第一版崩在 `subprocess` 吃不下 + `/c/cygwin64/...` 这种 POSIX 路径(Windows Python 要 `C:\cygwin64\...`), + 异常直接把注错留在了源文件里。(同批小坑:长测试文件用 Bash heredoc 写会 + `unexpected EOF`,改用 Write 工具。) -## 这个驱动 / 这一层的几条约定(写 A9 / A12 之前看一眼) -- **除 `sr595.c` 外,任何地方都不许自己去打 SER/SRCLK/RCLK。** 改按键或 EDL 一律是 - 「`sr595_modify()` 改影子 → `sr595_flush()`」。多处直接操作移位链的症状是 - **另一台手机的按键被顺手清掉**,而且要两个动作正好撞在一起才复现。 -- **组合键必须 `sr595_write(mask, value)` 一次给两位**(VOL− + PWR 落在同一帧)。 - 分两次写会先单按一个键。`RCLK` 之前输出不变,所以整帧是原子的。 -- **不许拿 `OE#` 当「临时全松开」用**(需求 §10.6):那是复位路径不是控制路径, - 所以 `sr595.h` 根本没提供抬 `OE#` 的接口。 -- **rail 是 1~5**,`sr595_key_bit()` / `sr595_edl_bit()` 越界 abort; - `sr595_modify()` 的 mask 碰到三个空脚(bit 4/8/12)也 abort —— - 悄悄忽略的话那一位永远是 0,而「按键没反应」要到接上真手机才发现。 -- **`sr595_map_ok()` 还没人调**:A7 的启动自检要把它和 `dac63204_map_ok()` 一起调上。 -- **按键的保持时长(100 ms / 2 s / 10 s)与序列互斥还没做**,那是 A9 的活。 - A5 只保证「写下去的那一帧是对的」。 +## 这个驱动 / 这一层的几条约定(写 A10 之前看一眼) +- **三种极性各只出现一次**(`pca9555_rail_fault` / `_rail_pg` / `_buck_pg` / `_ina_alert`)。 + 上层一律问「有没有故障」,**不许自己去 & 位** —— 和 `sr595_set_hub_reset()` 里那个 + 反相、`rail_mv_to_code()` 里那个减法同一个道理:多一处就多一处写反的机会。 +- **`IOEXP_INT#`(IO38)是加速路径不是主路径**:PCA9555 要**读走输入端口**才释放 INT, + 而且输入变回原值时它也会释放。只靠中断的话,漏掉一次读就再也等不到下一个沿。 + 所以 `t_safety` 的 20 ms 轮询是无条件的。A6 只留了引脚宏,读它是 A8/A10 的活。 +- **这一片不应答不许关输出**(§10.10):只是失去 FAULT / PG 可见性,改用 INA226 的 + 电压判据兜底并持续告警。驱动的读失败一律只返回错误码,不做任何动作。 +- ⚠️ **「全 0」不是安全默认值**:它和「五路真的全炸了」读起来一模一样 + (五路全故障 + 五路全不良 + Buck 不良 + 告警)。上拉没焊、总线被拉低都是这个样子, + 所以 A10 判故障之前要先确认这一帧是真读回来的,不是失败后的残留缓冲。 diff --git a/firmware/components/drv/CMakeLists.txt b/firmware/components/drv/CMakeLists.txt index 5afed1c..8222581 100644 --- a/firmware/components/drv/CMakeLists.txt +++ b/firmware/components/drv/CMakeLists.txt @@ -6,5 +6,6 @@ idf_component_register( SRCS "ina226.c" "dac63204.c" "sr595.c" + "pca9555.c" INCLUDE_DIRS "include" REQUIRES phonerail_hal) diff --git a/firmware/components/drv/include/pca9555.h b/firmware/components/drv/include/pca9555.h new file mode 100644 index 0000000..7e29827 --- /dev/null +++ b/firmware/components/drv/include/pca9555.h @@ -0,0 +1,139 @@ +/* PCA9555(U23,地址 0x20)—— 板上唯一的 I/O 扩展器,**16 个脚全部是输入**。 + + 挂的是五路 eFuse 的 FLT#/PGOOD、Buck 的线或 PWRGD、六片 INA226 的线或 ALERT, + 共 12 个信号 + 4 个直接接地的备用脚(设计文档 §17.2)。 + + ⚠️ **本驱动不提供「把某个脚配成输出」的接口,一个都不提供。** + 四个备用脚 P0_7 / P1_5~7 在板上**直接接地、不串限流电阻**(§17.2 的注, + 图纸上已标)。而 PCA9555 的 Output Port 寄存器**复位值是 0xFF**—— + 所以只要把其中一个配成输出,它下一刻就输出高电平,**直接对地短路**。 + 配错既不会报错也不会 NAK,只会发烫。所以方向字一律走 pca9555_dir_safe(), + 不安全直接 abort。这跟 sr595.h 不提供「抬 OE#」是同一条规矩。 + + §10.10 的降级行为:这片不应答 = 失去 FAULT / PG 可见性,**不关输出**, + 改用 INA226 的电压判据兜底并持续告警。所以本驱动的读失败一律只是返回错误码, + 不做任何动作。 + + 本驱动只经 hal_i2c_*,宿主机上由 sim 的假 PCA9555(`sim_pca9555_*`)全部测完。 */ +#ifndef PCA9555_H +#define PCA9555_H + +#include +#include + +#include "hal_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ---- 地址与引脚 ---------------------------------------------------------- */ + +/* A0 = A1 = A2 = GND(U23 的 pin21 / pin2 / pin3,已按 PCB 焊盘网络核过)。 */ +#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 + +/* ---- 寄存器(全部 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 + +/* 极性反转寄存器必须是 0。写成非 0 的现象是「五路同时报故障」或者更糟的 + 「五路故障全都看不见」,而 I²C 一切正常 —— init 会写 0 并回读校验。 */ +#define PCA9555_POLINV_NONE 0x00u + +/* ---- 位分配(从 `.claude/pcb1-parts.json` 的真实焊盘网络逐脚核对) ------- + + Port0 bit0~4 = EFUSE_FLT1~5 bit5 = BUCK_PG bit6 = INA_ALERT bit7 = 接地 + Port1 bit0~4 = EFUSE_PG1~5 bit5~7 = 接地 + + 这一次两张表**确实是顺排的**(`bit = rail - 1`),和 INA226 地址、595 位号 + 那两张表不一样。所以这里最容易写出来的错不是「算式」,而是 + **把 FLT 和 PG 的端口搞反** —— 两个端口的 bit0~4 长得一模一样, + 搞反之后「有故障」会被读成「电源良好」,方向正好相反。 + 所以取位号一律走下面两个函数,不要在别处写 `1 << (rail - 1)`。 */ + +#define PCA9555_RAIL_COUNT 5 + +#define PCA9555_BIT_BUCK_PG 5 /* Port0,五路 Buck 的 PWRGD 线或(都是开漏) */ +#define PCA9555_BIT_INA_ALERT 6 /* Port0,六片 INA226 的 ALERT 线或 */ + +/* 接地的备用脚。**永远、永远是输入。** */ +#define PCA9555_P0_GND_MASK 0x80u /* P0_7 */ +#define PCA9555_P1_GND_MASK 0xE0u /* P1_5 P1_6 P1_7 */ + +/* rail 是 **1~5**,越界 abort。返回的是**位号**(0~4),不是掩码。 */ +int pca9555_flt_bit(int rail); /* Port0 */ +int pca9555_pg_bit(int rail); /* Port1 */ + +/* 12 个信号位互不重复、都没落到接地脚上、FLT 与 PG 分属两个端口。 + 启动自检调一次(A7,和 ina226_addr_table_ok() / sr595_map_ok() 一起)。 */ +bool pca9555_map_ok(void); + +/* 0x20 不与 INA226(0x40/41/44/45/42/43) / DAC63204(0x4A/0x4B) / LM75(0x48) 撞车。 */ +bool pca9555_addr_ok(void); + +/* dir 里 1 = 输入。返回 false = 这个方向字会把某个**接地脚**配成输出。 + 驱动内部走到 false 直接 abort:悄悄放过去的后果是板子发烫,不是功能不好用。 */ +bool pca9555_dir_safe(int port, uint8_t dir); + +/* ---- 采样 ---------------------------------------------------------------- */ + +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() 里那个反相同一个道理:多一处就多一处写反 + 的机会)。上层一律问「有没有故障」,不许自己去 & 位。 */ + +/* TPS25940A 的 `FLT#`:开漏、板上 R113 47 kΩ 上拉 ⇒ **低 = 有故障**。 */ +bool pca9555_rail_fault(const pca9555_in_t *in, int rail); + +/* TPS25940A 的 `PGOOD`:开漏、R115 47 kΩ 上拉,输出 2.97 V 以上才释放 + ⇒ **高 = 电源良好**。 */ +bool pca9555_rail_pg(const pca9555_in_t *in, int rail); + +/* 五路 Buck 的 `PWRGD` 线或(开漏)⇒ **高 = 五路都正常**;低只说明「有某一路 + 不正常」,看不出是哪一路(§16.1)。 */ +bool pca9555_buck_pg(const pca9555_in_t *in); + +/* 六片 INA226 的 `ALERT` 线或:开漏、低有效 ⇒ **低 = 有片子在告警**。 */ +bool pca9555_ina_alert(const pca9555_in_t *in); + +#ifdef __cplusplus +} +#endif + +#endif /* PCA9555_H */ diff --git a/firmware/components/drv/pca9555.c b/firmware/components/drv/pca9555.c new file mode 100644 index 0000000..58b653c --- /dev/null +++ b/firmware/components/drv/pca9555.c @@ -0,0 +1,194 @@ +/* PCA9555 驱动。只经 hal_i2c_*,不碰任何平台头文件。 */ +#include "pca9555.h" + +#include +#include + +#include "hal_i2c.h" + +/* 位表见 pca9555.h。这两张表**这次确实是顺排的**,但仍然写成表: + 哪天硬件改一根线,改这里一处就够,而不是满工程找 `1 << (rail - 1)`。 */ +static const uint8_t s_flt_bit[PCA9555_RAIL_COUNT] = { 0, 1, 2, 3, 4 }; /* Port0 */ +static const uint8_t s_pg_bit[PCA9555_RAIL_COUNT] = { 0, 1, 2, 3, 4 }; /* Port1 */ + +int pca9555_flt_bit(int rail) +{ + assert(rail >= 1 && rail <= PCA9555_RAIL_COUNT); /* 越界是调用方算错了,不许悄悄返回 */ + return s_flt_bit[rail - 1]; +} + +int pca9555_pg_bit(int rail) +{ + assert(rail >= 1 && rail <= PCA9555_RAIL_COUNT); + return s_pg_bit[rail - 1]; +} + +bool pca9555_map_ok(void) +{ + uint8_t p0 = 0, p1 = 0; + + for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) { + const uint8_t f = (uint8_t)(1u << s_flt_bit[rail - 1]); + const uint8_t g = (uint8_t)(1u << s_pg_bit[rail - 1]); + if ((p0 & f) != 0 || (p1 & g) != 0) { + return false; /* 两路占了同一位 */ + } + p0 |= f; + p1 |= g; + } + + const uint8_t buck = (uint8_t)(1u << PCA9555_BIT_BUCK_PG); + const uint8_t alrt = (uint8_t)(1u << PCA9555_BIT_INA_ALERT); + if ((p0 & buck) != 0 || (p0 & alrt) != 0 || buck == alrt) { + return false; /* 线或的两根撞到某路 FLT 上,或者撞到一起 */ + } + p0 |= (uint8_t)(buck | alrt); + + /* 一个信号都不许落到接地脚上 —— 落上去就等于永远读到 0(FLT 永远报故障, + PG 永远报「不良」),而 I²C 一切正常。 */ + if ((p0 & PCA9555_P0_GND_MASK) != 0 || (p1 & PCA9555_P1_GND_MASK) != 0) { + return false; + } + return true; +} + +bool pca9555_addr_ok(void) +{ + /* 同一条 I²C 上的其它器件(§8.2)。 */ + static const uint8_t others[] = { 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x48, 0x4A, 0x4B }; + + if (PCA9555_ADDR > 0x7f) { + return false; + } + for (size_t i = 0; i < sizeof(others); i++) { + if (others[i] == PCA9555_ADDR) { + return false; + } + } + return true; +} + +bool pca9555_dir_safe(int port, uint8_t dir) +{ + if (port != 0 && port != 1) { + return false; + } + /* 1 = 输入。接地脚对应的位必须是 1,一位都不能少。 */ + const uint8_t gnd = (port == 0) ? PCA9555_P0_GND_MASK : PCA9555_P1_GND_MASK; + return (dir & gnd) == gnd; +} + +/* ---- 寄存器读写 --------------------------------------------------------- + PCA9555 的寄存器是 8 位的,命令字节在**成对**的寄存器之间 toggle + (0/1、2/3、4/5、6/7),不是无限自增。本驱动只在一对之内连读 2 字节, + 落在 datasheet 保证的范围里;跨对连读(比如从 0x06 读 4 字节)行为不同, + 别那么写。 */ + +static hal_err_t read_reg(uint8_t reg, uint8_t *val) +{ + return hal_i2c_write_read(PCA9555_ADDR, ®, 1, val, 1); +} + +static hal_err_t write_reg(uint8_t reg, uint8_t val) +{ + const uint8_t wr[2] = { reg, val }; + return hal_i2c_write(PCA9555_ADDR, wr, sizeof(wr)); +} + +/* 写完立刻回读比对。回读不一致 = 总线上有东西但不是我们以为的那片。 */ +static hal_err_t write_verify(uint8_t reg, uint8_t val) +{ + hal_err_t err = write_reg(reg, val); + if (err != HAL_OK) { + return err; + } + uint8_t rb = 0; + err = read_reg(reg, &rb); + if (err != HAL_OK) { + return err; + } + return (rb == val) ? HAL_OK : HAL_ERR_BUS; +} + +hal_err_t pca9555_probe(void) +{ + uint8_t c0 = 0, c1 = 0; + hal_err_t err = read_reg(PCA9555_REG_CONFIG0, &c0); + if (err != HAL_OK) { + return err; + } + err = read_reg(PCA9555_REG_CONFIG1, &c1); + if (err != HAL_OK) { + return err; + } + /* 刚上电的 PCA9555 两个方向寄存器都是 0xFF。不是的话,要么这地址上蹲着别的 + 器件,要么它已经被谁配过了 —— 两种都要在启动时就叫出来。 */ + return (c0 == PCA9555_DIR_ALL_INPUT && c1 == PCA9555_DIR_ALL_INPUT) ? HAL_OK : HAL_ERR_BUS; +} + +hal_err_t pca9555_init(void) +{ + /* 方向必须先于一切:这一步写错就是四个接地脚被驱动成高电平。 + 全输入是唯一合法值,assert 拦的是「哪天有人顺手改了这个常量」。 */ + assert(pca9555_dir_safe(0, PCA9555_DIR_ALL_INPUT)); + assert(pca9555_dir_safe(1, PCA9555_DIR_ALL_INPUT)); + + hal_err_t err = write_verify(PCA9555_REG_CONFIG0, PCA9555_DIR_ALL_INPUT); + if (err != HAL_OK) { + return err; + } + err = write_verify(PCA9555_REG_CONFIG1, PCA9555_DIR_ALL_INPUT); + if (err != HAL_OK) { + return err; + } + + /* 极性反转清零。复位值本来就是 0,但「本来就是」不值得赌 —— 看门狗复位 + 只复位 ESP32,PCA9555 是不掉电的,上一条命的残留值会原样留着。 */ + err = write_verify(PCA9555_REG_POLINV0, PCA9555_POLINV_NONE); + if (err != HAL_OK) { + return err; + } + return write_verify(PCA9555_REG_POLINV1, PCA9555_POLINV_NONE); +} + +hal_err_t pca9555_read_inputs(pca9555_in_t *out) +{ + if (out == NULL) { + return HAL_ERR_ARG; + } + const uint8_t reg = PCA9555_REG_INPUT0; + uint8_t rd[2] = { 0, 0 }; + hal_err_t err = hal_i2c_write_read(PCA9555_ADDR, ®, 1, rd, sizeof(rd)); + if (err != HAL_OK) { + return err; + } + out->port0 = rd[0]; + out->port1 = rd[1]; + return HAL_OK; +} + +/* ---- 语义判读(极性各出现一次,见头文件) -------------------------------- */ + +bool pca9555_rail_fault(const pca9555_in_t *in, int rail) +{ + assert(in != NULL); + return (in->port0 & (uint8_t)(1u << pca9555_flt_bit(rail))) == 0; /* FLT# 低有效 */ +} + +bool pca9555_rail_pg(const pca9555_in_t *in, int rail) +{ + assert(in != NULL); + return (in->port1 & (uint8_t)(1u << pca9555_pg_bit(rail))) != 0; /* PGOOD 高 = 良好 */ +} + +bool pca9555_buck_pg(const pca9555_in_t *in) +{ + assert(in != NULL); + return (in->port0 & (uint8_t)(1u << PCA9555_BIT_BUCK_PG)) != 0; /* 高 = 五路都正常 */ +} + +bool pca9555_ina_alert(const pca9555_in_t *in) +{ + assert(in != NULL); + return (in->port0 & (uint8_t)(1u << PCA9555_BIT_INA_ALERT)) == 0; /* ALERT 低有效 */ +} diff --git a/firmware/components/phonerail_hal/sim/hal_i2c_sim.c b/firmware/components/phonerail_hal/sim/hal_i2c_sim.c index f0cb649..ff4a33a 100644 --- a/firmware/components/phonerail_hal/sim/hal_i2c_sim.c +++ b/firmware/components/phonerail_hal/sim/hal_i2c_sim.c @@ -111,6 +111,9 @@ static hal_err_t do_write(sim_i2c_dev_t *d, const uint8_t *data, size_t len) } d->regs[d->ptr++] = v; } + if (d->hook != NULL) { + d->hook(d, SIM_I2C_EV_POST_WRITE); + } return HAL_OK; } @@ -123,6 +126,9 @@ static hal_err_t do_read(sim_i2c_dev_t *d, uint8_t *data, size_t len) return HAL_ERR_ARG; } d->n_read++; + if (d->hook != NULL) { + d->hook(d, SIM_I2C_EV_PRE_READ); /* 按 datasheet 建模的器件在这里合成输入寄存器 */ + } for (size_t i = 0; i < len; i += d->reg_width) { uint16_t v = d->regs[d->ptr++]; if (d->reg_width == 2) { diff --git a/firmware/components/phonerail_hal/sim/sim_hal.c b/firmware/components/phonerail_hal/sim/sim_hal.c index 19c3c5b..b2159d5 100644 --- a/firmware/components/phonerail_hal/sim/sim_hal.c +++ b/firmware/components/phonerail_hal/sim/sim_hal.c @@ -5,6 +5,7 @@ void sim_i2c_reset_internal(void); void sim_gpio_reset_internal(void); void sim_sr595_reset_internal(void); +void sim_pca9555_reset_internal(void); void sim_pwm_reset_internal(void); void sim_time_reset_internal(void); @@ -13,6 +14,7 @@ void sim_hal_reset(void) sim_i2c_reset_internal(); sim_gpio_reset_internal(); sim_sr595_reset_internal(); + sim_pca9555_reset_internal(); sim_pwm_reset_internal(); sim_time_reset_internal(); } diff --git a/firmware/components/phonerail_hal/sim/sim_hal.h b/firmware/components/phonerail_hal/sim/sim_hal.h index daf2c25..fd9b27e 100644 --- a/firmware/components/phonerail_hal/sim/sim_hal.h +++ b/firmware/components/phonerail_hal/sim/sim_hal.h @@ -28,7 +28,17 @@ void sim_hal_reset(void); #define SIM_I2C_REGS 256 #define SIM_I2C_LOG_BYTES 8 -typedef struct { +/* 假器件的窃听事件。通用寄存器表模拟不了「配成输出就对地短路」这种事, + 所以按 datasheet 建模的假器件(sim_pca9555)挂一个钩子进来。 */ +typedef enum { + SIM_I2C_EV_POST_WRITE = 0, /* 一次写事务刚落进寄存器表 */ + SIM_I2C_EV_PRE_READ, /* 一次读事务即将取寄存器表 —— 在这里合成输入 */ +} sim_i2c_ev_t; + +struct sim_i2c_dev; +typedef void (*sim_i2c_hook_t)(struct sim_i2c_dev *d, sim_i2c_ev_t ev); + +typedef struct sim_i2c_dev { uint8_t addr; uint8_t reg_width; /* 1 或 2 字节。2 字节按**大端**——INA226 / DAC63204 / LM75 都是 */ bool present; /* false = 器件失联(A15 降级矩阵用),事务一律 NAK */ @@ -39,6 +49,7 @@ typedef struct { uint32_t n_read; uint8_t last_write[SIM_I2C_LOG_BYTES]; /* 最后一次写进去的原始字节 */ uint8_t last_write_len; + sim_i2c_hook_t hook; /* NULL = 就是一张寄存器表 */ } sim_i2c_dev_t; /* 往假总线上挂一个器件。地址重复或超出 16 个会 abort(测试写错了,不该悄悄过)。 */ @@ -91,6 +102,32 @@ uint32_t sim_sr595_enable_outputs(void); /* 某一位在栅极上的实际电平;OE# 为高时返回 -1(高阻,不是 0)。 */ int sim_sr595_pin(int bit); +/* ---------------- PCA9555(按 datasheet 建模的假器件) ---------------- */ + +/* 通用寄存器表证明不了 A6 的核心判据。这个模型只多做三件真器件会做的事: + ① **Output Port 复位值 0xFF** —— 所以把一个脚配成输出,它下一刻就输出高; + ② 输入寄存器按位合成:配成输入的位读**外部电平**,配成输出的位读**自己的输出**; + ③ 四个备用脚 P0_7 / P1_5~7 **焊死在地上** —— 读永远是 0,被驱动成高就是短路。 + ③ 是这块板子上「配错方向」唯一会留下痕迹的地方:真板子上它既不报错也不 NAK。 */ +void sim_pca9555_attach(uint8_t addr); + +/* 外部世界把某个端口的 8 根线驱动成 level(接地脚不受影响,恒 0)。 + FLT#/PGOOD 都是开漏上拉,所以「什么都没发生」的静息值是 0xFF。 */ +void sim_pca9555_drive(int port, uint8_t level); + +uint8_t sim_pca9555_config(int port); /* 方向寄存器现值,1 = 输入 */ +uint8_t sim_pca9555_output(int port); /* 输出锁存器现值(复位 0xFF) */ +uint8_t sim_pca9555_polinv(int port); + +/* 接地脚被配成输出过几次(不管输出高低,配了就算 —— 那已经是在对地灌电流)。 */ +uint32_t sim_pca9555_gnd_driven(void); +/* 其中真的输出了高电平、也就是**直接对地短路**过几次。 */ +uint32_t sim_pca9555_shorted(void); + +/* INT# 现在是不是被拉低(低有效)。输入变化拉低,**读走输入端口才释放**。 */ +bool sim_pca9555_int_asserted(void); +uint32_t sim_pca9555_int_asserts(void); /* 累计拉低次数 */ + /* ---------------- PWM ---------------- */ int sim_pwm_pin(int ch); /* 未配置返回 -1 */ diff --git a/firmware/components/phonerail_hal/sim/sim_pca9555.c b/firmware/components/phonerail_hal/sim/sim_pca9555.c new file mode 100644 index 0000000..38f0f56 --- /dev/null +++ b/firmware/components/phonerail_hal/sim/sim_pca9555.c @@ -0,0 +1,162 @@ +/* 按 datasheet 建模的假 PCA9555。宿主机专用。 + + 通用寄存器表(sim_i2c)能测「写下去的字节对不对」,但测不出 A6 真正要拦的那件事: + **把一个焊死在地上的脚配成输出**。真板子上这既不报错也不 NAK,只是发烫。 + 所以这里把三件真器件会做的事补上(见 sim_hal.h 那一节)。 + + ⚠️ 这个模型是用来证明**代码逻辑**对的。别拿它推翻设计文档 §17.2 的任何一条。 */ +#include +#include + +#include "pca9555.h" +#include "sim_hal.h" + +/* 焊死在地上的备用脚(设计文档 §17.2 的注)。 */ +static const uint8_t s_gnd_mask[2] = { PCA9555_P0_GND_MASK, PCA9555_P1_GND_MASK }; + +typedef struct { + bool attached; + uint8_t addr; + uint8_t ext[2]; /* 外部世界驱动的电平;开漏上拉,静息 0xFF */ + uint8_t int_latch[2];/* 上一次「被读走」时的输入值,用来判 INT# */ + bool int_low; + uint32_t int_asserts; + uint32_t gnd_driven; + uint32_t shorted; +} sim_pca9555_t; + +static sim_pca9555_t s; + +void sim_pca9555_reset_internal(void); /* 只给 sim_hal_reset() 用 */ + +void sim_pca9555_reset_internal(void) +{ + memset(&s, 0, sizeof(s)); +} + +/* 输入寄存器 = 逐位选择:配成输入的位取外部电平,配成输出的位取自己的输出锁存器。 + 接地脚无论配成什么,线上都是 0(它焊在地上,赢的永远是铜)。 */ +static uint8_t input_value(const sim_i2c_dev_t *d, int port) +{ + const uint8_t cfg = (uint8_t)d->regs[PCA9555_REG_CONFIG0 + port]; + const uint8_t out = (uint8_t)d->regs[PCA9555_REG_OUTPUT0 + port]; + const uint8_t pol = (uint8_t)d->regs[PCA9555_REG_POLINV0 + port]; + + uint8_t v = (uint8_t)((s.ext[port] & cfg) | (out & (uint8_t)~cfg)); + v &= (uint8_t)~s_gnd_mask[port]; /* 接地脚读回来永远是 0 */ + return (uint8_t)(v ^ pol); /* 极性反转寄存器作用在**输入**上 */ +} + +/* 方向寄存器一变就查一次:有没有把接地脚配成输出,输出的又是不是高电平。 */ +static void check_grounded_pins(const sim_i2c_dev_t *d) +{ + for (int port = 0; port < 2; port++) { + const uint8_t cfg = (uint8_t)d->regs[PCA9555_REG_CONFIG0 + port]; + const uint8_t out = (uint8_t)d->regs[PCA9555_REG_OUTPUT0 + port]; + const uint8_t driven = (uint8_t)(s_gnd_mask[port] & (uint8_t)~cfg); /* cfg 0 = 输出 */ + if (driven == 0) { + continue; + } + s.gnd_driven++; + if ((driven & out) != 0) { + s.shorted++; /* 真板子上这一刻开始冒烟 */ + } + } +} + +/* INT#:任何输入位相对「上次被读走的那一帧」发生变化就拉低,读走输入端口才释放。 */ +static void update_int(const sim_i2c_dev_t *d) +{ + for (int port = 0; port < 2; port++) { + if (input_value(d, port) != s.int_latch[port]) { + if (!s.int_low) { + s.int_low = true; + s.int_asserts++; + } + return; + } + } +} + +static void hook(sim_i2c_dev_t *d, sim_i2c_ev_t ev) +{ + if (ev == SIM_I2C_EV_POST_WRITE) { + check_grounded_pins(d); + update_int(d); + return; + } + + /* PRE_READ:把两个输入寄存器合成好再让总线取走。 */ + for (int port = 0; port < 2; port++) { + d->regs[PCA9555_REG_INPUT0 + port] = input_value(d, port); + } + /* 读输入端口 = 确认中断。读别的寄存器不清 INT#(真器件就是这个行为, + 也是「只靠中断会永远收不到下一个沿」的根源)。 */ + if (d->ptr == PCA9555_REG_INPUT0 || d->ptr == PCA9555_REG_INPUT1) { + s.int_latch[0] = (uint8_t)d->regs[PCA9555_REG_INPUT0]; + s.int_latch[1] = (uint8_t)d->regs[PCA9555_REG_INPUT1]; + s.int_low = false; + } +} + +void sim_pca9555_attach(uint8_t addr) +{ + assert(!s.attached); /* 板上只有一片 */ + sim_i2c_dev_t *d = sim_i2c_add(addr, 1); + + s.attached = true; + s.addr = addr; + s.ext[0] = 0xFF; /* FLT# / PGOOD 都是开漏上拉,静息是高 */ + s.ext[1] = 0xFF; + + /* 上电复位值(datasheet 表 6):方向全输入 0xFF,**输出锁存器 0xFF**,极性 0。 + 输出的 0xFF 是本器件最阴的一条:配成输出的那一刻就输出高电平。 */ + d->regs[PCA9555_REG_CONFIG0] = 0xFF; + d->regs[PCA9555_REG_CONFIG1] = 0xFF; + d->regs[PCA9555_REG_OUTPUT0] = 0xFF; + d->regs[PCA9555_REG_OUTPUT1] = 0xFF; + d->regs[PCA9555_REG_POLINV0] = 0x00; + d->regs[PCA9555_REG_POLINV1] = 0x00; + + s.int_latch[0] = input_value(d, 0); + s.int_latch[1] = input_value(d, 1); + d->hook = hook; +} + +static sim_i2c_dev_t *must_dev(void) +{ + assert(s.attached); /* 先 sim_pca9555_attach() */ + sim_i2c_dev_t *d = sim_i2c_dev(s.addr); + assert(d != NULL); + return d; +} + +void sim_pca9555_drive(int port, uint8_t level) +{ + assert(port == 0 || port == 1); + s.ext[port] = level; + update_int(must_dev()); +} + +uint8_t sim_pca9555_config(int port) +{ + assert(port == 0 || port == 1); + return (uint8_t)must_dev()->regs[PCA9555_REG_CONFIG0 + port]; +} + +uint8_t sim_pca9555_output(int port) +{ + assert(port == 0 || port == 1); + return (uint8_t)must_dev()->regs[PCA9555_REG_OUTPUT0 + port]; +} + +uint8_t sim_pca9555_polinv(int port) +{ + assert(port == 0 || port == 1); + return (uint8_t)must_dev()->regs[PCA9555_REG_POLINV0 + port]; +} + +uint32_t sim_pca9555_gnd_driven(void) { return s.gnd_driven; } +uint32_t sim_pca9555_shorted(void) { return s.shorted; } +bool sim_pca9555_int_asserted(void) { return s.int_low; } +uint32_t sim_pca9555_int_asserts(void) { return s.int_asserts; } diff --git a/firmware/host/CMakeLists.txt b/firmware/host/CMakeLists.txt index 3ba020c..92e0b32 100644 --- a/firmware/host/CMakeLists.txt +++ b/firmware/host/CMakeLists.txt @@ -33,6 +33,7 @@ set(PHONERAIL_HAL_SIM_SRCS ${PHONERAIL_HAL_DIR}/sim/hal_i2c_sim.c ${PHONERAIL_HAL_DIR}/sim/hal_gpio_sim.c ${PHONERAIL_HAL_DIR}/sim/sim_sr595.c + ${PHONERAIL_HAL_DIR}/sim/sim_pca9555.c ${PHONERAIL_HAL_DIR}/sim/hal_pwm_sim.c ${PHONERAIL_HAL_DIR}/sim/hal_time_sim.c ) @@ -43,6 +44,7 @@ set(PHONERAIL_DRV_SRCS ${PHONERAIL_DRV_DIR}/ina226.c ${PHONERAIL_DRV_DIR}/dac63204.c ${PHONERAIL_DRV_DIR}/sr595.c + ${PHONERAIL_DRV_DIR}/pca9555.c ) add_library(phonerail_core STATIC diff --git a/firmware/main/phonerail.h b/firmware/main/phonerail.h index 82bb068..e8e1502 100644 --- a/firmware/main/phonerail.h +++ b/firmware/main/phonerail.h @@ -3,7 +3,7 @@ #define PHONERAIL_H /* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */ -#define PHONERAIL_FW_VERSION "0.1.0-a5" +#define PHONERAIL_FW_VERSION "0.1.0-a6" /* 通道数。五路直供,见设计文档 §2。 */ #define PHONERAIL_RAIL_COUNT 5 diff --git a/firmware/test/test_pca9555.c b/firmware/test/test_pca9555.c new file mode 100644 index 0000000..c22388f --- /dev/null +++ b/firmware/test/test_pca9555.c @@ -0,0 +1,340 @@ +/* A6:PCA9555 驱动。 + 验收主项:**方向寄存器永远不会把 Port0[7] / Port1[7:5] 配成输出** —— + 那四个脚在板上直接接地,配成输出等于对地短路,而真器件既不报错也不 NAK。 + 第二主项:FLT / PG / BUCK_PG / INA_ALERT 三种极性一个都不许写反。 */ +#include "pca9555.h" +#include "sim_hal.h" +#include "unity.h" + +void setUp(void) { sim_hal_reset(); } +void tearDown(void) {} + +static void mount(void) +{ + sim_pca9555_attach(PCA9555_ADDR); + TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(NULL)); +} + +/* 挂好并 init 完,外部电平停在静息值(全高 = 无故障、全部良好)。 */ +static void mount_ready(void) +{ + mount(); + TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_init()); +} + +/* ---- 验收主项一:接地脚永远是输入 ---------------------------------------- */ + +/* init 之后两个方向寄存器都必须是 0xFF —— 不是「大致全输入」,是逐位。 */ +static void test_init_configures_every_pin_as_input(void) +{ + mount_ready(); + TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_config(0)); + TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_config(1)); + TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_gnd_driven()); + TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_shorted()); +} + +/* dir_safe 的判据穷举:**当且仅当**接地位全为 1(输入)才算安全。 + 写成「dir != 0」「dir & 0x80」这类近似判据的,会在这 512 个组合里露馅。 */ +static void test_dir_safe_is_exactly_the_grounded_pins(void) +{ + for (int dir = 0; dir <= 0xFF; dir++) { + const bool p0_want = ((uint8_t)dir & PCA9555_P0_GND_MASK) == PCA9555_P0_GND_MASK; + const bool p1_want = ((uint8_t)dir & PCA9555_P1_GND_MASK) == PCA9555_P1_GND_MASK; + TEST_ASSERT_EQUAL_INT(p0_want, pca9555_dir_safe(0, (uint8_t)dir)); + TEST_ASSERT_EQUAL_INT(p1_want, pca9555_dir_safe(1, (uint8_t)dir)); + } + /* 几个会被真写出来的错值 */ + TEST_ASSERT_FALSE(pca9555_dir_safe(0, 0x7F)); /* 只把 P0_7 配成输出:最像笔误的一个 */ + TEST_ASSERT_FALSE(pca9555_dir_safe(1, 0x1F)); /* 「低五位是 PG,高三位不用」 */ + TEST_ASSERT_FALSE(pca9555_dir_safe(1, 0x00)); /* 全输出 */ + TEST_ASSERT_TRUE(pca9555_dir_safe(0, PCA9555_DIR_ALL_INPUT)); + TEST_ASSERT_TRUE(pca9555_dir_safe(1, PCA9555_DIR_ALL_INPUT)); + TEST_ASSERT_FALSE(pca9555_dir_safe(2, PCA9555_DIR_ALL_INPUT)); /* 端口号越界 */ +} + +/* 这条证明的是**假器件真的会照出短路**,否则上面那条「shorted == 0」是句空话。 + 手工绕过驱动,按「有人把 P0_7 配成输出」写一次方向寄存器: + PCA9555 的 Output Port 复位值是 0xFF,所以这一刻它就在对地灌电流。 */ +static void test_sim_catches_a_grounded_pin_driven_high(void) +{ + mount_ready(); + + TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_output(0)); /* 复位值就是全高,这才是要命的地方 */ + + const uint8_t wr[2] = { PCA9555_REG_CONFIG0, 0x7F }; + TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write(PCA9555_ADDR, wr, sizeof(wr))); + + TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_gnd_driven()); + TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_shorted()); +} + +/* 同上,但先把输出锁存器写成 0 再配成输出:不冒烟,可仍然是在驱动一个接地脚。 + 两个计数分开就是为了让这一格不被漏掉。 */ +static void test_sim_counts_driving_a_grounded_pin_even_when_low(void) +{ + mount_ready(); + + const uint8_t out0[2] = { PCA9555_REG_OUTPUT0, 0x00 }; + TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write(PCA9555_ADDR, out0, sizeof(out0))); + const uint8_t cfg0[2] = { PCA9555_REG_CONFIG0, 0x7F }; + TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write(PCA9555_ADDR, cfg0, sizeof(cfg0))); + + TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_gnd_driven()); + TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_shorted()); +} + +/* 驱动全程一个字节都不许写 Output Port:没有输出脚,写它只可能是写错寄存器号。 */ +static void test_init_never_touches_the_output_registers(void) +{ + mount_ready(); + TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_output(0)); + TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_output(1)); +} + +/* ---- 极性反转寄存器 ------------------------------------------------------ */ + +/* 极性反转是「看门狗复位后残留」的重灾区:ESP32 重启了,PCA9555 没掉电。 + init 必须无条件清零,而不是「复位值本来就是 0」。 */ +static void test_init_clears_a_stale_polarity_inversion(void) +{ + mount(); + sim_i2c_set_reg(PCA9555_ADDR, PCA9555_REG_POLINV0, 0xFF); /* 上一条命的残留 */ + sim_i2c_set_reg(PCA9555_ADDR, PCA9555_REG_POLINV1, 0x1F); + + TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_init()); + + TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_polinv(0)); + TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_polinv(1)); +} + +/* ---- 验收主项二:三种极性 ------------------------------------------------ */ + +/* 静息态(两个端口都被上拉到 0xFF)= 五路无故障、五路电源良好、Buck 正常、无告警。 + 任何一处极性写反,这条立刻红。 */ +static void test_idle_bus_means_everything_is_fine(void) +{ + mount_ready(); + pca9555_in_t in; + TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in)); + + TEST_ASSERT_EQUAL_HEX8(0x7F, in.port0); /* P0_7 接地,读回来必是 0 */ + TEST_ASSERT_EQUAL_HEX8(0x1F, in.port1); /* P1_5~7 接地 */ + + for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) { + TEST_ASSERT_FALSE(pca9555_rail_fault(&in, rail)); + TEST_ASSERT_TRUE(pca9555_rail_pg(&in, rail)); + } + TEST_ASSERT_TRUE(pca9555_buck_pg(&in)); + TEST_ASSERT_FALSE(pca9555_ina_alert(&in)); +} + +/* 全 0(比如整条总线被拉低、或者上拉没焊)= 五路全故障、五路全不良、Buck 不良、告警。 + 注意这**不是**「安全默认值」:它和「五路真的全炸了」长得一模一样, + §10.10 因此规定这片不应答时改用 INA226 的电压判据兜底,而不是信这一帧。 */ +static void test_all_low_means_everything_is_bad(void) +{ + mount_ready(); + sim_pca9555_drive(0, 0x00); + sim_pca9555_drive(1, 0x00); + + pca9555_in_t in; + TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in)); + for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) { + TEST_ASSERT_TRUE(pca9555_rail_fault(&in, rail)); + TEST_ASSERT_FALSE(pca9555_rail_pg(&in, rail)); + } + TEST_ASSERT_FALSE(pca9555_buck_pg(&in)); + TEST_ASSERT_TRUE(pca9555_ina_alert(&in)); +} + +/* 逐路点名:只让一路报故障,其余四路必须纹丝不动。 + FLT 和 PG 两张表在两个端口上长得一模一样(都是 bit0~4),把端口搞反之后 + 「有故障」会被读成「电源良好」,所以这里一路一路地把两个端口错开来验。 */ +static void test_one_rail_at_a_time(void) +{ + mount_ready(); + + for (int hurt = 1; hurt <= PCA9555_RAIL_COUNT; hurt++) { + /* FLT 只拉低 hurt 那一路;PG 只拉低**另一路**(hurt 的下一路,绕回来)。 */ + const int other = (hurt % PCA9555_RAIL_COUNT) + 1; + sim_pca9555_drive(0, (uint8_t)(0xFFu & ~(1u << pca9555_flt_bit(hurt)))); + sim_pca9555_drive(1, (uint8_t)(0xFFu & ~(1u << pca9555_pg_bit(other)))); + + pca9555_in_t in; + TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in)); + + for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) { + TEST_ASSERT_EQUAL_INT(rail == hurt, pca9555_rail_fault(&in, rail)); + TEST_ASSERT_EQUAL_INT(rail != other, pca9555_rail_pg(&in, rail)); + } + /* 两根线或信号不受任何一路 FLT 影响(它们在 P0_5 / P0_6 上)。 */ + TEST_ASSERT_TRUE(pca9555_buck_pg(&in)); + TEST_ASSERT_FALSE(pca9555_ina_alert(&in)); + } +} + +/* 两根线或信号自己点名:拉低 BUCK_PG 不许把任何一路读成故障,反之亦然。 */ +static void test_wire_or_signals_do_not_alias_a_rail(void) +{ + mount_ready(); + pca9555_in_t in; + + sim_pca9555_drive(0, (uint8_t)(0xFFu & ~(1u << PCA9555_BIT_BUCK_PG))); + TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in)); + TEST_ASSERT_FALSE(pca9555_buck_pg(&in)); + TEST_ASSERT_FALSE(pca9555_ina_alert(&in)); + for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) { + TEST_ASSERT_FALSE(pca9555_rail_fault(&in, rail)); + } + + sim_pca9555_drive(0, (uint8_t)(0xFFu & ~(1u << PCA9555_BIT_INA_ALERT))); + TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in)); + TEST_ASSERT_TRUE(pca9555_buck_pg(&in)); + TEST_ASSERT_TRUE(pca9555_ina_alert(&in)); + for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) { + TEST_ASSERT_FALSE(pca9555_rail_fault(&in, rail)); + } +} + +/* ---- 位表与地址 ---------------------------------------------------------- */ + +static void test_bit_map_is_consistent(void) +{ + TEST_ASSERT_TRUE(pca9555_map_ok()); + TEST_ASSERT_TRUE(pca9555_addr_ok()); + TEST_ASSERT_EQUAL_HEX8(0x20, PCA9555_ADDR); /* A0=A1=A2=GND,U23 的 pin21/2/3 */ + + for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) { + TEST_ASSERT_EQUAL_INT(rail - 1, pca9555_flt_bit(rail)); + TEST_ASSERT_EQUAL_INT(rail - 1, pca9555_pg_bit(rail)); + /* 一路都不许落到接地脚或两根线或信号上 */ + TEST_ASSERT_TRUE(pca9555_flt_bit(rail) < PCA9555_BIT_BUCK_PG); + TEST_ASSERT_EQUAL_HEX8(0, (uint8_t)((1u << pca9555_flt_bit(rail)) & PCA9555_P0_GND_MASK)); + TEST_ASSERT_EQUAL_HEX8(0, (uint8_t)((1u << pca9555_pg_bit(rail)) & PCA9555_P1_GND_MASK)); + } + TEST_ASSERT_EQUAL_INT(5, PCA9555_BIT_BUCK_PG); + TEST_ASSERT_EQUAL_INT(6, PCA9555_BIT_INA_ALERT); +} + +/* ---- 总线行为 ------------------------------------------------------------ */ + +/* §10.3 的 0.12 ms 预算是按**一次事务**算的(写命令字节 + 重复起始读 2 字节)。 + 拆成两次事务照样能跑,但 t_safety 的 I²C 占用会对不上 A8 要断言的那个数。 */ +static void test_read_inputs_uses_one_transaction(void) +{ + mount_ready(); + const sim_i2c_dev_t *d = sim_i2c_dev(PCA9555_ADDR); + const uint32_t w0 = d->n_write, r0 = d->n_read; + + pca9555_in_t in; + TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in)); + + TEST_ASSERT_EQUAL_UINT32(1, d->n_write - w0); + TEST_ASSERT_EQUAL_UINT32(1, d->n_read - r0); +} + +static void test_probe_accepts_a_fresh_device(void) +{ + mount(); + TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_probe()); +} + +/* 这个地址上蹲着别的东西(应答了,但方向寄存器不是复位值)。 */ +static void test_probe_rejects_a_stranger(void) +{ + mount(); + sim_i2c_set_reg(PCA9555_ADDR, PCA9555_REG_CONFIG1, 0x00); + TEST_ASSERT_EQUAL_INT(HAL_ERR_BUS, pca9555_probe()); +} + +static void test_probe_absent_device_naks(void) +{ + mount(); + sim_i2c_set_present(PCA9555_ADDR, false); + TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_probe()); +} + +/* §10.10:这片不应答只是失去可见性,驱动如实返回 NAK,什么动作都不做。 */ +static void test_read_propagates_nak(void) +{ + mount_ready(); + sim_i2c_inject_nak(PCA9555_ADDR, -1); + + pca9555_in_t in = { 0xAA, 0x55 }; + TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_read_inputs(&in)); + TEST_ASSERT_EQUAL_HEX8(0xAA, in.port0); /* 失败时不许改写调用方的缓冲 */ + TEST_ASSERT_EQUAL_HEX8(0x55, in.port1); +} + +static void test_init_propagates_nak(void) +{ + mount(); + sim_i2c_inject_nak(PCA9555_ADDR, -1); + TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_init()); +} + +/* 回读对不上 = 总线上有东西但不是我们以为的那片,init 必须失败而不是接着往下跑。 + 这里用「写第二个方向寄存器时 NAK」来造回读失败之外的另一条错路。 */ +static void test_init_stops_at_the_first_failure(void) +{ + mount(); + sim_i2c_inject_nak(PCA9555_ADDR, 3); /* 头三次事务 NAK,之后恢复 */ + TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_init()); + /* 失败了就别把剩下的寄存器也写一遍 —— 极性反转还停在没被碰过的状态。 */ + TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_polinv(1)); +} + +static void test_null_output_rejected(void) +{ + mount_ready(); + TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, pca9555_read_inputs(NULL)); +} + +/* ---- INT#(A8/A10 会用,这里只钉住它的行为) ----------------------------- */ + +/* 中断是加速路径不是主路径:**读走输入端口才释放**。 + 漏读一次就再也等不到下一个沿,所以 t_safety 必须无条件轮询。 */ +static void test_int_is_only_cleared_by_reading_the_input_ports(void) +{ + mount_ready(); + TEST_ASSERT_FALSE(sim_pca9555_int_asserted()); + + sim_pca9555_drive(0, 0xFE); /* CH1 报故障 */ + TEST_ASSERT_TRUE(sim_pca9555_int_asserted()); + TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_int_asserts()); + + /* 读别的寄存器不清中断 */ + TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_probe()); + TEST_ASSERT_TRUE(sim_pca9555_int_asserted()); + + pca9555_in_t in; + TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in)); + TEST_ASSERT_FALSE(sim_pca9555_int_asserted()); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_init_configures_every_pin_as_input); + RUN_TEST(test_dir_safe_is_exactly_the_grounded_pins); + RUN_TEST(test_sim_catches_a_grounded_pin_driven_high); + RUN_TEST(test_sim_counts_driving_a_grounded_pin_even_when_low); + RUN_TEST(test_init_never_touches_the_output_registers); + RUN_TEST(test_init_clears_a_stale_polarity_inversion); + RUN_TEST(test_idle_bus_means_everything_is_fine); + RUN_TEST(test_all_low_means_everything_is_bad); + RUN_TEST(test_one_rail_at_a_time); + RUN_TEST(test_wire_or_signals_do_not_alias_a_rail); + RUN_TEST(test_bit_map_is_consistent); + RUN_TEST(test_read_inputs_uses_one_transaction); + RUN_TEST(test_probe_accepts_a_fresh_device); + RUN_TEST(test_probe_rejects_a_stranger); + RUN_TEST(test_probe_absent_device_naks); + RUN_TEST(test_read_propagates_nak); + RUN_TEST(test_init_propagates_nak); + RUN_TEST(test_init_stops_at_the_first_failure); + RUN_TEST(test_null_output_rejected); + RUN_TEST(test_int_is_only_cleared_by_reading_the_input_ports); + return UNITY_END(); +} diff --git a/需求文档.md b/需求文档.md index 911de50..cf524c4 100644 --- a/需求文档.md +++ b/需求文档.md @@ -413,6 +413,26 @@ typedef struct { 同理,**PCA9555 的方向寄存器只能配成全输入**:备用脚 Port0[7] / Port1[7:5] 在硬件上 直接接地,配成输出并写 1 就是对地短路(设计文档 §17.2 已在图纸上注明)。 +> **A6 已落地**:驱动在 `firmware/components/drv/include/pca9555.h`, +> **根本不提供「把某个脚配成输出」的接口**(和 sr595.h 不提供「抬 OE#」同一条规矩)。 +> 方向字一律过 `pca9555_dir_safe()`,不安全直接 abort。 +> 同处还记了四条这一片的实现约定: +> ① ⚠️ **Output Port 寄存器的复位值是 0xFF** —— 所以把接地脚配成输出的那一刻它就 +> 输出高电平,中间没有「先写个 0 再说」的余地,这比「配错方向」本身更急; +> ② **极性反转寄存器必须无条件清零**,不能赌复位值:看门狗只复位 ESP32, +> PCA9555 不掉电,上一条命的残留值原样留着,而写错的现象是「五路同时报故障」 +> 或者更糟的「五路故障全都看不见」,I²C 一切正常; +> ③ **三种极性**(FLT# 低有效 / PGOOD 高有效 / INA_ALERT 低有效)各自只在 +> `pca9555_rail_fault()` 等四个函数里出现一次,上层不许自己去 & 位; +> ④ **INT# 是加速路径不是主路径**:它要读走输入端口才释放,漏读一次就再也等不到 +> 下一个沿,所以 `t_safety` 必须无条件轮询(§10.3 的 20 ms 就是按这个排的)。 +> +> ⚠️ **FLT 与 PG 两张位表这次确实是顺排的**(都是 `bit = rail - 1`), +> 和 INA226 地址、595 位号那两张表不一样。所以这里最容易写出来的错不是「算式」, +> 而是**把 Port0 和 Port1 搞反** —— 两个端口的 bit0~4 长得一模一样,搞反之后 +> 「有故障」会被读成「电源良好」,方向正好相反,而且**静息态两个端口读回来都是 +> 全 1,单看一帧完全看不出来**。`test_one_rail_at_a_time` 专门拦这一手。 + ### 10.5 电压通道:DAC 只做微调,而且是反向的 | 事实 | 含义 |