A5:74HC595 24 位链 + 影子寄存器单一写入点

15 路侧键 + 5 路 EDL_EN + HUB_RST 在一条 24 位链上,内存里只有一份影子,
唯一写入点是 sr595_flush()。发送顺序 U33 → U22 → U21,每字节 QH 先发。

位表是从 .claude/pcb1-parts.json 的真实焊盘网络逐脚核回来的,不是照文档抄 ——
设计文档 §17.2 那张是 09-04 的两片版,§26.18 重排后 15 路里有 12 路对不上。
顺手把 §17.2 与 §8.2 里过期的两张表都改了。

sim 侧新增按 datasheet 建模的假移位链(SRCLK 上升沿移一格、RCLK 上升沿锁存),
只看线上真实发生的边 —— 对着驱动自己的影子变量断言等于自己证明自己。
四个注错进去的 bug(字节序反、OE# 先使能、安全影子写 0、位表写成算式)全被拦住。

上电顺序按「先锁存安全值、再使能 OE#」写死:三片 MR# 接死在 +3V3,
上电时移位寄存器内容不确定,先拉低 OE# 就是把随机值放到 15 个栅极上。

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
这个提交包含在:
zhou yang
2026-09-06 19:33:51 +08:00
共同撰写人 Claude Opus 5
父节点 9eb1b39da6
当前提交 ab37f9dc49
共修改 13 个文件,包含 7895 行新增和 6869 行删除
+57 -44
查看文件
@@ -1,63 +1,76 @@
# 交接
更新:2026-09-06 完成任务包 **A4**(DAC63204 驱动 + `rail_set_mv()`)
更新:2026-09-06 完成任务包 **A5**(74HC595 24 位链 + 影子寄存器单一写入点)
## 一句话现状
五路输出电压能设了:`rail_set_mv(rail, mv)` 是唯一入口(rail 是 **1~5**),
内部走 mV → Vd → 12 位码,**码越大电压越低**;4010 mV 以上、3524 mV 以下一律拒。
两片 DAC 认件(DEVICE-ID=01h)、开内部基准、增益 2×、只给用到的通道上电,
上电即 3.90 V。6 条 ctest(46 个用例)全绿,`idf.py build` 通过。
15 路侧键 + 5 路 `EDL_EN` + `HUB_RST` 全在一条 24 位链上,内存里只有一份影子,
唯一写入点是 `sr595_flush()`。位表**从 PCB 的真实焊盘网络逐脚核回来**(不是照文档抄),
上电顺序按「先锁存安全值、再使能 `OE#`」写死。7 条 ctest(61 个用例)全绿,
`idf.py build` 通过。
## 下一步
**A5**:74HC595 24 位链 + 影子寄存器单一写入点(需求 §10.6「影子寄存器」那段 +
设计文档 §17.2 的位表)。两条断言:①改 CH1 按键不动 CH2~5 的位 ②字节顺序 U33→U22→U21。
⚠️ 位表也被 §26.18 重排过(`.claude/pinmap.py` 的 `P3B` / `P6` 两节是真表),
和这次的 DAC 通道一样,**别照 §17.2 的旧表写**。
**A6**:PCA9555 驱动(需求 §10.4 末段)。验收断言:**方向寄存器永远不会把
Port0[7] / Port1[7:5] 配成输出**(配错直接 abort)—— 那 4 个备用脚在板上**直接接地**
(设计文档 §17.2 的注),配成输出并输出高就是对地短路。
A3/A4/A5/A6 四个驱动包彼此独立,做完 A6 就能进 A7(通道配置表)。
## 本次新增/修改的文件
- `components/drv/include/dac63204.h` — 寄存器、位域、五路→(片,通道)映射(唯一对外契约)
- `components/drv/dac63204.c` — 驱动本体(≈180 行,只经 `hal_i2c_*`)
- `main/rail.h` / `main/rail.c` — mV ↔ 码的**唯一**换算与 `rail_set_mv()` / `rail_get_mv()`
- `test/test_dac63204.c`(21 个用例)/ `test/test_rail.c`(12 个用例)
- 三处 CMakeLists + `main/phonerail.h`(版本 `0.1.0-a4`)
- `../需求文档.md` §10.5、`../五路直供电源板设计.md` §17.2 — 记落地 + **修掉过期的通道分配表**
- `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 通道分配也顺手改了)
## 验收命令与结果
```
$ bash host/test.sh
100% tests passed, 0 tests failed out of 6 # +test_dac63204(21) +test_rail(12)
100% tests passed, 0 tests failed out of 7 # +test_sr595(15)
$ .\build_idf.ps1 # PowerShell
phonerail.bin binary size 0x32550 bytes. ... 0xcdab0 bytes (80%) free.
Project build complete. # dac63204.c.obj 与 rail.c.obj 都在;bin 与 A3 同样大是正常的
#(还没人调它们,链接器把符号丢了)
Project build complete. # sr595.c.obj 在;bin 与 A4 同样大是正常的(还没人调它)
# 三个「一定会被写出来的错」逐个改进去跑过,确认测试拦得住:
# 反向关系写反 -> test_rail 红;忘了左对齐 / 通道按 rail-1 算 -> test_dac63204 红
# 四个「一定会被写出来的错」逐个改进去跑过,确认测试拦得住:
# 字节顺序发反(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 位码在 `DAC-X-DATA` 里左对齐**(bit15-4,低 4 位 don't care)。忘了 `<< 4`
不报错,只让输出变成 1/16 —— 而在本板上那是「五路都 4.010 V 满电」,看不出异常。
- **两种步长**:`DAC-X-VOUT-CMP-CONFIG` 每通道跨 6(03/09/0F/15),`DAC-X-DATA` 连续(19~1C)。
- **上电顺序**:`EN-INT-REF` 要先写(datasheet 明说增益设定之前必须已置位),
安全码要在通道上电之前写(复位码 0 = Vd 0 V = 输出上限 4.010 V)。
所以 `COMMON-CONFIG` 写两次。sim 只留寄存器终值看不到中间态,
顺序是靠「`CC_REF_ONLY` 里四路输出全按住」+「`n_read` 计数」两条间接钉的。
- **未用通道不许上电**:FBx 悬空时电压输出是开环运放(datasheet 表 5-1)。
U6 用 OUT0/1/2、U7 只用 OUT0/1,所以 `COMMON-CONFIG` 是每片一个值,不是常量。
- **cygwin 的偶发 `Subprocess aborted` 不止发生在链接**:这次 ctest 跑测试时
`test_hal_sim` / `test_ina226` 同时挂了一次,重跑就全绿。**别去查代码。**
- **位表不能照 §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。重跑就正常。**别去查代码。**
## 这个驱动 / 这一层的几条约定(写 A7 / A10 / A12 之前看一眼)
- **除 `rail.c` 外,任何地方都不许调 `dac63204_write_code()`。** 反向关系只在
`rail_mv_to_code()` 里出现一次(那个减法),多一处就多一处写反的机会。
- **查表不要算**:`dac63204_rail_addr()` / `dac63204_rail_ch()`,rail 是 1~5,越界 abort。
CH1→U6.OUT2 / CH2→U6.OUT0 / CH3→**U7**.OUT0 / CH4→U6.OUT1 / CH5→U7.OUT1(§26.18)。
四通道电气对等 ⇒ **接错了板子照样正常工作,只是调错了路**,功能测试测不出来。
- **单位**:`rail_set_mv/rail_get_mv` 用 mV,`rail_code_to_vd_uv()` 用 µV。一个码在输出端
只值 **0.119 mV**,往返误差 ≤ 1 mV 正常。`rail_get_mv()` 读的是**设定值不是实测**(实测走 INA226)。
- **不要用 `rail_set_mv()` 做保护**(需求 §10.5 规矩 2):异常处理是 `EFUSE_EN` 的活。
- **越界请求现在只返回 `HAL_ERR_ARG`,还没记事件** —— SR-13 要求记,`rail.c` 里
留了 `TODO(A11)`,事件环形缓冲做完要回来补。
- **标定还没做**(需求 §10.5 规矩 3):现在走理论公式,误差约 ±40 mV。
将来两点标定要顶替的是 `RAIL_VOUT0_UV`(4010018) 与 `RAIL_DAC_SPAN_UV`(486416) 这两个常量。
## 这个驱动 / 这一层的几条约定(写 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 只保证「写下去的那一帧是对的」。