A6:PCA9555 驱动 + 「接地脚永远是输入」的硬约束
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 <[email protected]>
这个提交包含在:
+57
-57
@@ -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 判故障之前要先确认这一帧是真读回来的,不是失败后的残留缓冲。
|
||||
|
||||
在新工单中引用
屏蔽一个用户