/* `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(); }