/* A8 之二:`t_safety` 的一个周期,以及需求 §10.3 的两条验收判据 —— ① sim 时钟下周期是 20 ms ±1 ms; ② I²C 占用打印出来,与 §10.3 手算的「约 1.5 ms / 约 8 %」对得上。 ⚠️ 这两条能成立,靠的是 sim 的假 I²C **真的把虚拟时钟花掉** (`sim_i2c_set_bus_time`,默认开)。没有那一条,巡检在 sim 里耗时为零, 「20 ms 周期」就退化成「测 delay 参数传对了没」—— 那是句空话: 真正会出事的是「巡检自己占掉了 1.5 ms,节拍还准不准」。 */ #include #include #include "hal_i2c.h" #include "hal_i2c_acct.h" #include "hal_lock.h" #include "hal_time.h" #include "ina226.h" #include "pca9555.h" #include "rail_cfg.h" #include "sched.h" #include "sim_hal.h" #include "t_safety.h" #include "tasks.h" #include "unity.h" #define N PHONERAIL_RAIL_COUNT /* 一圈发出去的事务数:6 片 INA226 各 2 次寄存器读 + PCA9555 一次。 */ #define TXNS_PER_CYCLE (INA226_COUNT * 2 + 1) /* 把假板子搭起来:6 片 INA226 + 1 片 PCA9555,地址全部**查表**取。 */ 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()); for (int rail = 1; rail <= N; rail++) { sim_i2c_dev_t *d = sim_i2c_add(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 * 1000); /* 每路不同,好认 */ } sim_i2c_add(INA226_ADDR_INPUT, 2); sim_pca9555_attach(PCA9555_ADDR); } void setUp(void) { sim_hal_reset(); board_up(); } void tearDown(void) {} /* ---- 验收主项一:20 ms 的节拍 -------------------------------------------- */ /* 500 圈。每一圈都要**正好**落在 20 ms 的格子上,而且真实唤醒时刻 (µs 级)对理想时刻的偏差始终 < 1 ms —— 这就是「20 ms ±1 ms」。 会漂的实现(干完睡 20)在这里第一圈就出局:那样周期是 21.56 ms, 500 圈之后差出 780 ms。 */ static void test_safety_holds_a_20ms_beat_for_500_cycles(void) { safety_snap_t snap; t_safety_init(&snap); sched_periodic_t tick; sched_periodic_init(&tick, task_desc(TASK_SAFETY)->period_ms); /* 先确认「干活」在 sim 里真的花掉了时间。不然下面 500 圈测的只是 「delay 参数传对了没」—— 巡检耗时为零的话,什么实现都能踩准 20 ms。 */ const uint64_t t0 = hal_time_now_us(); TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap)); TEST_ASSERT_EQUAL_UINT64(t_safety_budget_us(), hal_time_now_us() - t0); sched_periodic_wait(&tick); uint32_t prev_ms = hal_time_now_ms(); for (int i = 1; i < 500; i++) { TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap)); sched_periodic_wait(&tick); const uint32_t now_ms = hal_time_now_ms(); TEST_ASSERT_EQUAL_UINT32(20, now_ms - prev_ms); prev_ms = now_ms; /* 理想时刻是 (i+1)×20 ms;实际唤醒的 µs 与它相差不到一个毫秒格。 */ const int64_t ideal_us = (int64_t)(i + 1) * 20000; const int64_t err_us = (int64_t)hal_time_now_us() - ideal_us; TEST_ASSERT_TRUE(err_us > -1000 && err_us < 1000); } TEST_ASSERT_EQUAL_UINT32(0, tick.overruns); TEST_ASSERT_EQUAL_UINT32(500, snap.cycles); TEST_ASSERT_EQUAL_UINT32(0, snap.skipped); } /* ---- 验收主项二:I²C 占用 ------------------------------------------------ */ /* §10.3 手算:一次寄存器读 ≈46 个位周期,12 次 INA + 1 次 PCA ≈1.5 ms / 20 ms ≈8 %。 这里按线上真实发生的字节数算,得 48 位/次 × 13 = 624 位 = 1560 µs = 7.8 %。 差的两位是 STOP 和一个重复 START,§10.3 没数进去(hal_i2c_acct.h 里记了这件事)。 */ static void test_i2c_load_per_cycle_matches_section_10_3(void) { safety_snap_t snap; t_safety_init(&snap); TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap)); TEST_ASSERT_EQUAL_UINT32(48, hal_i2c_txn_bits(1, 2)); TEST_ASSERT_EQUAL_UINT32(1560, snap.bus_us); const uint32_t pm = t_safety_load_permille(snap.bus_us, task_desc(TASK_SAFETY)->period_ms); TEST_ASSERT_EQUAL_UINT32(78, pm); /* §10.3 的「约 8 %」:7.0 %~8.5 % 都算对得上,超出就是巡检里多读/少读了东西。 */ TEST_ASSERT_TRUE(pm >= 70 && pm <= 85); char line[128]; const int n = t_safety_format(&snap, line, sizeof(line)); TEST_ASSERT_TRUE(n > 0 && n < (int)sizeof(line)); TEST_ASSERT_NOT_NULL(strstr(line, "bus=1560 us/20 ms")); TEST_ASSERT_NOT_NULL(strstr(line, "load=7.8%")); printf(" %s(§10.3 手算:约 1.5 ms / 约 8 %%)\n", line); } /* 静态预算与实测必须一模一样。不一样只有两种可能:巡检里多读/少读了东西, 或者别的任务在同一圈里插了事务进来 —— 两种都该被这条拦住。 (这也是为什么预算函数不是一个写死的常数:它按同一个算式从事务形状来。) */ static void test_measured_bus_time_equals_the_static_budget(void) { safety_snap_t snap; t_safety_init(&snap); for (int i = 0; i < 10; i++) { TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap)); TEST_ASSERT_EQUAL_UINT32(t_safety_budget_us(), snap.bus_us); } } /* 一圈正好 13 次事务,而且每片器件被读的次数是钉死的 —— 多读一次寄存器(比如顺手 check 一下 Calibration)会当场变红, 然后写的人才会想起来去把预算那一行也改了。 */ static void test_one_cycle_is_exactly_thirteen_transactions(void) { safety_snap_t snap; t_safety_init(&snap); const uint32_t before = hal_i2c_acct_txns(); TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap)); TEST_ASSERT_EQUAL_UINT32(TXNS_PER_CYCLE, hal_i2c_acct_txns() - before); for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_EQUAL_UINT32(2, sim_i2c_dev(rail_cfg(rail)->ina_addr)->n_read); } TEST_ASSERT_EQUAL_UINT32(2, sim_i2c_dev(INA226_ADDR_INPUT)->n_read); TEST_ASSERT_EQUAL_UINT32(1, sim_i2c_dev(PCA9555_ADDR)->n_read); } /* 五路读的是**表里的**地址。挂一片在「0x40+ch 算出来」的地址上, 它一次也不该被碰到 —— 这是 A7 那条禁令在巡检路径上的回归拦网。 */ static void test_rails_are_read_by_table_not_by_formula(void) { sim_i2c_dev_t *wrong = sim_i2c_add(0x46, 2); /* 谁都不是的地址 */ safety_snap_t snap; t_safety_init(&snap); TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap)); TEST_ASSERT_EQUAL_UINT32(0, wrong->n_read); TEST_ASSERT_EQUAL_UINT32(0, wrong->n_write); /* 顺带确认值确实按路对上了(每路电流不同)。 */ for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_TRUE(snap.rail_ok[rail - 1]); TEST_ASSERT_EQUAL_INT32(rail * 1000 * INA226_CURRENT_LSB_UA, snap.rail[rail - 1].current_ua); } } /* ---- 加锁纪律 ------------------------------------------------------------- */ /* 一整圈里的每一次事务都持着 I²C 锁;出了这个函数锁必须已经放掉。 */ static void test_every_transaction_happens_under_the_i2c_lock(void) { sim_lock_require(HAL_LOCK_I2C, true); safety_snap_t snap; t_safety_init(&snap); TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap)); TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C)); TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C)); /* 出门放锁 */ TEST_ASSERT_EQUAL_INT(-1, sim_lock_owner(HAL_LOCK_I2C)); } /* 上一条不是句空话:不拿锁直接读一片,违规计数必须 +1。 (检查本身失灵的话,上一条会永远绿。) */ static void test_the_lock_check_itself_can_fail(void) { sim_lock_require(HAL_LOCK_I2C, true); ina226_vi_t vi; (void)ina226_read_vi(rail_cfg(1)->ina_addr, &vi); TEST_ASSERT_EQUAL_UINT32(2, sim_lock_violations(HAL_LOCK_I2C)); /* 两次事务 */ } /* 拿不到锁:**跳过这一圈**,不死等(t_safety.h 规矩 1)。 等掉的是 5 ms,还在 20 ms 之内,所以节拍不丢。 */ static void test_a_busy_bus_skips_the_cycle_instead_of_waiting_forever(void) { 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); safety_snap_t snap; t_safety_init(&snap); const uint32_t t0 = hal_time_now_ms(); const uint32_t txns = hal_i2c_acct_txns(); TEST_ASSERT_EQUAL_INT(HAL_ERR_TIMEOUT, t_safety_step(&snap)); TEST_ASSERT_EQUAL_UINT32(1, snap.skipped); TEST_ASSERT_EQUAL_UINT32(1, snap.cycles); TEST_ASSERT_EQUAL_UINT32(0, hal_i2c_acct_txns() - txns); /* 一次事务也没发 */ TEST_ASSERT_EQUAL_UINT32(T_SAFETY_LOCK_TIMEOUT_MS, hal_time_elapsed_ms(t0)); TEST_ASSERT_TRUE(T_SAFETY_LOCK_TIMEOUT_MS < task_desc(TASK_SAFETY)->period_ms); } /* ---- 器件出问题的时候(§10.2:失联 ≠ 断电) ------------------------------ */ /* 六片里坏一片,剩下五片照读、PCA 照读。中断这一圈的话, 现象是「一片 INA226 掉线,五路判据全瞎」。 */ static void test_one_dead_device_does_not_stop_the_round(void) { sim_i2c_set_present(rail_cfg(3)->ina_addr, false); safety_snap_t snap; t_safety_init(&snap); TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, t_safety_step(&snap)); TEST_ASSERT_FALSE(snap.rail_ok[2]); for (int rail = 1; rail <= N; rail++) { if (rail != 3) { TEST_ASSERT_TRUE(snap.rail_ok[rail - 1]); } } TEST_ASSERT_TRUE(snap.input_ok); TEST_ASSERT_TRUE(snap.io_ok); TEST_ASSERT_EQUAL_UINT32(1, snap.i2c_errors); } /* 读失败时保留**上一圈的值**并把 ok 标成 false —— 不清零。 清零的话,A10 的过压判据会看到一个 0 V 的「正常」电压, 而 ok 标志正是让它先看一眼「这数是不是新的」。 */ static void test_a_failed_read_keeps_the_stale_value_and_flags_it(void) { safety_snap_t snap; t_safety_init(&snap); TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap)); const int32_t good_mv = snap.rail[0].bus_mv; TEST_ASSERT_EQUAL_INT32(3900, good_mv); sim_i2c_set_present(rail_cfg(1)->ina_addr, false); TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, t_safety_step(&snap)); TEST_ASSERT_FALSE(snap.rail_ok[0]); TEST_ASSERT_EQUAL_INT32(good_mv, snap.rail[0].bus_mv); /* 还是上一圈那个 */ } /* 器件掉线之后节拍照样是 20 ms(NAK 的事务也占总线时间,但占得更少)。 ⚠️ 这条测的是「巡检不会因为总线出错就乱了节奏」。 */ static void test_the_beat_survives_a_dead_device(void) { sim_i2c_set_present(rail_cfg(2)->ina_addr, false); safety_snap_t snap; t_safety_init(&snap); sched_periodic_t tick; sched_periodic_init(&tick, 20); uint32_t prev = hal_time_now_ms(); for (int i = 0; i < 50; i++) { (void)t_safety_step(&snap); sched_periodic_wait(&tick); TEST_ASSERT_EQUAL_UINT32(20, hal_time_now_ms() - prev); prev = hal_time_now_ms(); } TEST_ASSERT_EQUAL_UINT32(0, tick.overruns); TEST_ASSERT_EQUAL_UINT32(50, snap.i2c_errors); } /* PCA9555 的输入被读进来了,而且语义判读能用(A10 要拿它做判据)。 */ static void test_pca9555_inputs_land_in_the_snapshot(void) { sim_pca9555_drive(0, 0xFF & ~(1u << pca9555_flt_bit(4))); /* CH4 的 FLT# 拉低 */ safety_snap_t snap; t_safety_init(&snap); TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap)); TEST_ASSERT_TRUE(snap.io_ok); TEST_ASSERT_TRUE(pca9555_rail_fault(&snap.io, 4)); TEST_ASSERT_FALSE(pca9555_rail_fault(&snap.io, 1)); } /* 第一圈的占用里不该混进启动期间的事务(游标要在 init 时对齐到「现在」)。 */ static void test_startup_traffic_is_not_charged_to_the_first_cycle(void) { TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(PCA9555_ADDR)); /* 假装启动扫了一下 */ safety_snap_t snap; t_safety_init(&snap); TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap)); TEST_ASSERT_EQUAL_UINT32(t_safety_budget_us(), snap.bus_us); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_safety_holds_a_20ms_beat_for_500_cycles); RUN_TEST(test_i2c_load_per_cycle_matches_section_10_3); RUN_TEST(test_measured_bus_time_equals_the_static_budget); RUN_TEST(test_one_cycle_is_exactly_thirteen_transactions); RUN_TEST(test_rails_are_read_by_table_not_by_formula); RUN_TEST(test_every_transaction_happens_under_the_i2c_lock); RUN_TEST(test_the_lock_check_itself_can_fail); RUN_TEST(test_a_busy_bus_skips_the_cycle_instead_of_waiting_forever); RUN_TEST(test_one_dead_device_does_not_stop_the_round); RUN_TEST(test_a_failed_read_keeps_the_stale_value_and_flags_it); RUN_TEST(test_the_beat_survives_a_dead_device); RUN_TEST(test_pca9555_inputs_land_in_the_snapshot); RUN_TEST(test_startup_traffic_is_not_charged_to_the_first_cycle); return UNITY_END(); }