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
这个提交包含在:
+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 只保证「写下去的那一帧是对的」。
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
idf_component_register(
|
||||
SRCS "ina226.c"
|
||||
"dac63204.c"
|
||||
"sr595.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES phonerail_hal)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/* 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,226 @@
|
||||
/* 74HC595 ×3 的 24 位链 + 影子寄存器。契约见 sr595.h。 */
|
||||
#include "sr595.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include "hal_gpio.h"
|
||||
|
||||
/* ---- 位表 ---------------------------------------------------------------- */
|
||||
|
||||
/* 从 `.claude/pcb1-parts.json` 的真实焊盘网络逐脚抄出来的(设计文档 §26.18 C)。
|
||||
位号 = 链上物理位置:0 = U21.QA … 23 = U33.QH。
|
||||
排布规矩:**每一路的三个键必须落在同一片的连续三脚上**(出芯片是一束线),
|
||||
所以三个键的位号在表里挨着,但**顺序每一路都不一样**(U22 上的 CH5 是倒着的)。 */
|
||||
static const int8_t k_key_bit[5][SR595_KEY_COUNT] = {
|
||||
/* PWR VUP VDN */
|
||||
/* CH1 */ { 1, 2, 3 }, /* U21.QB QC QD */
|
||||
/* CH2 */ { 19, 17, 18 }, /* U33.QD QB QC */
|
||||
/* CH3 */ { 22, 21, 20 }, /* U33.QG QF QE */
|
||||
/* CH4 */ { 9, 10, 11 }, /* U22.QB QC QD */
|
||||
/* CH5 */ { 15, 14, 13 }, /* U22.QH QG QF */
|
||||
};
|
||||
|
||||
/* EDL_EN1~5。⚠️ 只有 EDL_EN1 还在 U33(QA),其余四路搬到了 U21 上,
|
||||
而且**不是顺序的**:QA=EDL_EN4、QF=EDL_EN5、QG=EDL_EN3、QH=EDL_EN2。 */
|
||||
static const int8_t k_edl_bit[5] = {
|
||||
16, /* CH1 → U33.QA */
|
||||
7, /* CH2 → U21.QH */
|
||||
6, /* CH3 → U21.QG */
|
||||
0, /* CH4 → U21.QA */
|
||||
5, /* CH5 → U21.QF */
|
||||
};
|
||||
|
||||
int sr595_key_bit(int rail, sr595_key_t key)
|
||||
{
|
||||
assert(rail >= 1 && rail <= 5);
|
||||
assert((unsigned)key < (unsigned)SR595_KEY_COUNT); /* 枚举可能是无符号,别写 key >= 0 */
|
||||
return k_key_bit[rail - 1][key];
|
||||
}
|
||||
|
||||
uint32_t sr595_key_mask(int rail)
|
||||
{
|
||||
uint32_t m = 0;
|
||||
for (int k = 0; k < SR595_KEY_COUNT; k++) {
|
||||
m |= (uint32_t)1u << sr595_key_bit(rail, (sr595_key_t)k);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
int sr595_edl_bit(int rail)
|
||||
{
|
||||
assert(rail >= 1 && rail <= 5);
|
||||
return k_edl_bit[rail - 1];
|
||||
}
|
||||
|
||||
uint32_t sr595_all_keys_mask(void)
|
||||
{
|
||||
uint32_t m = 0;
|
||||
for (int rail = 1; rail <= 5; rail++) {
|
||||
m |= sr595_key_mask(rail);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
bool sr595_map_ok(void)
|
||||
{
|
||||
uint32_t seen = 0;
|
||||
int n = 0;
|
||||
|
||||
for (int rail = 1; rail <= 5; rail++) {
|
||||
for (int k = 0; k < SR595_KEY_COUNT; k++) {
|
||||
uint32_t b = (uint32_t)1u << k_key_bit[rail - 1][k];
|
||||
if ((b & SR595_MASK_VALID) == 0 || (seen & b) != 0) {
|
||||
return false; /* 落到空脚上,或者两个信号抢同一位 */
|
||||
}
|
||||
seen |= b;
|
||||
n++;
|
||||
}
|
||||
uint32_t e = (uint32_t)1u << k_edl_bit[rail - 1];
|
||||
if ((e & SR595_MASK_VALID) == 0 || (seen & e) != 0) {
|
||||
return false;
|
||||
}
|
||||
seen |= e;
|
||||
n++;
|
||||
}
|
||||
|
||||
uint32_t h = (uint32_t)1u << SR595_BIT_HUB_RST;
|
||||
if ((h & SR595_MASK_VALID) == 0 || (seen & h) != 0) {
|
||||
return false;
|
||||
}
|
||||
seen |= h;
|
||||
n++;
|
||||
|
||||
/* 21 个信号占满 21 个有效位,一位不多一位不少。 */
|
||||
return n == 21 && seen == SR595_MASK_VALID;
|
||||
}
|
||||
|
||||
/* ---- 影子 ---------------------------------------------------------------- */
|
||||
|
||||
static uint32_t s_shadow;
|
||||
static bool s_dirty;
|
||||
static bool s_inited;
|
||||
|
||||
uint32_t sr595_shadow(void) { return s_shadow; }
|
||||
bool sr595_dirty(void) { return s_dirty; }
|
||||
|
||||
void sr595_modify(uint32_t mask, uint32_t value)
|
||||
{
|
||||
/* 越出有效位 = 位号算错了(多半是把 rail 当下标又减了一次 1)。
|
||||
悄悄忽略的话,那一位会一直是 0,而「按键没反应」要到接上真手机才发现。 */
|
||||
assert((mask & ~SR595_MASK_VALID) == 0);
|
||||
|
||||
uint32_t next = (s_shadow & ~mask) | (value & mask);
|
||||
if (next != s_shadow) {
|
||||
s_shadow = next;
|
||||
s_dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
void sr595_set_hub_reset(bool in_reset)
|
||||
{
|
||||
/* 低有效:按住复位 = 输出 0。整个固件里这个取反只出现这一次。 */
|
||||
uint32_t bit = (uint32_t)1u << SR595_BIT_HUB_RST;
|
||||
sr595_modify(bit, in_reset ? 0u : bit);
|
||||
}
|
||||
|
||||
/* ---- 移位链 -------------------------------------------------------------- */
|
||||
|
||||
/* 先出错的那个错误码留住,后面照样把该发的边发完 —— 半帧数据留在链上比整帧发完更糟。 */
|
||||
static void keep(hal_err_t *first, hal_err_t e)
|
||||
{
|
||||
if (*first == HAL_OK) {
|
||||
*first = e;
|
||||
}
|
||||
}
|
||||
|
||||
static void clock_bit(hal_err_t *err, int bit)
|
||||
{
|
||||
keep(err, hal_gpio_write(SR595_PIN_SER, bit));
|
||||
keep(err, hal_gpio_write(SR595_PIN_SRCLK, 1)); /* 上升沿采样 SER */
|
||||
keep(err, hal_gpio_write(SR595_PIN_SRCLK, 0));
|
||||
}
|
||||
|
||||
/* 先移入的字节走到链尾 ⇒ 发送顺序 **U33 → U22 → U21**;
|
||||
每个字节里 QH(bit7)先发,同样因为先移入的位走得最远。 */
|
||||
static const uint8_t k_send_order[SR595_CHIP_COUNT] = {
|
||||
SR595_CHIP_U33,
|
||||
SR595_CHIP_U22,
|
||||
SR595_CHIP_U21,
|
||||
};
|
||||
|
||||
static hal_err_t shift_and_latch(uint32_t value)
|
||||
{
|
||||
hal_err_t err = HAL_OK;
|
||||
|
||||
for (int i = 0; i < SR595_CHIP_COUNT; i++) {
|
||||
uint8_t byte = (uint8_t)((value >> (8 * k_send_order[i])) & 0xFFu);
|
||||
for (int b = 7; b >= 0; b--) {
|
||||
clock_bit(&err, (byte >> b) & 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* RCLK 上升沿之前输出一点不变 ⇒ 整帧是原子的。 */
|
||||
keep(&err, hal_gpio_write(SR595_PIN_RCLK, 1));
|
||||
keep(&err, hal_gpio_write(SR595_PIN_RCLK, 0));
|
||||
return err;
|
||||
}
|
||||
|
||||
hal_err_t sr595_init(void)
|
||||
{
|
||||
hal_err_t err = HAL_OK;
|
||||
|
||||
s_inited = false;
|
||||
|
||||
/* ① 三根数据线配成输出并置低。`OE#` **故意还不碰** —— 板上 R609 把它拉在高位,
|
||||
三片输出保持高阻,15 个栅极被各自的 100 kΩ 拉低。 */
|
||||
keep(&err, hal_gpio_config(SR595_PIN_SER, HAL_GPIO_OUTPUT));
|
||||
keep(&err, hal_gpio_config(SR595_PIN_SRCLK, HAL_GPIO_OUTPUT));
|
||||
keep(&err, hal_gpio_config(SR595_PIN_RCLK, HAL_GPIO_OUTPUT));
|
||||
keep(&err, hal_gpio_write(SR595_PIN_SER, 0));
|
||||
keep(&err, hal_gpio_write(SR595_PIN_SRCLK, 0));
|
||||
keep(&err, hal_gpio_write(SR595_PIN_RCLK, 0));
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ② 安全影子先锁存进去(上电后移位寄存器内容不确定,`MR#` 接死在 +3V3,
|
||||
没有别的办法把它清干净)。 */
|
||||
s_shadow = SR595_SHADOW_SAFE;
|
||||
s_dirty = false;
|
||||
err = shift_and_latch(s_shadow);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ③ 这才使能输出。②③ 换个个儿 = 上电瞬间把随机值放到 15 个栅极上。 */
|
||||
keep(&err, hal_gpio_config(SR595_PIN_OE_N, HAL_GPIO_OUTPUT));
|
||||
keep(&err, hal_gpio_write(SR595_PIN_OE_N, 0));
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
s_inited = true;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t sr595_flush(void)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (!s_dirty) {
|
||||
return HAL_OK; /* 不空发一帧:链上已经是这个值了 */
|
||||
}
|
||||
|
||||
hal_err_t err = shift_and_latch(s_shadow);
|
||||
if (err == HAL_OK) {
|
||||
s_dirty = false; /* 发失败就留着脏,下一次 flush 会重发整帧 */
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
hal_err_t sr595_write(uint32_t mask, uint32_t value)
|
||||
{
|
||||
sr595_modify(mask, value);
|
||||
return sr595_flush();
|
||||
}
|
||||
@@ -15,6 +15,11 @@ static pin_t s_pin[SIM_GPIO_PINS];
|
||||
|
||||
void sim_gpio_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
|
||||
/* 假 74HC595 链的窃听口(sim_sr595.c)。没 attach 过就是个空动作。
|
||||
放在这里而不是让测试自己数边:移位链要看的是**电平的跳变**,
|
||||
而 hal_gpio_write() 写同一个值也会计一次 writes。 */
|
||||
void sim_sr595_gpio_hook(int pin, int level);
|
||||
|
||||
void sim_gpio_reset_internal(void) { memset(s_pin, 0, sizeof(s_pin)); }
|
||||
|
||||
static bool valid(int pin) { return pin >= 0 && pin < SIM_GPIO_PINS; }
|
||||
@@ -30,6 +35,12 @@ hal_err_t hal_gpio_config(int pin, hal_gpio_mode_t mode)
|
||||
/* 上拉输入没人驱动时读回高电平 —— 五路 FLT/PG 都是开漏 + 上拉,
|
||||
「没有故障」在假板子上也必须是 1,不然测试会把默认 0 当成一直有故障。 */
|
||||
p->level = (mode == HAL_GPIO_INPUT_PULLUP) ? 1 : 0;
|
||||
/* 配成输出的那一刻引脚就开始驱动了(ESP-IDF 的 gpio_config 也是这个行为,
|
||||
输出寄存器复位值 0)。`KEY_OE#` 正是靠这一条:配成输出 = 输出立刻使能,
|
||||
所以 sr595_init() 必须等安全值锁存完了再配它。 */
|
||||
if (mode == HAL_GPIO_OUTPUT) {
|
||||
sim_sr595_gpio_hook(pin, p->level);
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
@@ -40,6 +51,7 @@ hal_err_t hal_gpio_write(int pin, int level)
|
||||
}
|
||||
s_pin[pin].level = level ? 1 : 0;
|
||||
s_pin[pin].writes++;
|
||||
sim_sr595_gpio_hook(pin, s_pin[pin].level);
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
void sim_i2c_reset_internal(void);
|
||||
void sim_gpio_reset_internal(void);
|
||||
void sim_sr595_reset_internal(void);
|
||||
void sim_pwm_reset_internal(void);
|
||||
void sim_time_reset_internal(void);
|
||||
|
||||
@@ -11,6 +12,7 @@ void sim_hal_reset(void)
|
||||
{
|
||||
sim_i2c_reset_internal();
|
||||
sim_gpio_reset_internal();
|
||||
sim_sr595_reset_internal();
|
||||
sim_pwm_reset_internal();
|
||||
sim_time_reset_internal();
|
||||
}
|
||||
|
||||
@@ -66,6 +66,31 @@ int sim_gpio_level(int pin);
|
||||
/* 被 hal_gpio_write() 写成功过几次。断言「改 CH1 没动 CH2~5」用得上(A5)。 */
|
||||
uint32_t sim_gpio_writes(int pin);
|
||||
|
||||
/* ---------------- 74HC595 ×3 的 24 位链 ---------------- */
|
||||
|
||||
/* 按 datasheet 建模的假移位链,**只看线上真实发生的边**(SRCLK / RCLK 上升沿)。
|
||||
对着驱动自己的影子变量断言等于自己证明自己 —— 字节顺序发反、每帧多打一个时钟、
|
||||
RCLK 打早了,影子变量一律看不出来。位号与 sr595.h 一致:0 = U21.QA,23 = U33.QH。
|
||||
|
||||
attach 之后移位寄存器与输出锁存器被塞成 0xA5A5A5:真板子上电时它们的内容
|
||||
**不确定**(`MR#` 接死在 +3V3,没有复位线),驱动必须先锁存安全值再使能 OE#。 */
|
||||
void sim_sr595_attach(int ser_pin, int srclk_pin, int rclk_pin, int oe_pin);
|
||||
|
||||
uint32_t sim_sr595_outputs(void); /* 输出锁存器(不管 OE# 是高是低) */
|
||||
uint32_t sim_sr595_shift(void); /* 移位寄存器当前内容(还没锁存的那一份) */
|
||||
uint32_t sim_sr595_clocks(void); /* SRCLK 上升沿总数:一帧应当正好 +24 */
|
||||
uint32_t sim_sr595_latches(void); /* RCLK 上升沿总数:一帧应当正好 +1 */
|
||||
int sim_sr595_oe(void); /* OE# 当前电平。初值 1 —— 板上 R609 拉着 */
|
||||
|
||||
/* OE# 第一次被拉低那一刻的现场。断言「输出使能之前已经锁存过安全值」用:
|
||||
驱动要是把顺序换了,enable_latches 会是 0、enable_outputs 会是那坨 0xA5A5A5。 */
|
||||
bool sim_sr595_enabled(void);
|
||||
uint32_t sim_sr595_enable_latches(void);
|
||||
uint32_t sim_sr595_enable_outputs(void);
|
||||
|
||||
/* 某一位在栅极上的实际电平;OE# 为高时返回 -1(高阻,不是 0)。 */
|
||||
int sim_sr595_pin(int bit);
|
||||
|
||||
/* ---------------- PWM ---------------- */
|
||||
|
||||
int sim_pwm_pin(int ch); /* 未配置返回 -1 */
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/* 假的 74HC595 ×3 移位链 —— 挂在 GPIO sim 上,只看**线上真实发生的边**。
|
||||
|
||||
为什么要这么一个东西:A5 的两条验收断言(改一路不动别路、字节顺序 U33→U22→U21)
|
||||
如果只对着驱动自己的影子变量断言,那是**自己证明自己** —— 字节顺序发反了、
|
||||
每帧多打一个时钟、RCLK 在数据发完之前就打了,影子变量一律看不出来。
|
||||
所以这里按 datasheet 建模:SRCLK 上升沿移一格,RCLK 上升沿把移位寄存器抄进输出锁存器。
|
||||
|
||||
24 级的编号与 sr595.h 一致:0 = U21.QA(DS 进来的第一级),23 = U33.QH(链尾)。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "sim_hal.h"
|
||||
|
||||
typedef struct {
|
||||
bool attached;
|
||||
int ser_pin, srclk_pin, rclk_pin, oe_pin;
|
||||
int ser, srclk, rclk, oe; /* 当前电平;oe 初值 1 = 板上 R609 拉着 */
|
||||
uint32_t shift; /* 移位寄存器(未锁存) */
|
||||
uint32_t out; /* 输出锁存器 */
|
||||
uint32_t clocks; /* SRCLK 上升沿总数 */
|
||||
uint32_t latches; /* RCLK 上升沿总数 */
|
||||
bool enabled_once;
|
||||
uint32_t enable_latches; /* OE# 第一次被拉低时,已经锁存过几次 */
|
||||
uint32_t enable_out; /* OE# 第一次被拉低那一刻,输出锁存器里是什么 */
|
||||
} chain_t;
|
||||
|
||||
static chain_t s_ch;
|
||||
|
||||
void sim_sr595_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
void sim_sr595_gpio_hook(int pin, int level);
|
||||
|
||||
void sim_sr595_reset_internal(void) { memset(&s_ch, 0, sizeof(s_ch)); }
|
||||
|
||||
void sim_sr595_attach(int ser_pin, int srclk_pin, int rclk_pin, int oe_pin)
|
||||
{
|
||||
memset(&s_ch, 0, sizeof(s_ch));
|
||||
s_ch.attached = true;
|
||||
s_ch.ser_pin = ser_pin;
|
||||
s_ch.srclk_pin = srclk_pin;
|
||||
s_ch.rclk_pin = rclk_pin;
|
||||
s_ch.oe_pin = oe_pin;
|
||||
/* `OE#` 上电就被 R609 拉高 —— 高阻,不是 0。 */
|
||||
s_ch.oe = 1;
|
||||
/* 上电后移位寄存器与输出锁存器的内容**不确定**(`MR#` 接死在 +3V3,没有复位线)。
|
||||
这里塞一个显眼的非零值:驱动要是先使能 OE# 再锁存安全值,
|
||||
`sim_sr595_enable_outputs()` 就会把这坨脏东西原样报出来。 */
|
||||
s_ch.shift = 0x00A5A5A5u & 0x00FFFFFFu;
|
||||
s_ch.out = s_ch.shift;
|
||||
}
|
||||
|
||||
void sim_sr595_gpio_hook(int pin, int level)
|
||||
{
|
||||
if (!s_ch.attached) {
|
||||
return;
|
||||
}
|
||||
level = level ? 1 : 0;
|
||||
|
||||
if (pin == s_ch.ser_pin) {
|
||||
s_ch.ser = level;
|
||||
} else if (pin == s_ch.srclk_pin) {
|
||||
if (level == 1 && s_ch.srclk == 0) { /* 上升沿:整条链往编号大的方向挪一格 */
|
||||
s_ch.shift = ((s_ch.shift << 1) | (uint32_t)s_ch.ser) & 0x00FFFFFFu;
|
||||
s_ch.clocks++;
|
||||
}
|
||||
s_ch.srclk = level;
|
||||
} else if (pin == s_ch.rclk_pin) {
|
||||
if (level == 1 && s_ch.rclk == 0) {
|
||||
s_ch.out = s_ch.shift;
|
||||
s_ch.latches++;
|
||||
}
|
||||
s_ch.rclk = level;
|
||||
} else if (pin == s_ch.oe_pin) {
|
||||
if (level == 0 && s_ch.oe != 0 && !s_ch.enabled_once) {
|
||||
s_ch.enabled_once = true;
|
||||
s_ch.enable_latches = s_ch.latches;
|
||||
s_ch.enable_out = s_ch.out;
|
||||
}
|
||||
s_ch.oe = level;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t sim_sr595_outputs(void) { return s_ch.out; }
|
||||
uint32_t sim_sr595_shift(void) { return s_ch.shift; }
|
||||
uint32_t sim_sr595_clocks(void) { return s_ch.clocks; }
|
||||
uint32_t sim_sr595_latches(void) { return s_ch.latches; }
|
||||
int sim_sr595_oe(void) { return s_ch.oe; }
|
||||
bool sim_sr595_enabled(void) { return s_ch.enabled_once; }
|
||||
uint32_t sim_sr595_enable_latches(void) { return s_ch.enable_latches; }
|
||||
uint32_t sim_sr595_enable_outputs(void) { return s_ch.enable_out; }
|
||||
|
||||
int sim_sr595_pin(int bit)
|
||||
{
|
||||
assert(bit >= 0 && bit < 24);
|
||||
if (s_ch.oe) {
|
||||
return -1; /* 高阻。栅极侧靠自己的 100 kΩ 下拉,HUB_RST 侧靠 R716 上拉 */
|
||||
}
|
||||
return (int)((s_ch.out >> bit) & 1u);
|
||||
}
|
||||
@@ -32,6 +32,7 @@ set(PHONERAIL_HAL_SIM_SRCS
|
||||
${PHONERAIL_HAL_DIR}/sim/sim_hal.c
|
||||
${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/hal_pwm_sim.c
|
||||
${PHONERAIL_HAL_DIR}/sim/hal_time_sim.c
|
||||
)
|
||||
@@ -41,6 +42,7 @@ set(PHONERAIL_DRV_DIR ${FW_ROOT}/components/drv)
|
||||
set(PHONERAIL_DRV_SRCS
|
||||
${PHONERAIL_DRV_DIR}/ina226.c
|
||||
${PHONERAIL_DRV_DIR}/dac63204.c
|
||||
${PHONERAIL_DRV_DIR}/sr595.c
|
||||
)
|
||||
|
||||
add_library(phonerail_core STATIC
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#define PHONERAIL_H
|
||||
|
||||
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
||||
#define PHONERAIL_FW_VERSION "0.1.0-a4"
|
||||
#define PHONERAIL_FW_VERSION "0.1.0-a5"
|
||||
|
||||
/* 通道数。五路直供,见设计文档 §2。 */
|
||||
#define PHONERAIL_RAIL_COUNT 5
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
/* A5:74HC595 24 位链 + 影子寄存器单一写入点。
|
||||
两条验收断言(固件开发计划 §5 A5 那一行)都打了 ⭐:
|
||||
① 改 CH1 的按键**不动** CH2~5 的位
|
||||
② 字节顺序 **U33 → U22 → U21**
|
||||
|
||||
全部断言都对着 `sim_sr595_*`(按 datasheet 建模的假移位链,只看线上真实发生的
|
||||
边),不对着驱动自己的影子变量 —— 后者是自己证明自己。 */
|
||||
#include "hal_gpio.h"
|
||||
#include "phonerail.h"
|
||||
#include "sim_hal.h"
|
||||
#include "sr595.h"
|
||||
#include "unity.h"
|
||||
|
||||
void setUp(void) { sim_hal_reset(); }
|
||||
void tearDown(void) {}
|
||||
|
||||
static void attach(void)
|
||||
{
|
||||
sim_sr595_attach(SR595_PIN_SER, SR595_PIN_SRCLK, SR595_PIN_RCLK, SR595_PIN_OE_N);
|
||||
}
|
||||
|
||||
static void up(void)
|
||||
{
|
||||
attach();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_init());
|
||||
}
|
||||
|
||||
/* 链上某一位现在是什么(输出锁存器,不是影子)。 */
|
||||
static int out_bit(int bit) { return (int)((sim_sr595_outputs() >> bit) & 1u); }
|
||||
|
||||
/* ---- 位表:只能查,不能算 ------------------------------------------------- */
|
||||
|
||||
/* 位表逐条对着 `.claude/pcb1-parts.json` 里的真实焊盘网络(设计文档 §26.18 C)。
|
||||
这张表**不是任何公式**:CH2/CH3 在链尾那片 U33 上,CH5 的三个键还是倒着排的。 */
|
||||
static void test_bit_table_matches_the_board(void)
|
||||
{
|
||||
/* CH1 -> U21.QB QC QD */
|
||||
TEST_ASSERT_EQUAL_INT(1, sr595_key_bit(1, SR595_KEY_PWR));
|
||||
TEST_ASSERT_EQUAL_INT(2, sr595_key_bit(1, SR595_KEY_VUP));
|
||||
TEST_ASSERT_EQUAL_INT(3, sr595_key_bit(1, SR595_KEY_VDN));
|
||||
/* CH2 -> U33.QD QB QC */
|
||||
TEST_ASSERT_EQUAL_INT(19, sr595_key_bit(2, SR595_KEY_PWR));
|
||||
TEST_ASSERT_EQUAL_INT(17, sr595_key_bit(2, SR595_KEY_VUP));
|
||||
TEST_ASSERT_EQUAL_INT(18, sr595_key_bit(2, SR595_KEY_VDN));
|
||||
/* CH3 -> U33.QG QF QE */
|
||||
TEST_ASSERT_EQUAL_INT(22, sr595_key_bit(3, SR595_KEY_PWR));
|
||||
TEST_ASSERT_EQUAL_INT(21, sr595_key_bit(3, SR595_KEY_VUP));
|
||||
TEST_ASSERT_EQUAL_INT(20, sr595_key_bit(3, SR595_KEY_VDN));
|
||||
/* CH4 -> U22.QB QC QD */
|
||||
TEST_ASSERT_EQUAL_INT(9, sr595_key_bit(4, SR595_KEY_PWR));
|
||||
TEST_ASSERT_EQUAL_INT(10, sr595_key_bit(4, SR595_KEY_VUP));
|
||||
TEST_ASSERT_EQUAL_INT(11, sr595_key_bit(4, SR595_KEY_VDN));
|
||||
/* CH5 -> U22.QH QG QF —— 倒着的 */
|
||||
TEST_ASSERT_EQUAL_INT(15, sr595_key_bit(5, SR595_KEY_PWR));
|
||||
TEST_ASSERT_EQUAL_INT(14, sr595_key_bit(5, SR595_KEY_VUP));
|
||||
TEST_ASSERT_EQUAL_INT(13, sr595_key_bit(5, SR595_KEY_VDN));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(16, sr595_edl_bit(1)); /* U33.QA —— 只剩这一路还在 U33 */
|
||||
TEST_ASSERT_EQUAL_INT(7, sr595_edl_bit(2)); /* U21.QH */
|
||||
TEST_ASSERT_EQUAL_INT(6, sr595_edl_bit(3)); /* U21.QG */
|
||||
TEST_ASSERT_EQUAL_INT(0, sr595_edl_bit(4)); /* U21.QA */
|
||||
TEST_ASSERT_EQUAL_INT(5, sr595_edl_bit(5)); /* U21.QF */
|
||||
}
|
||||
|
||||
/* 把「一定会被写出来的那个算式」按死:三键顺排 `bit = 3(rail−1) + key`。
|
||||
⚠️ CH4 **碰巧**就是 9/10/11,和公式一模一样 —— 所以这里只能整体断言
|
||||
「15 位里只有 CH4 那三位对得上」,不能逐路断言「都不等于公式」。
|
||||
写成算式的症状是**按 CH1 的电源键按到了别路的音量键**:板子照样正常工作,
|
||||
功能测试测不出来(§26.18)。 */
|
||||
static void test_bit_table_is_not_a_formula(void)
|
||||
{
|
||||
int naive_hits = 0;
|
||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||
for (int k = 0; k < SR595_KEY_COUNT; k++) {
|
||||
if (sr595_key_bit(rail, (sr595_key_t)k) == 3 * (rail - 1) + k) {
|
||||
naive_hits++;
|
||||
}
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(3, naive_hits,
|
||||
"15 位里只有 CH4 的三位碰巧和顺排公式重合");
|
||||
|
||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||
/* 每一路的三个键在表里**未必**是 PWR<VUP<VDN 的升序(CH3 / CH5 是倒着的) */
|
||||
if (rail == 3 || rail == 5) {
|
||||
TEST_ASSERT_TRUE(sr595_key_bit(rail, SR595_KEY_PWR) >
|
||||
sr595_key_bit(rail, SR595_KEY_VDN));
|
||||
}
|
||||
/* 「U33 是 EDL 专用片、EDL_ENn 在它的第 n−1 位」—— 只剩 EDL_EN1 还对 */
|
||||
if (rail >= 2) {
|
||||
TEST_ASSERT_NOT_EQUAL_INT(16 + (rail - 1), sr595_edl_bit(rail));
|
||||
}
|
||||
}
|
||||
|
||||
/* CH2 / CH3 的六个键位全在链尾那片 U33(bit16~23)上,一个都不在 U21 */
|
||||
for (int k = 0; k < SR595_KEY_COUNT; k++) {
|
||||
TEST_ASSERT_TRUE(sr595_key_bit(2, (sr595_key_t)k) >= 16);
|
||||
TEST_ASSERT_TRUE(sr595_key_bit(3, (sr595_key_t)k) >= 16);
|
||||
}
|
||||
}
|
||||
|
||||
/* 21 个信号占满 21 个有效位,一位不多一位不少,空脚一个都没被占。启动自检用。 */
|
||||
static void test_map_ok(void)
|
||||
{
|
||||
TEST_ASSERT_TRUE(sr595_map_ok());
|
||||
TEST_ASSERT_EQUAL_HEX32(0x00FFEEEFu, SR595_MASK_VALID); /* 去掉 bit4/8/12 */
|
||||
|
||||
uint32_t keys = sr595_all_keys_mask();
|
||||
int n = 0;
|
||||
for (int b = 0; b < SR595_BITS; b++) {
|
||||
n += (int)((keys >> b) & 1u);
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(15, n, "15 路侧键");
|
||||
/* 按键掩码里不许混进 EDL 与 HUB_RST */
|
||||
TEST_ASSERT_EQUAL_HEX32(0, keys & ((uint32_t)1u << SR595_BIT_HUB_RST));
|
||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||
TEST_ASSERT_EQUAL_HEX32(0, keys & ((uint32_t)1u << sr595_edl_bit(rail)));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- ② 字节顺序 ---------------------------------------------------------- */
|
||||
|
||||
/* ⭐ 验收主项二:一帧的字节顺序是 **U33 → U22 → U21**。
|
||||
先移入的位走得最远,所以链尾那片的字节要先发。发反了 U21 的字节会停在 U33 上
|
||||
—— 现象是「按 CH1 的电源键,CH3 的手机弹出了 EDL」。
|
||||
这里靠的是假移位链自己按 datasheet 移的结果,不是驱动的影子变量。 */
|
||||
static void test_byte_order_is_u33_u22_u21(void)
|
||||
{
|
||||
up();
|
||||
|
||||
/* 三片各给一个只有该片认得的图案,这样字节走错片一眼就看出来。 */
|
||||
const uint32_t pattern = 0x00A1B2C3u & SR595_MASK_VALID;
|
||||
sr595_modify(SR595_MASK_VALID, pattern);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_flush());
|
||||
|
||||
TEST_ASSERT_EQUAL_HEX32(pattern, sim_sr595_outputs());
|
||||
TEST_ASSERT_EQUAL_HEX32_MESSAGE(pattern & 0xFFu, sim_sr595_outputs() & 0xFFu,
|
||||
"U21 = 影子的 bit0~7");
|
||||
TEST_ASSERT_EQUAL_HEX32_MESSAGE((pattern >> 8) & 0xFFu, (sim_sr595_outputs() >> 8) & 0xFFu,
|
||||
"U22 = 影子的 bit8~15");
|
||||
TEST_ASSERT_EQUAL_HEX32_MESSAGE((pattern >> 16) & 0xFFu, (sim_sr595_outputs() >> 16) & 0xFFu,
|
||||
"U33 = 影子的 bit16~23(链尾,字节要先发)");
|
||||
|
||||
/* 字节顺序反过来发的话,输出会变成字节序倒过来的那个值 —— 图案不对称,认得出来。 */
|
||||
uint32_t reversed = ((pattern & 0xFFu) << 16) | (pattern & 0xFF00u) | ((pattern >> 16) & 0xFFu);
|
||||
TEST_ASSERT_NOT_EQUAL_HEX32(reversed, sim_sr595_outputs());
|
||||
}
|
||||
|
||||
/* 一帧正好 24 个 SRCLK 上升沿 + 1 个 RCLK 上升沿。
|
||||
多打一个时钟整条链就转一格(**每一位都错开一位**,症状是十五个按键全乱套),
|
||||
而只看最终电压电流是完全正常的。 */
|
||||
static void test_one_frame_is_24_clocks_and_1_latch(void)
|
||||
{
|
||||
up();
|
||||
uint32_t c0 = sim_sr595_clocks(), l0 = sim_sr595_latches();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_write(sr595_key_mask(2), sr595_key_mask(2)));
|
||||
TEST_ASSERT_EQUAL_UINT32(c0 + 24, sim_sr595_clocks());
|
||||
TEST_ASSERT_EQUAL_UINT32(l0 + 1, sim_sr595_latches());
|
||||
}
|
||||
|
||||
/* RCLK 打点之前输出一点不变 ⇒ 整帧是原子的,组合键不会先单按一个。
|
||||
假移位链把「移位寄存器」和「输出锁存器」分开建模,正是为了能断言这一条。 */
|
||||
static void test_frame_is_atomic(void)
|
||||
{
|
||||
up();
|
||||
uint32_t before = sim_sr595_outputs();
|
||||
|
||||
/* 手动把一帧发一半:只移 8 位,不打 RCLK。 */
|
||||
for (int i = 0; i < 8; i++) {
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_write(SR595_PIN_SER, 1));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_write(SR595_PIN_SRCLK, 1));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_write(SR595_PIN_SRCLK, 0));
|
||||
}
|
||||
TEST_ASSERT_NOT_EQUAL_HEX32(before, sim_sr595_shift()); /* 移位寄存器动了 */
|
||||
TEST_ASSERT_EQUAL_HEX32(before, sim_sr595_outputs()); /* 输出一点没动 */
|
||||
}
|
||||
|
||||
/* ---- ① 改一路不动别路 ---------------------------------------------------- */
|
||||
|
||||
/* ⭐ 验收主项一:改 CH1 的按键,CH2~5 的 12 位一位都不许动。
|
||||
这是「全固件只有一个函数能写 595」的核心理由 —— 多处直接操作移位链的症状是
|
||||
**另一台手机的按键被顺手清掉**,要两个动作正好撞在一起才复现。 */
|
||||
static void test_changing_ch1_leaves_ch2_to_ch5_alone(void)
|
||||
{
|
||||
up();
|
||||
|
||||
/* 先让 CH2~5 各按住一个不同的键,作为「别人的现场」。 */
|
||||
uint32_t others = 0;
|
||||
others |= (uint32_t)1u << sr595_key_bit(2, SR595_KEY_PWR);
|
||||
others |= (uint32_t)1u << sr595_key_bit(3, SR595_KEY_VUP);
|
||||
others |= (uint32_t)1u << sr595_key_bit(4, SR595_KEY_VDN);
|
||||
others |= (uint32_t)1u << sr595_key_bit(5, SR595_KEY_PWR);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_write(others, others));
|
||||
TEST_ASSERT_EQUAL_HEX32(others, sim_sr595_outputs() & sr595_all_keys_mask());
|
||||
|
||||
/* CH1 按下电源键 -> 松开。两次之后,别路的四位必须原封不动。 */
|
||||
uint32_t k1_pwr = (uint32_t)1u << sr595_key_bit(1, SR595_KEY_PWR);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_write(k1_pwr, k1_pwr));
|
||||
TEST_ASSERT_EQUAL_HEX32_MESSAGE(others, sim_sr595_outputs() & ~sr595_key_mask(1) & sr595_all_keys_mask(),
|
||||
"按下 CH1 动了别路的位");
|
||||
TEST_ASSERT_EQUAL_INT(1, out_bit(sr595_key_bit(1, SR595_KEY_PWR)));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_write(k1_pwr, 0));
|
||||
TEST_ASSERT_EQUAL_HEX32_MESSAGE(others, sim_sr595_outputs() & ~sr595_key_mask(1) & sr595_all_keys_mask(),
|
||||
"松开 CH1 动了别路的位");
|
||||
TEST_ASSERT_EQUAL_INT(0, out_bit(sr595_key_bit(1, SR595_KEY_PWR)));
|
||||
|
||||
/* 顺带:HUB_RST 从头到尾没被别人顺手清掉。 */
|
||||
TEST_ASSERT_EQUAL_INT(1, out_bit(SR595_BIT_HUB_RST));
|
||||
}
|
||||
|
||||
/* 「一路全松开」用 sr595_key_mask(),同样不许碰别路,也不许碰本路的 EDL_EN。 */
|
||||
static void test_releasing_one_rail_keeps_edl_and_others(void)
|
||||
{
|
||||
up();
|
||||
|
||||
uint32_t all_keys = sr595_all_keys_mask();
|
||||
uint32_t edl3 = (uint32_t)1u << sr595_edl_bit(3);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_write(all_keys | edl3, all_keys | edl3));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_write(sr595_key_mask(3), 0));
|
||||
TEST_ASSERT_EQUAL_HEX32(0, sim_sr595_outputs() & sr595_key_mask(3));
|
||||
TEST_ASSERT_EQUAL_HEX32(all_keys & ~sr595_key_mask(3), sim_sr595_outputs() & all_keys);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, out_bit(sr595_edl_bit(3)), "松开按键不该碰 EDL_EN3");
|
||||
}
|
||||
|
||||
/* 组合键(VOL− + PWR)必须落在**同一帧**里:一次 sr595_write() 两位一起给。
|
||||
分两次写会先单按一个键 —— EDL 的组合键对顺序是敏感的(需求 §10.6)。 */
|
||||
static void test_combo_lands_in_one_frame(void)
|
||||
{
|
||||
up();
|
||||
uint32_t l0 = sim_sr595_latches();
|
||||
|
||||
uint32_t combo = ((uint32_t)1u << sr595_key_bit(4, SR595_KEY_PWR)) |
|
||||
((uint32_t)1u << sr595_key_bit(4, SR595_KEY_VDN));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_write(combo, combo));
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(l0 + 1, sim_sr595_latches(), "组合键必须一帧发完");
|
||||
TEST_ASSERT_EQUAL_HEX32(combo, sim_sr595_outputs() & sr595_key_mask(4));
|
||||
}
|
||||
|
||||
/* ---- 上电顺序与 HUB_RST -------------------------------------------------- */
|
||||
|
||||
/* `HUB_RST` 是**低有效**(USB2517I 的 RESET_N,设计文档 §21.7)。
|
||||
影子全 0 会把 Hub 一直摁在复位里 —— 现象是「插上枚举不出来」,
|
||||
而五路手机的电压电流全都正常。所以上电安全值不是 0。 */
|
||||
static void test_hub_rst_is_active_low_and_released_after_init(void)
|
||||
{
|
||||
up();
|
||||
TEST_ASSERT_EQUAL_HEX32(SR595_SHADOW_SAFE, sr595_shadow());
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, out_bit(SR595_BIT_HUB_RST), "上电后 Hub 不能被摁在复位里");
|
||||
|
||||
sr595_set_hub_reset(true);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_flush());
|
||||
TEST_ASSERT_EQUAL_INT(0, out_bit(SR595_BIT_HUB_RST));
|
||||
|
||||
sr595_set_hub_reset(false);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_flush());
|
||||
TEST_ASSERT_EQUAL_INT(1, out_bit(SR595_BIT_HUB_RST));
|
||||
|
||||
/* 复位 Hub 期间 15 个按键一个都不许被按下去。 */
|
||||
sr595_set_hub_reset(true);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_flush());
|
||||
TEST_ASSERT_EQUAL_HEX32(0, sim_sr595_outputs() & sr595_all_keys_mask());
|
||||
}
|
||||
|
||||
/* 上电顺序:先锁存安全值,**再**使能 OE#。
|
||||
换个个儿 = 把移位寄存器里的随机值放到 15 个栅极上,也就是五台手机的电源键
|
||||
可能一起被按下去(设计文档 §8.1「复位安全三条」)。
|
||||
假移位链 attach 时被塞了 0xA5A5A5,正是用来照出这个错的。 */
|
||||
static void test_outputs_are_enabled_only_after_a_safe_latch(void)
|
||||
{
|
||||
attach();
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, sim_sr595_oe(), "上电时 OE# 由 R609 拉高");
|
||||
TEST_ASSERT_FALSE(sim_sr595_enabled());
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_init());
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(sim_sr595_enabled(), "init 之后 OE# 必须被拉低");
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_sr595_oe());
|
||||
TEST_ASSERT_TRUE_MESSAGE(sim_sr595_enable_latches() >= 1,
|
||||
"OE# 拉低之前一帧都还没锁存过 —— 15 个栅极上是随机值");
|
||||
TEST_ASSERT_EQUAL_HEX32_MESSAGE(SR595_SHADOW_SAFE, sim_sr595_enable_outputs(),
|
||||
"使能那一刻链上不是安全值");
|
||||
TEST_ASSERT_EQUAL_HEX32(0, sim_sr595_outputs() & sr595_all_keys_mask());
|
||||
}
|
||||
|
||||
/* ---- 影子与刷新 ---------------------------------------------------------- */
|
||||
|
||||
/* 「改影子 -> 请求刷新」是两步:`sr595_modify()` 一个字节都不许发出去。 */
|
||||
static void test_modify_does_not_touch_the_chain(void)
|
||||
{
|
||||
up();
|
||||
uint32_t c0 = sim_sr595_clocks(), out0 = sim_sr595_outputs();
|
||||
|
||||
sr595_modify(sr595_key_mask(5), sr595_key_mask(5));
|
||||
TEST_ASSERT_TRUE(sr595_dirty());
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(c0, sim_sr595_clocks(), "modify 不许碰总线");
|
||||
TEST_ASSERT_EQUAL_HEX32(out0, sim_sr595_outputs());
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_flush());
|
||||
TEST_ASSERT_FALSE(sr595_dirty());
|
||||
TEST_ASSERT_EQUAL_HEX32(sr595_shadow(), sim_sr595_outputs());
|
||||
}
|
||||
|
||||
/* 不脏就不空发一帧:24 个时钟 + 一次锁存不是免费的,而且真板子上每发一帧
|
||||
都要占住那三根线。写一个和现在一样的值同样不算脏。 */
|
||||
static void test_flush_is_a_noop_when_clean(void)
|
||||
{
|
||||
up();
|
||||
uint32_t c0 = sim_sr595_clocks(), l0 = sim_sr595_latches();
|
||||
|
||||
TEST_ASSERT_FALSE(sr595_dirty());
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_flush());
|
||||
TEST_ASSERT_EQUAL_UINT32(c0, sim_sr595_clocks());
|
||||
TEST_ASSERT_EQUAL_UINT32(l0, sim_sr595_latches());
|
||||
|
||||
sr595_set_hub_reset(false); /* 已经是这个值了 */
|
||||
TEST_ASSERT_FALSE(sr595_dirty());
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_flush());
|
||||
TEST_ASSERT_EQUAL_UINT32(l0, sim_sr595_latches());
|
||||
}
|
||||
|
||||
/* value 里 mask 以外的位不许漏进影子(`sr595_write(bit, 0xFFFFFFFF)` 这种写法)。 */
|
||||
static void test_modify_only_touches_the_masked_bits(void)
|
||||
{
|
||||
up();
|
||||
uint32_t k2 = (uint32_t)1u << sr595_key_bit(2, SR595_KEY_VUP);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_write(k2, 0xFFFFFFFFu));
|
||||
TEST_ASSERT_EQUAL_HEX32(SR595_SHADOW_SAFE | k2, sim_sr595_outputs());
|
||||
}
|
||||
|
||||
/* 发送失败(把 SER 那根脚踢回输入,模拟「没配成输出就用」)要留着脏,
|
||||
下一次 flush 重发**整帧**。丢了脏标志 = 链上和影子永久对不上,
|
||||
而后面每一次 flush 都会「成功」。 */
|
||||
static void test_failed_flush_stays_dirty(void)
|
||||
{
|
||||
up();
|
||||
uint32_t k1 = (uint32_t)1u << sr595_key_bit(1, SR595_KEY_VDN);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_config(SR595_PIN_SER, HAL_GPIO_INPUT));
|
||||
sr595_modify(k1, k1);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, sr595_flush());
|
||||
TEST_ASSERT_TRUE_MESSAGE(sr595_dirty(), "发失败必须留着脏");
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_config(SR595_PIN_SER, HAL_GPIO_OUTPUT));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_flush());
|
||||
TEST_ASSERT_EQUAL_HEX32(SR595_SHADOW_SAFE | k1, sim_sr595_outputs());
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_bit_table_matches_the_board);
|
||||
RUN_TEST(test_bit_table_is_not_a_formula);
|
||||
RUN_TEST(test_map_ok);
|
||||
RUN_TEST(test_byte_order_is_u33_u22_u21);
|
||||
RUN_TEST(test_one_frame_is_24_clocks_and_1_latch);
|
||||
RUN_TEST(test_frame_is_atomic);
|
||||
RUN_TEST(test_changing_ch1_leaves_ch2_to_ch5_alone);
|
||||
RUN_TEST(test_releasing_one_rail_keeps_edl_and_others);
|
||||
RUN_TEST(test_combo_lands_in_one_frame);
|
||||
RUN_TEST(test_hub_rst_is_active_low_and_released_after_init);
|
||||
RUN_TEST(test_outputs_are_enabled_only_after_a_safe_latch);
|
||||
RUN_TEST(test_modify_does_not_touch_the_chain);
|
||||
RUN_TEST(test_flush_is_a_noop_when_clean);
|
||||
RUN_TEST(test_modify_only_touches_the_masked_bits);
|
||||
RUN_TEST(test_failed_flush_stays_dirty);
|
||||
return UNITY_END();
|
||||
}
|
||||
+134
-8
@@ -662,8 +662,8 @@ CC1/CC2 各 5.1 kΩ 下拉,对外表现为 UFP。
|
||||
|---|---|---|---|
|
||||
| INA226 | U8 ~ U12 | CH1~CH5 输出 V + I(5mΩ 分流) | **0x40 / 0x41 / 0x44 / 0x45 / 0x42** |
|
||||
| INA226 | U13 | 输入侧 V + I(整板功率预算) | **0x43**(A1=GND, A0=SCL) |
|
||||
| DAC63204 | U6 | VDAC1 / VDAC2 / VDAC3 | **0x4A**(A0 接 SDA) |
|
||||
| DAC63204 | U7 | VDAC4 / VDAC5 | **0x4B**(A0 接 SCL) |
|
||||
| DAC63204 | U6 | **VDAC1 / VDAC2 / VDAC4**(§26.18 重排过,不是 1/2/3)| **0x4A**(A0 接 SDA) |
|
||||
| DAC63204 | U7 | **VDAC3 / VDAC5** | **0x4B**(A0 接 SCL) |
|
||||
| PCA9555 | U23 | 5×FLT + 5×PG + BUCK_PG + INA_ALERT | **0x20**(A0=A1=A2=GND) |
|
||||
| **LM75BD** | **U34(载板)** | 载板温度,风扇闭环用它 | **0x48**(A0=A1=A2=GND)|
|
||||
|
||||
@@ -2202,13 +2202,40 @@ P3-CONTROL / P3B-EXPAND / P4-USB / P5-HARNESS,共 10 页。
|
||||
|
||||
### 17.2 P3B-EXPAND
|
||||
|
||||
**74HC595 级联**:U21 在前(DS 接 ESP32 的 KEY_SER),`U21.Q7S → U22.DS`。
|
||||
先移入的位落到 **U22.Q7**。输出分配:
|
||||
**74HC595 级联**(三片一条 24 位链,U33 在 P6 那一页,见 §22.4):
|
||||
|
||||
| | Q0 | Q1 | Q2 | Q3 | Q4 | Q5 | Q6 | Q7 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| U21 | K1_PWR | K1_VUP | K1_VDN | K2_PWR | K2_VUP | K2_VDN | K3_PWR | K3_VUP |
|
||||
| U22 | K3_VDN | K4_PWR | K4_VUP | K4_VDN | K5_PWR | K5_VUP | K5_VDN | 空 |
|
||||
```
|
||||
ESP32.KEY_SER → U21.DS(14) U21.Q7S(9) ──SR_CAS──> U22.DS U22.Q7S(9) ──SR_CAS2──> U33.DS
|
||||
U33.Q7S(9) 不接(链尾)
|
||||
```
|
||||
|
||||
先移入的位走得最远 ⇒ 落到 **U33.QH**。所以固件发送顺序是
|
||||
**U33 字节 → U22 字节 → U21 字节**,每个字节 **QH 先发**,最后一个 RCLK 上升沿统一生效。
|
||||
|
||||
输出分配(**2026-09-06 按 PCB 几何重排,取代 09-04 那一版**,§26.18 C。
|
||||
下表是 2026-09-06 从 `.claude/pcb1-parts.json` 的**真实焊盘网络**逐脚核回来的):
|
||||
|
||||
| 影子位 | | QA(15) | QB(1) | QC(2) | QD(3) | QE(4) | QF(5) | QG(6) | QH(7) |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| 0~7 | **U21** | EDL_EN4 | K1_PWR | K1_VUP | K1_VDN | 空 | EDL_EN5 | EDL_EN3 | EDL_EN2 |
|
||||
| 8~15 | **U22** | 空 | K4_PWR | K4_VUP | K4_VDN | 空 | K5_VDN | K5_VUP | K5_PWR |
|
||||
| 16~23 | **U33** | EDL_EN1 | K2_VUP | K2_VDN | K2_PWR | K3_VDN | K3_VUP | K3_PWR | HUB_RST |
|
||||
|
||||
约束是**每一路的三个键必须落在同一片的连续三脚上**(出芯片是一束线),
|
||||
所以三个键的位号挨着,但**顺序每一路都不一样** —— CH3 / CH5 是倒着的。
|
||||
|
||||
> ⚠️ **「K1/K2/K3 顺排在 U21、U33 是 EDL 专用片」是旧版**(09-04 那张表),
|
||||
> 15 路里有 12 路对不上。三片本来就是一条链,谁在哪一位只是移位的字节序 ⇒
|
||||
> **搞错了板子照样正常工作,只是按 CH1 的电源键按到了 CH4 的音量键**,
|
||||
> 功能测试完全测不出来。和 DAC63204 的通道分配是同一类坑。
|
||||
> 固件里查表不要算:`sr595_key_bit()` / `sr595_edl_bit()`(A5 已落地,
|
||||
> `test_bit_table_is_not_a_formula` 专门拦这一手 —— ⚠️ **CH4 碰巧就是 9/10/11**,
|
||||
> 和顺排公式一模一样,所以那条测试只能整体断言「15 位里只有三位重合」)。
|
||||
>
|
||||
> ⚠️ **`HUB_RST` 是低有效**(USB2517I 的 `RESET_N`,§21.7,R716 10 kΩ 上拉):
|
||||
> 影子位 = 1 才是「Hub 正常跑」。全 0 的上电影子会把 Hub 一直摁在复位里 ——
|
||||
> 现象是「插上枚举不出来」,而五路手机的电压电流全都正常。
|
||||
> 所以固件的上电安全影子是 `1 << 23`,不是 0。
|
||||
|
||||
`MR#` 接 +3V3,`OE#` 走 KEY_OE# 并在本页用 **100 kΩ 上拉到 +3V3**——
|
||||
ESP32 复位期间 GPIO 高阻,595 输出随之高阻,配合 P2 上每个栅极的 100 kΩ 下拉,
|
||||
@@ -6998,3 +7025,102 @@ I²C 不受影响(`SDA` / `SCL` / `INA_ALERT` 都不在重排名单里)。
|
||||
2. **DRC 的「连接性」和「间距」新鲜度不一样。** 程序化建线之后连接性那一支是陈旧的
|
||||
(实测 `location.reload()` 之后 855 → 198),**但间距那一支是即时的** ——
|
||||
重载前后都是 74 条、内容逐字相同 ⇒ 间距报出来的东西**不能当成陈旧数据糊弄过去**。
|
||||
|
||||
#### 26.21b 善后跑下来的三条(都值一条规矩)
|
||||
|
||||
1. **`--ripup` 能把一个碎片的铜拆到一根不剩** ⇒ `Comp.shapes` 空表 ⇒
|
||||
`E._bbox()` 里 `min([])` 抛 `ValueError`。而 `pcb1_route_sig` 是**跑完才一次性下发**的,
|
||||
一崩整轮白跑 —— 实测那次板上正停在「530 段拆了没补」。
|
||||
⇒ `connect()` 开头加防护:**空碎片当成"这一条布不通",不许掀桌子**。
|
||||
2. **铺铜边界不要卡在净距的整数上。** `VINSW-TOP` 东边界 30.47 是贴着 `C6.1` 西边缘
|
||||
30.670 留**整 0.200** 定的。`pcb1_parts_dump.py` 重抓之后(§26.17 挪过 C6 的位置 + 转了 180°)
|
||||
本地焊盘偏移差了 **0.04 mil ≈ 1 µm**,实测就变成 **0.199**,`--push` 当场不下发。
|
||||
⇒ 改 **30.46**,留 0.01 的余量。
|
||||
3. **浮铜检查加了下限 `MIN_FLOAT_AREA = 1.0 mm²`**(`pcb1_pour.py`)。
|
||||
布线一多,过孔反焊盘之间必然夹出 0.25×0.25(**正好一个栅格**)这种碎屑;
|
||||
实测两粒 0.06 / 0.19 mm²,比一个过孔盘还小。
|
||||
门槛之上的两条真问题(南沿 **1312.8 mm²**、`3V3-SW` 里 **7.2 mm²**)一条都不会漏。
|
||||
小于门槛的照样打印,只是不判红。
|
||||
|
||||
#### 26.21c 验收(2026-09-06 第二十五轮)
|
||||
|
||||
| 项 | 修之前 | 修之后 |
|
||||
|---|---|---|
|
||||
| **DRC 间距错误** | **74** | **0** |
|
||||
| DRC 连接性错误 | 855 | 113 |
|
||||
| 已连通网络 | 148 | **176** |
|
||||
| 还欠连接 | 87 条 / 74 网络 | **56 条 / 46 网络** |
|
||||
| 真实几何全板复核(线-焊盘 / 孔-焊盘 / 线-线) | 45 处真重叠 | **0** |
|
||||
| 焊盘网络 板上 vs 缓存 | 44 处不一致 | **0** |
|
||||
| 铺铜 `--verify` | ❌ | ✅ 实测和计划对得上 |
|
||||
| Inner2 I²C 干线 | — | 55 段 / 302 mm,两轮 `--ripup` 一段没丢 |
|
||||
|
||||
---
|
||||
|
||||
### 26.22 剩余布不通的分类处理(2026-09-06 第二十六轮,用户拍板 A-1 + A-2)
|
||||
|
||||
56 条布不通按**失败原因**分五类,处理办法完全不同:
|
||||
|
||||
| 类 | 条数 | 症状 | 处理 |
|
||||
|---|---:|---|---|
|
||||
| **A 焊盘几何上出不去** | ~5 | 出线道 0.31~0.38 mm(要 0.60)| **挪件**(A-1)/ 改走内层(A-2)|
|
||||
| **B USB 差分对** | 4 | 整束 0.7 mm 找不到走廊,布线器主动跳过 | 未做 |
|
||||
| **C 五路母版系统性缺口** | ~14 | `$*N676/680/698/727`,**2.4~5.7 mm 的短跳**在多个通道同时失败 | 未做 |
|
||||
| **D I²C / INA_ALERT 末端** | ~7 | 器件脚下顶层口袋里没有合法孔位 | 未做 |
|
||||
| **E 单发拥堵** | ~26 | 被自己先布的线堵死 | 多跑几轮(`RIP_LIMIT=1` 是有意的,见源码注释)|
|
||||
|
||||
#### ⭐ A-1「够净距」≠「出得去线」——检查体系里缺的一条
|
||||
|
||||
`pcb1_place.pad_clearance_errs()` 判的是**焊盘净距 ≥ 0.20**,
|
||||
而一根 0.2 mm 的线要从两个焊盘中间出去需要 **0.20 + 0.20 + 0.20 = 0.60**。
|
||||
|
||||
`U6` / `U7`(DAC63204,WQFN-16 3×3,**四边出脚**)周围的卫星件正好卡在 **0.31~0.38**:
|
||||
|
||||
| 堵点 | 挡住哪一排 | 道宽 → 挪 0.30 后 |
|
||||
|---|---|---|
|
||||
| `R615` / `R617` | U6/U7 **南排**(含 `U6.6` = SCL)| 0.312 → 0.612 |
|
||||
| `C607` / `C610` | **北排**(含 `U6.16` / `U7.16`)| 0.315 → 0.615 |
|
||||
| `C608` / `C611` | **东列**(VDAC2/3/4/5)| 0.376 → 0.676 |
|
||||
|
||||
**本地六项全过、DRC 也零间距错误**,只有 `pcb1_escape_audit` 报「被围死」、
|
||||
布线器报「几何上就出不去」。实测 14 个脚的出线道 < 0.60,挪完**全部 ≥ 0.60**。
|
||||
|
||||
⇒ `pcb1_place.py` 新增 **`ESCAPE_NUDGE`** 表(在 `POS` 定稿后统一施加),
|
||||
这样以后重跑摆位器不会把这一手抹掉。
|
||||
⚠️ **不能跑 `pcb1_place.py --push`** —— 它会把全部 342 个元件重新下发、
|
||||
把所有布线作废。只对这 6 颗做定点移动。
|
||||
|
||||
**挪完的善后**(一步都不能省):
|
||||
挪动留下 20 处净距违规 + 2 个 GND 焊盘丢了扇出短线 ⇒
|
||||
① 删掉六颗周围 6 mm 内被压到的铜(**44 段 / 106.6 mm / 6 孔**,7 个网络);
|
||||
② 给 `R615.1` / `R617.1` 补「短线 + 过孔」(要求孔心离焊盘 ≥ 0.45,别做成 0402 的盘内孔);
|
||||
③ 重跑 `pcb1_i2c_l3.py`(`C607` / `R617` 的新位置会压到 `SDA` / `SCL` 的换层孔,
|
||||
那两个孔是它每轮重算的,自己会让开);④ 再 `route_sig --ripup --push`。
|
||||
⚠️ **绝不能用 `pcb_fanout.py --clean`** 善后 —— 它会把全板走线删光。
|
||||
|
||||
#### ⭐ A-2 `J25.24` 根本不用换针位
|
||||
|
||||
`pcb1_escape_audit` 报它「被围死」,第一反应是排针内圈针要换针位 ——
|
||||
**查下来是错的**:`J25` 是双排 40 针,**24 号在外排(y=63.77),北边根本没有针**。
|
||||
真正堵住它的是 **`+5V` 那条 0.84 mm 的顶层干线**(L1,y=65.40,横贯 x 107.6~150.9),
|
||||
离焊盘上沿只剩 **0.31 mm**;而底层那一片整个压在 `VO3_P` 小岛下。
|
||||
|
||||
**那条干线在 L1,挡不着内层。** 而排针针是通孔、L3 上本来就有铜 ⇒
|
||||
`POGO*` 从 Inner2 出去,**那一端连过孔都不用打**,
|
||||
不用换针位、**不用动载板 PCB2 的对接定义**。
|
||||
|
||||
⇒ `pcb1_i2c_l3.NETS` 加进 5 条侧键线(`POGO3_PWR` / `POGO3_VDN` / `POGO4_VDN` /
|
||||
`POGO5_PWR` / `POGO5_VUP`,全是 µA 级)。实测**五条全部一次布通**(各 2 片 → 1 片)。
|
||||
|
||||
#### `BUCK_PG` 试过,挪不动 —— 于是候选表改成「自动踢」
|
||||
|
||||
`BUCK_PG` 顶层占 234 mm、跨 121 mm,看着是最该进内层的。实测**五个分支一条都布不通**:
|
||||
它们要去板子四角,而 L3 上 `y < 34` 往东只剩 `x 115~116` 那条 **1.0 mm** 的缝
|
||||
(净距后 0.6,刚好一根线),`+12V` 把别的通道全占了。
|
||||
|
||||
⇒ `pcb1_i2c_l3.py` 的连通性保险从「有一个变差就毙掉整轮」改成
|
||||
**「把变差的踢出候选表、重算一轮、剩下的照常下发」**。
|
||||
⚠️ 改这一段时踩到一个真 bug:第一版只换了 `run_plan` 的网络表,
|
||||
**`drop_l/drop_v` 还是按完整候选表算的** ⇒ `--push` 会把被踢掉那个网络的铜
|
||||
删掉、又不给它重布,板子直接变差**而净距校核照样全绿**。
|
||||
踢网络时**拆除清单必须跟着重算**。
|
||||
|
||||
@@ -475,6 +475,22 @@ ESP32.KEY_SER → U21 → U22 → U33 (先移入的字节落在链尾 U
|
||||
> 的方式「临时全松开」——那会让 15 个栅极同时进高阻,靠的是每个栅极自己的 100 kΩ 下拉,
|
||||
> 是复位路径,不是正常控制路径。
|
||||
|
||||
> **2026-09-06 A5 落地**:`components/drv/sr595.{h,c}`。影子是一个 24 位整数
|
||||
> (bit0 = U21.QA 链首 … bit23 = U33.QH 链尾),唯一写入点是 `sr595_flush()`;
|
||||
> 别处只准 `sr595_modify()` 改影子,组合键走 `sr595_write(mask, value)` 一帧发完。
|
||||
> 位表见设计文档 §17.2(§26.18 重排后的那张,**不是 09-04 那张**)。
|
||||
>
|
||||
> 同处记下三条这一层的实现约定:
|
||||
> - **上电顺序是「先锁存安全值,再使能 `OE#`」,不能换。** 三片的 `MR#` 接死在 +3V3,
|
||||
> 没有复位线 ⇒ 上电时移位寄存器内容**不确定**。先拉低 `OE#` 就是把那坨随机值
|
||||
> 放到 15 个栅极上 —— 五台手机的电源键可能一起被按下去。所以 `sr595_init()` 里
|
||||
> `KEY_OE#` 在安全值锁存完之前**根本不配成输出**,靠板上 R609 上拉着。
|
||||
> (ESP-IDF 的 `gpio_config()` 一配成输出就开始驱动,输出寄存器复位值是 0。)
|
||||
> - **`HUB_RST` 低有效**,所以上电安全影子是 `1 << 23` 而不是 0。写成 0 的现象是
|
||||
> 「Hub 插上枚举不出来」,而五路电压电流全都正常,看不出跟固件有关。
|
||||
> - **发送失败要留着脏**(`sr595_flush()` 失败不清 dirty,下次重发整帧)。
|
||||
> 丢了脏标志 = 链上和影子永久对不上,而后面每一次 flush 都会「成功」。
|
||||
|
||||
### 10.7 事件记录与存储
|
||||
|
||||
事件是 SW-3 的交付物,也是排查排线接触不良的唯一线索,所以**记录路径不能依赖上位机在线**。
|
||||
|
||||
在新工单中引用
屏蔽一个用户