固件:R2 迁移 R-0 ~ R-6(底板 + 5 片同构刀片)

2026-09-16 完成、当时未提交,这里补提交(原样,不含 09-27 上板的修改)。

- 器件层 / 拓扑层整体从 R1 换到 R2:TCA9548A 分通道、五路同址 INA226 0x40 + 输入侧 0x43、
  刀片 PCA9555(VBUS_EN 挪到 P0.4)、CH224A 只读驱动、风扇闭环 t_thermal
- 新增 i2cbus(mux 原子事务)、slot(刀片 I/O 门面)、host/qtest.sh
- 74HC595 链退役(代码与测试保留)
- 判据阈值与时间常量未动;19 个 ctest 全绿,idf.py build 通过

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-27 10:26:24 +08:00
共同撰写人 Claude Opus 5.5
父节点 5a80d8a872
当前提交 b8ad69681f
共修改 51 个文件,包含 10882 行新增和 4068 行删除
+464
查看文件
@@ -0,0 +1,464 @@
/* `slot` —— 一片刀片的 I/O 门面(R2 新增)。
这一层存在的全部理由是**一类不会报错的错**:
`pca9555_write_out0(slot, ...)` 的 `slot` 只用来记影子,驱动没法验证它和当前
mux 通道是不是同一路。传错的后果是「按了另一台手机的电源键」——
而五路的电压、电流、温度全部正常,I²C 一次都不 NAK,日志里什么都没有。
`slot.c` 把「选通道」和「记影子」绑在同一个函数里,让这件事**不可能**传错。
所以本文件的断言几乎都长成同一个样子:
**动了第 N 路 ⇒ 假 mux 的当前通道 == `rail_cfg(N)->mux_ch`,
而且只有第 N 片假 PCA9555 的输出锁存器变了。**
光看返回值和终态是看不出通道错的 —— 五片同址 0x20,写哪一片都成功。
另外三件会真出事的事:
· VBUS 写失败时影子往**哪一边**倒(关失败 ⇒ 留在 on,还会再关一次);
· `_locked` 版本在 `xact` 回调外被调(通道没选)会写到错的片子上;
· `PRSNT#` 读失败时返回**上一次成功的值**,而不是「不在位」。 */
#include "slot.h"
#include "board_pins.h"
#include "i2cbus.h"
#include "rail_cfg.h"
#include "sim_hal.h"
#include "tca9548a.h"
#include "unity.h"
/* ---- 脚手架 -------------------------------------------------------------- */
/* 假器件 idx 与路号的对应:本文件一律用 `blade(rail)`,不写 `rail - 1`
(`rail_cfg.h` 的规矩:由路号算出来的编号只许在那张表里出现)。 */
static int blade(int rail) { return rail - 1; }
static void mount(void)
{
sim_hal_reset();
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(NULL));
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_init());
sim_i2c_add_mux(TCA9548A_ADDR, BOARD_PIN_MUX_RST_N);
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
/* ⚠️ 通道从**表**里取。写成 `rail - 1` 的话,哪天表改了(`PRSNT#` 那一列
就是现成的先例),测试会跟着一起错,而且仍然全绿。 */
sim_pca9555_attach_ch(blade(rail), rail_cfg(rail)->mux_ch, PCA9555_ADDR);
}
pca9555_shadow_reset(); /* 驱动的影子是文件级静态,跨用例不会自己清 */
TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_init());
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); /* VBUS 受控之前 eFuse 必须先关 */
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_prsnt_init());
}
void setUp(void) { mount(); }
void tearDown(void) {}
static void init_all(void)
{
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_init(rail));
}
}
/* 除了 `except` 那一片,其余四片的输出锁存器都必须是 `rest`。 */
static void assert_only_blade_changed(int except_rail, uint8_t want, uint8_t rest)
{
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
const uint8_t got = sim_pca9555_output(blade(rail), 0);
TEST_ASSERT_EQUAL_HEX8(rail == except_rail ? want : rest, got);
}
}
/* ---- 路号 → 通道(唯一换算点) ------------------------------------------ */
/* `slot_mux_ch()` 必须**查表**,不许算。现在表恰好是 0 1 2 3 4,所以任何
「`rail - 1`」的写法都跑得过 —— 这条用例挡不住那种写法,它挡的是
「哪天表改了而 `slot.c` 没跟着改」。`rail_cfg.h` 里 `PRSNT#` 那一列就是先例。 */
static void test_mux_ch_comes_from_the_table(void)
{
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
TEST_ASSERT_EQUAL_INT(rail_cfg(rail)->mux_ch, slot_mux_ch(rail));
}
}
/* ---- ⭐ 通道选择:动了这一路 = 只动了这一片 ------------------------------ */
/* 核心用例。改 CH3 的电源键时:
① 假 mux 的当前通道必须是 `rail_cfg(3)->mux_ch`;
② **只有第 3 片**的输出锁存器变了;
③ 从头到尾没有两片一起应答(`ambiguous == 0`)。
通道传错的现象是「按了另一台手机的电源键」,五路电压电流全部正常。 */
static void test_out0_write_selects_this_rails_channel(void)
{
init_all();
const uint8_t pwr = (uint8_t)(1u << PCA9555_BIT_KEY_PWR);
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_out0_write(3, pwr, pwr));
TEST_ASSERT_EQUAL_INT(rail_cfg(3)->mux_ch, sim_i2c_mux_ch());
assert_only_blade_changed(3, pwr, 0x00);
TEST_ASSERT_EQUAL_INT(1, sim_pca9555_pin(blade(3), 0, PCA9555_BIT_KEY_PWR));
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous());
}
/* 五路各点一次名。上一条只证明了 CH3 对,这条排掉「常数 2」这种碰巧对的写法。 */
static void test_every_rail_touches_only_its_own_blade(void)
{
init_all();
const uint8_t pwr = (uint8_t)(1u << PCA9555_BIT_KEY_PWR);
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_out0_write(rail, pwr, pwr));
TEST_ASSERT_EQUAL_INT(rail_cfg(rail)->mux_ch, sim_i2c_mux_ch());
assert_only_blade_changed(rail, pwr, 0x00);
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_out0_write(rail, pwr, 0x00)); /* 松开再走下一路 */
}
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous());
}
/* 通道来回切之后,各路的影子还得是各路自己的 —— 影子串了的现象是
「按 CH5 的音量键顺手把 CH1 的电源键松开了」,而两次写都成功。 */
static void test_shadows_survive_channel_switching(void)
{
init_all();
const uint8_t pwr = (uint8_t)(1u << PCA9555_BIT_KEY_PWR);
const uint8_t vup = (uint8_t)(1u << PCA9555_BIT_KEY_VUP);
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_out0_write(1, pwr, pwr));
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_out0_write(5, vup, vup));
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_out0_write(1, vup, vup)); /* CH1 再加一个键 */
TEST_ASSERT_EQUAL_HEX8((uint8_t)(pwr | vup), sim_pca9555_output(blade(1), 0));
TEST_ASSERT_EQUAL_HEX8(vup, sim_pca9555_output(blade(5), 0));
TEST_ASSERT_EQUAL_HEX8((uint8_t)(pwr | vup), slot_shadow(1));
TEST_ASSERT_EQUAL_HEX8(vup, slot_shadow(5));
}
/* `slot_init()` 也得选对通道,而且**不许把别的刀片初始化掉** ——
只 init 一路时,其余四片必须还停在上电复位态(方向全输入)。
顺带再钉一次「初始化顺序」那道拦网:经 `slot` 走也一次都不许触发。 */
static void test_slot_init_configures_only_this_blade(void)
{
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_init(4));
TEST_ASSERT_EQUAL_INT(rail_cfg(4)->mux_ch, sim_i2c_mux_ch());
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
const uint8_t want = (rail == 4) ? PCA9555_CONFIG0_INIT : PCA9555_DIR_ALL_INPUT;
TEST_ASSERT_EQUAL_HEX8(want, sim_pca9555_config(blade(rail), 0));
}
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_unsafe_enable());
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_gnd_driven());
}
/* ---- VBUS ---------------------------------------------------------------- */
/* 极性:**高 = 通**(TPS22918 的 ON 脚 + R701 100 k 下拉)。写反的现象是
「上电就通、命令关它反而开」,而 I²C 全程正常。判据打在栅极电平上。 */
static void test_vbus_enable_drives_the_right_pin_on_the_right_blade(void)
{
init_all();
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(2, true));
TEST_ASSERT_EQUAL_INT(rail_cfg(2)->mux_ch, sim_i2c_mux_ch());
TEST_ASSERT_EQUAL_INT(1, sim_pca9555_pin(blade(2), 0, PCA9555_BIT_VBUS_EN));
assert_only_blade_changed(2, (uint8_t)(1u << PCA9555_BIT_VBUS_EN), 0x00);
TEST_ASSERT_TRUE(slot_vbus_is_on(2));
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(2, false));
TEST_ASSERT_EQUAL_INT(0, sim_pca9555_pin(blade(2), 0, PCA9555_BIT_VBUS_EN));
TEST_ASSERT_FALSE(slot_vbus_is_on(2));
}
/* 影子只有一份(在驱动里)。`slot.c` 要是自己再存一份,两处迟早分家:
现象是「`slot_vbus_is_on()` 说通着,下一次写键的读改写却把 VBUS 位清了」。
这里把「另存一份」钉死:绕过 `slot_vbus_enable()` 直接写驱动,
`slot_vbus_is_on()` 必须跟着变。 */
static void test_vbus_shadow_is_the_drivers_one(void)
{
init_all();
TEST_ASSERT_FALSE(slot_vbus_is_on(4));
/* 走 `slot_out0_write()`(键的那条路)把 VBUS 位置上 —— 同一个字节、同一个影子。 */
const uint8_t vbus = (uint8_t)(1u << PCA9555_BIT_VBUS_EN);
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_out0_write(4, vbus, vbus));
TEST_ASSERT_TRUE(slot_vbus_is_on(4));
TEST_ASSERT_EQUAL_HEX8(vbus, slot_shadow(4));
/* 反过来:按一个键不许把 VBUS 顺手带掉(读改写的那条语义)。 */
const uint8_t pwr = (uint8_t)(1u << PCA9555_BIT_KEY_PWR);
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_out0_write(4, pwr, pwr));
TEST_ASSERT_TRUE(slot_vbus_is_on(4));
TEST_ASSERT_EQUAL_INT(1, sim_pca9555_pin(blade(4), 0, PCA9555_BIT_VBUS_EN));
}
/* ⭐ **关 VBUS 失败 ⇒ 影子留在 on。**
方向是有讲究的:留在 on ⇒ 上层的反灌判据继续武装,下一圈还会再关一次。
要是写失败却把影子改成 off,判据会认为「已经关掉了」而解除武装 ——
现象是「日志里写着 VBUS 已断,实际还通着」,而这一路的电压电流看上去正常。 */
static void test_a_failed_vbus_off_leaves_the_shadow_on(void)
{
init_all();
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(5, true));
TEST_ASSERT_TRUE(slot_vbus_is_on(5));
sim_i2c_inject_nak_ch(rail_cfg(5)->mux_ch, PCA9555_ADDR, -1);
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, slot_vbus_enable(5, false));
TEST_ASSERT_TRUE(slot_vbus_is_on(5)); /* 还武装着 */
TEST_ASSERT_EQUAL_INT(1, sim_pca9555_pin(blade(5), 0, PCA9555_BIT_VBUS_EN)); /* 线上确实还通 */
/* 总线恢复之后再关一次就该真的关掉(判据没解除武装,所以会有这一次)。 */
sim_i2c_inject_nak_ch(rail_cfg(5)->mux_ch, PCA9555_ADDR, 0);
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(5, false));
TEST_ASSERT_FALSE(slot_vbus_is_on(5));
TEST_ASSERT_EQUAL_INT(0, sim_pca9555_pin(blade(5), 0, PCA9555_BIT_VBUS_EN));
}
/* ⭐ **开 VBUS 失败 ⇒ 影子留在 off。**
同一条规则(写成功才更新影子)在另一个方向上的表现,两边都倒向安全的那一侧:
这次影子要是变成 on,上层会以为 USB 已经供上了,从而不再重试,
而手机那头其实一直没电 —— 而且下一次写键的读改写会把 VBUS 位"补"上去,
变成一次没人下令的上电。 */
static void test_a_failed_vbus_on_leaves_the_shadow_off(void)
{
init_all();
sim_i2c_inject_nak_ch(rail_cfg(1)->mux_ch, PCA9555_ADDR, -1);
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, slot_vbus_enable(1, true));
TEST_ASSERT_FALSE(slot_vbus_is_on(1));
/* 恢复后按一个键:VBUS 位不许被带上去。 */
sim_i2c_inject_nak_ch(rail_cfg(1)->mux_ch, PCA9555_ADDR, 0);
const uint8_t pwr = (uint8_t)(1u << PCA9555_BIT_KEY_PWR);
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_out0_write(1, pwr, pwr));
TEST_ASSERT_EQUAL_INT(0, sim_pca9555_pin(blade(1), 0, PCA9555_BIT_VBUS_EN));
}
/* ---- ⭐ `_locked` 版本:为什么必须在 `xact` 回调里 ---------------------- */
typedef struct {
int rail;
bool on;
} vbus_arg_t;
static hal_err_t cb_vbus(void *arg)
{
const vbus_arg_t *a = (const vbus_arg_t *)arg;
return slot_vbus_enable_locked(a->rail, a->on);
}
/* 正路:`t_safety` 的用法 —— 拿 `slot_mux_ch(rail)` 开 `xact`,在回调里断 VBUS。
SW-8 要求「同一圈、同一把锁」里完成(计划 §5.4)。 */
static void test_vbus_locked_inside_a_xact_hits_the_right_blade(void)
{
init_all();
vbus_arg_t a = { 3, true };
TEST_ASSERT_EQUAL_INT(HAL_OK,
i2cbus_xact(slot_mux_ch(3), SLOT_LOCK_TIMEOUT_MS, cb_vbus, &a));
TEST_ASSERT_EQUAL_INT(rail_cfg(3)->mux_ch, sim_i2c_mux_ch());
assert_only_blade_changed(3, (uint8_t)(1u << PCA9555_BIT_VBUS_EN), 0x00);
TEST_ASSERT_TRUE(slot_vbus_is_on(3));
}
/* ⭐⭐ 反路:在回调**外面**调 `_locked` 版本。通道还停在上一次事务选的那一路,
于是这次写落在了**那一片**上,而影子记在了第 3 路的格子里。
返回值是 HAL_OK,五路电压电流全部正常,日志里什么都没有 ——
**这就是 `slot.c` 存在的理由**:不带 `_locked` 的版本自己开 `xact`,
让「选通道」和「记影子」不可能分开。
⚠️ 这条用例是在演示一个**错误用法的后果**,不是在钉一个正确行为。
哪天 `_locked` 版本自己也会选通道了,它会变红 —— 那时该改的是这条注释。 */
static void test_vbus_locked_outside_a_xact_writes_to_the_wrong_blade(void)
{
init_all();
const uint8_t vbus = (uint8_t)(1u << PCA9555_BIT_KEY_PWR);
/* 上一次事务把通道留在了 CH1 上。 */
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_out0_write(1, vbus, 0x00));
TEST_ASSERT_EQUAL_INT(rail_cfg(1)->mux_ch, sim_i2c_mux_ch());
/* 现在「关 CH3 的 VBUS」——没人给它选通道。 */
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable_locked(3, true)); /* 居然成功了 */
/* 线上真实发生的事:CH1 的 VBUS 被打开了,CH3 一动没动。 */
TEST_ASSERT_EQUAL_INT(1, sim_pca9555_pin(blade(1), 0, PCA9555_BIT_VBUS_EN));
TEST_ASSERT_EQUAL_INT(0, sim_pca9555_pin(blade(3), 0, PCA9555_BIT_VBUS_EN));
/* 而固件这边一口咬定 CH3 通着、CH1 没通 —— 两份认知就此分家。 */
TEST_ASSERT_TRUE(slot_vbus_is_on(3));
TEST_ASSERT_FALSE(slot_vbus_is_on(1));
}
/* ⚠️ `i2cbus.h` 末尾那条警告:回调里再调不带 `_locked` 的版本 = 嵌套拿同一把锁。
`i2cbus` 直接判 `HAL_ERR_STATE` 而不是死锁 —— 死锁在板子上的现象是
「巡检任务不再喂狗,整机每 8 s 重启一次」,而错误码至少能进事件日志。 */
static hal_err_t cb_nested(void *arg)
{
(void)arg;
return slot_out0_write(2, (uint8_t)(1u << PCA9555_BIT_KEY_PWR), 0x00);
}
static void test_calling_the_unlocked_version_inside_a_xact_is_rejected(void)
{
init_all();
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE,
i2cbus_xact(slot_mux_ch(2), SLOT_LOCK_TIMEOUT_MS, cb_nested, NULL));
}
/* ---- 输入(只有 `_locked` 版本) ---------------------------------------- */
static pca9555_in_t s_in;
static hal_err_t cb_read(void *arg)
{
const int rail = *(const int *)arg;
return slot_read_inputs_locked(rail, &s_in);
}
/* 读的是**这一路**的刀片。只让 CH4 报 eFuse 故障,五路挨个读一遍:
通道错的现象是「CH4 的故障被记在了 CH2 头上」,于是断的是好的那一路。 */
static void test_read_inputs_locked_reads_this_rails_blade(void)
{
init_all();
sim_pca9555_drive(blade(4), 1, (uint8_t)(0xFFu & ~(1u << PCA9555_BIT_EFUSE_FLT)));
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
int r = rail;
s_in.port0 = 0xAA;
s_in.port1 = 0xAA;
TEST_ASSERT_EQUAL_INT(HAL_OK,
i2cbus_xact(slot_mux_ch(rail), SLOT_LOCK_TIMEOUT_MS, cb_read, &r));
TEST_ASSERT_EQUAL_INT(rail == 4, pca9555_efuse_fault(&s_in));
}
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous());
}
/* ---- 参数与掩码的守门 ---------------------------------------------------- */
/* 路号是 **1~5**(不是 0~4)。越界必须返回 `HAL_ERR_ARG` 而不是悄悄当成 CH1 ——
悄悄当成 CH1 的现象是「命令发给了另一台手机」。 */
static void test_a_bad_rail_is_rejected_everywhere(void)
{
init_all();
const uint8_t pwr = (uint8_t)(1u << PCA9555_BIT_KEY_PWR);
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, slot_out0_write(0, pwr, pwr));
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, slot_out0_write(PHONERAIL_RAIL_COUNT + 1, pwr, pwr));
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, slot_init(0));
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, slot_vbus_enable(0, true));
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, slot_vbus_enable_locked(PHONERAIL_RAIL_COUNT + 1, true));
TEST_ASSERT_FALSE(slot_vbus_is_on(0));
TEST_ASSERT_FALSE(slot_present(0));
TEST_ASSERT_FALSE(slot_present(PHONERAIL_RAIL_COUNT + 1));
/* 一片都没被碰过。 */
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_output(blade(rail), 0));
}
}
/* 掩码碰到接地脚 → 驱动的 `HAL_ERR_ARG` 必须原样透出来(不是被 `xact` 吃掉
变成 HAL_OK)。透不出来的现象是「上层以为键按下了,其实一个字节都没发」。 */
static void test_a_mask_touching_a_grounded_pin_is_rejected(void)
{
init_all();
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, slot_out0_write(3, 0x20, 0x20)); /* P0.5 接地 */
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, slot_out0_write(3, 0xFF, 0x00));
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_gnd_driven());
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_output(blade(3), 0));
}
static void test_read_inputs_locked_rejects_null(void)
{
init_all();
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, slot_read_inputs_locked(1, NULL));
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, slot_read_inputs_locked(0, &s_in));
}
/* ---- `PRSNT#`:纯 GPIO,低 = 在位 ---------------------------------------- */
/* 极性:刀片把金手指 B31 短到 A1 的 GND,底板 R606~R610 上拉 ⇒ **低 = 在位**。
写反的现象是「五个槽全空 ⇒ 固件拒绝上电」(还算安全),或者
「空槽被当成在位 ⇒ 给一排裸露的金手指送 12 V」(不安全)。 */
static void test_present_is_low_true(void)
{
/* `slot_prsnt_init()` 刚跑完:内部上拉、没人拉低 = 五个槽都空着。 */
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
TEST_ASSERT_FALSE(slot_present(rail));
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(rail_cfg(rail)->prsnt_n)); /* 上拉生效 */
}
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
sim_gpio_drive(rail_cfg(rail)->prsnt_n, 0); /* 插上一片 */
}
slot_prsnt_poll();
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
TEST_ASSERT_TRUE(slot_present(rail));
}
}
/* 五个槽各读各的脚。⚠️ `PRSNT5#` 是 **IO7**,不接着前四路顺排 ——
写成 `15 + rail - 1` 的话第五路会去读 IO19(那上面是别的信号),
现象是「CH5 的刀片时有时无」。 */
static void test_each_slot_reads_its_own_pin(void)
{
for (int on = 1; on <= PHONERAIL_RAIL_COUNT; on++) {
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
sim_gpio_drive(rail_cfg(rail)->prsnt_n, rail == on ? 0 : 1);
}
slot_prsnt_poll();
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
TEST_ASSERT_EQUAL_INT(rail == on, slot_present(rail));
}
}
}
/* ⭐ **GPIO 读失败时返回上一次成功的值,不是「不在位」。**
让一次读失败把五台正在跑的手机判成"拔掉了",代价比漏判大得多
(上层会因此断电、记事件、甚至拒绝重新上电)。
这里用「引脚整体回到没 config 的状态」造读失败(`hal_gpio_read()` 在两个目标上
也只可能因为这个失败)。断言用**混合现状**:两路不在位、三路在位。
写成「失败就当不在位」⇒ 那三路变红;写成「失败就当在位」⇒ 那两路变红。 */
static void test_present_keeps_the_last_good_value_when_the_read_fails(void)
{
const bool want[PHONERAIL_RAIL_COUNT] = { false, true, false, true, true };
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
sim_gpio_drive(rail_cfg(rail)->prsnt_n, want[rail - 1] ? 0 : 1);
}
slot_prsnt_poll();
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
TEST_ASSERT_EQUAL_INT(want[rail - 1], slot_present(rail));
}
/* 引脚全部回到「没 config」⇒ `hal_gpio_read()` 一律 HAL_ERR_ARG。 */
sim_hal_reset();
slot_prsnt_poll();
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
TEST_ASSERT_EQUAL_INT(want[rail - 1], slot_present(rail)); /* 一格没变 */
}
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_mux_ch_comes_from_the_table);
RUN_TEST(test_out0_write_selects_this_rails_channel);
RUN_TEST(test_every_rail_touches_only_its_own_blade);
RUN_TEST(test_shadows_survive_channel_switching);
RUN_TEST(test_slot_init_configures_only_this_blade);
RUN_TEST(test_vbus_enable_drives_the_right_pin_on_the_right_blade);
RUN_TEST(test_vbus_shadow_is_the_drivers_one);
RUN_TEST(test_a_failed_vbus_off_leaves_the_shadow_on);
RUN_TEST(test_a_failed_vbus_on_leaves_the_shadow_off);
RUN_TEST(test_vbus_locked_inside_a_xact_hits_the_right_blade);
RUN_TEST(test_vbus_locked_outside_a_xact_writes_to_the_wrong_blade);
RUN_TEST(test_calling_the_unlocked_version_inside_a_xact_is_rejected);
RUN_TEST(test_read_inputs_locked_reads_this_rails_blade);
RUN_TEST(test_a_bad_rail_is_rejected_everywhere);
RUN_TEST(test_a_mask_touching_a_grounded_pin_is_rejected);
RUN_TEST(test_read_inputs_locked_rejects_null);
RUN_TEST(test_present_is_low_true);
RUN_TEST(test_each_slot_reads_its_own_pin);
RUN_TEST(test_present_keeps_the_last_good_value_when_the_read_fails);
return UNITY_END();
}