/* A10-R2 之一:`t_safety` **一圈的形状与账**(需求 §10.3、计划 §5.5)。 判据本身在 `test_t_safety_rules.c` 里,这里只管「这一圈发了什么、发给了谁」。 ---- R1 → R2,这个文件测的东西换了重心 ------------------------------------ R1:一条总线、六片不同地址的 INA226,一圈 13 次事务。那时唯一会出事的形态是 「地址算错」,而地址算错**会 NAK**,一眼就能看见。 R2:五片刀片完全同构(INA226 全是 0x40、PCA9555 全是 0x20),区分它们的 唯一手段是底板 TCA9548A 的通道。于是出现了一种 R1 根本没有的错误形态: **通道选错 ⇒ 读到另一台手机的电压电流**,而量纲、范围、变化趋势全都正常, 没有任何一层会报错、会 NAK、会计数。⭐ 本文件里带星的那两条就是它的拦网。 ⚠️ 两条能成立的前提: · 假总线里有一颗**真的 mux**(`sim_i2c_add_mux`)—— 没有它,五片刀片会 塌成一片,「选错通道」在宿主机上一次也复现不出来; · 假 I²C **真的把虚拟时钟花掉**(`sim_i2c_set_bus_time`,默认开)—— 没有它,「20 ms 周期」就退化成「测 delay 参数传对了没」,而真正会出事的是 「巡检自己占掉了 2.34 ms,节拍还准不准」。 */ #include #include #include "board_pins.h" #include "hal_gpio.h" #include "hal_i2c.h" #include "hal_i2c_acct.h" #include "hal_lock.h" #include "hal_time.h" #include "i2cbus.h" #include "ina226.h" #include "pca9555.h" #include "rail.h" #include "rail_cfg.h" #include "sched.h" #include "sim_hal.h" #include "slot.h" #include "t_actuator.h" #include "t_safety.h" #include "tasks.h" #include "tca9548a.h" #include "unity.h" #define N PHONERAIL_RAIL_COUNT /* 主干上那两片 DAC(`rail.h` 的 U6 / U7)。巡检一次也不该碰它们 —— 设定值是影子里的(`rail_setpoint_mv()`),去读 DAC 就是每圈多五次事务。 */ #define DAC_ADDR_A 0x4A #define DAC_ADDR_B 0x4B /* ⭐ 五路预置成**互不相同**的电压:CH1 3600 → CH5 4000 mV。 只有值各不相同,「读到了另一台手机的数据」才留得下痕迹。 */ #define RAIL_MV(rail) (3600 + 100 * ((rail) - 1)) /* 电流也各不相同:CH1 1 A → CH5 5 A。 */ #define RAIL_UA(rail) ((rail) * 1000000) /* 五片刀片的 Port1(四个故障/PG 信号,开漏上拉 ⇒ 静息 0x0F)各驱动成不同的图案, 每一片**恰好有一个信号在响**,而且响的不是同一个。 全驱成一样的话,「五路读的都是同一片」这条错就看不出来。 */ static const uint8_t k_port1[N] = { (uint8_t)(0x0Fu & ~(1u << PCA9555_BIT_EFUSE_FLT)), /* CH1:eFuse 报故障 */ (uint8_t)(0x0Fu & ~(1u << PCA9555_BIT_BUCK_PG)), /* CH2:Buck PG 掉了 */ (uint8_t)(0x0Fu & ~(1u << PCA9555_BIT_INA_ALERT)), /* CH3:INA226 在告警 */ (uint8_t)(0x0Fu & ~(1u << PCA9555_BIT_EFUSE_PG)), /* CH4:eFuse PG 掉了 */ 0x0Fu, /* CH5:什么事都没有 */ }; static safety_snap_t g_snap; /* `board_up()` 跑完那一刻的基准值。启动本身会在总线和引脚上留下痕迹, 拿绝对值去断言等于把启动的账算进第一圈(那正是 `t_safety_init()` 对齐游标要防的事)。 */ static uint32_t g_resets0; /* `MUX_RST#` 复位次数 —— `i2cbus_init()` 必定留下一次,见下 */ static int ch_of(int rail) { return (int)rail_cfg(rail)->mux_ch; } static sim_i2c_dev_t *ina_of(int rail) { return sim_i2c_dev_ch(ch_of(rail), rail_cfg(rail)->ina_addr); } static sim_i2c_dev_t *pca_of(int rail) { return sim_i2c_dev_ch(ch_of(rail), PCA9555_ADDR); } static sim_i2c_dev_t *mux_dev(void) { return sim_i2c_dev(TCA9548A_ADDR); } /* 从**假器件的寄存器**注入,不去改快照 —— 改快照等于跳过驱动和 mux, 那样连「读的是哪一片」都证明不了。 */ static void put_vi(int rail, int mv, int ua) { const int ch = ch_of(rail); const uint8_t addr = rail_cfg(rail)->ina_addr; TEST_ASSERT_EQUAL_INT(0, mv % 5); /* 母线 LSB 1.25 mV:不是 5 的倍数就注不准 */ sim_i2c_set_reg_ch(ch, addr, INA226_REG_BUS_V, (uint16_t)(mv * 4 / 5)); sim_i2c_set_reg_ch(ch, addr, INA226_REG_CURRENT, (uint16_t)(int16_t)(ua / INA226_CURRENT_LSB_UA)); } /* 假板子:主干(mux + 输入侧 INA226 + 两片 DAC)+ 五片刀片(各 INA226 + PCA9555)。 */ 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++) { const int ch = ch_of(rail); sim_i2c_add_ch(ch, rail_cfg(rail)->ina_addr, 2); put_vi(rail, RAIL_MV(rail), RAIL_UA(rail)); sim_pca9555_attach_ch(rail - 1, ch, PCA9555_ADDR); sim_pca9555_drive(rail - 1, 1, k_port1[rail - 1]); } sim_i2c_add(INA226_ADDR_INPUT, 2); sim_i2c_set_reg(INA226_ADDR_INPUT, INA226_REG_BUS_V, (uint16_t)(12000 * 4 / 5)); sim_i2c_set_reg(INA226_ADDR_INPUT, INA226_REG_CURRENT, (uint16_t)(2000000 / 500)); sim_i2c_add(DAC_ADDR_A, 2); sim_i2c_add(DAC_ADDR_B, 2); TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_init()); TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); TEST_ASSERT_EQUAL_INT(HAL_OK, slot_prsnt_init()); /* ⚠️ `PRSNT#` 配的是**上拉输入**,没人驱动就是高 = 刀片不在位。 不驱低的话每一圈都会触发「刀片不在位」判据,而现象是「所有用例都多出几条 事件、五路自己关掉」—— 看上去像判据写错了,其实是假板子插反了。 */ for (int rail = 1; rail <= N; rail++) { sim_gpio_drive(rail_cfg(rail)->prsnt_n, 0); } slot_prsnt_poll(); for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_EQUAL_INT(HAL_OK, slot_init(rail)); } t_actuator_init(NULL, NULL); rail_setpoint_reset(); t_safety_init(&g_snap); /* ⚠️ 这里一定是 1,不是 0:`i2cbus_init()` 先把 `MUX_RST#` 配成输出(两个 HAL 都保证「配完是 0」)再写 1,中间那一瞬间对真器件就是一次复位脉冲,假 mux 如实照做。这个窗口是已知且可接受的(那时还没人用总线),但**不能当它不存在** —— 所以后面的用例一律和这个基准比差值。 */ g_resets0 = sim_i2c_mux_resets(); TEST_ASSERT_EQUAL_UINT32(1, g_resets0); } void setUp(void) { sim_hal_reset(); board_up(); } /* 每个用例收尾都查一次「有没有同时开两路通道」。真板上那是两片同地址的 INA226 一起应答、读回来是线与 —— 数值正常、还很稳,没有任何一层会报错。 所以这个数**必须恒为 0**,放在 tearDown 里等于给每一条用例都加上这条断言。 */ void tearDown(void) { TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous()); } /* ---- 一圈的事务账(计划 §5.5) ------------------------------------------- */ /* 一圈正好 `t_safety_cycle_txns()` 次事务,而且**每一片器件被读的次数是钉死的**。 顺手多读一个寄存器(比如 check 一下 Calibration)会当场变红, 然后写的人才会想起来去把预算那一行也改了。 */ static void test_one_cycle_is_exactly_the_budgeted_transaction_count(void) { uint32_t ina_rd[N], pca_rd[N], pca_wr[N]; for (int rail = 1; rail <= N; rail++) { ina_rd[rail - 1] = ina_of(rail)->n_read; pca_rd[rail - 1] = pca_of(rail)->n_read; pca_wr[rail - 1] = pca_of(rail)->n_write; } const uint32_t in_rd = sim_i2c_dev(INA226_ADDR_INPUT)->n_read; const uint32_t mux_wr = mux_dev()->n_write; const uint32_t mux_rd = mux_dev()->n_read; const uint32_t dac_rd = sim_i2c_dev(DAC_ADDR_A)->n_read + sim_i2c_dev(DAC_ADDR_B)->n_read; const uint32_t before = hal_i2c_acct_txns(); TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); TEST_ASSERT_EQUAL_UINT32(t_safety_cycle_txns(), hal_i2c_acct_txns() - before); TEST_ASSERT_EQUAL_UINT32(t_safety_cycle_txns(), g_snap.txns); /* 逐片对账:五路各 2 次 INA226 寄存器读 + 1 次 PCA9555 输入读; 主干那片 INA226 2 次;mux 被写 6 次(五路各一次 + 回主干一次)。 ⚠️ 比的是**这一圈的增量**:`slot_init()` 自己就读了 PCA9555 六次(写一个回读一个), 拿绝对值比等于把启动的账算进巡检。 */ for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_EQUAL_UINT32(2, ina_of(rail)->n_read - ina_rd[rail - 1]); TEST_ASSERT_EQUAL_UINT32(1, pca_of(rail)->n_read - pca_rd[rail - 1]); /* 「读输入」在线上是「写一个寄存器号 + 读两字节」,所以写计数也会 +1。 真正要拦的是**写输出锁存器**:平安无事的一圈里,最后落到这一片上的写 只能是那一个字节的寄存器指针(0x00),不能是 OUTPUT0(0x02)。 */ TEST_ASSERT_EQUAL_UINT32(1, pca_of(rail)->n_write - pca_wr[rail - 1]); TEST_ASSERT_EQUAL_UINT8(PCA9555_REG_INPUT0, pca_of(rail)->last_write[0]); TEST_ASSERT_EQUAL_UINT8(1, pca_of(rail)->last_write_len); TEST_ASSERT_EQUAL_UINT8(0, sim_pca9555_output(rail - 1, 0)); /* 键与 VBUS 全是 0 */ } TEST_ASSERT_EQUAL_UINT32(2, sim_i2c_dev(INA226_ADDR_INPUT)->n_read - in_rd); TEST_ASSERT_EQUAL_UINT32((uint32_t)N + 1u, mux_dev()->n_write - mux_wr); TEST_ASSERT_EQUAL_UINT32(0, mux_dev()->n_read - mux_rd); /* 选完不回读,那是 probe 的事 */ /* 设定值从影子里取(`rail_setpoint_mv()`),判据一次也不该去读 DAC。 读一次就是每圈多五次事务,把 §10.3 的预算直接顶穿。 */ TEST_ASSERT_EQUAL_UINT32( 0, sim_i2c_dev(DAC_ADDR_A)->n_read + sim_i2c_dev(DAC_ADDR_B)->n_read - dac_rd); } /* 静态预算与实测必须**一模一样**。不一样只有两种可能:巡检里多读/少读了东西, 或者有别的任务在同一圈里插了事务进来 —— 两种都该被这条拦住。 (这也是为什么预算函数不是一个写死的常数:它按同一个算式从事务形状来。) */ static void test_measured_bus_time_equals_the_static_budget(void) { for (int i = 0; i < 10; i++) { TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); TEST_ASSERT_EQUAL_UINT32(t_safety_budget_us(), g_snap.bus_us); TEST_ASSERT_EQUAL_UINT32(t_safety_cycle_txns(), g_snap.txns); } } /* 计划 §5.5 那张表的可执行版本:23 次事务 ≈ 2.5 ms / 12.5 %。 这里按线上真实发生的位数算,得 6×20 + 17×48 = 936 位 = 2340 µs = 11.7 %。 §5.5 手算的 2.48 ms 用的是每次事务的近似值,差的那 140 µs 是近似的账。 ⚠️ 这一条同时钉住「R1 的 13 次 / 1.56 ms 已经不是这块板子的数了」—— 把预算函数改回 R1 那版,这里第一行就红。 */ static void test_the_bus_account_matches_plan_5_5(void) { TEST_ASSERT_EQUAL_UINT32(23, t_safety_cycle_txns()); TEST_ASSERT_EQUAL_UINT32(20, hal_i2c_txn_bits(1, 0)); /* mux 选通:裸写一个字节 */ TEST_ASSERT_EQUAL_UINT32(48, hal_i2c_txn_bits(1, 2)); /* 寄存器读 */ TEST_ASSERT_EQUAL_UINT32(2340, t_safety_budget_us()); TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); const uint32_t period = task_desc(TASK_SAFETY)->period_ms; const uint32_t pm = t_safety_load_permille(g_snap.bus_us, period); TEST_ASSERT_EQUAL_UINT32(117, pm); /* 计划 §5.5 的「≈12.4 %」:11 %~13 % 都算对得上。超出就是巡检里多读/少读了东西。 */ TEST_ASSERT_TRUE(pm >= 110 && pm <= 130); /* 一圈占的总线时间必须远小于周期,否则巡检自己就把节拍吃掉了。 */ TEST_ASSERT_TRUE(g_snap.bus_us < period * 1000u / 4u); char line[160]; const int n = t_safety_format(&g_snap, line, sizeof(line)); TEST_ASSERT_TRUE(n > 0 && n < (int)sizeof(line)); TEST_ASSERT_NOT_NULL(strstr(line, "txns=23")); TEST_ASSERT_NOT_NULL(strstr(line, "bus=2340 us/20 ms")); TEST_ASSERT_NOT_NULL(strstr(line, "load=11.7%")); printf(" %s(计划 §5.5:≈2.5 ms / 12.4 %%)\n", line); } /* ---- ⭐ R2 最危险的错误形态:读到了另一台手机 ---------------------------- */ /* 五个通道上的假 INA226 预置成**互不相同**的电压与电流,跑一圈,逐路对上。 通道选错的后果是「读到另一台手机的数据」—— 量纲、范围、变化趋势全都正常, 遥测曲线好看得很,五台机器的过压判据却全部盯错了对象。 R1 里这条错误不存在(六片六个地址,错了就 NAK);R2 里它不报错、不 NAK、 不计数,**只有值本身能拆穿它**。 */ static void test_each_rail_is_read_on_its_own_mux_channel(void) { TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_TRUE(g_snap.rail_ok[rail - 1]); TEST_ASSERT_EQUAL_INT32(RAIL_MV(rail), g_snap.rail[rail - 1].bus_mv); TEST_ASSERT_EQUAL_INT32(RAIL_UA(rail), g_snap.rail[rail - 1].current_ua); } /* 主干那片(0x43)不在任何通道后面,它的值不许混进五路里。 */ TEST_ASSERT_TRUE(g_snap.input_ok); TEST_ASSERT_EQUAL_INT32(12000, g_snap.input.bus_mv); /* 换一组值再跑一圈:万一上面那组恰好被「按顺序发牌」蒙对,换一组就露馅。 */ for (int rail = 1; rail <= N; rail++) { put_vi(rail, 4000 - 100 * (rail - 1), -(rail * 100000)); /* 顺序反过来、电流转负 */ } TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_EQUAL_INT32(4000 - 100 * (rail - 1), g_snap.rail[rail - 1].bus_mv); TEST_ASSERT_EQUAL_INT32(-(rail * 100000), g_snap.rail[rail - 1].current_ua); } } /* 同一件事,换 PCA9555 那一路:五片的四个故障信号各响一个,逐片对上。 INA226 那条对上了、这条没对上,说明「选通道」和「读哪一片」在两条路径上 各自算了一遍 —— 而 `slot.c` 存在的全部理由就是让这两件事只算一次。 */ static void test_each_blade_reports_its_own_pca9555_inputs(void) { TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_TRUE(g_snap.io_ok[rail - 1]); TEST_ASSERT_EQUAL_UINT8(k_port1[rail - 1], g_snap.io[rail - 1].port1); } /* 换成语义判读再验一遍(极性写反在原始字节上是看不出来的)。 */ TEST_ASSERT_TRUE(pca9555_efuse_fault(&g_snap.io[0])); /* CH1 FLT# 低 = 有故障 */ TEST_ASSERT_FALSE(pca9555_efuse_fault(&g_snap.io[1])); TEST_ASSERT_FALSE(pca9555_buck_pg(&g_snap.io[1])); /* CH2 Buck PG 低 = 不良 */ TEST_ASSERT_TRUE(pca9555_buck_pg(&g_snap.io[0])); TEST_ASSERT_TRUE(pca9555_ina_alert(&g_snap.io[2])); /* CH3 ALERT 低 = 在告警 */ TEST_ASSERT_FALSE(pca9555_ina_alert(&g_snap.io[3])); TEST_ASSERT_FALSE(pca9555_efuse_pg(&g_snap.io[3])); /* CH4 eFuse PG 低 */ TEST_ASSERT_TRUE(pca9555_efuse_pg(&g_snap.io[4])); /* CH5 全好 */ } /* 任何时刻都只许开一路通道,而且一圈走完停在**主干**上。 同时开两路在真板上是两片一起应答、读回线与(数值正常、还很稳)—— `sim_i2c_ambiguous()` 是这件事在宿主机上唯一的痕迹。 */ static void test_the_mux_never_has_two_channels_open(void) { for (int i = 0; i < 50; i++) { TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous()); /* 一圈的最后一次选通是回主干(读输入侧那片),所以收尾时通道是全关的。 */ TEST_ASSERT_EQUAL_INT(-1, sim_i2c_mux_ch()); TEST_ASSERT_EQUAL_UINT8(0, sim_i2c_mux_mask()); } TEST_ASSERT_EQUAL_UINT32(0, i2cbus_select_fails()); TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_mux_resets() - g_resets0); /* 一切正常时不该再复位 */ } /* 通道 5 上是 CH224A(PD 受电芯片),通道 6 / 7 悬空。巡检一次也不许选到它们。 选到通道 5 再去读 0x40 会一个字节也读不到,而现象是「这一路电压电流突然全是 0」 —— 看上去像刀片拔了。这里在那三路上各挂一片 0x40,它们必须一次也没被碰过。 */ static void test_the_unused_channels_are_never_selected(void) { sim_i2c_dev_t *pd = sim_i2c_add_ch(TCA9548A_CH_PD, INA226_ADDR_RAIL, 2); sim_i2c_dev_t *sp6 = sim_i2c_add_ch(6, INA226_ADDR_RAIL, 2); sim_i2c_dev_t *sp7 = sim_i2c_add_ch(7, INA226_ADDR_RAIL, 2); for (int i = 0; i < 5; i++) { TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); } TEST_ASSERT_EQUAL_UINT32(0, pd->n_read); TEST_ASSERT_EQUAL_UINT32(0, sp6->n_read); TEST_ASSERT_EQUAL_UINT32(0, sp7->n_read); TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous()); /* 它们也没和刀片撞上过 */ } /* ---- 节拍 ---------------------------------------------------------------- */ /* 500 圈。每一圈都要**正好**落在 20 ms 的格子上,而且真实唤醒时刻(µs 级) 对理想时刻的偏差始终 < 1 ms —— 这就是 §10.3 的「20 ms ±1 ms」。 会漂的实现(干完睡 20)在这里第一圈就出局:R2 一圈自己要占 2.34 ms, 那样周期是 22.34 ms,500 圈之后差出 1.17 s。 */ static void test_safety_holds_a_20ms_beat_for_500_cycles(void) { 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(&g_snap)); TEST_ASSERT_EQUAL_UINT64(t_safety_budget_us(), hal_time_now_us() - t0); sched_periodic_wait(&tick); /* ⚠️ 相位起点是**第一次醒来那一刻**,不是 0:R2 的启动自己就要跑 mux 认件 + 五片 PCA9555 初始化,上百次事务在虚拟时钟上是真花掉了的。拿绝对 0 当起点, 测出来的是「启动花了多久」,和节拍没关系。 */ const uint64_t base_us = hal_time_now_us(); uint32_t prev_ms = hal_time_now_ms(); for (int i = 1; i < 500; i++) { TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_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; const int64_t ideal_us = (int64_t)base_us + (int64_t)i * 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, g_snap.cycles); TEST_ASSERT_EQUAL_UINT32(0, g_snap.skipped); TEST_ASSERT_EQUAL_UINT32(0, g_snap.events); } /* 20 ms 的周期必须**长于** INA226 的一帧转换时间(AVG=4 × 2×1.1 ms = 8.8 ms)。 短了的话同一帧会被读两次,而现象是「电流特别稳」—— 不是任何一种错误, 不会 NAK、不会报警、遥测曲线还更好看。R2 一圈占 2.34 ms,离 8.8 ms 还很远, 所以每一圈拿到的都是新的一帧。 */ static void test_the_period_outlives_one_ina226_conversion(void) { const uint32_t period_us = task_desc(TASK_SAFETY)->period_ms * 1000u; TEST_ASSERT_EQUAL_UINT32(8800, (uint32_t)INA226_CONV_US); TEST_ASSERT_TRUE(period_us > (uint32_t)INA226_CONV_US); /* 两倍余量:巡检自己占的总线时间加进去也不能逼近一帧。 */ TEST_ASSERT_TRUE(period_us >= 2u * (uint32_t)INA226_CONV_US); TEST_ASSERT_TRUE(t_safety_budget_us() < (uint32_t)INA226_CONV_US); TEST_ASSERT_TRUE(task_table_ok()); /* 任务表自检里钉着同一条 */ } /* ---- 加锁纪律 ------------------------------------------------------------ */ /* 一整圈里的每一次事务都持着 I²C 锁;出了这个函数锁必须已经放掉。 R2 里这一圈要拿六次锁(五路 + 主干),每一次都得规规矩矩。 */ static void test_every_transaction_happens_under_the_i2c_lock(void) { sim_lock_require(HAL_LOCK_I2C, true); const uint32_t takes0 = sim_lock_takes(HAL_LOCK_I2C); for (int i = 0; i < 5; i++) { TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_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)); /* 一圈六次 `xact` = 六次拿锁(R1 是一把大锁包住整圈,只拿一次)。 这个数正是「给 t_actuator 留的缝」有几条 —— 变少了缝就没了。 */ TEST_ASSERT_EQUAL_UINT32(5u * ((uint32_t)N + 1u), sim_lock_takes(HAL_LOCK_I2C) - takes0); } /* 上一条不是句空话:不拿锁直接读一片 INA226,违规计数必须 +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)。死等的现象是巡检整个停摆, 而它是优先级最高的那个任务 —— 停摆了没有任何人会发现。 ⚠️ R2 的一笔新账:一圈要等**六次**锁,最坏 6 × 5 = 30 ms,**比 20 ms 的周期还长** (R1 只等一次,5 ms)。这条断言把这个数钉在这里:谁改了超时常量或者 `xact` 的次数,都得先来正面回答「一圈最坏等多久」这个问题。 */ static void test_a_busy_bus_skips_the_cycle_instead_of_waiting_forever(void) { TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); /* 先跑一圈好的,留下基准 */ const int32_t mv0 = g_snap.rail[0].bus_mv; const uint32_t txns0 = hal_i2c_acct_txns(); const uint32_t en_writes0 = sim_gpio_writes(rail_cfg(1)->efuse_en); sim_lock_set_task(1); /* 假装 t_actuator 正在按键,总线在它手里 */ TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10)); sim_lock_set_task(0); const uint32_t t0 = hal_time_now_ms(); TEST_ASSERT_EQUAL_INT(HAL_ERR_TIMEOUT, t_safety_step(&g_snap)); /* 这一圈**什么也没做**:一次事务没发、一根引脚没动、快照原封不动。 */ TEST_ASSERT_EQUAL_UINT32(0, hal_i2c_acct_txns() - txns0); TEST_ASSERT_EQUAL_UINT32(0, g_snap.txns); TEST_ASSERT_EQUAL_UINT32(0, g_snap.bus_us); TEST_ASSERT_EQUAL_INT32(mv0, g_snap.rail[0].bus_mv); TEST_ASSERT_EQUAL_UINT32(en_writes0, sim_gpio_writes(rail_cfg(1)->efuse_en)); TEST_ASSERT_EQUAL_UINT32(0, g_snap.events); /* 但这一圈**被记下来了**:不计数的跳过等于悄悄失明。 */ TEST_ASSERT_EQUAL_UINT32(1, g_snap.skipped); TEST_ASSERT_EQUAL_UINT32(2, g_snap.cycles); /* ⭐ **整圈的账,不是单次的账。** R2 一圈要拿六次锁,六次各等满 5 ms 就是 30 ms —— 比 20 ms 的周期还长,现象是巡检成片丢拍而每一次 `xact` 都老老实实 按时返回了。所以 `T_SAFETY_LOCK_BUDGET_MS` 是一条整圈的闸: 花到 10 ms 就不再试剩下的路。 这里断言的正是那条闸:**只试了 2 次**(5 + 5 = 10 ms 用完预算), 而不是六次全试一遍。把预算那一段删掉的话,下面两条会变成 6 和 30。 */ TEST_ASSERT_EQUAL_UINT32(T_SAFETY_LOCK_BUDGET_MS / T_SAFETY_LOCK_TIMEOUT_MS, i2cbus_lock_timeouts()); TEST_ASSERT_EQUAL_UINT32(T_SAFETY_LOCK_BUDGET_MS, hal_time_elapsed_ms(t0)); /* 而且整圈花掉的时间**必须短于巡检周期**,否则节拍本身就保不住了。 */ TEST_ASSERT_TRUE(hal_time_elapsed_ms(t0) < task_desc(TASK_SAFETY)->period_ms); /* 总线还回来,下一圈照常。 */ sim_lock_set_task(1); hal_lock_give(HAL_LOCK_I2C); sim_lock_set_task(0); TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); TEST_ASSERT_EQUAL_UINT32(t_safety_cycle_txns(), g_snap.txns); TEST_ASSERT_EQUAL_UINT32(1, g_snap.skipped); } /* ---- 器件出问题的时候(§10.2:失联 ≠ 断电) ------------------------------ */ /* 一片刀片的 INA226 掉线,**剩下四路的读照常**。中断这一圈的现象是 「一片 INA226 掉线,五路判据全瞎」—— 而那四台手机什么毛病都没有。 */ static void test_one_dead_ina226_does_not_stop_the_other_four(void) { sim_i2c_set_present_ch(ch_of(3), rail_cfg(3)->ina_addr, false); TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, t_safety_step(&g_snap)); TEST_ASSERT_FALSE(g_snap.rail_ok[2]); for (int rail = 1; rail <= N; rail++) { if (rail == 3) { continue; } TEST_ASSERT_TRUE(g_snap.rail_ok[rail - 1]); TEST_ASSERT_EQUAL_INT32(RAIL_MV(rail), g_snap.rail[rail - 1].bus_mv); TEST_ASSERT_TRUE(g_snap.io_ok[rail - 1]); } /* 坏掉那一路的 PCA9555 没跟着遭殃(是两颗器件,不是一颗)。 */ TEST_ASSERT_TRUE(g_snap.io_ok[2]); TEST_ASSERT_EQUAL_UINT8(k_port1[2], g_snap.io[2].port1); TEST_ASSERT_TRUE(g_snap.input_ok); TEST_ASSERT_EQUAL_UINT32(1, g_snap.i2c_errors); /* 读 V 那一次就 NAK 了,读 I 那一次不会再发 ⇒ 这一圈**少一次事务**。 把这个数写下来,是为了让「失联之后还傻读第二次」也会变红。 */ TEST_ASSERT_EQUAL_UINT32(t_safety_cycle_txns() - 1u, g_snap.txns); TEST_ASSERT_EQUAL_UINT32( t_safety_budget_us() - (uint32_t)hal_i2c_bits_to_us(hal_i2c_txn_bits(1, 2)), g_snap.bus_us); } /* 空槽**一个字节也不许发**(2026-09-27 上板实测)。空机时五路无条件各读一次, 真板上每次 NAK 要十几 ms,槽 1 两次就把整圈预算吃光 —— 槽 2~5 和主干的 0x43 一圈都没读到,输入侧监控整个是瞎的。这里把空槽的器件也拔掉:谁碰了它就会 NAK、 `i2c_errors` 立刻非零;全空那一圈还要求主干那片照读不误。 */ static void test_empty_slots_are_never_touched_and_the_input_still_reads(void) { const int empty[] = { 2, 4 }; for (unsigned i = 0; i < sizeof(empty) / sizeof(empty[0]); i++) { const int rail = empty[i]; sim_gpio_drive(rail_cfg(rail)->prsnt_n, 1); /* 高 = 不在位 */ sim_i2c_set_present_ch(ch_of(rail), rail_cfg(rail)->ina_addr, false); sim_i2c_set_present_ch(ch_of(rail), PCA9555_ADDR, false); } TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); TEST_ASSERT_EQUAL_UINT32(0, g_snap.i2c_errors); TEST_ASSERT_EQUAL_UINT32(0, g_snap.skipped); for (int rail = 1; rail <= N; rail++) { const bool here = (rail != 2 && rail != 4); TEST_ASSERT_EQUAL(here, g_snap.rail_ok[rail - 1]); TEST_ASSERT_EQUAL(here, g_snap.io_ok[rail - 1]); TEST_ASSERT_EQUAL(here, g_snap.present[rail - 1]); } TEST_ASSERT_TRUE(g_snap.input_ok); /* 五槽全空(上板时的真实状态):总线上只剩主干那一片。 */ for (int rail = 1; rail <= N; rail++) { sim_gpio_drive(rail_cfg(rail)->prsnt_n, 1); sim_i2c_set_present_ch(ch_of(rail), rail_cfg(rail)->ina_addr, false); sim_i2c_set_present_ch(ch_of(rail), PCA9555_ADDR, false); } TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); TEST_ASSERT_EQUAL_UINT32(0, g_snap.i2c_errors); TEST_ASSERT_TRUE(g_snap.input_ok); TEST_ASSERT_EQUAL_INT32(12000, g_snap.input.bus_mv); } /* 读失败时保留**上一圈的值**并把 ok 标成 false —— 不清零。 清零的话,判据会看到一个 0 V 的「正常数」;而 `ok` 正是让它先看一眼 「这数是不是新的」。拿一个没更新的电压去判过压是最难查的那种错。 */ static void test_a_failed_read_keeps_the_stale_value_and_flags_it(void) { TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); TEST_ASSERT_EQUAL_INT32(RAIL_MV(1), g_snap.rail[0].bus_mv); sim_i2c_set_present_ch(ch_of(1), rail_cfg(1)->ina_addr, false); /* 器件掉线之后把寄存器也改掉:万一实现偷偷读到了,值就会跟着变。 */ put_vi(1, 3000, 0); for (int i = 0; i < 5; i++) { TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, t_safety_step(&g_snap)); TEST_ASSERT_FALSE(g_snap.rail_ok[0]); TEST_ASSERT_EQUAL_INT32(RAIL_MV(1), g_snap.rail[0].bus_mv); /* 还是上一圈那个 */ TEST_ASSERT_EQUAL_INT32(RAIL_UA(1), g_snap.rail[0].current_ua); } /* 器件回来了就立刻用新值(`ok` 不是个一去不返的标志)。 */ sim_i2c_set_present_ch(ch_of(1), rail_cfg(1)->ina_addr, true); TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); TEST_ASSERT_TRUE(g_snap.rail_ok[0]); TEST_ASSERT_EQUAL_INT32(3000, g_snap.rail[0].bus_mv); } /* 同一路上的两颗器件互不牵连:PCA9555 读不到,不影响这一路 INA226 已经读到的值。 `cycle_rail()` 里「器件读失败不提前返回」那一条就是这个意思 —— 提前返回的现象是「一片 I/O 扩展器失联,这一路的过压判据也跟着停了」。 */ static void test_a_dead_pca9555_does_not_spoil_the_same_rails_ina226(void) { sim_i2c_set_present_ch(ch_of(2), PCA9555_ADDR, false); TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, t_safety_step(&g_snap)); TEST_ASSERT_FALSE(g_snap.io_ok[1]); TEST_ASSERT_TRUE(g_snap.rail_ok[1]); /* INA226 是好的,值是新的 */ TEST_ASSERT_EQUAL_INT32(RAIL_MV(2), g_snap.rail[1].bus_mv); TEST_ASSERT_EQUAL_INT32(RAIL_UA(2), g_snap.rail[1].current_ua); for (int rail = 1; rail <= N; rail++) { if (rail != 2) { TEST_ASSERT_TRUE(g_snap.io_ok[rail - 1]); } } TEST_ASSERT_EQUAL_UINT32(1, g_snap.i2c_errors); } /* 器件掉线之后节拍照样是 20 ms(NAK 的事务也占总线时间,只是占得更少)。 测的是「巡检不会因为总线出错就乱了节奏」。 */ static void test_the_beat_survives_a_dead_blade(void) { sim_i2c_set_present_ch(ch_of(5), rail_cfg(5)->ina_addr, false); sim_i2c_set_present_ch(ch_of(5), PCA9555_ADDR, false); 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(&g_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(100, g_snap.i2c_errors); /* 每圈两颗器件各一次 */ /* 别的四路一点没受影响 —— 这才是「失联 ≠ 别的路也不看了」。 */ for (int rail = 1; rail <= 4; rail++) { TEST_ASSERT_TRUE(g_snap.rail_ok[rail - 1]); TEST_ASSERT_EQUAL_INT32(RAIL_MV(rail), g_snap.rail[rail - 1].bus_mv); } } /* 第一圈的占用里不该混进启动期间的事务(游标要在 `t_safety_init()` 时对齐到「现在」)。 混进去的现象是「开机那一下占用率 40 %」,然后再也不复现。 */ static void test_startup_traffic_is_not_charged_to_the_first_cycle(void) { /* 假装启动时又扫了一遍总线。 */ TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10)); (void)hal_i2c_probe(TCA9548A_ADDR); (void)hal_i2c_probe(INA226_ADDR_INPUT); hal_lock_give(HAL_LOCK_I2C); t_safety_init(&g_snap); TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); TEST_ASSERT_EQUAL_UINT32(t_safety_budget_us(), g_snap.bus_us); TEST_ASSERT_EQUAL_UINT32(t_safety_cycle_txns(), g_snap.txns); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_one_cycle_is_exactly_the_budgeted_transaction_count); RUN_TEST(test_measured_bus_time_equals_the_static_budget); RUN_TEST(test_the_bus_account_matches_plan_5_5); RUN_TEST(test_each_rail_is_read_on_its_own_mux_channel); RUN_TEST(test_each_blade_reports_its_own_pca9555_inputs); RUN_TEST(test_the_mux_never_has_two_channels_open); RUN_TEST(test_the_unused_channels_are_never_selected); RUN_TEST(test_safety_holds_a_20ms_beat_for_500_cycles); RUN_TEST(test_the_period_outlives_one_ina226_conversion); 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_ina226_does_not_stop_the_other_four); RUN_TEST(test_empty_slots_are_never_touched_and_the_input_still_reads); RUN_TEST(test_a_failed_read_keeps_the_stale_value_and_flags_it); RUN_TEST(test_a_dead_pca9555_does_not_spoil_the_same_rails_ina226); RUN_TEST(test_the_beat_survives_a_dead_blade); RUN_TEST(test_startup_traffic_is_not_charged_to_the_first_cycle); return UNITY_END(); }