/* A9(**R2 版**):三个执行器状态机 + 互斥(需求 §10.6)。计划里这一包的验收原话是 「场景测试:硬重启走完 17 ms 上电时序;按键序列不重入;EDL 顺序固定」。 ⚠️ 这三条都**不是**「函数返回了 HAL_OK」能证明的东西,所以本文件里的断言 一律打在**线上真实发生的事**上: · 按键 / EDL / VBUS 看五片假 PCA9555 的**输出锁存器与栅极电平** (`sim_pca9555_output/pin`)与**写事务计数**(`sim_i2c_dev_ch(...)->n_write`), 不看驱动自己的影子 —— 对着影子断言等于自己证明自己, 「组合键分两次写」「影子记到了另一片上」影子变量一律看不出来; · `EFUSE_EN` 看 `sim_gpio_level()` 与全局写序号 `sim_gpio_write_seq()`。 ---- R1 → R2 这一层换了什么 ---------------------------------------------- R1 里按键 / EDL 在 74HC595 的 24 位链上(纯 GPIO),`VBUS_EN` 是直连 GPIO, 所以执行器**与 I²C 无交集**。R2 里这三样全在刀片那颗 PCA9555 的 Port0 上: bit0 KEY_PWR bit1 KEY_VUP bit2 KEY_VDN bit3 EDL_EN bit4 VBUS_EN (位号五片逐格相同,区分五路的是 **mux 通道**) 于是本层的失败形态整个换了一批,测试也跟着换: | R1 的失败形态 | R2 的对应形态 | 本文件里哪一条抓 | |---|---|---| | 位号算错(链上算成另一路的位) | **mux 通道选错 / 影子下标传错** | `..._every_action_selects_this_rails_mux_channel`、`..._pressing_ch1_does_not_touch_ch4` | | 组合键分两帧 RCLK | 组合键**分两次 `pca9555_write_out0`** | `..._combo_keys_land_in_exactly_one_write` | | 收尾时 595 锁被别人占着 | 收尾时 **I²C 锁**被别人占着 | `..._cleanup_retries_until_the_key_is_really_released` | | 两把锁不许嵌套 | **只剩一把锁**,改成与巡检**争用** | `..._and_safety_share_one_lock_without_starving` | 虚拟时钟下这些场景是零耗时的:`run_ms()` 干的就是任务体干的事 —— 调一次 `t_actuator_step()`,按它返回的时间往前拨表。 ⚠️ 假总线上的事务**会花掉虚拟时钟**(`sim_i2c_set_bus_time` 默认开), 这里**故意不关**:执行器与巡检争用同一把锁这件事,靠的就是总线时间是真的。 */ #include #include #include "board_pins.h" #include "hal_gpio.h" #include "hal_i2c.h" #include "hal_lock.h" #include "hal_time.h" #include "i2cbus.h" #include "ina226.h" #include "pca9555.h" #include "phonerail.h" #include "rail_cfg.h" #include "sim_hal.h" #include "slot.h" #include "t_actuator.h" #include "t_safety.h" #include "tca9548a.h" #include "unity.h" #define N PHONERAIL_RAIL_COUNT /* ---- 假 PG --------------------------------------------------------------- 真板上这个值来自 `t_safety` 每 20 ms 读回来的那一路 PCA9555 快照(app_main.c 里接的)。 `readable = false` 模拟「这一片 PCA9555 失联 / 巡检还没跑过一圈」—— 那是**读不到**, 和「读到了,是低」(电源还没好)是两回事,两条路径下面都要测到。 */ static bool s_pg_readable; static bool s_pg_val[N]; static bool fake_pg(void *ctx, int rail, bool *pg) { (void)ctx; if (!s_pg_readable) { return false; } *pg = s_pg_val[rail - 1]; return true; } /* ---- 五片刀片的观测口 ---------------------------------------------------- `idx`(假器件下标)与 `rail - 1` 对应,但通道一律查 `rail_cfg()`,**不算** —— 「mux_ch = rail − 1」是当下的事实,不是不变量(rail_cfg.h 那条禁令)。 */ static sim_i2c_dev_t *blade(int rail) { sim_i2c_dev_t *d = sim_i2c_dev_ch(rail_cfg(rail)->mux_ch, PCA9555_ADDR); TEST_ASSERT_NOT_NULL(d); return d; } static uint8_t latch(int rail) { return sim_pca9555_output(rail - 1, 0); } static int pin(int rail, int bit) { return sim_pca9555_pin(rail - 1, 0, bit); } static int key_pin(int rail, pca9555_key_t k) { return pin(rail, pca9555_key_bit(k)); } static void snap_latches(uint8_t out[N]) { for (int r = 1; r <= N; r++) { out[r - 1] = latch(r); } } static void snap_writes(uint32_t out[N]) { for (int r = 1; r <= N; r++) { out[r - 1] = blade(r)->n_write; } } /* 「除了 `except` 那一路,别的四片一个字节都没动」。 ⭐ R2 里「按了另一台手机的电源键」就长这个样子:本路的位号完全正确, 只是通道选错 / 影子下标传错,事务落到了另一片上。五路电压电流全部正常。 */ static void assert_only_this_blade_moved(int except, const uint8_t l0[N], const uint32_t w0[N]) { for (int r = 1; r <= N; r++) { if (r == except) { continue; } TEST_ASSERT_EQUAL_HEX8(l0[r - 1], latch(r)); TEST_ASSERT_EQUAL_UINT32(w0[r - 1], blade(r)->n_write); } TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous()); /* 同时开两路 = 两片一起应答 */ } /* ---- 窃听钩子:把「一次 I²C 写」和「一次 GPIO 写」排进同一条时间轴 -------- R1 靠比较 `RCLK` 与 `EFUSE_EN` 两根脚的全局写序号证明「先松键、再断电」。 R2 里松键是一次 I²C 写、断电仍是一次 GPIO 写,两边的计数器不通用, 所以在假器件上挂一个钩子:每次写落地时记下「`EFUSE_EN` 到此刻被写过几次」。 钩子**链到**假 PCA9555 自己的那一个,不是把它顶掉(顶掉就没人查短路了)。 */ static sim_i2c_hook_t s_inner_hook; static int s_spy_pin = -1; static uint32_t s_spy_writes; static uint32_t s_spy_gpio_writes_at_first; /* 第一次写发生时,那根脚的写次数 */ static void spy_hook(sim_i2c_dev_t *d, sim_i2c_ev_t ev) { if (ev == SIM_I2C_EV_POST_WRITE) { if (s_spy_writes == 0 && s_spy_pin >= 0) { s_spy_gpio_writes_at_first = sim_gpio_writes(s_spy_pin); } s_spy_writes++; } if (s_inner_hook != NULL) { s_inner_hook(d, ev); } } static void spy_on(int rail, int gpio_pin) { sim_i2c_dev_t *d = blade(rail); s_inner_hook = d->hook; d->hook = spy_hook; s_spy_pin = gpio_pin; s_spy_writes = 0; s_spy_gpio_writes_at_first = 0; } /* ---- 假板子 -------------------------------------------------------------- 底板一颗 TCA9548A,五个通道后面各挂一片同地址(0x20)的 PCA9555 —— **五片同址**正是 R2 D8 的全部意义,不是笔误。 */ static void board_up(void) { const hal_i2c_cfg_t cfg = { .sda_gpio = BOARD_PIN_I2C_SDA, .scl_gpio = BOARD_PIN_I2C_SCL, .freq_hz = 400000 }; TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(&cfg)); 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 <= N; rail++) { sim_pca9555_attach_ch(rail - 1, rail_cfg(rail)->mux_ch, PCA9555_ADDR); } TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_init()); TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); /* 五根 EN 线,全部拉低 */ for (int rail = 1; rail <= N; rail++) { /* 输出锁存器先清零、方向最后改(pca9555.h 那个框)。之后影子与锁存器都是 0。 */ TEST_ASSERT_EQUAL_INT(HAL_OK, slot_init(rail)); TEST_ASSERT_EQUAL_HEX8(0x00, latch(rail)); } /* 初始化一步都不许把接地脚配成输出,也不许在锁存器还是 0xFF 时就开方向。 */ TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_gnd_driven()); TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_unsafe_enable()); /* 强制加锁纪律 —— 但要在初始化**之后**打开:启动期是独占跑的, 本来就不该被要求先拿锁。打开之后,任何一次 I²C 事务没持锁就记一次违规。 */ sim_lock_require(HAL_LOCK_I2C, true); s_pg_readable = true; memset(s_pg_val, 0, sizeof(s_pg_val)); s_inner_hook = NULL; s_spy_pin = -1; s_spy_writes = 0; s_spy_gpio_writes_at_first = 0; t_actuator_init(fake_pg, NULL); } void setUp(void) { sim_hal_reset(); board_up(); } void tearDown(void) {} /* 把虚拟时钟往前推 ms,期间按 `t_actuator_step()` 自己要的节奏叫它 —— 这正是 app_main.c 里那个任务体在干的事。 ⚠️ 用**绝对目标时刻**而不是「累减 left」:等锁超时会在 step 内部把虚拟时钟 花掉 20 ms(hal_lock_sim.c),I²C 事务本身也花时钟,累减的写法会因此推过头。 */ static void run_ms(uint32_t ms) { const uint32_t target = hal_time_now_ms() + ms; for (;;) { uint32_t w = t_actuator_step(); const uint32_t now = hal_time_now_ms(); if ((int32_t)(now - target) >= 0) { return; /* 到点了,而且最后这一步已经推过 */ } const uint32_t left = target - now; if (w == 0 || w > left) { w = left; } sim_time_advance_ms(w); } } /* 扮演第二个任务,把 I²C 锁占住 / 放开(单线程 sim 里「别人正持着锁」就这么造)。 */ static void other_task_takes_the_bus(void) { sim_lock_set_task(1); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10)); sim_lock_set_task(0); } static void other_task_frees_the_bus(void) { sim_lock_set_task(1); hal_lock_give(HAL_LOCK_I2C); sim_lock_set_task(0); } /* ==== 验收主项一:硬重启走完 17 ms 上电时序 =============================== */ /* 用 CH3 —— 它的 `EFUSE_EN` 是 IO11、mux 通道 2、DAC 在**另一片**上, 全是「按路号算就一定算错」的那一路。 */ static void test_hard_reset_walks_the_full_power_up_sequence(void) { const int rail = 3; const int en = rail_cfg(rail)->efuse_en; /* 先让这一路开着 —— 硬重启是给**正在跑的**手机用的。 */ TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(rail, true)); TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(en)); s_pg_val[rail - 1] = true; /* PG 一直是好的 */ const uint32_t takes0 = sim_lock_takes(HAL_LOCK_I2C); const uint32_t t_req = hal_time_now_ms(); uint8_t l0[N]; uint32_t w0[N]; snap_latches(l0); snap_writes(w0); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail)); TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(en)); /* 请求那一刻就断电,不等下一拍 */ TEST_ASSERT_TRUE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_INT(ACT_SEQ_HARD_RESET, t_actuator_seq(rail)); /* 断电窗口是 §7.4 的 3~5 s(放电由硬件联锁做,τ 22 ms)。差一毫秒都不许上电: 缩短它的现象**不是**「重启快了」,而是「有的机型压根没重启」。 */ run_ms(ACT_HR_OFF_MIN_MS - 1); TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(en)); TEST_ASSERT_EQUAL_INT(ACT_ST_HR_OFF, t_actuator_state(rail)); run_ms(ACT_HR_OFF_MS - (ACT_HR_OFF_MIN_MS - 1)); TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(en)); /* EN↑ */ TEST_ASSERT_EQUAL_INT(ACT_ST_HR_RAMP, t_actuator_state(rail)); const uint32_t t_on = hal_time_now_ms(); const uint32_t off_ms = t_on - t_req; TEST_ASSERT_TRUE(off_ms >= ACT_HR_OFF_MIN_MS && off_ms <= ACT_HR_OFF_MAX_MS); /* ⭐ 这一条就是「17 ms 上电时序」:**PG 早就是 1 了**,序列也一步都不许提前完成。 tON 5.1 ms + 缓启动斜坡 11.5 ms 之内,输出还在爬。 少等这 17 ms 的实现在这里当场变红。 */ run_ms(ACT_HR_RAMP_MS - 1); TEST_ASSERT_EQUAL_INT(ACT_ST_HR_RAMP, t_actuator_state(rail)); TEST_ASSERT_TRUE(t_actuator_busy(rail)); run_ms(1); TEST_ASSERT_EQUAL_UINT32(ACT_HR_RAMP_MS, hal_time_now_ms() - t_on); /* ⭐ PG 到了 ⇒ 当场按下电源键开机。电池端上电的手机停在关机态, 不按这一下的硬重启等于「关机」(小米8 实测,t_actuator.h)。 */ TEST_ASSERT_EQUAL_INT(ACT_ST_HR_BOOT, t_actuator_state(rail)); TEST_ASSERT_EQUAL_INT(1, key_pin(rail, PCA9555_KEY_PWR)); TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_VUP)); TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_VDN)); run_ms(ACT_HR_BOOT_KEY_MS - 1); TEST_ASSERT_EQUAL_INT(1, key_pin(rail, PCA9555_KEY_PWR)); TEST_ASSERT_TRUE(t_actuator_busy(rail)); run_ms(1); TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_PWR)); /* 松开 */ TEST_ASSERT_FALSE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail)); TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(en)); TEST_ASSERT_EQUAL_UINT32(1, t_actuator_done(rail)); /* ⭐ 总线上只有按下、松开这两次事务,而且只动这一片。断电那一段一个都不发: 键本来就没按着 ⇒ 影子不脏。R2 里每一次多余的事务都是从巡检嘴里抢锁。 */ TEST_ASSERT_EQUAL_UINT32(takes0 + 2, sim_lock_takes(HAL_LOCK_I2C)); assert_only_this_blade_moved(rail, l0, w0); TEST_ASSERT_EQUAL_HEX8(l0[rail - 1], latch(rail)); /* 收工后这一片回到原样 */ TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C)); } /* 别的四路的 `EFUSE_EN`,从头到尾一次都不该被写。 */ static void test_hard_reset_touches_only_its_own_en_pin(void) { const int rail = 2; uint32_t before[N]; for (int r = 1; r <= N; r++) { before[r - 1] = sim_gpio_writes(rail_cfg(r)->efuse_en); } s_pg_val[rail - 1] = true; TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail)); run_ms(ACT_HR_OFF_MS + ACT_HR_RAMP_MS + ACT_HR_BOOT_KEY_MS); TEST_ASSERT_FALSE(t_actuator_busy(rail)); for (int r = 1; r <= N; r++) { if (r == rail) { continue; } TEST_ASSERT_EQUAL_UINT32(before[r - 1], sim_gpio_writes(rail_cfg(r)->efuse_en)); } /* VBUS 是 EDL 的事,硬重启不碰 —— R2 里它在 PCA9555 的 P0.4 上。 */ for (int r = 1; r <= N; r++) { TEST_ASSERT_EQUAL_INT(0, pin(r, PCA9555_BIT_VBUS_EN)); TEST_ASSERT_FALSE(slot_vbus_is_on(r)); } } /* PG 等不到(这一片 PCA9555 失联)⇒ 记 NO_PG,但**电继续开着**。 §10.10:这片不应答 = 失去 PG 可见性,**不关输出**。把「看不见」当「坏了」去断电, 等于一片 I/O 扩展器失联就让五台手机一起停机。 */ static void test_no_pg_leaves_the_rail_powered(void) { const int rail = 4; const int en = rail_cfg(rail)->efuse_en; s_pg_readable = false; TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail)); run_ms(ACT_HR_OFF_MS + ACT_HR_RAMP_MS + ACT_HR_PG_TIMEOUT_MS); TEST_ASSERT_FALSE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_INT(ACT_RES_NO_PG, t_actuator_last_result(rail)); TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(en)); /* ⭐ 不许再关掉 */ /* 开机键也不按:键和 PG 在同一片 PCA9555 上,读不到 PG 时去按只会卡在 CLEANUP。 */ TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_PWR)); } /* 读得到、但一直是低(电源确实没好)走的是同一条路:照样不自作主张断电。 */ static void test_pg_stuck_low_also_leaves_the_rail_powered(void) { const int rail = 1; s_pg_readable = true; s_pg_val[rail - 1] = false; TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail)); run_ms(ACT_HR_OFF_MS + ACT_HR_RAMP_MS + ACT_HR_PG_TIMEOUT_MS); TEST_ASSERT_EQUAL_INT(ACT_RES_NO_PG, t_actuator_last_result(rail)); TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(rail_cfg(rail)->efuse_en)); } /* PG 迟到(斜坡走完之后 200 ms 才置位)也要认,不能只在进 HR_PG 那一刻看一眼。 */ static void test_late_pg_is_still_accepted(void) { const int rail = 5; s_pg_readable = true; s_pg_val[rail - 1] = false; TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail)); run_ms(ACT_HR_OFF_MS + ACT_HR_RAMP_MS + 200); TEST_ASSERT_EQUAL_INT(ACT_ST_HR_PG, t_actuator_state(rail)); s_pg_val[rail - 1] = true; run_ms(ACT_HR_PG_POLL_MS); TEST_ASSERT_EQUAL_INT(ACT_ST_HR_BOOT, t_actuator_state(rail)); run_ms(ACT_HR_BOOT_KEY_MS); TEST_ASSERT_FALSE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail)); } /* ⚠️ 上面那些用的都是 `run_ms()` —— 它按**测试自己的**节奏叫 step, 所以测不出「step 返回的睡眠时长本身对不对」。真板上的任务体是**完全听 step 的**: 它说睡多久就睡多久。这个测试照那样跑,PG 在斜坡走完之后才置位, 序列必须在一个轮询间隔之内收工 —— 「等 PG 时一觉睡满 500 ms 超时」的写法在这里变红 (那种写法把「等到 PG 就结束」悄悄变成了「一律等满 500 ms」, 而测试与遥测都看不出区别:结果照样是 OK,只是每次硬重启都慢半秒)。 */ static void test_the_task_loop_only_sleeps_what_step_asks_for(void) { const int rail = 2; const uint32_t pg_at = ACT_HR_OFF_MS + ACT_HR_RAMP_MS + 1; s_pg_readable = true; s_pg_val[rail - 1] = false; TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail)); const uint32_t t0 = hal_time_now_ms(); bool pg_raised = false; uint32_t total = 0; for (int i = 0; i < 10000; i++) { const uint32_t w = t_actuator_step(); /* 任务体:它说睡多久就睡多久 */ if (t_actuator_state(rail) == ACT_ST_HR_BOOT) { total = hal_time_now_ms() - t0; /* 等到 PG 的**那一刻**,不是下一次醒来 */ break; } sim_time_advance_ms(w == 0 ? 1 : w); if (!pg_raised && hal_time_now_ms() - t0 >= pg_at) { s_pg_val[rail - 1] = true; pg_raised = true; } } TEST_ASSERT_TRUE(pg_raised); TEST_ASSERT_TRUE(total > 0 && total <= pg_at + 2 * ACT_HR_PG_POLL_MS); /* 按键保持期也照样听 step 的:睡到点就松键收工,不多睡一整段。 */ for (int i = 0; i < 10000 && t_actuator_busy(rail); i++) { const uint32_t w = t_actuator_step(); if (t_actuator_busy(rail)) { sim_time_advance_ms(w == 0 ? 1 : w); } } TEST_ASSERT_FALSE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail)); TEST_ASSERT_TRUE(hal_time_now_ms() - t0 <= total + ACT_HR_BOOT_KEY_MS + ACT_CLEANUP_RETRY_MS); } /* 硬重启之前先把这一路的三个键松开:断电重启时有键按着,手机会开进 Recovery / Fastboot 而不是正常起来。这里手工造一个「上一次收尾没落地」的脏位。 ⭐ 顺序也要对:**先松键、再断电**。R2 里这两件事一件在 I²C 上、一件在 GPIO 上, 两边的计数器不通用,所以靠钩子记下「松键那一刻 EN 被写过几次」。 反过来写(先断电再松键)的现象是:手机在断电那一刻键还按着 —— 而重启之后 它进的是 Recovery,遥测上电压电流一切正常。 */ static void test_hard_reset_releases_a_stuck_key_before_cutting_power(void) { const int rail = 1; const int other = 4; const int en = rail_cfg(rail)->efuse_en; const uint8_t km = pca9555_key_mask(); /* 造脏位:这一路三个键按着,另一路按着音量上(它一动都不许动)。 */ TEST_ASSERT_EQUAL_INT(HAL_OK, slot_out0_write(rail, km, km)); TEST_ASSERT_EQUAL_INT( HAL_OK, slot_out0_write(other, (uint8_t)(1u << pca9555_key_bit(PCA9555_KEY_VUP)), (uint8_t)(1u << pca9555_key_bit(PCA9555_KEY_VUP)))); TEST_ASSERT_EQUAL_INT(1, key_pin(rail, PCA9555_KEY_PWR)); TEST_ASSERT_EQUAL_INT(1, key_pin(other, PCA9555_KEY_VUP)); spy_on(rail, en); const uint32_t en_writes0 = sim_gpio_writes(en); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail)); TEST_ASSERT_EQUAL_HEX8(0x00, (uint8_t)(latch(rail) & km)); /* 三个键一起松开 */ TEST_ASSERT_EQUAL_INT(1, key_pin(other, PCA9555_KEY_VUP)); /* 别路的键一动没动 */ /* 松键**只发一帧**(三位在同一次写里),而且发的时候 EN 还没被碰过。 */ TEST_ASSERT_EQUAL_UINT32(1, s_spy_writes); TEST_ASSERT_EQUAL_UINT32(en_writes0, s_spy_gpio_writes_at_first); TEST_ASSERT_EQUAL_UINT32(en_writes0 + 1, sim_gpio_writes(en)); TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(en)); } /* 影子是干净的时候(正常情况)**一个事务都不发** —— 每一次多余的写都是从 每 20 ms 一圈的巡检嘴里抢同一把锁。 */ static void test_the_defensive_release_sends_nothing_when_the_shadow_is_clean(void) { const uint32_t takes0 = sim_lock_takes(HAL_LOCK_I2C); uint8_t l0[N]; uint32_t w0[N]; snap_latches(l0); snap_writes(w0); for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail)); } for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_EQUAL_UINT32(w0[rail - 1], blade(rail)->n_write); TEST_ASSERT_EQUAL_HEX8(l0[rail - 1], latch(rail)); } TEST_ASSERT_EQUAL_UINT32(takes0, sim_lock_takes(HAL_LOCK_I2C)); } /* ==== 验收主项二:按键序列不重入 ========================================= */ /* 短按 100 ms(SW-5 的第一档)。位号一律查 `pca9555_key_bit()`:R2 里它确实 与路号无关,但「与路号无关」是当下的事实,不是写死 `1 << 0` 的理由。 */ static void test_key_tap_presses_the_table_bit_and_releases_it(void) { const int rail = 5; const int bit = pca9555_key_bit(PCA9555_KEY_PWR); const uint32_t w0 = blade(rail)->n_write; TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_TAP)); TEST_ASSERT_EQUAL_INT(1, pin(rail, bit)); /* 栅极真的被拉起来了 */ TEST_ASSERT_EQUAL_HEX8((uint8_t)(1u << bit), latch(rail)); TEST_ASSERT_EQUAL_UINT32(w0 + 1, blade(rail)->n_write); /* ⭐ 这一次写落在**这一路的**通道上。选错通道 = 按了另一台手机的电源键。 */ TEST_ASSERT_EQUAL_INT(rail_cfg(rail)->mux_ch, sim_i2c_mux_ch()); run_ms(ACT_KEY_MS_TAP - 1); TEST_ASSERT_EQUAL_INT(1, pin(rail, bit)); TEST_ASSERT_TRUE(t_actuator_busy(rail)); run_ms(1); TEST_ASSERT_EQUAL_INT(0, pin(rail, bit)); TEST_ASSERT_EQUAL_HEX8(0x00, latch(rail)); TEST_ASSERT_EQUAL_UINT32(w0 + 2, blade(rail)->n_write); /* 按下一次、松开一次 */ TEST_ASSERT_FALSE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail)); TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C)); } /* ⭐ §10.6 的硬约束:组合键必须**一次写**进去。 R1 靠「同一帧 RCLK」证明,R2 靠「一次 `pca9555_write_out0`」—— 假器件的写事务计数是唯一能证明这件事的东西:分两次写的实现,影子最终值、 栅极最终电平、函数返回码**全部一模一样**,只有 `n_write` 会是 2。 而分两次写的真实后果是「先单按了一下 VOL−」,在有的机型上就是调音量。 */ static void test_combo_keys_land_in_exactly_one_write(void) { const int rail = 2; const uint8_t want = (uint8_t)((1u << pca9555_key_bit(PCA9555_KEY_VDN)) | (1u << pca9555_key_bit(PCA9555_KEY_PWR))); const uint32_t w0 = blade(rail)->n_write; TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_FASTBOOT, ACT_KEY_MS_ON)); TEST_ASSERT_EQUAL_UINT32(w0 + 1, blade(rail)->n_write); /* ⭐ 正好一次 */ TEST_ASSERT_EQUAL_HEX8(want, latch(rail)); TEST_ASSERT_EQUAL_INT(1, key_pin(rail, PCA9555_KEY_VDN)); TEST_ASSERT_EQUAL_INT(1, key_pin(rail, PCA9555_KEY_PWR)); TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_VUP)); /* 第三个键没被顺手带上 */ run_ms(ACT_KEY_MS_ON); TEST_ASSERT_EQUAL_HEX8(0x00, latch(rail)); TEST_ASSERT_EQUAL_UINT32(w0 + 2, blade(rail)->n_write); /* 松开也是一次 */ } /* Recovery 是 VOL+ + PWR,和 Fastboot 差在哪一个音量键上 —— 两者不能相等。 */ static void test_recovery_and_fastboot_are_different_key_sets(void) { const int rail = 3; TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_RECOVERY, ACT_KEY_MS_ON)); const uint8_t recovery = latch(rail); run_ms(ACT_KEY_MS_ON); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_FASTBOOT, ACT_KEY_MS_ON)); const uint8_t fastboot = latch(rail); run_ms(ACT_KEY_MS_ON); TEST_ASSERT_TRUE(recovery != fastboot); TEST_ASSERT_EQUAL_HEX8((uint8_t)(1u << pca9555_key_bit(PCA9555_KEY_PWR)), (uint8_t)(recovery & fastboot)); /* 共有的只有电源键 */ } /* ⭐ R2 新增:**不许在持锁期间等时间。** 按键保持 100 ms / 2 s / 10 s 是状态机的事, 不是持锁的事 —— 持着锁等 10 s,那 10 s 里巡检一圈都跑不了,SR-5 整条判据停摆, 而按键功能本身完全正常(测试与遥测都看不出区别)。 证明方式:保持期的每一拍,这把锁都必须是**没人持着**的; 整个序列上锁的次数正好 **2**(按下一次、松开一次)。 */ static void test_the_key_hold_never_holds_the_i2c_lock(void) { const int rail = 3; const uint32_t takes0 = sim_lock_takes(HAL_LOCK_I2C); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_ON)); TEST_ASSERT_EQUAL_INT(-1, sim_lock_owner(HAL_LOCK_I2C)); /* 写完当场就放掉 */ TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C)); for (int i = 0; i < (int)(ACT_KEY_MS_ON / 20) + 20; i++) { (void)t_actuator_step(); TEST_ASSERT_EQUAL_INT(-1, sim_lock_owner(HAL_LOCK_I2C)); TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C)); sim_time_advance_ms(20); /* 巡检的节拍 */ } TEST_ASSERT_FALSE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_HEX8(0x00, latch(rail)); TEST_ASSERT_EQUAL_UINT32(takes0 + 2, sim_lock_takes(HAL_LOCK_I2C)); } /* ⭐ 不重入:一路上正在跑序列时,三个请求全部被拒,而且**一个字节都不发**。 */ static void test_a_running_sequence_rejects_every_new_request(void) { const int rail = 1; const int en = rail_cfg(rail)->efuse_en; TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_FORCE)); const uint32_t writes1 = blade(rail)->n_write; const uint8_t latch1 = latch(rail); const uint32_t en_writes1 = sim_gpio_writes(en); TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, t_actuator_key(rail, ACT_KEYS_VUP, ACT_KEY_MS_TAP)); TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, t_actuator_hard_reset(rail)); TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, t_actuator_edl(rail)); TEST_ASSERT_EQUAL_UINT32(writes1, blade(rail)->n_write); TEST_ASSERT_EQUAL_HEX8(latch1, latch(rail)); TEST_ASSERT_EQUAL_UINT32(en_writes1, sim_gpio_writes(en)); /* 被拒的硬重启没断电 */ /* 被拒的请求也不许改掉正在跑的那一个:仍然按 10 s 松开,不是 100 ms。 */ run_ms(ACT_KEY_MS_TAP + 1); TEST_ASSERT_TRUE(t_actuator_busy(rail)); run_ms(ACT_KEY_MS_FORCE - ACT_KEY_MS_TAP - 1); TEST_ASSERT_FALSE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_UINT32(1, t_actuator_done(rail)); /* 只跑过一个序列 */ } /* ⭐ 改 CH1 的键不许动 CH4 的。 R1 里这是「同一条链上的不同位」,位号算错就串路;R2 里这是**两片不同的器件**, 串路的形态变成了「mux 通道选错」或「影子下标传错」—— 后果一样是「按了另一台手机的电源键」,而五路电压电流全部正常。 所以断言打在两片假器件**各自的输出锁存器**上,外加「另外三片一个字节没动」。 */ static void test_pressing_ch1_does_not_touch_ch4(void) { const int pwr = pca9555_key_bit(PCA9555_KEY_PWR); const int vup = pca9555_key_bit(PCA9555_KEY_VUP); uint8_t l0[N]; uint32_t w0[N]; snap_latches(l0); snap_writes(w0); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(1, ACT_KEYS_PWR, ACT_KEY_MS_ON)); /* 2 s */ TEST_ASSERT_EQUAL_INT(rail_cfg(1)->mux_ch, sim_i2c_mux_ch()); assert_only_this_blade_moved(1, l0, w0); run_ms(50); snap_latches(l0); snap_writes(w0); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(4, ACT_KEYS_VUP, ACT_KEY_MS_TAP)); /* 100 ms */ TEST_ASSERT_EQUAL_INT(rail_cfg(4)->mux_ch, sim_i2c_mux_ch()); assert_only_this_blade_moved(4, l0, w0); /* CH1 的电源键仍按着,没被顺手改掉 */ TEST_ASSERT_EQUAL_INT(1, pin(1, pwr)); TEST_ASSERT_EQUAL_INT(1, pin(4, vup)); run_ms(ACT_KEY_MS_TAP); /* CH4 到点 */ TEST_ASSERT_EQUAL_INT(0, pin(4, vup)); TEST_ASSERT_EQUAL_INT(1, pin(1, pwr)); /* ⭐ CH1 没被顺手清掉 */ TEST_ASSERT_TRUE(t_actuator_busy(1)); TEST_ASSERT_FALSE(t_actuator_busy(4)); run_ms(ACT_KEY_MS_ON); /* CH1 到点 */ for (int r = 1; r <= N; r++) { TEST_ASSERT_EQUAL_HEX8(0x00, latch(r)); } TEST_ASSERT_FALSE(t_actuator_busy(1)); } /* ⭐ R2 新增:**每一次动作发生时,mux 通道选的是这一路。** 五路逐个跑一遍完整按键序列,每一次写都要满足两件事: ① 写完之后当前通道就是 `rail_cfg(rail)->mux_ch`(查表,不算); ② 只有这一片的锁存器和写计数变了,别的四片一个字节没动。 ⚠️ 「mux_ch = rail − 1」现在确实成立,所以把它写死也能过 ① —— ② 才是真正拦得住「影子下标传错」的那一条(那时通道是对的,影子记到了别片上, 下一次写会把那一片的键顺手改掉)。 */ static void test_every_action_selects_this_rails_mux_channel(void) { for (int rail = 1; rail <= N; rail++) { uint8_t l0[N]; uint32_t w0[N]; snap_latches(l0); snap_writes(w0); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_RECOVERY, ACT_KEY_MS_TAP)); TEST_ASSERT_EQUAL_INT(rail_cfg(rail)->mux_ch, sim_i2c_mux_ch()); TEST_ASSERT_EQUAL_UINT32(w0[rail - 1] + 1, blade(rail)->n_write); assert_only_this_blade_moved(rail, l0, w0); snap_latches(l0); snap_writes(w0); run_ms(ACT_KEY_MS_TAP); TEST_ASSERT_FALSE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_INT(rail_cfg(rail)->mux_ch, sim_i2c_mux_ch()); TEST_ASSERT_EQUAL_HEX8(0x00, latch(rail)); assert_only_this_blade_moved(rail, l0, w0); } } /* ==== 验收主项三:EDL 顺序固定 =========================================== */ /* SW-6:`EDL_EN=1` → 等 10 ms → `VBUS_EN=1` → 等枚举 3 s → `EDL_EN=0`。 **先短接再插 USB** 是硬要求。R1 里这两样一个在 595 上、一个是 GPIO,只能比写序号; R2 里它们是**同一个字节的两位**,所以改成在三个时刻各看一眼 —— 顺序写反的实现,中间那一眼会看到「USB 已经插上、短接还没建立」。 */ static void test_edl_order_is_short_then_vbus_then_release(void) { const int rail = 3; const uint32_t w0 = blade(rail)->n_write; TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN)); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_edl(rail)); TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_EDL_EN)); TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN)); /* USB 还没插 */ TEST_ASSERT_EQUAL_UINT32(w0 + 1, blade(rail)->n_write); run_ms(ACT_EDL_SHORT_MS - 1); TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN)); run_ms(1); TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_VBUS_EN)); TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_EDL_EN)); /* 还短接着 */ TEST_ASSERT_TRUE(slot_vbus_is_on(rail)); TEST_ASSERT_EQUAL_UINT32(w0 + 2, blade(rail)->n_write); run_ms(ACT_EDL_ENUM_MS - 1); TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_EDL_EN)); run_ms(1); TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_EDL_EN)); /* 枚举完才松短接 */ TEST_ASSERT_EQUAL_UINT32(w0 + 3, blade(rail)->n_write); /* VBUS **留着** —— 手机现在正挂在 9008 上,把它断了等于白跑一趟。 */ TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_VBUS_EN)); TEST_ASSERT_TRUE(slot_vbus_is_on(rail)); TEST_ASSERT_FALSE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail)); /* 一个键都没按过:EDL 只该动 bit3 和 bit4。 */ TEST_ASSERT_EQUAL_HEX8(0x00, (uint8_t)(latch(rail) & pca9555_key_mask())); } /* 五路逐个跑 EDL:位号五片相同(`PCA9555_BIT_EDL_EN`),但**器件是五片不同的**, 所以每一路都要落在自己那一片上,别的四片不许出现 `EDL_EN`。 */ static void test_edl_lands_on_its_own_blade_for_every_rail(void) { for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_edl(rail)); TEST_ASSERT_EQUAL_INT(rail_cfg(rail)->mux_ch, sim_i2c_mux_ch()); for (int r = 1; r <= N; r++) { TEST_ASSERT_EQUAL_INT(r == rail ? 1 : 0, pin(r, PCA9555_BIT_EDL_EN)); } run_ms(ACT_EDL_SHORT_MS + ACT_EDL_ENUM_MS); TEST_ASSERT_FALSE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_EDL_EN)); TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_VBUS_EN)); } /* 跑完五路:五片的 VBUS 都开着、EDL 都松了,一个键都没按着。 */ for (int r = 1; r <= N; r++) { TEST_ASSERT_EQUAL_HEX8((uint8_t)(1u << PCA9555_BIT_VBUS_EN), latch(r)); } } /* ==== 收尾:按下去的东西一定要松开 ======================================= */ /* ⭐ I²C 锁被别人占着时,松键落不下去。**不许放弃** —— 放弃 = 电源键留在按下状态,手机再也起不来,而遥测上五路电压电流一切正常。 R1 靠占住 595 那把锁制造写失败,R2 里只剩一把锁,占的就是巡检也在用的那一把, 所以这条路径在真板上**每天都会走到**(不是异常路径)。 */ static void test_cleanup_retries_until_the_key_is_really_released(void) { const int rail = 1; const int bit = pca9555_key_bit(PCA9555_KEY_PWR); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_TAP)); other_task_takes_the_bus(); run_ms(ACT_KEY_MS_TAP); TEST_ASSERT_EQUAL_INT(ACT_ST_CLEANUP, t_actuator_state(rail)); TEST_ASSERT_TRUE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_INT(1, pin(rail, bit)); /* 键还按着 */ for (int i = 0; i < 5; i++) { (void)t_actuator_step(); } TEST_ASSERT_TRUE(t_actuator_cleanup_retries(rail) > 0); TEST_ASSERT_EQUAL_INT(1, pin(rail, bit)); /* 重试期间键仍然按着,没被当成已松开 */ /* 收尾期间这一路照样算 busy,新请求进不来。 */ TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_TAP)); other_task_frees_the_bus(); (void)t_actuator_step(); TEST_ASSERT_EQUAL_INT(0, pin(rail, bit)); /* 锁一放开就落地 */ TEST_ASSERT_FALSE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_HEX8(0x00, latch(rail)); } /* 收尾重试**永远不会自己放弃**:把锁占住不放,跑上几百拍, 这一路必须一直是 CLEANUP、一直 busy、键一直按着。 写成「重试 N 次就算了」的实现在这里会悄悄回到 IDLE —— 那正是最坏的失败。 */ static void test_cleanup_never_gives_up_no_matter_how_long_the_bus_is_busy(void) { const int rail = 2; const int bit = pca9555_key_bit(PCA9555_KEY_PWR); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_TAP)); other_task_takes_the_bus(); run_ms(ACT_KEY_MS_TAP); for (int i = 0; i < 500; i++) { (void)t_actuator_step(); TEST_ASSERT_EQUAL_INT(ACT_ST_CLEANUP, t_actuator_state(rail)); TEST_ASSERT_TRUE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_INT(1, pin(rail, bit)); } TEST_ASSERT_TRUE(t_actuator_cleanup_retries(rail) >= 500); other_task_frees_the_bus(); (void)t_actuator_step(); TEST_ASSERT_EQUAL_INT(0, pin(rail, bit)); TEST_ASSERT_FALSE(t_actuator_busy(rail)); } /* 按下那一步就失败(I²C 锁被别人占着)。请求返回错误码,但**不能就这么算了** —— 事务有可能发出去一半(驱动的影子不更新,可器件里那个字节是未知的), 直接回 IDLE 等于把一个也许已经按下的键留在那儿,而没有任何人会来松开它。 所以失败的按下照样要走收尾。 */ static void test_a_failed_press_still_goes_through_cleanup(void) { const int rail = 4; const int bit = pca9555_key_bit(PCA9555_KEY_PWR); other_task_takes_the_bus(); TEST_ASSERT_EQUAL_INT(HAL_ERR_TIMEOUT, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_TAP)); TEST_ASSERT_EQUAL_INT(ACT_ST_CLEANUP, t_actuator_state(rail)); TEST_ASSERT_EQUAL_INT(ACT_RES_IO, t_actuator_last_result(rail)); TEST_ASSERT_EQUAL_INT(0, pin(rail, bit)); /* 这一次键根本没按下去 */ other_task_frees_the_bus(); (void)t_actuator_step(); TEST_ASSERT_FALSE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_INT(0, pin(rail, bit)); TEST_ASSERT_EQUAL_HEX8(0x00, latch(rail)); /* 收尾真的把那几位显式清了一遍 */ } /* `t_actuator_abort()`:立刻收尾,键当场松开,但**不动 `EFUSE_EN` / `VBUS_EN`** —— 把电再关一次或再开一次都是自作主张(那是 A10 的判据和上位机命令的事)。 */ static void test_abort_releases_the_key_and_leaves_the_pins_alone(void) { const int rail = 2; const int bit = pca9555_key_bit(PCA9555_KEY_PWR); const int en = rail_cfg(rail)->efuse_en; TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_FORCE)); run_ms(500); const uint32_t en_writes = sim_gpio_writes(en); t_actuator_abort(rail); TEST_ASSERT_EQUAL_INT(0, pin(rail, bit)); TEST_ASSERT_FALSE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, t_actuator_last_result(rail)); TEST_ASSERT_EQUAL_UINT32(en_writes, sim_gpio_writes(en)); } /* 硬重启走到一半被打断:序列停下,但已经断掉的电**不会**被 abort 悄悄开回来。 */ static void test_abort_during_hard_reset_does_not_restore_power(void) { const int rail = 5; const int en = rail_cfg(rail)->efuse_en; TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(rail, true)); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail)); run_ms(1000); t_actuator_abort(rail); TEST_ASSERT_FALSE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, t_actuator_last_result(rail)); TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(en)); /* 还是关着的,等上层拿主意 */ } /* 开机键按着的时候被打断(过压关路走的就是这条):键当场松开,电不动。 键留在按下状态的现象是手机再也起不来,而遥测上一切正常。 */ static void test_abort_during_boot_press_releases_the_key(void) { const int rail = 2; const int en = rail_cfg(rail)->efuse_en; s_pg_val[rail - 1] = true; TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail)); run_ms(ACT_HR_OFF_MS + ACT_HR_RAMP_MS + 1000); TEST_ASSERT_EQUAL_INT(ACT_ST_HR_BOOT, t_actuator_state(rail)); TEST_ASSERT_EQUAL_INT(1, key_pin(rail, PCA9555_KEY_PWR)); t_actuator_abort(rail); TEST_ASSERT_FALSE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, t_actuator_last_result(rail)); TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_PWR)); TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(en)); } /* EDL 跑到「已插 USB」那一段被打断:`EDL_EN` 立刻松(那是本序列按下去的), **`VBUS_EN` 一动不动** —— R2 里它和按键在同一个字节里,一不小心就会被 收尾那次读改写顺手清掉,而现象是「手机莫名其妙从 9008 掉线」。 */ static void test_abort_during_edl_releases_the_short_but_keeps_vbus(void) { const int rail = 1; TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_edl(rail)); run_ms(ACT_EDL_SHORT_MS + 100); TEST_ASSERT_EQUAL_INT(ACT_ST_EDL_ENUM, t_actuator_state(rail)); TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_VBUS_EN)); t_actuator_abort(rail); TEST_ASSERT_FALSE(t_actuator_busy(rail)); TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, t_actuator_last_result(rail)); TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_EDL_EN)); TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_VBUS_EN)); /* ⭐ USB 还插着 */ TEST_ASSERT_TRUE(slot_vbus_is_on(rail)); } /* ==== ⭐ R2 新增:执行器与巡检争用同一把锁 =============================== */ /* R1 里两把锁永远不会嵌套,所以这一层和巡检井水不犯河水。 R2 只剩一把锁,按键 / EDL / VBUS 全在上面 ⇒ 两个任务开始抢。 抢锁写错了的现象**不是崩**,是慢性的:巡检每圈都跳过(SR-5 判据停摆), 或者某一路的键永远松不开(手机再也起不来)—— 两样都是遥测上一切正常。 这里交替叫 `t_safety_step()` 与 `t_actuator_step()` 跑一千多圈, 并且每隔 13 圈让第三个任务把总线占住一拍(制造真实的争用),断言三件事: ① 巡检 `skipped` **有界**(只在被占住的那些圈里跳,不会雪崩); ② 键最终一定全部松开; ③ 没有哪一路卡在 CLEANUP 出不来。 */ static void test_actuator_and_safety_share_one_lock_without_starving(void) { /* 巡检要读的六片 INA226:五路各一片(同址 0x40,靠通道区分)+ 输入侧在主干。 */ for (int rail = 1; rail <= N; rail++) { sim_i2c_dev_t *d = sim_i2c_add_ch(rail_cfg(rail)->mux_ch, rail_cfg(rail)->ina_addr, 2); d->regs[INA226_REG_BUS_V] = 3120; /* 3.9 V ÷ 1.25 mV */ d->regs[INA226_REG_CURRENT] = (uint16_t)(rail * 100); } sim_i2c_add(INA226_ADDR_INPUT, 2); safety_snap_t snap; t_safety_init(&snap); const int rounds = 1300; uint32_t busy_rounds = 0; uint32_t issued = 0; for (int i = 0; i < rounds; i++) { const bool bus_busy = (i % 13 == 0); if (bus_busy) { other_task_takes_the_bus(); /* 第三个任务(比如 A12 的命令处理器)占住总线 */ busy_rounds++; } (void)t_safety_step(&snap); (void)t_actuator_step(); if (bus_busy) { other_task_frees_the_bus(); } /* 不断有新的按键请求进来 —— 五路轮着来,包括总线正忙的那些圈。 */ if (i % 7 == 0) { const int rail = (i / 7) % N + 1; if (t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_TAP) == HAL_OK) { issued++; } } sim_time_advance_ms(20); } /* 收场:不再下新请求,总线空着,所有序列必须走完。 */ for (int i = 0; i < 300; i++) { (void)t_safety_step(&snap); (void)t_actuator_step(); sim_time_advance_ms(20); } TEST_ASSERT_TRUE(issued > 50); /* 争用之下照样接得下活,不是被拒到底 */ for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_FALSE(t_actuator_busy(rail)); /* ③ 没卡在 CLEANUP */ TEST_ASSERT_EQUAL_INT(ACT_ST_IDLE, t_actuator_state(rail)); TEST_ASSERT_EQUAL_HEX8(0x00, (uint8_t)(latch(rail) & pca9555_key_mask())); /* ② */ } /* ① `skipped` 有界:只可能落在被占住的那些圈里。真跳过了才算测到了争用。 */ TEST_ASSERT_TRUE(snap.skipped > 0); TEST_ASSERT_TRUE(snap.skipped <= busy_rounds); /* 抢了一千多圈,加锁纪律一次都没破,也没出现「两片一起应答」。 */ TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C)); TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous()); TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_gnd_driven()); printf(" 争用 %d 圈:巡检跳过 %u 圈(总线被占 %u 圈),下发按键 %u 次\n", rounds, (unsigned)snap.skipped, (unsigned)busy_rounds, (unsigned)issued); } /* ==== 参数与常量 ========================================================== */ static void test_bad_arguments_are_rejected(void) { TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_hard_reset(0)); TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_hard_reset(N + 1)); TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_edl(0)); TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_key(0, ACT_KEYS_PWR, 100)); TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_key(1, 0, 100)); /* 一个键都没选 */ TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_key(1, 1u << 5, 100)); /* 不存在的键 */ TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_key(1, 1u << PCA9555_BIT_EDL_EN, 100)); TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_key(1, ACT_KEYS_PWR, 0)); /* 按 0 ms */ /* 上限拦的是调用方的手误:传一个 10 分钟进来等于把电源键按死。 */ TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_key(1, ACT_KEYS_PWR, ACT_KEY_MAX_MS + 1)); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(1, ACT_KEYS_PWR, ACT_KEY_MAX_MS)); /* 被拒的请求一律不留痕迹:别的四片一个字节都没写过。 */ TEST_ASSERT_FALSE(t_actuator_busy(2)); for (int r = 2; r <= N; r++) { TEST_ASSERT_EQUAL_HEX8(0x00, latch(r)); } TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C)); } /* 时间常量与文档逐格对账。改了这里就是改了需求,得先去改文档。 */ static void test_timings_match_the_documents(void) { TEST_ASSERT_EQUAL_UINT32(17, ACT_HR_RAMP_MS); /* tON 5.1 + 斜坡 11.5,§7.4 */ TEST_ASSERT_EQUAL_UINT32(3000, ACT_HR_OFF_MIN_MS); /* §7.4 的 3~5 s 窗口 */ TEST_ASSERT_EQUAL_UINT32(5000, ACT_HR_OFF_MAX_MS); TEST_ASSERT_TRUE(ACT_HR_OFF_MS >= ACT_HR_OFF_MIN_MS && ACT_HR_OFF_MS <= ACT_HR_OFF_MAX_MS); TEST_ASSERT_EQUAL_UINT32(500, ACT_HR_PG_TIMEOUT_MS); /* 轮询间隔必须远小于超时,否则「等到 PG 就结束」退化成「一律等满 500 ms」。 */ TEST_ASSERT_TRUE(ACT_HR_PG_POLL_MS > 0 && ACT_HR_PG_POLL_MS * 10 <= ACT_HR_PG_TIMEOUT_MS); TEST_ASSERT_EQUAL_UINT32(100, ACT_KEY_MS_TAP); /* SW-5 三档 */ TEST_ASSERT_EQUAL_UINT32(5000, ACT_KEY_MS_ON); /* 小米8 实测 2 s 不够(t_actuator.h) */ TEST_ASSERT_TRUE(ACT_KEY_MS_ON < ACT_KEY_MS_FORCE); /* 开机那一下不能按成强制重启 */ TEST_ASSERT_EQUAL_UINT32(ACT_KEY_MS_ON, ACT_HR_BOOT_KEY_MS); TEST_ASSERT_EQUAL_UINT32(10000, ACT_KEY_MS_FORCE); TEST_ASSERT_TRUE(ACT_KEY_MS_FORCE <= ACT_KEY_MAX_MS); /* 最长那一档不能被自己的上限拦掉 */ TEST_ASSERT_EQUAL_UINT32(10, ACT_EDL_SHORT_MS); /* SW-6:先短接再插 USB */ TEST_ASSERT_EQUAL_UINT32(3000, ACT_EDL_ENUM_MS); /* ⭐ R2:等锁的超时就是 `slot` 那一个,不许在这里另配一个数 —— 两个数分家之后,「执行器等 20 ms、巡检等 5 ms」这套优先级就悄悄失效了。 */ TEST_ASSERT_EQUAL_UINT32(SLOT_LOCK_TIMEOUT_MS, ACT_LOCK_TIMEOUT_MS); TEST_ASSERT_EQUAL_UINT32(20, ACT_LOCK_TIMEOUT_MS); /* 逻辑键位就是 PCA9555 那三个键的下标,组合键是它们的并。 */ TEST_ASSERT_EQUAL_UINT32(1u << PCA9555_KEY_PWR, (uint32_t)ACT_KEYS_PWR); TEST_ASSERT_EQUAL_UINT32(1u << PCA9555_KEY_VUP, (uint32_t)ACT_KEYS_VUP); TEST_ASSERT_EQUAL_UINT32(1u << PCA9555_KEY_VDN, (uint32_t)ACT_KEYS_VDN); TEST_ASSERT_EQUAL_UINT32(ACT_KEYS_VDN | ACT_KEYS_PWR, (uint32_t)ACT_KEYS_FASTBOOT); TEST_ASSERT_EQUAL_UINT32(ACT_KEYS_VUP | ACT_KEYS_PWR, (uint32_t)ACT_KEYS_RECOVERY); TEST_ASSERT_TRUE((uint32_t)ACT_KEYS_FASTBOOT != (uint32_t)ACT_KEYS_RECOVERY); /* 三个键的位号一律查表得到,而且必须落在 Port0 那五个输出位里。 */ TEST_ASSERT_EQUAL_HEX8(0x07, pca9555_key_mask()); TEST_ASSERT_EQUAL_HEX8(0x00, (uint8_t)(pca9555_key_mask() & ~PCA9555_P0_OUT_MASK)); } /* 加锁纪律那条检查本身不能是句空话:不拿锁直接写一片 PCA9555,违规必须 +1。 (没有这条反向测试,上面那些「violations == 0」在检查失灵时会永远绿。) */ static void test_the_lock_discipline_check_actually_fires(void) { TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C)); const uint8_t wr[2] = { PCA9555_REG_OUTPUT0, 0x00 }; /* 值本身无害,越权的是「没拿锁」 */ (void)hal_i2c_write(PCA9555_ADDR, wr, sizeof(wr)); TEST_ASSERT_TRUE(sim_lock_violations(HAL_LOCK_I2C) > 0); } /* 空闲时 `t_actuator_step()` 什么也不做,也不该去动锁或任何一片器件 —— R2 里「顺手刷一次影子」的代价是每一拍都从巡检手里抢一次锁。 */ static void test_idle_step_is_free(void) { const uint32_t takes = sim_lock_takes(HAL_LOCK_I2C); uint8_t l0[N]; uint32_t w0[N]; snap_latches(l0); snap_writes(w0); for (int i = 0; i < 100; i++) { TEST_ASSERT_EQUAL_UINT32(ACT_IDLE_WAIT_MS, t_actuator_step()); sim_time_advance_ms(ACT_IDLE_WAIT_MS); } TEST_ASSERT_EQUAL_UINT32(takes, sim_lock_takes(HAL_LOCK_I2C)); assert_only_this_blade_moved(0, l0, w0); } static void test_format_prints_every_rail(void) { char buf[160]; TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(2, ACT_KEYS_PWR, ACT_KEY_MS_ON)); const int n = t_actuator_format(buf, sizeof(buf)); TEST_ASSERT_TRUE(n > 0 && n < (int)sizeof(buf)); TEST_ASSERT_NOT_NULL(strstr(buf, "ch2=key-hold")); TEST_ASSERT_NOT_NULL(strstr(buf, "ch5=idle")); printf(" %s\n", buf); /* 缓冲太小也不许越界。 */ char small[12]; memset(small, 0x7F, sizeof(small)); const int m = t_actuator_format(small, sizeof(small)); TEST_ASSERT_TRUE(m < (int)sizeof(small)); TEST_ASSERT_EQUAL_CHAR('\0', small[sizeof(small) - 1]); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_hard_reset_walks_the_full_power_up_sequence); RUN_TEST(test_hard_reset_touches_only_its_own_en_pin); RUN_TEST(test_no_pg_leaves_the_rail_powered); RUN_TEST(test_pg_stuck_low_also_leaves_the_rail_powered); RUN_TEST(test_late_pg_is_still_accepted); RUN_TEST(test_the_task_loop_only_sleeps_what_step_asks_for); RUN_TEST(test_hard_reset_releases_a_stuck_key_before_cutting_power); RUN_TEST(test_the_defensive_release_sends_nothing_when_the_shadow_is_clean); RUN_TEST(test_key_tap_presses_the_table_bit_and_releases_it); RUN_TEST(test_combo_keys_land_in_exactly_one_write); RUN_TEST(test_recovery_and_fastboot_are_different_key_sets); RUN_TEST(test_the_key_hold_never_holds_the_i2c_lock); RUN_TEST(test_a_running_sequence_rejects_every_new_request); RUN_TEST(test_pressing_ch1_does_not_touch_ch4); RUN_TEST(test_every_action_selects_this_rails_mux_channel); RUN_TEST(test_edl_order_is_short_then_vbus_then_release); RUN_TEST(test_edl_lands_on_its_own_blade_for_every_rail); RUN_TEST(test_cleanup_retries_until_the_key_is_really_released); RUN_TEST(test_cleanup_never_gives_up_no_matter_how_long_the_bus_is_busy); RUN_TEST(test_a_failed_press_still_goes_through_cleanup); RUN_TEST(test_abort_releases_the_key_and_leaves_the_pins_alone); RUN_TEST(test_abort_during_hard_reset_does_not_restore_power); RUN_TEST(test_abort_during_boot_press_releases_the_key); RUN_TEST(test_abort_during_edl_releases_the_short_but_keeps_vbus); RUN_TEST(test_actuator_and_safety_share_one_lock_without_starving); RUN_TEST(test_bad_arguments_are_rejected); RUN_TEST(test_timings_match_the_documents); RUN_TEST(test_the_lock_discipline_check_actually_fires); RUN_TEST(test_idle_step_is_free); RUN_TEST(test_format_prints_every_rail); return UNITY_END(); }