/* A10:安全巡检的两条判据(SR-5 / SW-8)。计划里这一包的验收原话是 「注入过压 → 20 ms 内关路并记事件;注入 −60 mA → 断 VBUS」。 ⚠️ 断言一律打在**线上真实发生的事**上:两根 EN 线看 `sim_gpio_level()` 与 全局写序号 `sim_gpio_write_seq()`(只看终值的话,「先断 VBUS 再关 eFuse」 和反过来长得一模一样),电压电流从**假 INA226 的寄存器**注入 —— 直接改快照里的 `bus_mv` 等于跳过驱动,那样连「读的是哪一片」都证明不了。 本文件里的 `beat()` 就是真板上两个任务合起来干的事:推一步执行器、跑一圈巡检、 睡到下一个 20 ms 格子。判据的「持续 20 ms」只有在真的按 20 ms 打拍子时才有意义。 */ #include #include "hal_gpio.h" #include "hal_i2c.h" #include "hal_lock.h" #include "hal_time.h" #include "ina226.h" #include "pca9555.h" #include "phonerail.h" #include "rail.h" #include "rail_cfg.h" #include "sched.h" #include "sim_hal.h" #include "sr595.h" #include "t_actuator.h" #include "t_safety.h" #include "tasks.h" #include "unity.h" #define N PHONERAIL_RAIL_COUNT /* 稳态:五路都设在 3900 mV(`RAIL_MV_SAFE`),电流 1 A 正向。 */ #define NOMINAL_MV 3900 static safety_snap_t g_snap; static sched_periodic_t g_tick; /* ---- 事件出口:把 A11 的环形缓冲换成一个能断言的小盒子 ------------------- */ static safety_event_t g_ev[8]; static int g_n_ev; /* 回调被调的那一刻两把锁都不该被持着(t_safety.c 规矩 1)。 */ static bool g_ev_held_lock; /* 事件被记下来的那一刻,动作**必须已经做完了**(t_safety.c:先保命,再记账)。 顺序反过来在功能上看不出任何区别 —— 除非像这里一样,在记录的那一刻回头看线。 */ static bool g_ev_before_action; static void catch_event(void *ctx, const safety_event_t *ev) { (void)ctx; if (hal_lock_held(HAL_LOCK_I2C) || hal_lock_held(HAL_LOCK_SR595)) { g_ev_held_lock = true; } switch ((safety_ev_t)ev->type) { case SAFETY_EV_OVER_VOLT: case SAFETY_EV_UNDER_VOLT: if (rail_efuse_is_on(ev->rail) || rail_vbus_is_on(ev->rail)) { g_ev_before_action = true; /* SR-5:记这条时电早该断了 */ } break; case SAFETY_EV_REVERSE: case SAFETY_EV_PUSHED_UP: if (rail_vbus_is_on(ev->rail)) { g_ev_before_action = true; /* SW-8:记这条时 VBUS 早该断了 */ } break; default: break; } if (g_n_ev < (int)(sizeof(g_ev) / sizeof(g_ev[0]))) { g_ev[g_n_ev] = *ev; } g_n_ev++; } /* ---- 假板子 -------------------------------------------------------------- */ /* 把某一路的实测值注进假 INA226 的寄存器(mV 必须是 5 的倍数:1.25 mV/LSB)。 */ static void inject(int rail, int mv, int ua) { TEST_ASSERT_EQUAL_INT(0, mv % 5); sim_i2c_set_reg(rail_cfg(rail)->ina_addr, INA226_REG_BUS_V, (uint16_t)(mv * 4 / 5)); sim_i2c_set_reg(rail_cfg(rail)->ina_addr, INA226_REG_CURRENT, (uint16_t)(int16_t)(ua / INA226_CURRENT_LSB_UA)); } static void board_up(void) { const hal_i2c_cfg_t cfg = { .sda_gpio = 14, .scl_gpio = 15, .freq_hz = 400000 }; TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(&cfg)); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_init()); TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); /* 十根 EN 线,全部拉低 */ rail_setpoint_reset(); /* 五路设定值回到 NVM 默认 */ for (int rail = 1; rail <= N; rail++) { sim_i2c_add(rail_cfg(rail)->ina_addr, 2); inject(rail, NOMINAL_MV, 1000000); /* 3.900 V / 1 A,稳稳当当 */ } sim_i2c_add(INA226_ADDR_INPUT, 2); sim_pca9555_attach(PCA9555_ADDR); sim_sr595_attach(SR595_PIN_SER, SR595_PIN_SRCLK, SR595_PIN_RCLK, SR595_PIN_OE_N); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_init()); t_actuator_init(NULL, NULL); /* PG「永远读不到」—— 硬重启会走完并记 NO_PG */ g_n_ev = 0; g_ev_held_lock = false; g_ev_before_action = false; memset(g_ev, 0, sizeof(g_ev)); t_safety_set_event_sink(catch_event, NULL); t_safety_init(&g_snap); sched_periodic_init(&g_tick, task_desc(TASK_SAFETY)->period_ms); } void setUp(void) { sim_hal_reset(); board_up(); } void tearDown(void) {} /* 一拍 = 真板上两个任务在这 20 ms 里干的事。执行器要先推,因为判据打断的正是它。 */ static void beat(void) { (void)t_actuator_step(); (void)t_safety_step(&g_snap); sched_periodic_wait(&g_tick); } static void beats(int n) { for (int i = 0; i < n; i++) { beat(); } } /* 开一路电并熬过缓启动窗口。判据在这个窗口里是**不武装**的(斜坡还没走完, 这时候比电压等于每次上电都自己把自己关掉)。 */ static void power_up(int rail) { const int before = g_n_ev; TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(rail, true)); beats(1 + (T_SAFETY_SETTLE_MS / (int)task_desc(TASK_SAFETY)->period_ms) + 1); TEST_ASSERT_TRUE(rail_efuse_is_on(rail)); TEST_ASSERT_EQUAL_INT(before, g_n_ev); /* 上电加稳态几拍,不该冒出新事件 */ } static int en_level(int rail) { return sim_gpio_level(rail_cfg(rail)->efuse_en); } static int vbus_level(int rail) { return sim_gpio_level(rail_cfg(rail)->vbus_en); } /* ---- 常量与文档对账(改了任何一个就是改了需求) -------------------------- */ static void test_thresholds_match_the_documents(void) { /* SR-5 / SW-2 的两个数,逐字来自需求 §9 那两行。 */ TEST_ASSERT_EQUAL_INT(100, T_SAFETY_DEV_MV); TEST_ASSERT_EQUAL_INT(20, T_SAFETY_DEV_HOLD_MS); /* 判据数的是**圈**,SR-5 写的是**毫秒** —— 这一条把两者钉在一起。 巡检周期哪天改了,它会当场对不上,而不是悄悄把「持续 20 ms」变成 40 ms。 */ TEST_ASSERT_EQUAL_INT(2, T_SAFETY_DEV_HOLD_CYCLES); TEST_ASSERT_EQUAL_UINT32((T_SAFETY_DEV_HOLD_CYCLES - 1) * task_desc(TASK_SAFETY)->period_ms, (uint32_t)T_SAFETY_DEV_HOLD_MS); /* SW-8 的两个数:设计文档 §15.2 对策 2 的 −50 mA,需求 SW-8 的 200 ms 预算。 */ TEST_ASSERT_EQUAL_INT32(-50000, T_SAFETY_REV_UA); TEST_ASSERT_EQUAL_INT(200, T_SAFETY_REV_BUDGET_MS); /* 反灌不设确认窗口 ⇒ 最坏 2 个周期,必须离 200 ms 的预算远得看得出来。 */ TEST_ASSERT_TRUE(2 * (int)task_desc(TASK_SAFETY)->period_ms < T_SAFETY_REV_BUDGET_MS / 2); /* 缓启动窗口必须盖得住 tON + 斜坡那 17 ms(§7.4),而且要是周期的整数倍 以上 —— 比一个周期还短的窗口等于没有。 */ TEST_ASSERT_TRUE(T_SAFETY_SETTLE_MS > (int)ACT_HR_RAMP_MS); TEST_ASSERT_TRUE(T_SAFETY_SETTLE_MS >= 2 * (int)task_desc(TASK_SAFETY)->period_ms); /* §12.1 注入的是 ±150 mV / −60 mA,两个都必须落在阈值**外侧**, 否则验收测的是「阈值刚好卡在注入值上」。 */ TEST_ASSERT_TRUE(150 > T_SAFETY_DEV_MV); TEST_ASSERT_TRUE(-60000 < T_SAFETY_REV_UA); } /* ---- 验收主项一:注入过压 → 20 ms 内关路并记事件 ------------------------- */ static void test_overvoltage_cuts_the_rail_within_one_period_and_logs_it(void) { power_up(1); inject(1, NOMINAL_MV + 150, 1000000); /* §12.1 的 +150 mV */ beat(); TEST_ASSERT_EQUAL_INT(1, en_level(1)); /* 第一次看见:只起表,不动电 */ TEST_ASSERT_EQUAL_INT(0, g_n_ev); beat(); TEST_ASSERT_EQUAL_INT(0, en_level(1)); /* 满 20 ms:这一拍断电 */ TEST_ASSERT_EQUAL_INT(1, g_n_ev); TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_OVER_VOLT, g_ev[0].type); TEST_ASSERT_EQUAL_UINT8(1, g_ev[0].rail); TEST_ASSERT_EQUAL_INT32(NOMINAL_MV + 150, g_ev[0].bus_mv); /* 记的是实测值 */ TEST_ASSERT_EQUAL_INT32(1000000, g_ev[0].current_ua); TEST_ASSERT_FALSE(g_ev_held_lock); TEST_ASSERT_FALSE(g_ev_before_action); TEST_ASSERT_EQUAL_UINT32(1, g_snap.events); } /* 从**第一次看见**到断电正好一个周期。 ⚠️ 这一条把假总线的耗时关掉(sim_hal.h 允许的那个正当理由:拿虚拟时钟 当纯计数器)—— 开着的话每一拍还要加上巡检自己占的 1.56 ms 总线时间, 量出来是 19/21 ms 交替,那 1 ms 是巡检花的,不是判据晚了。 */ static void test_the_cut_lands_exactly_one_hold_window_after_first_sight(void) { sim_i2c_set_bus_time(false); power_up(2); inject(2, NOMINAL_MV - 150, 1000000); const uint32_t t_first_sight = hal_time_now_ms(); beat(); beat(); TEST_ASSERT_EQUAL_INT(1, g_n_ev); TEST_ASSERT_EQUAL_UINT32(T_SAFETY_DEV_HOLD_MS, g_ev[0].t_ms - t_first_sight); } /* ⭐ 同一件事,把总线耗时打开(也就是真板上的样子):判据必须**照样在第二圈** 断电,量出来的墙钟是 19 或 21 ms。 这一条是本包最值钱的回归拦网:判据一旦改回按墙钟算 `>= 20 ms`,19 ms 那半数 情况就要等到第三圈 —— 40 ms,超了 §12.1 的验收,而且是**间歇性**的 (同一个故障注两次,一次过一次不过)。它跑 20 次,两种相位都会踩到。 */ static void test_the_cut_is_never_late_because_of_sub_millisecond_jitter(void) { power_up(2); for (int i = 0; i < 20; i++) { /* 每一轮都从「刚开电」重来,让第一次看见落在不同的亚毫秒相位上。 */ TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(2, true)); inject(2, NOMINAL_MV, 1000000); beats(6); /* 熬过缓启动窗口 */ g_n_ev = 0; inject(2, NOMINAL_MV + 150, 1000000); const uint32_t t0 = hal_time_now_ms(); beat(); TEST_ASSERT_EQUAL_INT(1, en_level(2)); /* 第一圈只起表 */ beat(); TEST_ASSERT_EQUAL_INT(0, en_level(2)); /* 第二圈必须断,一次都不许拖 */ /* 墙钟量出来是 19~22 ms:一圈自己占 1.56 ms 总线,加上毫秒截断。 那几毫秒是巡检花的,不是判据晚的 —— 判据数的是圈,上面两条断言才是主判据。 */ const uint32_t late = g_ev[0].t_ms - t0; TEST_ASSERT_TRUE(late >= T_SAFETY_DEV_HOLD_MS - 2 && late <= T_SAFETY_DEV_HOLD_MS + 2); } } /* 偏低同样关路(SR-5 是 ±,不是「过压」)。掉压的现因多半是排线接触不良, 而那正是这块板子最常见的故障(§10.7 开头那句)。 */ static void test_undervoltage_cuts_the_rail_too(void) { power_up(3); inject(3, NOMINAL_MV - 150, 1000000); beats(2); TEST_ASSERT_EQUAL_INT(0, en_level(3)); TEST_ASSERT_EQUAL_INT(1, g_n_ev); TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_UNDER_VOLT, g_ev[0].type); } /* 「持续」的意思是连续。只闪了一圈就回到范围内的偏差不许关路 —— 不然一次负载阶跃(0→4 A)就能把五台手机全关掉。 */ static void test_a_one_cycle_glitch_does_not_cut_anything(void) { power_up(1); for (int i = 0; i < 20; i++) { inject(1, NOMINAL_MV + 150, 1000000); beat(); inject(1, NOMINAL_MV, 1000000); beat(); } TEST_ASSERT_EQUAL_INT(1, en_level(1)); TEST_ASSERT_EQUAL_INT(0, g_n_ev); } /* 阈值是「**超过** 100 mV」:正好 100 mV 不算。 写成 >= 的话,SW-4 允许运维把某一路微调 100 mV 之后那一路就再也开不起来。 */ static void test_exactly_one_hundred_millivolts_is_still_in_range(void) { power_up(1); inject(1, NOMINAL_MV + T_SAFETY_DEV_MV, 1000000); beats(10); TEST_ASSERT_EQUAL_INT(1, en_level(1)); inject(1, NOMINAL_MV - T_SAFETY_DEV_MV, 1000000); beats(10); TEST_ASSERT_EQUAL_INT(1, en_level(1)); TEST_ASSERT_EQUAL_INT(0, g_n_ev); } /* 比的是**该路 DAC 的设定值**,不是 3900 这个常数。SW-4 允许单路微调, 拿常数比的话,一台被调到 4.0 V 的手机会被当成过压关掉 —— 而且只有那一路。 */ static void test_the_reference_is_the_setpoint_not_a_constant(void) { /* 假两片 DAC,把 CH1 调到 4010 mV(SR-13 的上限)。 */ sim_i2c_add(0x4A, 2); sim_i2c_add(0x4B, 2); TEST_ASSERT_EQUAL_INT(HAL_OK, rail_set_mv(1, RAIL_MV_MAX)); TEST_ASSERT_EQUAL_INT(RAIL_MV_MAX, rail_setpoint_mv(1)); inject(1, RAIL_MV_MAX, 1000000); /* 实测跟着设定走 */ power_up(1); beats(10); TEST_ASSERT_EQUAL_INT(1, en_level(1)); TEST_ASSERT_EQUAL_INT(0, g_n_ev); /* 回到 3900:现在这才是「偏低 110 mV」。 */ inject(1, NOMINAL_MV, 1000000); beats(2); TEST_ASSERT_EQUAL_INT(0, en_level(1)); TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_UNDER_VOLT, g_ev[0].type); } /* ---- 判据什么时候**不**武装(三道闸,每一道都是一次真事故的预防) -------- */ /* 关着的路不判据。不设这一条的现象是:巡检每 20 ms 就「发现」五路各偏低 3900 mV,然后没完没了地去关已经关着的路、刷满事件日志。 */ static void test_a_rail_that_is_off_is_never_judged(void) { inject(1, 0, 0); /* 关着的路就是 0 V —— 离设定值差着 3900 mV */ beats(50); TEST_ASSERT_EQUAL_INT(0, g_n_ev); TEST_ASSERT_EQUAL_UINT32(0, sim_gpio_writes(rail_cfg(1)->efuse_en) - 1u); /* 只有 init 那一次 */ } /* 刚开电的那几十毫秒不判据:tON 5.1 ms + 斜坡 11.5 ms 期间电压本来就在爬。 没有这个窗口的现象是**每次上电都自己把自己关掉**,而且五路一起。 */ static void test_the_soft_start_window_is_not_judged(void) { inject(1, 1200, 100000); /* 斜坡爬到一半 */ TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(1, true)); const int in_window = T_SAFETY_SETTLE_MS / (int)task_desc(TASK_SAFETY)->period_ms; beats(in_window); /* 窗口之内:不管电压多离谱都不判 */ TEST_ASSERT_EQUAL_INT(1, en_level(1)); TEST_ASSERT_EQUAL_INT(0, g_n_ev); /* 窗口一过,判据武装,还是这么低就该关了。 */ beats(3); TEST_ASSERT_EQUAL_INT(0, en_level(1)); TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_UNDER_VOLT, g_ev[0].type); } /* 器件失联的那一路不判据(§10.10:「保持供电,标记为盲跑,**禁用该路的 SR-5 / SW-8 判据**」)。快照里留着的是上一圈的值,拿它去关电是最难查的那种错。 两条判据共用同一道闸,所以这里拿 SR-5 验它就够。 */ static void test_a_blind_rail_keeps_its_power(void) { power_up(4); /* ⚠️ 顺序是有讲究的:**先让坏值真的读进快照一次**,再让器件失联。 反过来写(先失联再改寄存器)的话快照里躺着的是个正常值, 「盲跑那一路照样判据」这个错就一点痕迹都不会留下 —— 测试永远绿。 */ inject(4, NOMINAL_MV - 150, 1000000); beat(); TEST_ASSERT_EQUAL_INT(1, en_level(4)); /* 第一个样本只起表 */ sim_i2c_set_present(rail_cfg(4)->ina_addr, false); beats(50); /* 1 秒,远超两条判据各自的窗口 */ TEST_ASSERT_EQUAL_INT(1, en_level(4)); /* §10.10:盲跑 = 保持供电 */ TEST_ASSERT_EQUAL_INT(0, g_n_ev); TEST_ASSERT_FALSE(g_snap.rail_ok[3]); /* 快照里躺着的确实是那个越界的旧值 —— 这一条是上面那句「顺序有讲究」的凭据。 */ TEST_ASSERT_EQUAL_INT32(NOMINAL_MV - 150, g_snap.rail[3].bus_mv); /* 器件回来了,值还是坏的 —— 这时才该关,而且要重新数满 20 ms。 */ sim_i2c_set_present(rail_cfg(4)->ina_addr, true); beat(); TEST_ASSERT_EQUAL_INT(1, en_level(4)); /* 第一个新鲜样本只起表 */ beat(); TEST_ASSERT_EQUAL_INT(0, en_level(4)); } /* ---- 关路这件事本身 ------------------------------------------------------ */ /* 只关出事的那一路,另外四路一根线都不许动。 */ static void test_only_the_offending_rail_is_touched(void) { for (int rail = 1; rail <= N; rail++) { power_up(rail); } const uint32_t before[N] = { sim_gpio_writes(rail_cfg(1)->efuse_en), sim_gpio_writes(rail_cfg(2)->efuse_en), sim_gpio_writes(rail_cfg(3)->efuse_en), sim_gpio_writes(rail_cfg(4)->efuse_en), sim_gpio_writes(rail_cfg(5)->efuse_en), }; inject(3, NOMINAL_MV + 150, 1000000); beats(2); TEST_ASSERT_EQUAL_INT(0, en_level(3)); for (int rail = 1; rail <= N; rail++) { if (rail == 3) { continue; } TEST_ASSERT_EQUAL_INT(1, en_level(rail)); TEST_ASSERT_EQUAL_UINT32(before[rail - 1], sim_gpio_writes(rail_cfg(rail)->efuse_en)); TEST_ASSERT_EQUAL_UINT32(0, sim_gpio_writes(rail_cfg(rail)->vbus_en) - 1u); } TEST_ASSERT_EQUAL_INT(1, g_n_ev); } /* 关路 = **先断 VBUS 再关 eFuse**(`rail_all_off()` 的同一条理由:中间不留 「手机只靠 USB 供着电」那个既不是开也不是关的窗口)。 只看两个脚的终值是看不出顺序的,所以看全局写序号。 */ static void test_the_cut_drops_vbus_before_the_efuse(void) { power_up(1); TEST_ASSERT_EQUAL_INT(HAL_OK, rail_vbus_enable(1, true)); inject(1, NOMINAL_MV - 150, 1000000); /* 欠压,不是反灌 */ beats(2); TEST_ASSERT_EQUAL_INT(0, vbus_level(1)); TEST_ASSERT_EQUAL_INT(0, en_level(1)); TEST_ASSERT_TRUE(sim_gpio_write_seq(rail_cfg(1)->vbus_en) < sim_gpio_write_seq(rail_cfg(1)->efuse_en)); } /* 关掉之后判据自己解除武装:不再刷第二条事件,也不再去写那两个脚。 */ static void test_a_cut_rail_does_not_keep_firing(void) { power_up(1); inject(1, NOMINAL_MV + 150, 1000000); beats(2); TEST_ASSERT_EQUAL_INT(1, g_n_ev); const uint32_t writes = sim_gpio_writes(rail_cfg(1)->efuse_en); beats(100); /* 2 秒 */ TEST_ASSERT_EQUAL_INT(1, g_n_ev); TEST_ASSERT_EQUAL_UINT32(writes, sim_gpio_writes(rail_cfg(1)->efuse_en)); } /* 被关掉的那一路重新上电之后,确认窗口要**从头数**。 `dev_n` 关路时不清零的话,这一路下次开电只要看见一个越界样本就立刻跳闸 —— 少了整个确认窗口,而现象只是「这一路特别容易掉」,查不到根上。 */ static void test_a_rail_that_comes_back_starts_its_hold_window_over(void) { power_up(1); inject(1, NOMINAL_MV + 150, 1000000); beats(2); TEST_ASSERT_EQUAL_INT(0, en_level(1)); inject(1, NOMINAL_MV, 1000000); /* 修好了,重新上电 */ power_up(1); inject(1, NOMINAL_MV + 150, 1000000); /* 又坏了 */ beat(); TEST_ASSERT_EQUAL_INT(1, en_level(1)); /* 第一圈还是只起表,不是立刻跳 */ beat(); TEST_ASSERT_EQUAL_INT(0, en_level(1)); TEST_ASSERT_EQUAL_INT(2, g_n_ev); } /* ⭐ 关路之前必须先 `t_actuator_abort()`。 不 abort 的现象最阴:硬重启的 `HR_OFF` 会在 4 s 之后自己把 `EFUSE_EN` 抬回去, 于是「过压关掉的那一路」几秒钟后又带电了,而事件日志里只有一条「已关路」。 */ static void test_a_running_hard_reset_cannot_bring_the_rail_back(void) { power_up(1); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(1)); /* 断电窗口 4 s 之后 `EFUSE_EN` 被抬回来,序列进入等 PG(PG 永远读不到)。 */ beats((ACT_HR_OFF_MS + T_SAFETY_SETTLE_MS) / 20 + 2); TEST_ASSERT_EQUAL_INT(1, en_level(1)); TEST_ASSERT_EQUAL_INT(ACT_ST_HR_PG, (int)t_actuator_state(1)); inject(1, NOMINAL_MV + 150, 1000000); beats(2); TEST_ASSERT_EQUAL_INT(0, en_level(1)); TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, (int)t_actuator_last_result(1)); /* 再跑 10 秒 —— 电不许自己回来。 */ beats(500); TEST_ASSERT_EQUAL_INT(0, en_level(1)); TEST_ASSERT_EQUAL_INT(1, g_n_ev); } /* abort 顺带把按下去的键松开了。按键序列跑到一半被过压打断, 键要是留在按下状态,手机再也起不来,而遥测上五路电压电流全都正常。 */ static void test_a_cut_releases_whatever_key_was_held(void) { power_up(1); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(1, ACT_KEYS_PWR, ACT_KEY_MS_ON)); beat(); TEST_ASSERT_EQUAL_INT(1, sim_sr595_pin(rail_cfg(1)->key_bit[SR595_KEY_PWR])); inject(1, NOMINAL_MV + 150, 1000000); beats(3); TEST_ASSERT_EQUAL_INT(0, en_level(1)); TEST_ASSERT_EQUAL_INT(0, sim_sr595_pin(rail_cfg(1)->key_bit[SR595_KEY_PWR])); TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, (int)t_actuator_last_result(1)); } /* ---- 验收主项二:注入 −60 mA → 断 VBUS(SW-8) --------------------------- */ static void test_reverse_current_drops_vbus(void) { power_up(1); TEST_ASSERT_EQUAL_INT(HAL_OK, rail_vbus_enable(1, true)); inject(1, NOMINAL_MV, -60000); /* §12.1 的 −60 mA */ const uint32_t t0 = hal_time_now_ms(); beat(); TEST_ASSERT_EQUAL_INT(0, vbus_level(1)); TEST_ASSERT_EQUAL_INT(1, en_level(1)); /* **只断 VBUS**,直供不动 */ TEST_ASSERT_EQUAL_INT(1, g_n_ev); TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_REVERSE, g_ev[0].type); TEST_ASSERT_EQUAL_INT32(-60000, g_ev[0].current_ua); TEST_ASSERT_FALSE(g_ev_before_action); /* SW-8 的预算是 200 ms,实际是一圈。 */ TEST_ASSERT_TRUE(hal_time_elapsed_ms(t0) < (uint32_t)T_SAFETY_REV_BUDGET_MS); } /* 反灌**不等确认窗口**(与过压判据有意不对称):一圈看见就断。 误断 VBUS 只丢 USB 数据,漏判会把能量泵回 12V 母线连累另外四台。 */ static void test_reverse_current_is_not_debounced(void) { power_up(1); TEST_ASSERT_EQUAL_INT(HAL_OK, rail_vbus_enable(1, true)); inject(1, NOMINAL_MV, -60000); beat(); /* 就一圈 */ TEST_ASSERT_EQUAL_INT(0, vbus_level(1)); } /* 没到 −50 mA 不算。手机在直供下本来就会有小幅的双向电流。 */ static void test_a_small_negative_current_is_not_a_backfeed(void) { power_up(1); TEST_ASSERT_EQUAL_INT(HAL_OK, rail_vbus_enable(1, true)); inject(1, NOMINAL_MV, -40000); beats(50); TEST_ASSERT_EQUAL_INT(1, vbus_level(1)); TEST_ASSERT_EQUAL_INT(0, g_n_ev); } /* VBUS 本来就断着的时候不判反灌:这条判据只在「USB 那一侧通着」时武装, 也正是它让「断完之后不再每 20 ms 刷一条事件」成立。 */ static void test_backfeed_is_only_judged_while_vbus_is_on(void) { power_up(1); /* VBUS 没开 */ inject(1, NOMINAL_MV, -200000); beats(50); TEST_ASSERT_EQUAL_INT(0, g_n_ev); TEST_ASSERT_EQUAL_UINT32(0, sim_gpio_writes(rail_cfg(1)->vbus_en) - 1u); } /* 顶高:VBUS 期间电压被抬过设定值 +100 mV,同样立刻断 VBUS(设计文档 §15.2)。 然后 20 ms 后 SR-5 才决定要不要把这一路也关掉 —— 两条判据是分层的: 先撤掉能量来源,再决定要不要关电。 */ static void test_being_pushed_up_drops_vbus_first_then_sr5_cuts_the_rail(void) { power_up(1); TEST_ASSERT_EQUAL_INT(HAL_OK, rail_vbus_enable(1, true)); inject(1, NOMINAL_MV + 150, 1000000); beat(); TEST_ASSERT_EQUAL_INT(0, vbus_level(1)); /* 第一圈:VBUS 没了 */ TEST_ASSERT_EQUAL_INT(1, en_level(1)); /* 电还开着 */ TEST_ASSERT_EQUAL_INT(1, g_n_ev); TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_PUSHED_UP, g_ev[0].type); beat(); TEST_ASSERT_EQUAL_INT(0, en_level(1)); /* 第二圈:还是高,SR-5 关路 */ TEST_ASSERT_EQUAL_INT(2, g_n_ev); TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_OVER_VOLT, g_ev[1].type); } /* ⭐ 断 VBUS 之前要打断 EDL 序列 —— 三个序列里只有它会写 `VBUS_EN`, 不打断的话下一拍它就把 VBUS 又抬回来了(`EDL_EN=1 → VBUS_EN=1 → 等枚举`)。 */ static void test_a_running_edl_sequence_cannot_bring_vbus_back(void) { power_up(1); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_edl(1)); beats(2); /* 短接 10 ms 之后 VBUS_EN 被抬起来,进入等枚举 */ TEST_ASSERT_EQUAL_INT(1, vbus_level(1)); TEST_ASSERT_EQUAL_INT(ACT_ST_EDL_ENUM, (int)t_actuator_state(1)); inject(1, NOMINAL_MV, -60000); beat(); TEST_ASSERT_EQUAL_INT(0, vbus_level(1)); TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, (int)t_actuator_last_result(1)); beats(200); /* 4 秒 —— 枚举窗口 3 s 早过了,VBUS 不许自己回来 */ TEST_ASSERT_EQUAL_INT(0, vbus_level(1)); TEST_ASSERT_EQUAL_INT(1, g_n_ev); } /* 反灌不去打断跟 VBUS 无关的序列(硬重启只碰 `EFUSE_EN`)—— 「出了事就把这一路上所有事都停掉」比看上去危险:那会把一次正在进行的 救砖重启打断,而重启和反灌是两码事。 */ static void test_a_backfeed_does_not_abort_an_unrelated_sequence(void) { power_up(1); TEST_ASSERT_EQUAL_INT(HAL_OK, rail_vbus_enable(1, true)); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(1, ACT_KEYS_VUP, ACT_KEY_MS_ON)); inject(1, NOMINAL_MV, -60000); beat(); TEST_ASSERT_EQUAL_INT(0, vbus_level(1)); TEST_ASSERT_TRUE(t_actuator_busy(1)); TEST_ASSERT_EQUAL_INT(ACT_ST_KEY_HOLD, (int)t_actuator_state(1)); } /* 五路各判各的:CH5 反灌不许动 CH2 的 VBUS。 */ static void test_backfeed_is_per_rail(void) { power_up(2); power_up(5); TEST_ASSERT_EQUAL_INT(HAL_OK, rail_vbus_enable(2, true)); TEST_ASSERT_EQUAL_INT(HAL_OK, rail_vbus_enable(5, true)); inject(5, NOMINAL_MV, -60000); beats(5); TEST_ASSERT_EQUAL_INT(0, vbus_level(5)); TEST_ASSERT_EQUAL_INT(1, vbus_level(2)); TEST_ASSERT_EQUAL_INT(1, en_level(5)); TEST_ASSERT_EQUAL_UINT8(5, g_ev[0].rail); } /* ---- 判据不许改变巡检的形状 ---------------------------------------------- */ /* 一条判据也不许发 I²C 事务:设定值、两根 EN、正在跑的序列全部有影子可查。 多读一次就把 §10.3 那 7.8 % 的预算顶穿。 */ static void test_the_rules_do_not_add_any_i2c_traffic(void) { power_up(1); inject(1, NOMINAL_MV + 150, 1000000); const uint32_t before = hal_i2c_acct_txns(); beat(); /* 起表那一圈 */ beat(); /* 断电那一圈 —— 判据在这一圈干了活 */ TEST_ASSERT_EQUAL_INT(0, en_level(1)); TEST_ASSERT_EQUAL_UINT32(2u * (INA226_COUNT * 2u + 1u), hal_i2c_acct_txns() - before); TEST_ASSERT_EQUAL_UINT32(t_safety_budget_us(), g_snap.bus_us); } /* 判据跑在**放锁之后**:整圈的加锁违规为 0,出门锁已经放掉。 拿着 I²C 锁去 `t_actuator_abort()` 会撞上「两把锁不许嵌套」(hal_lock.h)。 */ static void test_the_rules_run_after_the_bus_lock_is_released(void) { power_up(1); sim_lock_require(HAL_LOCK_I2C, true); sim_lock_require(HAL_LOCK_SR595, true); TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(1, ACT_KEYS_PWR, ACT_KEY_MS_ON)); inject(1, NOMINAL_MV + 150, 1000000); beats(3); TEST_ASSERT_EQUAL_INT(0, en_level(1)); TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C)); TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_SR595)); TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C)); TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_SR595)); TEST_ASSERT_FALSE(g_ev_held_lock); } /* 拿不到总线锁的那一圈整圈作废,判据一条都不许跑 —— 那一圈的值是上一圈的。 跑的话,「总线忙」就会被当成「电压没变」,而实际上根本没读。 */ static void test_a_skipped_cycle_runs_no_rules(void) { power_up(1); inject(1, NOMINAL_MV + 150, 1000000); beat(); /* 起表 */ sim_lock_set_task(1); /* 假装 t_link 占着总线 */ TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10)); sim_lock_set_task(0); TEST_ASSERT_EQUAL_INT(HAL_ERR_TIMEOUT, t_safety_step(&g_snap)); TEST_ASSERT_EQUAL_INT(1, en_level(1)); /* 满 20 ms 了,但这一圈没数 */ TEST_ASSERT_EQUAL_INT(0, g_n_ev); TEST_ASSERT_EQUAL_UINT32(1, g_snap.skipped); sim_lock_set_task(1); hal_lock_give(HAL_LOCK_I2C); sim_lock_set_task(0); beat(); /* 总线回来了,这一圈才断电 */ TEST_ASSERT_EQUAL_INT(0, en_level(1)); } /* 没接事件出口也照样断电(A11 还没来,日志接不上不是不断电的理由)。 */ static void test_the_cut_happens_even_with_no_event_sink(void) { t_safety_set_event_sink(NULL, NULL); power_up(1); inject(1, NOMINAL_MV + 150, 1000000); beats(2); TEST_ASSERT_EQUAL_INT(0, en_level(1)); TEST_ASSERT_EQUAL_INT(0, g_n_ev); TEST_ASSERT_EQUAL_UINT32(1, g_snap.events); /* 快照里仍然记着 */ TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_OVER_VOLT, g_snap.last.type); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_thresholds_match_the_documents); RUN_TEST(test_overvoltage_cuts_the_rail_within_one_period_and_logs_it); RUN_TEST(test_the_cut_lands_exactly_one_hold_window_after_first_sight); RUN_TEST(test_the_cut_is_never_late_because_of_sub_millisecond_jitter); RUN_TEST(test_undervoltage_cuts_the_rail_too); RUN_TEST(test_a_one_cycle_glitch_does_not_cut_anything); RUN_TEST(test_exactly_one_hundred_millivolts_is_still_in_range); RUN_TEST(test_the_reference_is_the_setpoint_not_a_constant); RUN_TEST(test_a_rail_that_is_off_is_never_judged); RUN_TEST(test_the_soft_start_window_is_not_judged); RUN_TEST(test_a_blind_rail_keeps_its_power); RUN_TEST(test_only_the_offending_rail_is_touched); RUN_TEST(test_the_cut_drops_vbus_before_the_efuse); RUN_TEST(test_a_cut_rail_does_not_keep_firing); RUN_TEST(test_a_rail_that_comes_back_starts_its_hold_window_over); RUN_TEST(test_a_running_hard_reset_cannot_bring_the_rail_back); RUN_TEST(test_a_cut_releases_whatever_key_was_held); RUN_TEST(test_reverse_current_drops_vbus); RUN_TEST(test_reverse_current_is_not_debounced); RUN_TEST(test_a_small_negative_current_is_not_a_backfeed); RUN_TEST(test_backfeed_is_only_judged_while_vbus_is_on); RUN_TEST(test_being_pushed_up_drops_vbus_first_then_sr5_cuts_the_rail); RUN_TEST(test_a_running_edl_sequence_cannot_bring_vbus_back); RUN_TEST(test_a_backfeed_does_not_abort_an_unrelated_sequence); RUN_TEST(test_backfeed_is_per_rail); RUN_TEST(test_the_rules_do_not_add_any_i2c_traffic); RUN_TEST(test_the_rules_run_after_the_bus_lock_is_released); RUN_TEST(test_a_skipped_cycle_runs_no_rules); RUN_TEST(test_the_cut_happens_even_with_no_event_sink); return UNITY_END(); }