A7:五路配置表 + 表驱动遍历 + 启动自检
六组编号(INA 地址 / DAC 片通道 / 三个按键位 / EDL 位 / 两根 EN 的 GPIO)拼成 一张 const 表 rail_cfg(1..5),没有一列是顺排的。开机 rail_selftest() 把它和 A3~A6 四个驱动的表逐格对账,再查全板引脚重复 / 越界,不过就 abort —— 那四个自检谓词至此全部有了调用点。 判据拎成 rail_selftest_table(t) 单独可调:真表是 const 的,不这么做就没法证明 「真有事的时候查得出来」。测试复制一张表出来逐样改坏,六种坏法都验过。 顺带修一个真 bug:HAL 两侧 GPIO 边界写的是 pin < 48,而 EFUSE_EN2 就在 IO48 上 —— CH2 的 eFuse 既开不了也关不掉。sim 与 ESP 两份都改成 0~48 含。 9 条 ctest(131 个用例)全绿,idf.py build 通过,16 条注错 15 条变红 (唯一没红的那条与原因记在 HANDOFF.md)。 Co-Authored-By: Claude Opus 5 <[email protected]>
这个提交包含在:
+75
-58
@@ -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 的降级矩阵管的是硬件坏了。带着错表跑起来的样子是「命令发给了另一路」,
|
||||
五台手机全在跑、一个错误码都不报。
|
||||
|
||||
@@ -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。 */
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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 上升沿)。
|
||||
|
||||
@@ -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 目标编)。
|
||||
|
||||
@@ -5,5 +5,6 @@ idf_component_register(
|
||||
SRCS "app_main.c"
|
||||
"version.c"
|
||||
"rail.c"
|
||||
"rail_cfg.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES phonerail_hal drv)
|
||||
|
||||
+38
-1
@@ -1,13 +1,50 @@
|
||||
/* ESP32-S3 入口。只在 ESP-IDF 目标里编译,宿主机构建不包含本文件。 */
|
||||
#include <stdlib.h>
|
||||
|
||||
#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。 */
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
/* 五路配置表 + 启动自检 + 两根 EN 线。契约与来历见 rail_cfg.h。 */
|
||||
#include "rail_cfg.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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 */
|
||||
@@ -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();
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户