diff --git a/firmware/HANDOFF.md b/firmware/HANDOFF.md index 9e38670..1add3d3 100644 --- a/firmware/HANDOFF.md +++ b/firmware/HANDOFF.md @@ -1,76 +1,93 @@ # 交接 -更新:2026-09-07 完成任务包 **A6**(PCA9555 驱动) +更新:2026-09-07 完成任务包 **A7**(通道配置表 + 表驱动遍历 + 启动自检) ## 一句话现状 -板上唯一的 I/O 扩展器(U23 @ 0x20)**16 个脚全部是输入**:5×FLT# + 5×PGOOD + -BUCK_PG + INA_ALERT + 4 个焊死在地上的备用脚。驱动**根本不提供「配成输出」的接口**, -方向字一律过 `pca9555_dir_safe()`。位表与地址从 PCB 焊盘网络逐脚核回来(与文档一致)。 -8 条 ctest(81 个用例)全绿,`idf.py build` 通过。 +五路的六组编号(INA 地址 / DAC 片通道 / 三个按键位 / EDL 位 / 两根 EN 的 GPIO) +拼成了**一张 const 表** `rail_cfg(1..5)`,开机自检 `rail_selftest()` 把它和四个驱动 +那四张表**逐格对账**,再查一遍全板引脚有没有重复 / 越界,不过就 `abort()`。 +A3~A6 留下的四个自检谓词至此全部有了调用点。9 条 ctest(131 个用例)全绿, +`idf.py build` 通过。 ## 下一步 -**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 是它们的归宿)。 +**A8**:任务骨架与调度(5 个任务、两把锁,需求 §10.3 全节)。 +验收是「sim 时钟下 `t_safety` 周期 20 ms ±1 ms;I²C 占用打印出来与 §10.3 的 8 % 对得上」。 +A7 给它备好的东西:`rail_cfg()` 表驱动遍历、`board_pins.h` 的 SDA/SCL 引脚、 +`sim_gpio_write_seq()`(断言顺序用,A9 的 17 ms 上电时序会更需要它)。 ## 本次新增/修改的文件 -- `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 落地 + 四条实现约定 +- `main/rail_cfg.h` / `main/rail_cfg.c` — 那张表、启动自检、两根 EN 线(≈250 行) +- `main/board_pins.h` — **全板引脚表**(§8.1 里「全板独一份」的那些脚:I²C、风扇、 + NTC、USB、UP_VBUS_DET,加上 strapping / 不存在 / R8 版 PSRAM 三张禁用清单)。 + 五路自己的 `EFUSE_ENn` / `VBUS_ENn` **不在这里**,它们是「每路一个」,在五路表里 +- `main/app_main.c` — 开机跑 `rail_selftest()`,不过就 abort;然后 `rail_en_init()` +- `test/test_rail_cfg.c`(28 个用例) +- `components/phonerail_hal/sim/sim_hal.h` + `sim/hal_gpio_sim.c` — 新增 + `sim_gpio_write_seq()`(全局写序号,断言「先断 VBUS 再关 eFuse」这类顺序用); + **`SIM_GPIO_PINS` 48 → 49** +- `components/phonerail_hal/esp/hal_gpio_esp.c` — **`PIN_MAX` 48 → 49** +- 两处 CMakeLists + `main/phonerail.h`(版本 `0.1.0-a7`) +- `../需求文档.md` §10.4 — 记 A7 落地 + 五条实现约定 + 那张六列表 ## 验收命令与结果 ``` $ bash host/test.sh -100% tests passed, 0 tests failed out of 8 # +test_pca9555(20) +100% tests passed, 0 tests failed out of 9 # +test_rail_cfg(28),总计 131 个用例 $ .\build_idf.ps1 # PowerShell -phonerail.bin binary size 0x32550 bytes. ... 0xcdab0 bytes (80%) free. -Project build complete. # pca9555.c.obj 在(已单独确认) +phonerail.bin binary size 0x34250 bytes. ... 0xcbdb0 bytes (80%) free. +Project build complete. # rail_cfg.c.obj 在(已单独确认) -# 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 条红 +# 16 个「一定会被写出来的错」逐个注进去跑过(一次性脚本,没入库): +# INA 地址改回 0x40+ch / CH3 的 DAC 挪回 U6 / CH4↔CH5 按键位对调 +# EDL 位改回 16+ch / PG 位跟着 FLT 加偏移 +# EFUSE_EN2 撞上 UP_VBUS_DET / EFUSE_EN1 落到 strapping / CH1 两根 EN 写反 +# EN 极性写反 / all_off 先关 eFuse / all_off 第一个错就 return +# en_init 不写那句明文的 0 / 表与驱动对账被删 / 引脚查重被删 +# GPIO 上限退回 48 +# → 以上 15 条全部变红。 +# +# ⚠️ 唯一没能变红的一条:**把 `rail_selftest()` 整个改成 `return 0;`**。 +# 判据本身(`rail_selftest_table()`)已经被测得很死,但「rail_selftest 有没有 +# 真的去调它」这一行接线,在一切正常的前提下测试**原理上分不出来** +# —— 真表是 const 的,没有任何输入能让它该报错。这是已知残留,不是漏做。 ``` ## 踩到的坑(只写会再犯的) -- ⚠️ **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 工具。) +- ⚠️ **HAL 两侧的 GPIO 边界原来都是 `pin < 48`,而 `EFUSE_EN2` 在 IO48 上。** + ESP32-S3 的编号是 **0~48(含 48)**,写成「个数 48」正好把它关在门外, + 现象是 CH2 的 eFuse 既开不了也**关不掉**(`hal_gpio_config()` 直接 HAL_ERR_ARG)。 + 已改成 49,`test_io48_is_a_usable_pin` 是回归拦网。 + 顺带记住:IO22~IO25 在 S3 上**根本不存在**、IO26~IO32 是模组内部 flash/PSRAM, + 写进配置表 `gpio_config()` 既不报错也不干活 —— 自检把这些号也当越界。 +- ⚠️ **「不是顺排」要看整列,逐格撞算式会撞空。** 第一版测试是「拿几个常见错算式 + 逐路比,断言错了几路」。写完逐格核对才发现:CH4 的按键位正好就是 9/10/11、 + CH1 的 EDL 位正好是 16、CH3 的两根 EN 正好落在猜的算式上 —— 三处「错了 5 路」 + 实际都只有 4 路。**猜错算式的测试是句空话**,而且这种空话看上去和真断言一样。 + 改成断言整列**不是等差数列**(`is_arithmetic()`),任何 `起点+步长×ch` 一次拦掉。 +- **注错实验里把源文件 mtime 推到未来**(`os.utime(path, now+5)`):注错与恢复 + 挤在同一秒里,make 有可能认为 `.o` 还是新的、跳过重编,于是跑的是**上一轮**的 + 二进制 —— 那样「注了错还是绿」是假绿,看起来却像「这条测试没覆盖到」。 + ⚠️ 本轮**没有观察到它真的发生**(三条绿是真绿,原因见上面那段), + 这条是预防性的;但假绿这种错一旦发生就会被当成结论写进文档,代价不对称。 + (A6 那两条纪律仍然有效:try/finally 恢复原文;编译期红的一条要重写成能编过的。) +- **判据要能脱离真表被调用**。真表是 const 的,测试改不动,所以「查得出问题吗」 + 这件事本来是测不了的 —— 把判据拎成 `rail_selftest_table(const rail_cfg_t *t)` + 之后,测试复制一份表出来逐样改坏即可。这条比多写十个断言管用。 -## 这个驱动 / 这一层的几条约定(写 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 判故障之前要先确认这一帧是真读回来的,不是失败后的残留缓冲。 +## 这一层的几条约定(写 A8/A9/A10 之前看一眼) +- **`rail_cfg()` 是唯一入口。** 任何地方都不许再出现 `0x40 + rail`、`3 * (rail-1)` + 这类由通道号算出来的编号。六列里没有一列是顺排的,而写错了**板子照样正常工作**, + 只是「重启 CH1」重启了 CH4。 +- ⚠️ **两列 GPIO 没有第二张表能对账**(INA/DAC/595/PCA 那四列都有驱动那边兜着)。 + 所以自检**查不出**「`EFUSE_EN1` 和 `VBUS_EN1` 写反」—— 两个都是合法空闲脚、 + 不重复、不越界。唯一的拦网是 `test_en_pins_match_the_pcb_netlist` 里钉死的十个值 + (出处 `.claude/pcb1-parts.json` 的 U20 焊盘网络)和 B1 上板实测。 +- **`rail_all_off()` 是复位 / 收尾路径,不是故障快路径。** A10 过压要断的是出事 + 那一路,直接调 `rail_efuse_enable(rail, false)`,别等着遍历五路。 +- **开机顺序**:`rail_selftest()` → `rail_en_init()`(十个脚配成输出并拉低)→ + 才轮到 `rail_volt_init()` 等器件。DAC 一上电输出就有电压,EN 这时必须已经确定是低的。 +- **高有效只出现一次**(`en_write()`)。和 `sr595_set_hub_reset()` 的反相、 + `rail_mv_to_code()` 的减法同一条规矩。 +- **自检不过就 abort,不降级。** 它查的全是编译进固件的常量表,不过 = 固件写错了, + §10.10 的降级矩阵管的是硬件坏了。带着错表跑起来的样子是「命令发给了另一路」, + 五台手机全在跑、一个错误码都不报。 diff --git a/firmware/components/phonerail_hal/esp/hal_gpio_esp.c b/firmware/components/phonerail_hal/esp/hal_gpio_esp.c index 336a508..f540418 100644 --- a/firmware/components/phonerail_hal/esp/hal_gpio_esp.c +++ b/firmware/components/phonerail_hal/esp/hal_gpio_esp.c @@ -4,7 +4,7 @@ #include "driver/gpio.h" #include "hal_gpio.h" -#define PIN_MAX 48 /* ESP32-S3 的 GPIO 编号上限,与 sim 保持一致 */ +#define PIN_MAX 49 /* ESP32-S3 的 GPIO 编号是 0~48(**含 48**),与 sim 保持一致 */ /* 记住方向,好让「写一个输入脚」在板子上也返回 HAL_ERR_ARG —— ESP-IDF 的 gpio_set_level() 对输入脚是静默无效的,那正是最难查的一类 bug。 */ diff --git a/firmware/components/phonerail_hal/sim/hal_gpio_sim.c b/firmware/components/phonerail_hal/sim/hal_gpio_sim.c index 0ac5c72..fce9dc4 100644 --- a/firmware/components/phonerail_hal/sim/hal_gpio_sim.c +++ b/firmware/components/phonerail_hal/sim/hal_gpio_sim.c @@ -9,9 +9,11 @@ typedef struct { hal_gpio_mode_t mode; int level; uint32_t writes; + uint32_t seq; /* 最后一次被写时的全局序号,0 = 没写过 */ } pin_t; static pin_t s_pin[SIM_GPIO_PINS]; +static uint32_t s_seq; /* 全局写计数,从 1 开始发号 */ void sim_gpio_reset_internal(void); /* 只给 sim_hal_reset() 用 */ @@ -20,7 +22,11 @@ void sim_gpio_reset_internal(void); /* 只给 sim_hal_reset() 用 */ 而 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)); } +void sim_gpio_reset_internal(void) +{ + memset(s_pin, 0, sizeof(s_pin)); + s_seq = 0; +} static bool valid(int pin) { return pin >= 0 && pin < SIM_GPIO_PINS; } @@ -51,6 +57,7 @@ hal_err_t hal_gpio_write(int pin, int level) } s_pin[pin].level = level ? 1 : 0; s_pin[pin].writes++; + s_pin[pin].seq = ++s_seq; sim_sr595_gpio_hook(pin, s_pin[pin].level); return HAL_OK; } @@ -81,3 +88,5 @@ int sim_gpio_level(int pin) } uint32_t sim_gpio_writes(int pin) { return valid(pin) ? s_pin[pin].writes : 0; } + +uint32_t sim_gpio_write_seq(int pin) { return valid(pin) ? s_pin[pin].seq : 0; } diff --git a/firmware/components/phonerail_hal/sim/sim_hal.h b/firmware/components/phonerail_hal/sim/sim_hal.h index fd9b27e..57f5421 100644 --- a/firmware/components/phonerail_hal/sim/sim_hal.h +++ b/firmware/components/phonerail_hal/sim/sim_hal.h @@ -66,7 +66,10 @@ void sim_i2c_set_present(uint8_t addr, bool present); /* ---------------- GPIO ---------------- */ -#define SIM_GPIO_PINS 48 /* ESP32-S3 的 GPIO 编号上限 */ +/* ESP32-S3 的 GPIO 编号 **0~48**,所以数组要 49 个 —— `EFUSE_EN2` 就在 IO48 上。 + A7 之前这里是 48,于是 CH2 的 eFuse 既开不了也**关不掉**(`hal_gpio_config()` + 直接 HAL_ERR_ARG)。边界写成「个数」而不是「上限」,就是这么错的。 */ +#define SIM_GPIO_PINS 49 /* 外部世界把某个**输入**脚拉成 level。对输出脚调用会 abort(那是总线打架)。 */ void sim_gpio_drive(int pin, int level); @@ -77,6 +80,11 @@ int sim_gpio_level(int pin); /* 被 hal_gpio_write() 写成功过几次。断言「改 CH1 没动 CH2~5」用得上(A5)。 */ uint32_t sim_gpio_writes(int pin); +/* 全局递增的写序号:每成功写一个脚 +1,本函数返回**该脚最后一次**被写时的序号 + (从没被写过返回 0)。用来断言顺序 —— 「先断 VBUS 再关 eFuse」(A7)、 + 「17 ms 上电时序」(A9)这类事,光看每个脚的终值和次数是看不出来的。 */ +uint32_t sim_gpio_write_seq(int pin); + /* ---------------- 74HC595 ×3 的 24 位链 ---------------- */ /* 按 datasheet 建模的假移位链,**只看线上真实发生的边**(SRCLK / RCLK 上升沿)。 diff --git a/firmware/host/CMakeLists.txt b/firmware/host/CMakeLists.txt index 92e0b32..e40fa84 100644 --- a/firmware/host/CMakeLists.txt +++ b/firmware/host/CMakeLists.txt @@ -23,6 +23,7 @@ set(FW_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/..) set(PHONERAIL_CORE_SRCS ${FW_ROOT}/main/version.c ${FW_ROOT}/main/rail.c + ${FW_ROOT}/main/rail_cfg.c ) # HAL:接口在 include/,宿主机链 sim/ 的实现(esp/ 那份只有 IDF 目标编)。 diff --git a/firmware/main/CMakeLists.txt b/firmware/main/CMakeLists.txt index 774cda0..a22c26b 100644 --- a/firmware/main/CMakeLists.txt +++ b/firmware/main/CMakeLists.txt @@ -5,5 +5,6 @@ idf_component_register( SRCS "app_main.c" "version.c" "rail.c" + "rail_cfg.c" INCLUDE_DIRS "." REQUIRES phonerail_hal drv) diff --git a/firmware/main/app_main.c b/firmware/main/app_main.c index 498581e..e8c8527 100644 --- a/firmware/main/app_main.c +++ b/firmware/main/app_main.c @@ -1,13 +1,50 @@ /* ESP32-S3 入口。只在 ESP-IDF 目标里编译,宿主机构建不包含本文件。 */ +#include + #include "esp_log.h" #include "hal_time.h" #include "phonerail.h" +#include "rail_cfg.h" static const char *TAG = "phonerail"; +/* 启动自检(需求 §10.4):五路的地址 / 位号 / 引脚无重复、无越界, + 而且那张五路表和四个驱动自己的表没有分家。 + + ⚠️ **不过就 abort,不降级。** 这里查的全是编译进固件的常量表, + 查不过 = 固件本身写错了,不是硬件坏了 —— §10.10 的降级矩阵管的是后者。 + 带着一张错表跑起来的样子是「命令发给了另一路」,五台手机全在跑、 + 一个错误码都不报,谁也不会去怀疑它。 */ +static void selftest_or_die(void) +{ + const uint32_t bad = rail_selftest(); + if (bad == 0) { + ESP_LOGI(TAG, "五路配置表自检通过"); + return; + } + for (uint32_t bit = 1; bit != 0; bit <<= 1) { + if ((bad & bit) != 0) { + ESP_LOGE(TAG, "自检失败:%s (0x%02x)", rail_selftest_str(bit), (unsigned)bit); + } + } + abort(); +} + void app_main(void) { - /* 引脚表与外设初始化留到 A7/A8;这里只确认 HAL 真的被链进来了。 */ ESP_LOGI(TAG, "PhoneRail %s boot, %d rails, t=%u ms", phonerail_version(), PHONERAIL_RAIL_COUNT, (unsigned)hal_time_now_ms()); + + selftest_or_die(); + + /* 十根 EN 线配成输出并全部拉低。**先于一切外设**:DAC / eFuse 一上电就有 + 输出,这两根线这时必须已经确定是低的。 */ + const hal_err_t err = rail_en_init(); + if (err != HAL_OK) { + ESP_LOGE(TAG, "EN 引脚初始化失败:%s", hal_err_str(err)); + abort(); + } + ESP_LOGI(TAG, "五路输出与 VBUS 全部关闭,等待 A8 的任务骨架"); + + /* 器件初始化与五个任务留到 A8/A15。 */ } diff --git a/firmware/main/board_pins.h b/firmware/main/board_pins.h new file mode 100644 index 0000000..0166476 --- /dev/null +++ b/firmware/main/board_pins.h @@ -0,0 +1,67 @@ +/* ESP32-S3 引脚分配 —— 全固件唯一一份(设计文档 §8.1,2026-09-06 按 PCB 几何 + 重排的那一版,§26.18)。 + + ⚠️ **引脚号是从「板上要去哪儿」倒推的,不是逻辑顺序。** 和 INA226 地址、 + 595 位号一样:查表,不要算。哪怕看上去像顺排(IO9~IO14 那一段), + 也是巧合 —— `EFUSE_EN` 那五根就一个规律都没有(39 / 48 / 6 / 41 / 8)。 + + 五路各自的 `EFUSE_ENn` / `VBUS_ENn` **不在这里**,在 `rail_cfg.h` 的那张五路表里 + (它们是「每路一个」的东西,和 INA 地址、DAC 通道、按键位号是同一类)。 + 本文件只放**全板独一份**的那些脚。`rail_selftest()` 会把两边合起来查重。 + + ⚠️ 这里的值与 `.claude/pcb1-parts.json` 里 U20(WROOM-1 模组)的真实焊盘网络 + 逐脚核对过(2026-09-07)。改动之前先回去核一遍,别照着记忆改。 */ +#ifndef BOARD_PINS_H +#define BOARD_PINS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* ---- 全板独一份的信号(模组脚号 → GPIO) -------------------------------- */ + +#define BOARD_PIN_I2C_SDA 14 /* 模组脚 22 —— 6×INA226 + 2×DAC + PCA9555(+ 载板 LM75) */ +#define BOARD_PIN_I2C_SCL 15 /* 模组脚 8 */ +#define BOARD_PIN_FAN_TACH 16 /* 模组脚 9 → R731/R732 分压 */ +#define BOARD_PIN_FAN_PWM 18 /* 模组脚 11 → R733 */ +#define BOARD_PIN_UP_VBUS_DET 47 /* 模组脚 24 → J24.25,上游 VBUS 有没有插 */ +#define BOARD_PIN_NTC1 1 /* 模组脚 39,ADC1_CH0 —— 必须是 IO1~IO10 */ +#define BOARD_PIN_NTC2 2 /* 模组脚 38,ADC1_CH1 */ + +/* USB-Serial-JTAG 的 D±。**S3 上是硬连线,动不了**(模组脚 13/14 → J22)。 + 下载 / 调试口就是它,板上没有 JTAG 排针、也没有 USB 转串口芯片。 + ⚠️ 永远别烧 `DIS_USB_JTAG` eFuse —— 烧了引脚 JTAG 和 USB 两边都没有。 */ +#define BOARD_PIN_USB_DM 19 +#define BOARD_PIN_USB_DP 20 + +/* ---- 不许占用的脚 -------------------------------------------------------- */ + +/* strapping:挂负载会影响上电模式(S3 只有这四个是)。 */ +#define BOARD_PINS_STRAPPING \ + { \ + 0, 3, 45, 46 \ + } +/* WROOM-1 的 **R8 版(八线 PSRAM)占用**,留空以便换料。 */ +#define BOARD_PINS_RESERVED \ + { \ + 35, 36, 37 \ + } +/* ⚠️ **IO22~IO25 在 ESP32-S3 上根本不存在**(编号是连续的,芯片不是), + IO26~IO32 是模组内部的 SPI flash / PSRAM,引不出来。 + 写进配置表不会报错 —— `gpio_config()` 对不存在的脚只是无声无息什么也不做。 */ +#define BOARD_PINS_ABSENT \ + { \ + 22, 23, 24, 25, \ + 26, 27, 28, 29, 30, 31, 32 \ + } + +/* ESP32-S3 的 GPIO 编号上限(IO48 **存在**,`EFUSE_EN2` 就在上面)。 + ⚠️ HAL 两侧的边界都写成 `pin <= BOARD_PIN_NUM_MAX`;A7 之前那里写的是 + `pin < 48`,正好把 IO48 关在门外 —— 现象是 CH2 的 eFuse 既开不了也**关不掉**。 */ +#define BOARD_PIN_NUM_MAX 48 + +#ifdef __cplusplus +} +#endif + +#endif /* BOARD_PINS_H */ diff --git a/firmware/main/phonerail.h b/firmware/main/phonerail.h index e8e1502..9fafa97 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-a6" +#define PHONERAIL_FW_VERSION "0.1.0-a7" /* 通道数。五路直供,见设计文档 §2。 */ #define PHONERAIL_RAIL_COUNT 5 diff --git a/firmware/main/rail_cfg.c b/firmware/main/rail_cfg.c new file mode 100644 index 0000000..4d09abc --- /dev/null +++ b/firmware/main/rail_cfg.c @@ -0,0 +1,278 @@ +/* 五路配置表 + 启动自检 + 两根 EN 线。契约与来历见 rail_cfg.h。 */ +#include "rail_cfg.h" + +#include +#include + +#include "board_pins.h" +#include "dac63204.h" +#include "hal_gpio.h" +#include "ina226.h" +#include "pca9555.h" +#include "sr595.h" + +/* ---- 那张表 -------------------------------------------------------------- + + 六列的出处:INA 地址 §8.2;DAC 片/通道 §8.2 + §26.18;按键位与 EDL 位 + §17.2 + §26.18 C(从 `.claude/pcb1-parts.json` 的真实焊盘网络逐脚核过); + 两根 EN 的 GPIO §8.1(同样按 U20 的焊盘网络核过,2026-09-07)。 + + ⚠️ **一列都不许改成算式。** 六列里没有一列是顺排的。 */ +static const rail_cfg_t k_cfg[PHONERAIL_RAIL_COUNT] = { + /* INA DAC片 ch EFUSE VBUS PWR VUP VDN EDL FLT PG */ + /* CH1 */ { 0x40, 0x4A, 2, 39, 40, { 1, 2, 3 }, 16, 0, 0 }, + /* CH2 */ { 0x41, 0x4A, 0, 48, 10, { 19, 17, 18 }, 7, 1, 1 }, + /* CH3 */ { 0x44, 0x4B, 0, 6, 11, { 22, 21, 20 }, 6, 2, 2 }, + /* CH4 */ { 0x45, 0x4A, 1, 41, 42, { 9, 10, 11 }, 0, 3, 3 }, + /* CH5 */ { 0x42, 0x4B, 1, 8, 17, { 15, 14, 13 }, 5, 4, 4 }, +}; + +_Static_assert(PHONERAIL_RAIL_COUNT == INA226_RAIL_COUNT, "五路数对不上 INA226"); +_Static_assert(PHONERAIL_RAIL_COUNT == DAC63204_RAIL_COUNT, "五路数对不上 DAC63204"); +_Static_assert(PHONERAIL_RAIL_COUNT == PCA9555_RAIL_COUNT, "五路数对不上 PCA9555"); + +const rail_cfg_t *rail_cfg(int rail) +{ + assert(rail >= 1 && rail <= PHONERAIL_RAIL_COUNT); /* 悄悄返回 CH1 比崩掉危险 */ + return &k_cfg[rail - 1]; +} + +/* ---- 自检之一:本表与四个驱动的表逐格对账 -------------------------------- + 分家的现象是「命令发给了另一路」,功能测试测不出来,所以这里是**逐格**比, + 不是抽查。四个驱动的取值函数越界会 abort,但这里的 rail 恒在 1~5。 */ +static bool table_matches_drivers(const rail_cfg_t *t) +{ + for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) { + const rail_cfg_t *c = &t[rail - 1]; + + if (c->ina_addr != ina226_rail_addr(rail)) { + return false; + } + if (c->dac_addr != dac63204_rail_addr(rail) || c->dac_ch != dac63204_rail_ch(rail)) { + return false; + } + for (int k = 0; k < SR595_KEY_COUNT; k++) { + if (c->key_bit[k] != (uint8_t)sr595_key_bit(rail, (sr595_key_t)k)) { + return false; + } + } + if (c->edl_bit != (uint8_t)sr595_edl_bit(rail)) { + return false; + } + if (c->flt_bit != (uint8_t)pca9555_flt_bit(rail)) { + return false; + } + if (c->pg_bit != (uint8_t)pca9555_pg_bit(rail)) { + return false; + } + } + return true; +} + +/* ---- 自检之二:引脚无重复、无越界 ---------------------------------------- + 查的是**全板**,不只是五路那十个脚:重复的两个脚一定分属两个模块, + 只查自己那十个的话,「`VBUS_EN2` 写成了 IO14」这种错(撞上 SDA)查不出来。 */ + +static bool pin_in(const int *list, size_t n, int pin) +{ + for (size_t i = 0; i < n; i++) { + if (list[i] == pin) { + return true; + } + } + return false; +} + +static bool pin_usable(int pin) +{ + static const int absent[] = BOARD_PINS_ABSENT; + static const int strapping[] = BOARD_PINS_STRAPPING; + static const int reserved[] = BOARD_PINS_RESERVED; + + if (pin < 0 || pin > BOARD_PIN_NUM_MAX) { + return false; + } + if (pin_in(absent, sizeof(absent) / sizeof(absent[0]), pin)) { + return false; /* 这个号在 S3 上根本不存在,或者接着模组内部的 flash */ + } + if (pin_in(strapping, sizeof(strapping) / sizeof(strapping[0]), pin)) { + return false; + } + if (pin_in(reserved, sizeof(reserved) / sizeof(reserved[0]), pin)) { + return false; /* R8 版八线 PSRAM 占着,留空以便换料 */ + } + return true; +} + +static bool pins_ok(const rail_cfg_t *t) +{ + /* 全板用掉的每一个脚。顺序无所谓,这里只查「都能用」和「不重复」。 */ + int used[2 * PHONERAIL_RAIL_COUNT + 14]; + size_t n = 0; + + for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) { + used[n++] = t[rail - 1].efuse_en; + used[n++] = t[rail - 1].vbus_en; + } + used[n++] = BOARD_PIN_I2C_SDA; + used[n++] = BOARD_PIN_I2C_SCL; + used[n++] = BOARD_PIN_FAN_TACH; + used[n++] = BOARD_PIN_FAN_PWM; + used[n++] = BOARD_PIN_UP_VBUS_DET; + used[n++] = BOARD_PIN_NTC1; + used[n++] = BOARD_PIN_NTC2; + used[n++] = BOARD_PIN_USB_DM; + used[n++] = BOARD_PIN_USB_DP; + /* 四根移位链的线与 PCA9555 的中断脚:**引用驱动里的宏,不重抄一遍数字**。 */ + used[n++] = SR595_PIN_SER; + used[n++] = SR595_PIN_SRCLK; + used[n++] = SR595_PIN_RCLK; + used[n++] = SR595_PIN_OE_N; + used[n++] = PCA9555_PIN_INT_N; + assert(n == sizeof(used) / sizeof(used[0])); + + for (size_t i = 0; i < n; i++) { + if (!pin_usable(used[i])) { + return false; + } + for (size_t j = i + 1; j < n; j++) { + if (used[i] == used[j]) { + return false; /* 两个信号抢同一个脚 */ + } + } + } + + /* 两路 NTC 必须在 ADC1(IO1~IO10)上:ADC2 在 WiFi 开着的时候读不了, + 而本板 WiFi 是常开的(§10.8 传输 A)。挪到 ADC2 上不会报错, + 只会在连上 WiFi 之后开始读失败 —— 台架上很难复现。 */ + if (BOARD_PIN_NTC1 < 1 || BOARD_PIN_NTC1 > 10 || BOARD_PIN_NTC2 < 1 || BOARD_PIN_NTC2 > 10) { + return false; + } + return true; +} + +/* ---- 启动自检 ------------------------------------------------------------ */ + +uint32_t rail_selftest_table(const rail_cfg_t *t) +{ + uint32_t bad = 0; + + if (t == NULL) { + return RAIL_SELFTEST_TABLE | RAIL_SELFTEST_PINS; + } + if (!table_matches_drivers(t)) { + bad |= RAIL_SELFTEST_TABLE; + } + if (!pins_ok(t)) { + bad |= RAIL_SELFTEST_PINS; + } + return bad; +} + +uint32_t rail_selftest(void) +{ + uint32_t bad = 0; + + /* 四个驱动各自的表(A3~A6 都留了这个谓词,A7 是它们的归宿)。 */ + if (!ina226_addr_table_ok()) { + bad |= RAIL_SELFTEST_INA; + } + if (!dac63204_map_ok()) { + bad |= RAIL_SELFTEST_DAC; + } + if (!sr595_map_ok()) { + bad |= RAIL_SELFTEST_SR595; + } + if (!pca9555_map_ok() || !pca9555_addr_ok()) { + bad |= RAIL_SELFTEST_PCA9555; + } + return bad | rail_selftest_table(k_cfg); +} + +const char *rail_selftest_str(uint32_t bit) +{ + switch (bit) { + case RAIL_SELFTEST_INA: + return "INA226 地址表"; + case RAIL_SELFTEST_DAC: + return "DAC63204 映射表"; + case RAIL_SELFTEST_SR595: + return "74HC595 位表"; + case RAIL_SELFTEST_PCA9555: + return "PCA9555 位表/地址"; + case RAIL_SELFTEST_TABLE: + return "五路表与驱动分家"; + case RAIL_SELFTEST_PINS: + return "引脚重复或越界"; + default: + return "?"; + } +} + +/* ---- 两根 EN 线 ---------------------------------------------------------- */ + +/* 高有效。**全固件唯一一处极性**(和 `sr595_set_hub_reset()` 里那个反相、 + `rail_mv_to_code()` 里那个减法同一个道理:多一处就多一处写反的机会)。 */ +static hal_err_t en_write(int pin, bool on) +{ + return hal_gpio_write(pin, on ? 1 : 0); +} + +hal_err_t rail_en_init(void) +{ + for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) { + const rail_cfg_t *c = &k_cfg[rail - 1]; + const int pins[2] = { c->efuse_en, c->vbus_en }; + + for (int i = 0; i < 2; i++) { + hal_err_t err = hal_gpio_config(pins[i], HAL_GPIO_OUTPUT); + if (err != HAL_OK) { + return err; + } + /* 配成输出的那一刻两边的 HAL 都已经在输出 0 了,这一句是把「关」 + 写成明文 —— 别让「五路上电即全关」依赖某个默认值。 */ + err = en_write(pins[i], false); + if (err != HAL_OK) { + return err; + } + } + } + return HAL_OK; +} + +hal_err_t rail_efuse_enable(int rail, bool on) +{ + if (rail < 1 || rail > PHONERAIL_RAIL_COUNT) { + return HAL_ERR_ARG; + } + return en_write(k_cfg[rail - 1].efuse_en, on); +} + +hal_err_t rail_vbus_enable(int rail, bool on) +{ + if (rail < 1 || rail > PHONERAIL_RAIL_COUNT) { + return HAL_ERR_ARG; + } + return en_write(k_cfg[rail - 1].vbus_en, on); +} + +hal_err_t rail_all_off(void) +{ + hal_err_t first = HAL_OK; + + /* 先断五路 VBUS,再关五路 eFuse:反过来的话中间有一小段「手机只靠 USB + 供着电」,那既不是开也不是关。十个都要试,不许中途 return。 */ + for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) { + hal_err_t err = rail_vbus_enable(rail, false); + if (err != HAL_OK && first == HAL_OK) { + first = err; + } + } + for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) { + hal_err_t err = rail_efuse_enable(rail, false); + if (err != HAL_OK && first == HAL_OK) { + first = err; + } + } + return first; +} diff --git a/firmware/main/rail_cfg.h b/firmware/main/rail_cfg.h new file mode 100644 index 0000000..5faf9ad --- /dev/null +++ b/firmware/main/rail_cfg.h @@ -0,0 +1,120 @@ +/* 五路一张表(需求 §10.4)—— **禁止用通道号算任何东西**。 + + 板上和「第几路」有关的编号有六组,**没有一组是顺排的,也没有两组是一致的**: + + CH INA226 DAC 按键位(PWR/VUP/VDN) EDL 位 EFUSE_EN VBUS_EN + 1 0x40 U6.OUT2 1 2 3 16 IO39 IO40 + 2 0x41 U6.OUT0 19 17 18 7 IO48 IO10 + 3 0x44 **U7**.OUT0 22 21 20 6 IO6 IO11 + 4 0x45 U6.OUT1 9 10 11 0 IO41 IO42 + 5 0x42 U7.OUT1 15 14 13 5 IO8 IO17 + + 六组编号各有各的来历(I²C 地址按布线难度、595 位号按 PCB 几何、GPIO 按出线 + 方向),三条都在设计文档里:§8.2 / §17.2 + §26.18 C / §8.1。 + + ⚠️ **这一片写错了,板子照样正常工作** —— 只是「重启 CH1」重启了 CH4、 + 「读 CH3 电流」读的是 CH5。功能测试测不出来,五台手机跑起来也看不出来, + 要等到某一路真出故障、而你关掉了另一路的时候才暴露。所以: + + **除了本文件里这张表,任何地方都不许出现 `0x40 + rail`、`rail - 1` + 这类由通道号算出来的器件编号。** + + ---- 这张表和四个驱动里那四张表是什么关系 ---------------------------------- + + A3~A6 四个驱动各自带着自己那一张表(`ina226_rail_addr()` 等),因为驱动要能 + 单独测。本表是**把它们横着拼起来**的那一张,多出来的只有两列 GPIO + (`EFUSE_EN` / `VBUS_EN` 是 GPIO 直连,没有驱动可挂)。 + + 两处写同一个数就有分家的风险,所以 `rail_selftest()` 会**逐格**拿本表和四个 + 驱动的取值函数对账,对不上直接判失败(`RAIL_SELFTEST_TABLE`)。 + 宿主机 `ctest` 每次都跑,上板 `app_main()` 开机也跑一次。 + —— 之所以不做成「开机时从驱动现算」,是因为那样**按定义就查不出分家**, + 而且表就不是 const 的了,用之前得先记着初始化。 */ +#ifndef RAIL_CFG_H +#define RAIL_CFG_H + +#include +#include + +#include "hal_err.h" +#include "phonerail.h" +#include "sr595.h" /* sr595_key_t:按键位那三格的下标就是它 */ + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + uint8_t ina_addr; /* 0x40 0x41 0x44 0x45 0x42 —— 查表,不能算 */ + uint8_t dac_addr; /* 0x4A: CH1/2/4 0x4B: CH3/5 —— 也不是「前三路在 U6」 */ + uint8_t dac_ch; /* 该片上的 OUT 号(0~3) */ + + /* GPIO 直连,安全关键(§8.1「任何情况下都要能关电」)。 + 类型是 `int`,因为 `hal_gpio_*()` 收的就是 int;本表是唯一的出处。 */ + int efuse_en; + int vbus_en; + + /* 24 位移位链上的位号。下标用 `SR595_KEY_PWR/VUP/VDN`,别用 0/1/2。 */ + uint8_t key_bit[SR595_KEY_COUNT]; + uint8_t edl_bit; + + uint8_t flt_bit; /* PCA9555 **Port0** bit0~4 */ + uint8_t pg_bit; /* PCA9555 **Port1** bit0~4 —— 两个端口长得一模一样,别搞反 */ +} rail_cfg_t; + +/* rail 是 **1~5**(不是 0~4)。越界直接 abort:那是调用方算错了路号, + 悄悄返回 CH1 的配置比崩掉危险得多。 */ +const rail_cfg_t *rail_cfg(int rail); + +/* ---- 启动自检(需求 §10.4 的验收项) ------------------------------------ + 全部是纯计算,不碰总线也不碰引脚,什么时候调都行。 + 返回 0 = 全过;非 0 是下面这些位的或。 */ + +typedef enum { + RAIL_SELFTEST_INA = 1u << 0, /* ina226_addr_table_ok() 不过 */ + RAIL_SELFTEST_DAC = 1u << 1, /* dac63204_map_ok() 不过 */ + RAIL_SELFTEST_SR595 = 1u << 2, /* sr595_map_ok() 不过 */ + RAIL_SELFTEST_PCA9555 = 1u << 3, /* pca9555_map_ok() / _addr_ok() 不过 */ + RAIL_SELFTEST_TABLE = 1u << 4, /* 本表和四个驱动的表分家了 */ + RAIL_SELFTEST_PINS = 1u << 5, /* EN 引脚重复 / 越界 / 撞上别的信号 */ +} rail_selftest_bit_t; + +uint32_t rail_selftest(void); + +/* 只查**一张给定的**五路表(`RAIL_SELFTEST_TABLE` / `RAIL_SELFTEST_PINS` 两位; + 另外四位是四个驱动自己的表,和 t 无关)。`t` 要指向 PHONERAIL_RAIL_COUNT 行。 + + 存在的理由:**自检逻辑本身也得被测。** 拿真表跑一遍只能证明「现在没事」, + 证明不了「真有事的时候查得出来」—— 而真表是 const 的,测试没法把它改坏。 + 所以判据单拎出来,测试喂几张故意写坏的表进去看它认不认。 */ +uint32_t rail_selftest_table(const rail_cfg_t *t); + +/* 单个失败位的人话名字,用来打日志。传 0 或者多于一位返回 "?"。 */ +const char *rail_selftest_str(uint32_t bit); + +/* ---- 两根 EN 线(表驱动遍历) -------------------------------------------- + `EFUSE_EN` / `VBUS_EN` 都是**高有效**,而且都在板上带 100 kΩ 下拉 + (R114 / R701~705)—— ESP32 复位期间 GPIO 高阻,五路输出全关、VBUS 全断。 + 极性只在 rail_cfg.c 的 `en_write()` 里出现这一次,上层一律传 bool。 */ + +/* 十个脚配成输出并**全部拉低**(= 五路全关、VBUS 全断),然后才谈别的。 + ⚠️ 必须在 `rail_volt_init()` 之前调:DAC 上电那一刻输出就有电压了, + eFuse 的 EN 这时得已经确定是低的。任何一个脚配失败就返回 HAL_ERR_ARG。 */ +hal_err_t rail_en_init(void); + +/* rail 越界 → HAL_ERR_ARG;没 `rail_en_init()` 过 → HAL_ERR_ARG(HAL 会拦)。 */ +hal_err_t rail_efuse_enable(int rail, bool on); +hal_err_t rail_vbus_enable(int rail, bool on); + +/* 五路全关:**先断 VBUS 再关 eFuse**,中间不留「只靠 USB 供着电」的窗口。 + 十个脚全部试一遍,不因为某一个失败就提前返回 —— 关电路径上「剩下九个也别关了」 + 是最坏的选择。返回第一个错误码(全成功就是 HAL_OK)。 + ⚠️ 这是复位 / 收尾路径。A10 的故障快路径要直接调 `rail_efuse_enable(rail,false)`, + 别绕这里:过压时该立刻断的是出事那一路,不是等着遍历五路。 */ +hal_err_t rail_all_off(void); + +#ifdef __cplusplus +} +#endif + +#endif /* RAIL_CFG_H */ diff --git a/firmware/test/test_rail_cfg.c b/firmware/test/test_rail_cfg.c new file mode 100644 index 0000000..169c237 --- /dev/null +++ b/firmware/test/test_rail_cfg.c @@ -0,0 +1,609 @@ +/* A7:五路配置表 + 表驱动遍历 + 启动自检(需求 §10.4)。 + + 验收主项:**启动自检 —— 五路的地址 / 位号 / 引脚无重复、无越界。** + 第二主项:那张五路表和四个驱动自己的表**没有分家**(逐格比,不是抽查)。 + 第三主项:**没有一列能用通道号算出来** —— 这一片最容易写出来的错就是 + 哪天有人把表换回 `0x40 + rail`,那之后板子照样跑,只是命令发给了另一路。 */ +#include "board_pins.h" +#include "dac63204.h" +#include "hal_gpio.h" +#include "ina226.h" +#include "pca9555.h" +#include "phonerail.h" +#include "rail_cfg.h" +#include "sim_hal.h" +#include "sr595.h" +#include "unity.h" + +#define N PHONERAIL_RAIL_COUNT + +void setUp(void) { sim_hal_reset(); } +void tearDown(void) {} + +/* ---- 验收主项一:自检 ----------------------------------------------------- */ + +static void test_selftest_passes(void) +{ + TEST_ASSERT_EQUAL_HEX32(0, rail_selftest()); +} + +/* 自检里的六项每一项都要真的被算过。这条挨个调它们背后的谓词 —— + `rail_selftest()` 要是哪天被改成 `return 0;`,上一条测试照样绿。 */ +static void test_selftest_covers_all_four_driver_tables(void) +{ + TEST_ASSERT_TRUE(ina226_addr_table_ok()); + TEST_ASSERT_TRUE(dac63204_map_ok()); + TEST_ASSERT_TRUE(sr595_map_ok()); + TEST_ASSERT_TRUE(pca9555_map_ok()); + TEST_ASSERT_TRUE(pca9555_addr_ok()); +} + +static void test_selftest_str_names_every_bit(void) +{ + static const uint32_t bits[] = { + RAIL_SELFTEST_INA, RAIL_SELFTEST_DAC, RAIL_SELFTEST_SR595, + RAIL_SELFTEST_PCA9555, RAIL_SELFTEST_TABLE, RAIL_SELFTEST_PINS, + }; + for (unsigned i = 0; i < sizeof(bits) / sizeof(bits[0]); i++) { + TEST_ASSERT_TRUE(rail_selftest_str(bits[i])[0] != '?'); + } + TEST_ASSERT_EQUAL_STRING("?", rail_selftest_str(0)); + TEST_ASSERT_EQUAL_STRING("?", rail_selftest_str(1u << 31)); + /* 两位一起传进来也是 "?":调用方该逐位拆。 */ + TEST_ASSERT_EQUAL_STRING("?", rail_selftest_str(RAIL_SELFTEST_INA | RAIL_SELFTEST_DAC)); +} + +/* 自检**逻辑本身**得被测:拿真表跑一遍只能证明「现在没事」。 + 真表是 const 的,改不动,所以这里复制一份出来逐样改坏,看它认不认。 + 这几条同时也是「哪天有人把某个判据删了」的拦网。 */ + +static void copy_real_table(rail_cfg_t *dst) +{ + for (int rail = 1; rail <= N; rail++) { + dst[rail - 1] = *rail_cfg(rail); + } +} + +static void test_a_faithful_copy_of_the_real_table_passes(void) +{ + rail_cfg_t t[N]; + copy_real_table(t); + TEST_ASSERT_EQUAL_HEX32(0, rail_selftest_table(t)); + /* 真表本身走的也是这条路 —— rail_cfg(1) 就是那张表的首行。 */ + TEST_ASSERT_EQUAL_HEX32(0, rail_selftest_table(rail_cfg(1))); + TEST_ASSERT_TRUE((rail_selftest_table(NULL) & RAIL_SELFTEST_TABLE) != 0); +} + +/* 表和驱动分家:只改一格,而且改成一个**合法**的地址(0x43 是输入侧那片)。 + 这一手光看「地址是不是合法、有没有重复」是查不出来的。 */ +static void test_a_cell_that_drifted_from_the_driver_is_caught(void) +{ + rail_cfg_t t[N]; + + copy_real_table(t); + t[2].ina_addr = 0x43; + TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_TABLE, rail_selftest_table(t)); + + copy_real_table(t); + t[0].dac_ch = 0; /* CH1 从 U6.OUT2 挪到 OUT0 —— 那是 CH2 的通道 */ + TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_TABLE, rail_selftest_table(t)); + + copy_real_table(t); + t[3].key_bit[SR595_KEY_VUP] = t[3].key_bit[SR595_KEY_VDN]; /* 音量键上下写反的近亲 */ + TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_TABLE, rail_selftest_table(t)); + + copy_real_table(t); + t[1].pg_bit = 4; + TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_TABLE, rail_selftest_table(t)); +} + +/* 引脚:重复 / 撞上别人 / strapping / 不存在的号 / 越界,五种都要认出来。 */ +static void test_every_kind_of_bad_pin_is_caught(void) +{ + rail_cfg_t t[N]; + + copy_real_table(t); + t[1].vbus_en = t[0].vbus_en; /* 两路共用一根 VBUS_EN */ + TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); + + copy_real_table(t); + t[2].efuse_en = BOARD_PIN_I2C_SDA; /* 撞上 SDA:I²C 直接哑掉 */ + TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); + + copy_real_table(t); + t[0].efuse_en = 45; /* strapping */ + TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); + + copy_real_table(t); + t[4].vbus_en = 24; /* S3 上根本没有 IO24:gpio_config() 不报错,什么也不做 */ + TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); + + copy_real_table(t); + t[4].vbus_en = 36; /* R8 版八线 PSRAM 占着 */ + TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); + + copy_real_table(t); + t[3].efuse_en = BOARD_PIN_NUM_MAX + 1; + TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); + + copy_real_table(t); + t[3].efuse_en = -1; + TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); + + /* 撞上 595 那四根线也算 —— 它们不在 board_pins.h 里,是从驱动的宏取的。 */ + copy_real_table(t); + t[0].vbus_en = SR595_PIN_RCLK; + TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); +} + +/* 两样一起坏,两位一起报 —— 别在第一个错上就 return。 */ +static void test_two_kinds_of_breakage_report_two_bits(void) +{ + rail_cfg_t t[N]; + copy_real_table(t); + t[2].ina_addr = 0x43; + t[0].efuse_en = 45; + TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_TABLE | RAIL_SELFTEST_PINS, rail_selftest_table(t)); +} + +/* ---- 验收主项二:无重复 --------------------------------------------------- */ + +/* 六组编号,每一组五路两两不同。撞了的现象都是「两路共用一个东西」: + 共用 INA 地址 = 两路读到同一个电流;共用 (DAC 片, 通道) = 调这路那路也动; + 共用按键位 = 按 CH1 的电源键 CH4 也被按。 */ +static void test_no_two_rails_share_anything(void) +{ + for (int a = 1; a <= N; a++) { + const rail_cfg_t *x = rail_cfg(a); + for (int b = a + 1; b <= N; b++) { + const rail_cfg_t *y = rail_cfg(b); + + TEST_ASSERT_NOT_EQUAL(x->ina_addr, y->ina_addr); + TEST_ASSERT_FALSE(x->dac_addr == y->dac_addr && x->dac_ch == y->dac_ch); + TEST_ASSERT_NOT_EQUAL(x->edl_bit, y->edl_bit); + TEST_ASSERT_NOT_EQUAL(x->flt_bit, y->flt_bit); + TEST_ASSERT_NOT_EQUAL(x->pg_bit, y->pg_bit); + TEST_ASSERT_NOT_EQUAL(x->efuse_en, y->efuse_en); + TEST_ASSERT_NOT_EQUAL(x->vbus_en, y->vbus_en); + + for (int i = 0; i < SR595_KEY_COUNT; i++) { + TEST_ASSERT_NOT_EQUAL(x->key_bit[i], y->edl_bit); + TEST_ASSERT_NOT_EQUAL(y->key_bit[i], x->edl_bit); + for (int j = 0; j < SR595_KEY_COUNT; j++) { + TEST_ASSERT_NOT_EQUAL(x->key_bit[i], y->key_bit[j]); + } + } + } + /* 同一路自己的三个键也不许撞,而且都不许撞自己的 EDL 位。 */ + for (int i = 0; i < SR595_KEY_COUNT; i++) { + TEST_ASSERT_NOT_EQUAL(x->key_bit[i], x->edl_bit); + for (int j = i + 1; j < SR595_KEY_COUNT; j++) { + TEST_ASSERT_NOT_EQUAL(x->key_bit[i], x->key_bit[j]); + } + } + /* 一路自己的两根 EN 更不能是同一个脚。 */ + TEST_ASSERT_NOT_EQUAL(x->efuse_en, x->vbus_en); + } +} + +/* 24 位链上还蹲着 `HUB_RST` 和三个空脚 —— 五路的 20 个位一个都不许踩上去。 */ +static void test_shift_chain_bits_avoid_hub_reset_and_the_unused_pins(void) +{ + uint32_t seen = 0; + for (int rail = 1; rail <= N; rail++) { + const rail_cfg_t *c = rail_cfg(rail); + for (int i = 0; i < SR595_KEY_COUNT; i++) { + seen |= (uint32_t)1u << c->key_bit[i]; + } + seen |= (uint32_t)1u << c->edl_bit; + } + TEST_ASSERT_EQUAL_HEX32(0, seen & SR595_MASK_UNUSED); + TEST_ASSERT_EQUAL_HEX32(0, seen & ((uint32_t)1u << SR595_BIT_HUB_RST)); + TEST_ASSERT_EQUAL_HEX32(seen, seen & SR595_MASK_VALID); + /* 5×3 键 + 5 EDL = 20 位,一位不多一位不少。 */ + int n = 0; + for (int b = 0; b < SR595_BITS; b++) { + if ((seen & ((uint32_t)1u << b)) != 0) { + n++; + } + } + TEST_ASSERT_EQUAL_INT(20, n); +} + +/* ---- 验收主项二续:无越界 ------------------------------------------------- */ + +static void test_every_field_is_in_range(void) +{ + for (int rail = 1; rail <= N; rail++) { + const rail_cfg_t *c = rail_cfg(rail); + + TEST_ASSERT_TRUE(c->ina_addr <= 0x7f); + TEST_ASSERT_TRUE(c->dac_addr == DAC63204_ADDR_U6 || c->dac_addr == DAC63204_ADDR_U7); + TEST_ASSERT_TRUE(c->dac_ch <= 3); + /* 该通道必须在那片的「真的接了线」位图里 —— 没接线的通道 FBx 悬空, + 上电就是开环(datasheet 表 5-1)。 */ + TEST_ASSERT_TRUE((dac63204_used_mask(c->dac_addr) & (1u << c->dac_ch)) != 0); + + for (int i = 0; i < SR595_KEY_COUNT; i++) { + TEST_ASSERT_TRUE(c->key_bit[i] < SR595_BITS); + } + TEST_ASSERT_TRUE(c->edl_bit < SR595_BITS); + TEST_ASSERT_TRUE(c->flt_bit < PCA9555_RAIL_COUNT); /* Port0 bit0~4 */ + TEST_ASSERT_TRUE(c->pg_bit < PCA9555_RAIL_COUNT); /* Port1 bit0~4 */ + } +} + +/* FLT / PG 分属两个端口,所以两者位号相同是**对的**,不是撞车。 + 这条把「顺手给 PG 表加个偏移让它和 FLT 不一样」这种好心办坏事拦下来。 */ +static void test_flt_and_pg_live_on_different_ports_so_equal_bits_are_fine(void) +{ + for (int rail = 1; rail <= N; rail++) { + TEST_ASSERT_EQUAL_INT(pca9555_flt_bit(rail), rail_cfg(rail)->flt_bit); + TEST_ASSERT_EQUAL_INT(pca9555_pg_bit(rail), rail_cfg(rail)->pg_bit); + } +} + +/* ---- 验收主项三:一列都不许是算式 ----------------------------------------- */ + +/* 六组编号里但凡有一组能被 `rail` 算出来,就会有人把它算出来。 + + 这条**不去猜别人会写哪个算式**(猜错了就成了一句空话),而是直接断言那几列 + 「不是等差数列」—— 任何 `起点 + 步长 × ch` 形式的算式都被它一次拦掉。 + ⚠️ 逐个格子去撞算式是不管用的:CH4 的按键位正好是 9/10/11,CH1 的 EDL 位 + 正好是 16,单看一路都「像顺排」。看整列才看得出来。 */ +static bool is_arithmetic(const int *v, int n) +{ + const int step = v[1] - v[0]; + for (int i = 2; i < n; i++) { + if (v[i] - v[i - 1] != step) { + return false; + } + } + return true; +} + +static void test_no_column_is_an_arithmetic_progression(void) +{ + int ina[N], pwr[N], vup[N], vdn[N], edl[N], efuse[N], vbus[N]; + + for (int rail = 1; rail <= N; rail++) { + const rail_cfg_t *c = rail_cfg(rail); + ina[rail - 1] = c->ina_addr; + pwr[rail - 1] = c->key_bit[SR595_KEY_PWR]; + vup[rail - 1] = c->key_bit[SR595_KEY_VUP]; + vdn[rail - 1] = c->key_bit[SR595_KEY_VDN]; + edl[rail - 1] = c->edl_bit; + efuse[rail - 1] = c->efuse_en; + vbus[rail - 1] = c->vbus_en; + } + + TEST_ASSERT_FALSE_MESSAGE(is_arithmetic(ina, N), "INA226 地址不是顺排(0x40+ch 是错的)"); + TEST_ASSERT_FALSE_MESSAGE(is_arithmetic(pwr, N), "PWR 键位不是顺排"); + TEST_ASSERT_FALSE_MESSAGE(is_arithmetic(vup, N), "VUP 键位不是顺排"); + TEST_ASSERT_FALSE_MESSAGE(is_arithmetic(vdn, N), "VDN 键位不是顺排"); + TEST_ASSERT_FALSE_MESSAGE(is_arithmetic(edl, N), "EDL 位不是顺排"); + TEST_ASSERT_FALSE_MESSAGE(is_arithmetic(efuse, N), "EFUSE_EN 引脚不是顺排"); + TEST_ASSERT_FALSE_MESSAGE(is_arithmetic(vbus, N), "VBUS_EN 引脚不是顺排"); +} + +/* 需求 §10.4 点名的那个算式:`0x40 + ch`。**只有 CH3/CH4/CH5 是错的** —— + 前两路对,所以「拿 CH1 试一下没问题」是这条错误最常见的验证方式。 */ +static void test_ina_addr_formula_is_wrong_for_exactly_three_rails(void) +{ + int wrong = 0; + for (int rail = 1; rail <= N; rail++) { + if (rail_cfg(rail)->ina_addr != (uint8_t)(0x40 + rail - 1)) { + wrong++; + } + } + TEST_ASSERT_EQUAL_INT(3, wrong); + TEST_ASSERT_EQUAL_HEX8(0x40, rail_cfg(1)->ina_addr); /* 算式在这两路上是对的 */ + TEST_ASSERT_EQUAL_HEX8(0x41, rail_cfg(2)->ina_addr); +} + +/* DAC 那两列不是等差数列能描述的(片 + 通道两个量),单独钉死: + 「前三路在 U6、通道号 = ch−1」这句话五路里有四路不成立。 */ +static void test_dac_mapping_is_not_the_obvious_one(void) +{ + int wrong = 0; + for (int rail = 1; rail <= N; rail++) { + const rail_cfg_t *c = rail_cfg(rail); + if (c->dac_addr != ((rail <= 3) ? DAC63204_ADDR_U6 : DAC63204_ADDR_U7) + || c->dac_ch != (uint8_t)((rail - 1) % 3)) { + wrong++; + } + } + TEST_ASSERT_EQUAL_INT_MESSAGE(4, wrong, "只有 CH5 正好撞上那个算式"); +} + +/* FLT / PG 这两列**确实是顺排的**(§10.4 特意说明)。写在这里是为了让 + 「哪些列可以是算式」有个明面上的清单:以后有人给 PG 加个偏移,这条会红。 */ +static void test_flt_and_pg_really_are_sequential(void) +{ + for (int rail = 1; rail <= N; rail++) { + TEST_ASSERT_EQUAL_UINT8(rail - 1, rail_cfg(rail)->flt_bit); + TEST_ASSERT_EQUAL_UINT8(rail - 1, rail_cfg(rail)->pg_bit); + } +} + +/* 抽三个最容易被「顺手改顺」的格子,直接钉死实测值(出处:§8.2 / §26.18 C / §8.1, + 都从 `.claude/pcb1-parts.json` 的真实焊盘网络核过)。 */ +static void test_the_three_most_surprising_cells(void) +{ + TEST_ASSERT_EQUAL_HEX8_MESSAGE(0x44, rail_cfg(3)->ina_addr, "CH3 的 INA226 是 0x44 不是 0x42"); + TEST_ASSERT_EQUAL_HEX8_MESSAGE(DAC63204_ADDR_U7, rail_cfg(3)->dac_addr, "CH3 的 DAC 在 U7"); + TEST_ASSERT_EQUAL_INT_MESSAGE(0, rail_cfg(4)->edl_bit, "EDL_EN4 在 U21.QA,也就是 bit0"); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(2, rail_cfg(1)->dac_ch, "CH1 挂在 U6.OUT2,不是 OUT0"); + TEST_ASSERT_EQUAL_INT_MESSAGE(48, rail_cfg(2)->efuse_en, "EFUSE_EN2 在 IO48"); +} + +/* ---- 表与四个驱动没有分家 ------------------------------------------------- */ + +/* `rail_selftest()` 内部也比一遍,但那是**被测代码自己**在比。 + 这一条是外面独立比的:内部那段哪天被删了,这里照样红。 */ +static void test_table_matches_every_driver_table(void) +{ + for (int rail = 1; rail <= N; rail++) { + const rail_cfg_t *c = rail_cfg(rail); + + TEST_ASSERT_EQUAL_HEX8(ina226_rail_addr(rail), c->ina_addr); + TEST_ASSERT_EQUAL_HEX8(dac63204_rail_addr(rail), c->dac_addr); + TEST_ASSERT_EQUAL_UINT8(dac63204_rail_ch(rail), c->dac_ch); + TEST_ASSERT_EQUAL_INT(sr595_key_bit(rail, SR595_KEY_PWR), c->key_bit[SR595_KEY_PWR]); + TEST_ASSERT_EQUAL_INT(sr595_key_bit(rail, SR595_KEY_VUP), c->key_bit[SR595_KEY_VUP]); + TEST_ASSERT_EQUAL_INT(sr595_key_bit(rail, SR595_KEY_VDN), c->key_bit[SR595_KEY_VDN]); + TEST_ASSERT_EQUAL_INT(sr595_edl_bit(rail), c->edl_bit); + TEST_ASSERT_EQUAL_INT(pca9555_flt_bit(rail), c->flt_bit); + TEST_ASSERT_EQUAL_INT(pca9555_pg_bit(rail), c->pg_bit); + } +} + +/* ---- 引脚 ----------------------------------------------------------------- */ + +static bool in_list(const int *v, int n, int pin) +{ + for (int i = 0; i < n; i++) { + if (v[i] == pin) { + return true; + } + } + return false; +} + +/* 十根 EN 不许踩到 strapping / 不存在的号 / R8 版 PSRAM 占的号, + 也不许撞上全板任何一个已经有主的脚。 */ +static void test_en_pins_do_not_collide_with_anything_else(void) +{ + static const int absent[] = BOARD_PINS_ABSENT; + static const int strapping[] = BOARD_PINS_STRAPPING; + static const int reserved[] = BOARD_PINS_RESERVED; + static const int others[] = { + BOARD_PIN_I2C_SDA, BOARD_PIN_I2C_SCL, BOARD_PIN_FAN_TACH, BOARD_PIN_FAN_PWM, + BOARD_PIN_NTC1, BOARD_PIN_NTC2, BOARD_PIN_USB_DM, BOARD_PIN_USB_DP, + BOARD_PIN_UP_VBUS_DET, SR595_PIN_SER, SR595_PIN_SRCLK, SR595_PIN_RCLK, + SR595_PIN_OE_N, PCA9555_PIN_INT_N, + }; + + for (int rail = 1; rail <= N; rail++) { + const int pins[2] = { rail_cfg(rail)->efuse_en, rail_cfg(rail)->vbus_en }; + for (int i = 0; i < 2; i++) { + TEST_ASSERT_TRUE(pins[i] >= 0 && pins[i] <= BOARD_PIN_NUM_MAX); + TEST_ASSERT_FALSE(in_list(absent, (int)(sizeof(absent) / sizeof(int)), pins[i])); + TEST_ASSERT_FALSE(in_list(strapping, (int)(sizeof(strapping) / sizeof(int)), pins[i])); + TEST_ASSERT_FALSE(in_list(reserved, (int)(sizeof(reserved) / sizeof(int)), pins[i])); + TEST_ASSERT_FALSE(in_list(others, (int)(sizeof(others) / sizeof(int)), pins[i])); + } + } +} + +/* ⚠️ 两列 GPIO 是**全固件唯一一处**出处 —— 没有第二张表能和它对账 + (INA/DAC/595/PCA 那四列都有驱动那边兜着,这两列没有)。 + 所以这里把十个值连同模组脚号一起钉死;出处是 `.claude/pcb1-parts.json` 里 + U20 的真实焊盘网络(2026-09-07 逐脚核过),改这条之前先回去重核一遍。 + ⚠️ 光靠自检查不出「`EFUSE_EN1` 和 `VBUS_EN1` 写反了」:两个都是合法的空闲脚、 + 不重复、不越界。那种错只有这条测试和板上实测能拦。 */ +static void test_en_pins_match_the_pcb_netlist(void) +{ + /* EFUSE_EN VBUS_EN (模组脚号写在注释里) */ + static const int want[N][2] = { + { 39, 40 }, /* CH1: 脚32 / 脚33 */ + { 48, 10 }, /* CH2: 脚25 / 脚18 */ + { 6, 11 }, /* CH3: 脚6 / 脚19 */ + { 41, 42 }, /* CH4: 脚34 / 脚35 */ + { 8, 17 }, /* CH5: 脚12 / 脚10 */ + }; + for (int rail = 1; rail <= N; rail++) { + TEST_ASSERT_EQUAL_INT(want[rail - 1][0], rail_cfg(rail)->efuse_en); + TEST_ASSERT_EQUAL_INT(want[rail - 1][1], rail_cfg(rail)->vbus_en); + } +} + +static void test_ntc_pins_are_on_adc1(void) +{ + /* ADC2 在 WiFi 开着的时候读不了,而本板 WiFi 常开。 */ + TEST_ASSERT_TRUE(BOARD_PIN_NTC1 >= 1 && BOARD_PIN_NTC1 <= 10); + TEST_ASSERT_TRUE(BOARD_PIN_NTC2 >= 1 && BOARD_PIN_NTC2 <= 10); +} + +/* ⚠️ 回归拦网:`EFUSE_EN2` 在 **IO48** 上,而 ESP32-S3 的编号是 0~48。 + HAL 两边原来的边界都是 `pin < 48`,正好把它关在门外 —— + 现象是 CH2 的 eFuse 既开不了也**关不掉**。 */ +static void test_io48_is_a_usable_pin(void) +{ + TEST_ASSERT_EQUAL_INT(48, rail_cfg(2)->efuse_en); + TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_config(48, HAL_GPIO_OUTPUT)); + TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_write(48, 1)); + TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(48)); + /* 49 就真的越界了。 */ + TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hal_gpio_config(49, HAL_GPIO_OUTPUT)); +} + +/* ---- 表驱动遍历:两根 EN 线 ----------------------------------------------- */ + +static void assert_all_off(void) +{ + for (int rail = 1; rail <= N; rail++) { + TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(rail)->efuse_en)); + TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(rail)->vbus_en)); + } +} + +/* 上电第一件事:十个脚都配成输出、都是低。「五路全关」不许靠某个默认值。 */ +static void test_en_init_leaves_all_ten_pins_driven_low(void) +{ + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); + assert_all_off(); + for (int rail = 1; rail <= N; rail++) { + /* sim_gpio_level() 对没配过的脚返回 −1,所以上面那句已经证明配过了; + 这里再确认「真的写过一次 0」,而不只是配置时的默认值。 */ + TEST_ASSERT_TRUE(sim_gpio_writes(rail_cfg(rail)->efuse_en) >= 1); + TEST_ASSERT_TRUE(sim_gpio_writes(rail_cfg(rail)->vbus_en) >= 1); + } +} + +/* 开一路只动那一路的那一个脚。这条拦的是「rail−1 当下标」那类错位: + 错位之后打开的是别人,而两个脚都是好的,功能上「有一路开了」。 */ +static void test_enabling_one_rail_touches_exactly_one_pin(void) +{ + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); + + for (int on = 1; on <= N; on++) { + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(on, true)); + for (int rail = 1; rail <= N; rail++) { + const int want = (rail == on) ? 1 : 0; + TEST_ASSERT_EQUAL_INT(want, sim_gpio_level(rail_cfg(rail)->efuse_en)); + /* VBUS 一根都不许跟着动 —— 两张表混用是这里最像的错。 */ + TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(rail)->vbus_en)); + } + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(on, false)); + } + + for (int on = 1; on <= N; on++) { + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_vbus_enable(on, true)); + for (int rail = 1; rail <= N; rail++) { + TEST_ASSERT_EQUAL_INT((rail == on) ? 1 : 0, sim_gpio_level(rail_cfg(rail)->vbus_en)); + TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(rail)->efuse_en)); + } + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_vbus_enable(on, false)); + } +} + +/* 高有效:true = 导通。写反了板子上是「命令开电结果全关」,一眼看得出来, + 但**关电命令会变成开电**,那才是危险的一半。 */ +static void test_enable_is_active_high(void) +{ + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(1, true)); + TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(rail_cfg(1)->efuse_en)); + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(1, false)); + TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(1)->efuse_en)); +} + +/* 全关:十个脚都回到低,而且**五根 VBUS 全部早于任何一根 eFuse** —— + 反过来的话中间有一小段「手机只靠 USB 供着电」。 */ +static void test_all_off_cuts_vbus_before_efuse(void) +{ + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); + for (int rail = 1; rail <= N; rail++) { + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(rail, true)); + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_vbus_enable(rail, true)); + } + + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_all_off()); + assert_all_off(); + + uint32_t last_vbus = 0, first_efuse = 0xFFFFFFFFu; + for (int rail = 1; rail <= N; rail++) { + const uint32_t v = sim_gpio_write_seq(rail_cfg(rail)->vbus_en); + const uint32_t e = sim_gpio_write_seq(rail_cfg(rail)->efuse_en); + if (v > last_vbus) { + last_vbus = v; + } + if (e < first_efuse) { + first_efuse = e; + } + } + TEST_ASSERT_TRUE_MESSAGE(last_vbus < first_efuse, "最后一根 VBUS 必须早于第一根 eFuse"); +} + +/* 关电路径上「有一个脚失败就别管剩下九个了」是最坏的选择。 + 注错的办法是把 CH1 的 VBUS 脚偷偷改回输入 —— 之后写它必然 HAL_ERR_ARG, + 而剩下九个脚该关的还得关。 */ +static void test_all_off_keeps_going_after_one_pin_fails(void) +{ + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); + for (int rail = 1; rail <= N; rail++) { + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(rail, true)); + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_vbus_enable(rail, true)); + } + + const int broken = rail_cfg(1)->vbus_en; + TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_config(broken, HAL_GPIO_INPUT)); + const uint32_t before = sim_gpio_writes(broken); + + TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, rail_all_off()); /* 如实报第一个错误码 */ + TEST_ASSERT_EQUAL_UINT32(before, sim_gpio_writes(broken)); /* 那个脚确实没写成 */ + + /* 另外九个必须都已经关掉了。 */ + for (int rail = 1; rail <= N; rail++) { + TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(rail)->efuse_en)); + if (rail != 1) { + TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(rail)->vbus_en)); + } + } +} + +/* 没 rail_en_init() 过就调,十个脚全失败:返回错误码,不崩、不乱写。 */ +static void test_all_off_before_init_reports_an_error(void) +{ + TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, rail_all_off()); + for (int rail = 1; rail <= N; rail++) { + TEST_ASSERT_EQUAL_INT(-1, sim_gpio_level(rail_cfg(rail)->efuse_en)); /* 没配过 */ + TEST_ASSERT_EQUAL_INT(-1, sim_gpio_level(rail_cfg(rail)->vbus_en)); + } +} + +static void test_rail_number_out_of_range_is_rejected(void) +{ + TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); + TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, rail_efuse_enable(0, true)); + TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, rail_efuse_enable(N + 1, true)); + TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, rail_efuse_enable(-1, false)); + TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, rail_vbus_enable(0, true)); + TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, rail_vbus_enable(N + 1, true)); + assert_all_off(); /* 一个脚都没被越界的调用碰过 */ +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_selftest_passes); + RUN_TEST(test_selftest_covers_all_four_driver_tables); + RUN_TEST(test_selftest_str_names_every_bit); + RUN_TEST(test_a_faithful_copy_of_the_real_table_passes); + RUN_TEST(test_a_cell_that_drifted_from_the_driver_is_caught); + RUN_TEST(test_every_kind_of_bad_pin_is_caught); + RUN_TEST(test_two_kinds_of_breakage_report_two_bits); + RUN_TEST(test_no_two_rails_share_anything); + RUN_TEST(test_shift_chain_bits_avoid_hub_reset_and_the_unused_pins); + RUN_TEST(test_every_field_is_in_range); + RUN_TEST(test_flt_and_pg_live_on_different_ports_so_equal_bits_are_fine); + RUN_TEST(test_no_column_is_an_arithmetic_progression); + RUN_TEST(test_ina_addr_formula_is_wrong_for_exactly_three_rails); + RUN_TEST(test_dac_mapping_is_not_the_obvious_one); + RUN_TEST(test_flt_and_pg_really_are_sequential); + RUN_TEST(test_the_three_most_surprising_cells); + RUN_TEST(test_table_matches_every_driver_table); + RUN_TEST(test_en_pins_do_not_collide_with_anything_else); + RUN_TEST(test_en_pins_match_the_pcb_netlist); + RUN_TEST(test_ntc_pins_are_on_adc1); + RUN_TEST(test_io48_is_a_usable_pin); + RUN_TEST(test_en_init_leaves_all_ten_pins_driven_low); + RUN_TEST(test_enabling_one_rail_touches_exactly_one_pin); + RUN_TEST(test_enable_is_active_high); + RUN_TEST(test_all_off_cuts_vbus_before_efuse); + RUN_TEST(test_all_off_keeps_going_after_one_pin_fails); + RUN_TEST(test_all_off_before_init_reports_an_error); + RUN_TEST(test_rail_number_out_of_range_is_rejected); + return UNITY_END(); +} diff --git a/需求文档.md b/需求文档.md index cf524c4..7ac8eb2 100644 --- a/需求文档.md +++ b/需求文档.md @@ -433,6 +433,45 @@ typedef struct { > 「有故障」会被读成「电源良好」,方向正好相反,而且**静息态两个端口读回来都是 > 全 1,单看一帧完全看不出来**。`test_one_rail_at_a_time` 专门拦这一手。 +> **A7 已落地**(2026-09-07):那张 `rail_cfg_t` 表在 `firmware/main/rail_cfg.h` / +> `rail_cfg.c`,取值只走 `rail_cfg(rail)`(1~5,越界 abort)。六列的样子: +> +> | CH | INA226 | DAC | 按键位 PWR/VUP/VDN | EDL 位 | EFUSE_EN | VBUS_EN | +> |---|---|---|---|---|---|---| +> | 1 | 0x40 | U6.OUT2 | 1 / 2 / 3 | 16 | IO39 | IO40 | +> | 2 | 0x41 | U6.OUT0 | 19 / 17 / 18 | 7 | **IO48** | IO10 | +> | 3 | 0x44 | **U7**.OUT0 | 22 / 21 / 20 | 6 | IO6 | IO11 | +> | 4 | 0x45 | U6.OUT1 | 9 / 10 / 11 | 0 | IO41 | IO42 | +> | 5 | 0x42 | U7.OUT1 | 15 / 14 / 13 | 5 | IO8 | IO17 | +> +> 落地时定下来的五条: +> ① **本表与四个驱动那四张表是「两处写同一个数」,所以要对账而不是现算。** +> 开机自检 `rail_selftest()` 逐格拿本表和 `ina226_rail_addr()` 等取值函数比一遍, +> 对不上就报 `RAIL_SELFTEST_TABLE`。做成「开机从驱动现算」的话按定义就查不出分家, +> 而且表不再是 const 的、用之前得先记着初始化。 +> ② **判据本身也要被测**:真表是 const 的,测试改不动它,所以判据单拎成 +> `rail_selftest_table(t)`,测试喂几张故意写坏的表进去看它认不认 +> (分家 / 引脚重复 / 撞 SDA / strapping / 不存在的号 / 越界,六种都验过)。 +> ③ **两列 GPIO 是全固件唯一一处出处**,没有第二张表能对账 —— 所以 +> 「`EFUSE_EN1` 和 `VBUS_EN1` 写反」这种错自检**查不出来**(两个都是合法空闲脚、 +> 不重复、不越界)。唯一的拦网是 `test_en_pins_match_the_pcb_netlist` 里钉死的 +> 十个值(出处:`.claude/pcb1-parts.json` 的 U20 焊盘网络)和 B1 上板实测。 +> ④ **「不是顺排」要看整列,不能逐格撞算式**:CH4 的按键位正好是 9/10/11、 +> CH1 的 EDL 位正好是 16、CH3 的两根 EN 正好等于「IO4 起 / IO9 起」那个算式 —— +> 单看任何一路都「像顺排」。测试改成断言整列**不是等差数列**。 +> ⑤ ⚠️ **HAL 两侧的 GPIO 上限原来是 `pin < 48`,把 IO48 关在了门外**, +> 而 `EFUSE_EN2` 就在 IO48 上 —— 现象是 CH2 的 eFuse 既开不了也**关不掉**。 +> 已改成 0~48(含)。ESP32-S3 的编号是连续的但芯片不是:IO22~IO25 不存在、 +> IO26~IO32 是模组内部 flash/PSRAM,写进配置表 `gpio_config()` 不报错也不干活, +> 所以自检把这些号也当成越界。 +> +> 同处还落地了**全板引脚表** `firmware/main/board_pins.h`(§8.1 那张表里 +> 「全板独一份」的那些脚)与两根 EN 线的表驱动遍历 +> (`rail_en_init()` / `rail_efuse_enable()` / `rail_vbus_enable()` / `rail_all_off()`)。 +> `rail_all_off()` **先断五路 VBUS 再关五路 eFuse**,中间不留「手机只靠 USB +> 供着电」的窗口;十个脚全部试一遍,不因为某一个失败就提前返回。 +> 高有效这件事只在 `en_write()` 里出现一次。 + ### 10.5 电压通道:DAC 只做微调,而且是反向的 | 事实 | 含义 |