/* R-0:`i2cbus` —— 带 mux 的 I²C 原子事务层(计划 §4.3)。 ⭐ 这一份是 R2 最重要的一个测试文件,因为它守的是**唯一一类不会报错的错**: R2 之后,一次「读 CH3 的电压」在总线上是两件事 —— 先写 TCA9548A 选通道, 再读 0x40 的寄存器。五片刀片完全同构,五片上的 INA226 都是 0x40。 所以通道选错时:地址对、应答正常、寄存器号对、数值在量程内、 还会跟着负载变化 —— **读到的是另一台手机的数据,而每一层都返回 HAL_OK。** 这个文件里的每一条用例都是在证明「这一类错发生时有人会红」。 凡是只能靠 `i2cbus` 自己的影子变量看出来的事,一律再补一条**数据层面**的断言: 影子变量说自己对不算数,要让假器件吐出另一片的读数才算数。 假板子的布置(与真板一致): · 0x70 mux,`MUX_RST#` 接 BOARD_PIN_MUX_RST_N; · 0x43 主干上的输入侧 INA226(全关时唯一还看得见的 INA226); · 0x40 通道 0~4 各一片 —— 五片同址,正是 D8 的设计。 五片的母线电压预置成**五个都很正常的电池电压**(3.75 / 4.20 / 3.80 / 4.00 / 4.10 V): 选错通道时读回来的值不会超量程、不会是 0、也不会是 0xFFFF, 所以「加个范围检查就能发现」这条退路在这块板上是不存在的。 */ #include "i2cbus.h" #include "board_pins.h" #include "hal_gpio.h" #include "hal_i2c.h" #include "hal_lock.h" #include "ina226.h" #include "sim_hal.h" #include "tca9548a.h" #include "unity.h" #define TRUNK_INA 0x43 #define BLADE_INA 0x40 #define N_BLADE 5 /* 通道 n 上那片刀片 INA226 的 BUS_V 原始值。bus_mv = raw × 1.25 mV。 */ static const uint16_t k_raw[N_BLADE] = { 3000, 3360, 3040, 3200, 3280 }; static const int32_t k_mv[N_BLADE] = { 3750, 4200, 3800, 4000, 4100 }; #define TRUNK_RAW 9600 /* 12.0 V —— 主干那片量的是 12 V 母线 */ #define TRUNK_MV 12000 /* ---- 脚手架 -------------------------------------------------------------- */ typedef struct { uint8_t addr; uint32_t calls; int32_t mv; hal_err_t inner; /* 嵌套那条用例用 */ } ctx_t; /* 事务正文:读一次母线电压。**只调 hal_i2c_\***,通道是上层选好的。 */ static hal_err_t cb_read_mv(void *arg) { ctx_t *c = (ctx_t *)arg; c->calls++; return ina226_read_bus_mv(c->addr, &c->mv); } /* 在事务里调 `i2cbus_recover()` —— 它自己要拿锁、还要睡 1 ms。 */ static hal_err_t cb_recover_inside(void *arg) { ctx_t *c = (ctx_t *)arg; c->calls++; c->inner = i2cbus_recover(); return HAL_OK; } /* 事务进行中,**另一个任务**也来要总线(sim 的假任务号 1 扮演执行器)。 */ static hal_err_t cb_other_task(void *arg) { ctx_t *c = (ctx_t *)arg; c->calls++; static ctx_t inner_ctx; inner_ctx.addr = BLADE_INA; sim_lock_set_task(1); c->inner = i2cbus_xact(1, 5, cb_read_mv, &inner_ctx); sim_lock_set_task(0); return HAL_OK; } /* 在事务里再调一次 `i2cbus_xact()` —— 头文件末尾那条警告。 */ static hal_err_t cb_nested(void *arg) { ctx_t *c = (ctx_t *)arg; c->calls++; static ctx_t inner_ctx; inner_ctx.addr = BLADE_INA; c->inner = i2cbus_xact(1, 10, cb_read_mv, &inner_ctx); return HAL_OK; } static void mount(void) { TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(NULL)); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_init()); sim_i2c_add_mux(TCA9548A_ADDR, BOARD_PIN_MUX_RST_N); sim_i2c_add(TRUNK_INA, 2); sim_i2c_set_reg(TRUNK_INA, INA226_REG_BUS_V, TRUNK_RAW); for (int ch = 0; ch < N_BLADE; ch++) { sim_i2c_add_ch(ch, BLADE_INA, 2); sim_i2c_set_reg_ch(ch, BLADE_INA, INA226_REG_BUS_V, k_raw[ch]); } } static void mount_ready(void) { mount(); TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_init()); } /* 读一路,返回码必须是 HAL_OK。 */ static int32_t read_blade(int ch) { ctx_t c = { BLADE_INA, 0, -1, HAL_OK }; TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_xact(ch, 10, cb_read_mv, &c)); TEST_ASSERT_EQUAL_UINT32(1, c.calls); return c.mv; } void setUp(void) { sim_hal_reset(); } /* ⭐ 全文件恒为 0:同一地址同时看得见两片 = 通道开重了。 真板上那是两片 INA226 一起应答、读回来是线与 —— 数值正常、还很稳, 除了这个计数没有任何东西会告诉你。 */ void tearDown(void) { TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous()); } /* ---- init ---------------------------------------------------------------- */ /* init 之后:`MUX_RST#` 是高(不复位)、通道全关、影子 = 主干。 ⚠️ `sim_i2c_mux_resets()` 这时是 **1**,不是 0:`hal_gpio_config()` 配成输出的 那一刻脚上是 0(ESP-IDF 也是这个行为),紧接着才被写成 1。 这个窗口只有几条指令、此刻总线上还没有人,是 i2cbus.c 里那段注释认下的代价。 后面所有涉及复位次数的断言都从这个基线上算增量。 */ static void test_init_leaves_the_mux_closed_and_out_of_reset(void) { mount(); TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_init()); TEST_ASSERT_EQUAL_INT(I2CBUS_CH_TRUNK, i2cbus_channel()); TEST_ASSERT_EQUAL_HEX8(0x00, sim_i2c_mux_mask()); TEST_ASSERT_EQUAL_INT(-1, sim_i2c_mux_ch()); TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(BOARD_PIN_MUX_RST_N)); TEST_ASSERT_EQUAL_UINT32(1, sim_i2c_mux_resets()); /* 见上面那段注释 */ TEST_ASSERT_EQUAL_UINT32(0, i2cbus_selects()); TEST_ASSERT_EQUAL_UINT32(0, i2cbus_select_fails()); TEST_ASSERT_EQUAL_UINT32(0, i2cbus_select_fail_streak()); TEST_ASSERT_EQUAL_UINT32(0, i2cbus_recoveries()); TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C)); /* 认件那一段的锁放回去了 */ } /* mux 没认出来照样要 init 完(计划 §5.8:那是运行期降级,不是固件写错)。 ⚠️ 关键在于**影子必须是 UNKNOWN 而不是主干** —— 认件都失败了, 凭什么相信器件里的通道是全关的。写成 TRUNK 的话, 插着的第一笔主干事务会跳过选通,发到一个状态不明的 mux 后面。 */ static void test_init_survives_a_missing_mux_but_trusts_nothing(void) { mount(); sim_i2c_set_present(TCA9548A_ADDR, false); TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, i2cbus_init()); TEST_ASSERT_EQUAL_INT(I2CBUS_CH_UNKNOWN, i2cbus_channel()); /* 仍然可用:事务会去试选通道,失败了如实往上报。 */ ctx_t c = { BLADE_INA, 0, -1, HAL_OK }; TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, i2cbus_xact(0, 10, cb_read_mv, &c)); TEST_ASSERT_EQUAL_UINT32(0, c.calls); /* 选通道没成,正文一次都不许跑 */ } /* ---- ⭐ 本文件的主项:选谁读谁 ------------------------------------------- */ /* ⭐⭐ 验收主项。五片刀片同址 0x40,`i2cbus_xact(ch, ...)` 必须选谁读谁。 这条挂掉 = R2 的整条监控链在报别人的数据。 */ static void test_xact_reads_the_blade_you_asked_for(void) { mount_ready(); for (int ch = 0; ch < N_BLADE; ch++) { TEST_ASSERT_EQUAL_INT32(k_mv[ch], read_blade(ch)); TEST_ASSERT_EQUAL_INT(ch, sim_i2c_mux_ch()); /* 线上真的切过去了 */ TEST_ASSERT_EQUAL_INT(ch, i2cbus_channel()); } /* 倒着再来一遍:证明上面不是「碰巧按顺序自增」。 */ for (int ch = N_BLADE - 1; ch >= 0; ch--) { TEST_ASSERT_EQUAL_INT32(k_mv[ch], read_blade(ch)); } } /* ⭐⭐ 反证:**绕过 `i2cbus` 直接读会读到另一台手机的数据,而且一切正常。** 这是 R2 固件最危险的错误形态,也是 §4.3 那条「应用层不许直接调 hal_i2c_*」 规矩的全部理由。剧本就是最自然的那一种写错: 1. 巡检刚读完 CH2(通道 1),mux 停在通道 1; 2. 有人写了 `ina226_read_bus_mv(0x40, &mv)` —— 他心里想的是 CH1, 毕竟 R1 时代这么写了一年,0x40 就是「那片 INA226」; 3. 返回 HAL_OK,拿到 4200 mV。 4200 mV 是一个**完全正常**的电池电压:在量程内、在告警阈值内、 会随负载变化、多读几次还很稳。没有任何一层会报错: · HAL 不会 —— 器件应答了; · 驱动不会 —— 寄存器号对、转换公式对; · 安全判据不会 —— 3.5~4.2 V 是合法区间; · `sim_i2c_ambiguous()` 也不会 —— 只有一片在应答,它就是"对"的那一片。 唯一能发现这件事的办法,是像下面这样把两片的值预置成不同的数去对。 ⚠️ 这条用例**证明的是错误存在且沉默**,不是在验收某个正确行为 —— 所以它断言的是「读到的是 CH2 的值,不是 CH1 的值」。 哪天它变红了,说明有人给底层加了通道校验,那是好事,改这条用例即可。 */ static void test_bypassing_i2cbus_silently_reads_the_wrong_phone(void) { mount_ready(); /* 1. 巡检读了 CH2(通道 1)。 */ TEST_ASSERT_EQUAL_INT32(k_mv[1], read_blade(1)); TEST_ASSERT_EQUAL_INT(1, sim_i2c_mux_ch()); /* 2~3. 有人以为自己在读 CH1(通道 0),直接调驱动。 */ int32_t mv = -1; const hal_err_t err = ina226_read_bus_mv(BLADE_INA, &mv); TEST_ASSERT_EQUAL_INT(HAL_OK, err); /* ★ 不报错 */ TEST_ASSERT_EQUAL_INT32(k_mv[1], mv); /* ★ 拿到的是 CH2 的 4200 mV */ TEST_ASSERT_NOT_EQUAL(k_mv[0], mv); /* ★ 不是他以为的 CH1 */ TEST_ASSERT_TRUE(mv >= 3500 && mv <= 4200); /* ★ 而且落在完全合法的区间里 */ /* 同一件事换个方向说:通道停在哪,0x40 就是哪一片。 */ TEST_ASSERT_EQUAL_INT32(k_mv[3], read_blade(3)); TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_read_bus_mv(BLADE_INA, &mv)); TEST_ASSERT_EQUAL_INT32(k_mv[3], mv); } /* 主干也是一个「通道」:全关的时候 0x43 看得见、0x40 一片都看不见。 写成「主干 = 不选通道」(也就是保持上一次的通道)的话, 读输入侧 0x43 会在某片刀片还挂着的支路上进行 —— 0x43 在主干上依然应答, 所以**照样不报错**,只是这时候整条总线上同时挂着一片 0x40, 离「通道开重了」只差一个人手滑。 */ static void test_trunk_is_a_channel_too(void) { mount_ready(); TEST_ASSERT_EQUAL_INT32(k_mv[2], read_blade(2)); /* 先把通道支到 2 */ ctx_t c = { TRUNK_INA, 0, -1, HAL_OK }; TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_xact(I2CBUS_CH_TRUNK, 10, cb_read_mv, &c)); TEST_ASSERT_EQUAL_INT32(TRUNK_MV, c.mv); TEST_ASSERT_EQUAL_HEX8(0x00, sim_i2c_mux_mask()); /* ★ 支路真的全断开了 */ TEST_ASSERT_EQUAL_INT(I2CBUS_CH_TRUNK, i2cbus_channel()); /* 这时候任何一片刀片的 0x40 都该是不应答的。 */ int32_t mv = -1; TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, ina226_read_bus_mv(BLADE_INA, &mv)); } /* ---- 影子:省事务,但省出来的不变量要守住 -------------------------------- */ /* 同一个通道连着两次事务,第二次不重发选通字节。 §5.5 的账就靠这个:t_safety 每 20 ms 一圈五路,无条件重发一圈多 5 次事务。 */ static void test_a_second_xact_on_the_same_channel_skips_the_select(void) { mount_ready(); sim_i2c_dev_t *mux = sim_i2c_dev(TCA9548A_ADDR); TEST_ASSERT_NOT_NULL(mux); TEST_ASSERT_EQUAL_INT32(k_mv[2], read_blade(2)); TEST_ASSERT_EQUAL_UINT32(1, i2cbus_selects()); const uint32_t n_write = mux->n_write; TEST_ASSERT_EQUAL_INT32(k_mv[2], read_blade(2)); TEST_ASSERT_EQUAL_UINT32(1, i2cbus_selects()); /* ★ 没有 +1 */ TEST_ASSERT_EQUAL_UINT32(n_write, mux->n_write); /* ★ mux 上一个字节都没多收 */ /* 换一路就必须重发。 */ TEST_ASSERT_EQUAL_INT32(k_mv[3], read_blade(3)); TEST_ASSERT_EQUAL_UINT32(2, i2cbus_selects()); } /* ⭐⭐ 选通失败必须让影子作废。 剧本: 1. 读 CH1(通道 0)成功,影子 = 0; 2. 去读 CH2(通道 1),选通字节被 NAK 掉 —— 失败的写**不改变器件状态**, mux 还停在通道 0(这条事实在 test_tca9548a.c 里单独钉过); 3. 现实里紧接着发生什么是**不知道**的:mux 可能自己复位了、 可能那一笔写其实落进去了只是 ACK 丢了、也可能另一次毛刺把它挪到了别处。 这里用一次裸写把它挪到通道 1 来代表「不知道」的一个具体实例; 4. 再去读 CH1(通道 0)。 影子作废了:第 4 步重发选通字节 → 读回 CH1 的 3750 mV。 影子没作废(把 `s_ch = I2CBUS_CH_UNKNOWN` 那一行删掉):影子还是 0、命中、 **跳过选通** → 事务发到通道 1 上 → 读回 4200 mV,返回 HAL_OK。 那正是本文件开头那类「读到另一台手机」的错,只不过是固件自己制造的。 */ static void test_a_failed_select_invalidates_the_shadow(void) { mount_ready(); /* 1 */ TEST_ASSERT_EQUAL_INT32(k_mv[0], read_blade(0)); TEST_ASSERT_EQUAL_UINT32(1, i2cbus_selects()); /* 2 */ sim_i2c_inject_nak(TCA9548A_ADDR, 1); ctx_t c = { BLADE_INA, 0, -1, HAL_OK }; TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, i2cbus_xact(1, 10, cb_read_mv, &c)); TEST_ASSERT_EQUAL_UINT32(0, c.calls); /* 选通没成,正文一次都不许跑 */ TEST_ASSERT_EQUAL_UINT32(2, i2cbus_selects()); TEST_ASSERT_EQUAL_UINT32(1, i2cbus_select_fails()); TEST_ASSERT_EQUAL_INT(I2CBUS_CH_UNKNOWN, i2cbus_channel()); TEST_ASSERT_EQUAL_HEX8(0x01, sim_i2c_mux_mask()); /* 器件没被动过,还在通道 0 */ /* 3:代表「失败之后通道到底在哪,谁也不知道」。 */ const uint8_t elsewhere = 0x02; TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write(TCA9548A_ADDR, &elsewhere, 1)); TEST_ASSERT_EQUAL_INT(1, sim_i2c_mux_ch()); /* 4:必须重发选通,读回来的必须是 CH1 自己的值。 */ TEST_ASSERT_EQUAL_INT32(k_mv[0], read_blade(0)); TEST_ASSERT_EQUAL_UINT32(3, i2cbus_selects()); /* ★ 又 +1 = 真的重发了 */ TEST_ASSERT_EQUAL_INT(0, sim_i2c_mux_ch()); } /* 选通失败时正文一次都不许跑 —— 换个角度再钉一次: 跑了的话事务会落在**上一次**那条支路上,而那条支路上的器件会照常应答。 这也是 `xact` 的返回码契约:「选通道的错误码 —— fn 一次都没被调」。 */ static void test_the_body_never_runs_on_a_channel_that_was_not_selected(void) { mount_ready(); TEST_ASSERT_EQUAL_INT32(k_mv[4], read_blade(4)); sim_i2c_inject_nak(TCA9548A_ADDR, 1); ctx_t c = { BLADE_INA, 0, -12345, HAL_OK }; TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, i2cbus_xact(2, 10, cb_read_mv, &c)); TEST_ASSERT_EQUAL_UINT32(0, c.calls); TEST_ASSERT_EQUAL_INT32(-12345, c.mv); /* 调用方的值一个字节没动 */ } /* ---- 连续失败计数:§5.8「mux 失联」的判据读的就是它 ---------------------- */ /* ⭐ **连续**失败,成功一次就清零。 写成累计量的后果:一次总线毛刺的账会一直留着,板子跑上几天几夜, `select_fails` 迟早爬过阈值 —— 于是某个凌晨五台手机一起断电, 而那一刻总线其实是好的。这种误停机在实验室里永远复现不出来。 */ static void test_fail_streak_is_consecutive_and_one_success_clears_it(void) { mount_ready(); sim_i2c_inject_nak(TCA9548A_ADDR, 3); ctx_t c = { BLADE_INA, 0, -1, HAL_OK }; for (uint32_t i = 1; i <= 3; i++) { TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, i2cbus_xact(0, 10, cb_read_mv, &c)); TEST_ASSERT_EQUAL_UINT32(i, i2cbus_select_fail_streak()); TEST_ASSERT_EQUAL_UINT32(i, i2cbus_select_fails()); } /* 第 4 次总线好了。 */ TEST_ASSERT_EQUAL_INT32(k_mv[0], read_blade(0)); TEST_ASSERT_EQUAL_UINT32(0, i2cbus_select_fail_streak()); /* ★ 清零 */ TEST_ASSERT_EQUAL_UINT32(3, i2cbus_select_fails()); /* 累计量不清零 */ /* 再来一次失败:从 1 开始重新数,不是从 4。 */ sim_i2c_inject_nak(TCA9548A_ADDR, 1); TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, i2cbus_xact(1, 10, cb_read_mv, &c)); TEST_ASSERT_EQUAL_UINT32(1, i2cbus_select_fail_streak()); TEST_ASSERT_EQUAL_UINT32(4, i2cbus_select_fails()); } /* 影子命中的那些事务**不算**成功的选通,所以也不该去清 streak —— 但它们同样不该去累加。这条守的是「反复读同一路时 streak 冻在原地」: 写成「每次 xact 都清零」的话,mux 失联判据永远等不到阈值。 */ static void test_a_shadow_hit_neither_counts_nor_clears(void) { mount_ready(); TEST_ASSERT_EQUAL_INT32(k_mv[0], read_blade(0)); /* 成功一次,streak = 0 */ sim_i2c_inject_nak(TCA9548A_ADDR, 1); ctx_t c = { BLADE_INA, 0, -1, HAL_OK }; TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, i2cbus_xact(1, 10, cb_read_mv, &c)); TEST_ASSERT_EQUAL_UINT32(1, i2cbus_select_fail_streak()); /* 失败之后影子是 UNKNOWN,所以下一次同通道**不会**命中; 这里先成功一次把影子立起来,再连读两次看 streak 与 selects 都不动。 */ TEST_ASSERT_EQUAL_INT32(k_mv[1], read_blade(1)); const uint32_t sel = i2cbus_selects(); TEST_ASSERT_EQUAL_UINT32(0, i2cbus_select_fail_streak()); TEST_ASSERT_EQUAL_INT32(k_mv[1], read_blade(1)); TEST_ASSERT_EQUAL_INT32(k_mv[1], read_blade(1)); TEST_ASSERT_EQUAL_UINT32(sel, i2cbus_selects()); TEST_ASSERT_EQUAL_UINT32(0, i2cbus_select_fail_streak()); } /* ---- 复位 ---------------------------------------------------------------- */ /* ⭐ `i2cbus_recover()`:`MUX_RST#` 真的被拉低过,之后影子作废、通道全关。 只断言 `i2cbus_channel() == UNKNOWN` 是不够的 —— 那只是自己说自己对。 这里同时断言假 mux 的 `resets` +1(脚真的低过)和掩码归 0(器件真的被复位了)。 不作废影子的后果:复位之后器件里的通道是**全关**的,影子却还记着上一路, 下一次事务跳过选通,落在全关的总线上 —— 现象是「五片刀片一起失联」, 而且 `i2cbus_recover()` 本来就是为了抢救失联才调的,会变成越救越死。 */ static void test_recover_pulses_mux_rst_and_forgets_the_channel(void) { mount_ready(); TEST_ASSERT_EQUAL_INT32(k_mv[3], read_blade(3)); TEST_ASSERT_EQUAL_HEX8(0x08, sim_i2c_mux_mask()); const uint32_t resets = sim_i2c_mux_resets(); const uint32_t writes = sim_gpio_writes(BOARD_PIN_MUX_RST_N); TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_recover()); TEST_ASSERT_EQUAL_UINT32(resets + 1, sim_i2c_mux_resets()); /* ★ 脚真低过 */ TEST_ASSERT_EQUAL_UINT32(writes + 2, sim_gpio_writes(BOARD_PIN_MUX_RST_N)); /* 低再高 */ TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(BOARD_PIN_MUX_RST_N)); /* 收尾必须放开 */ TEST_ASSERT_EQUAL_HEX8(0x00, sim_i2c_mux_mask()); /* ★ 通道全关 */ TEST_ASSERT_EQUAL_INT(I2CBUS_CH_UNKNOWN, i2cbus_channel()); /* ★ 影子作废 */ TEST_ASSERT_EQUAL_UINT32(1, i2cbus_recoveries()); /* 复位之后不重新选通的话,刀片就是全部失联 —— 这就是影子必须作废的代价证明。 */ int32_t mv = -1; TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, ina226_read_bus_mv(BLADE_INA, &mv)); /* 而经 i2cbus 走就自动恢复:重发选通、读回本路自己的值。 */ const uint32_t sel = i2cbus_selects(); TEST_ASSERT_EQUAL_INT32(k_mv[3], read_blade(3)); TEST_ASSERT_EQUAL_UINT32(sel + 1, i2cbus_selects()); } /* recover 自己要拿锁、而且中间要睡 1 ms,所以**不许在事务的回调里调** —— 直接判 HAL_ERR_STATE。不拦的话它会持着总线锁去 `hal_time_delay_ms(1)`, 把巡检整个钉住;而且那 1 ms 里 `MUX_RST#` 是低的, 外层事务回来之后会在一个刚被复位(通道全关)的 mux 上继续跑。 */ static void test_recover_is_rejected_inside_a_transaction(void) { mount_ready(); TEST_ASSERT_EQUAL_INT32(k_mv[0], read_blade(0)); const uint32_t resets = sim_i2c_mux_resets(); ctx_t c = { BLADE_INA, 0, -1, HAL_OK }; TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_xact(0, 10, cb_recover_inside, &c)); TEST_ASSERT_EQUAL_UINT32(1, c.calls); TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, c.inner); /* ★ 被挡下来了 */ TEST_ASSERT_EQUAL_UINT32(resets, sim_i2c_mux_resets()); /* ★ 脚一次都没低过 */ TEST_ASSERT_EQUAL_UINT32(0, i2cbus_recoveries()); TEST_ASSERT_EQUAL_HEX8(0x01, sim_i2c_mux_mask()); /* 通道也没被关掉 */ /* 事务外面调是合法的。 */ TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_recover()); TEST_ASSERT_EQUAL_UINT32(resets + 1, sim_i2c_mux_resets()); } /* ---- 两条「别把自己锁死」 ------------------------------------------------ */ /* ⭐ 嵌套调用返回 HAL_ERR_STATE,而且**不死锁**:外层事务照常跑完、锁照常放回去。 不拦的话会在同一个任务里第二次 take 同一把锁 —— sim 上是 HAL_ERR_STATE, FreeRTOS 上(非递归的 mutex)就是那个任务当场停住, 现象是 `t_safety` 整个不跑了,而看门狗之外没有任何日志。 */ /* ⭐ 0.1.11:别的任务正持着锁 ≠ 嵌套。它该**等锁**(超时回 TIMEOUT),而不是回 STATE。 原来的全局标志把两者混为一谈,执行器撞上巡检就回 STATE ⇒ `key` 随机 busy、`vbus` 随机 io。 */ static void test_another_tasks_xact_waits_instead_of_looking_nested(void) { mount_ready(); ctx_t c = { BLADE_INA, 0, -1, HAL_OK }; TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_xact(0, 10, cb_other_task, &c)); TEST_ASSERT_EQUAL_UINT32(1, c.calls); TEST_ASSERT_EQUAL_INT(HAL_ERR_TIMEOUT, c.inner); TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C)); /* 锁放了之后,那个任务再来就拿得到。 */ sim_lock_set_task(1); ctx_t d = { BLADE_INA, 0, -1, HAL_OK }; TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_xact(1, 5, cb_read_mv, &d)); sim_lock_set_task(0); } static void test_a_nested_xact_is_rejected_and_does_not_deadlock(void) { mount_ready(); ctx_t c = { BLADE_INA, 0, -1, HAL_OK }; const uint32_t sel = i2cbus_selects(); TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_xact(0, 10, cb_nested, &c)); TEST_ASSERT_EQUAL_UINT32(1, c.calls); TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, c.inner); /* ★ 内层被挡在拿锁之前 */ TEST_ASSERT_EQUAL_UINT32(sel + 1, i2cbus_selects()); /* 内层一个字节都没发 */ /* 锁真的放回去了:拿得到就说明没漏。 */ TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C)); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10)); hal_lock_give(HAL_LOCK_I2C); /* 外层结束之后总线恢复正常,不是一次性坏掉。 */ TEST_ASSERT_EQUAL_INT32(k_mv[2], read_blade(2)); } /* ⭐ 拿不到锁时返回 HAL_ERR_TIMEOUT,而且**什么也没做**。 「什么也没做」是调用方(t_safety)敢直接跳过这一周期的前提: 要是这里已经把通道切过去了、或者把影子改了,跳过这一周期之后 下一个拿到锁的人会在一个被人改过的通道上开工 —— 又是一次静默的读错片。 */ static void test_a_lock_timeout_changes_nothing_at_all(void) { mount_ready(); sim_i2c_dev_t *mux = sim_i2c_dev(TCA9548A_ADDR); TEST_ASSERT_NOT_NULL(mux); TEST_ASSERT_EQUAL_INT32(k_mv[1], read_blade(1)); /* 通道停在 1 */ const uint32_t sel = i2cbus_selects(); const uint32_t nw = mux->n_write; const uint8_t mask = sim_i2c_mux_mask(); const int shadow = i2cbus_channel(); /* 1 号任务持着 I²C 锁,0 号任务来跑一次事务。 */ sim_lock_set_task(1); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10)); sim_lock_set_task(0); ctx_t c = { BLADE_INA, 0, -777, HAL_OK }; TEST_ASSERT_EQUAL_INT(HAL_ERR_TIMEOUT, i2cbus_xact(3, 5, cb_read_mv, &c)); TEST_ASSERT_EQUAL_UINT32(0, c.calls); /* ★ 正文没跑 */ TEST_ASSERT_EQUAL_INT32(-777, c.mv); /* ★ 调用方的值没动 */ TEST_ASSERT_EQUAL_UINT32(sel, i2cbus_selects()); /* ★ 没发选通 */ TEST_ASSERT_EQUAL_UINT32(nw, mux->n_write); /* ★ mux 一个字节没收 */ TEST_ASSERT_EQUAL_HEX8(mask, sim_i2c_mux_mask()); /* ★ 通道没被动 */ TEST_ASSERT_EQUAL_INT(shadow, i2cbus_channel()); /* ★ 影子没被动 */ TEST_ASSERT_EQUAL_UINT32(1, i2cbus_lock_timeouts()); TEST_ASSERT_EQUAL_UINT32(0, i2cbus_select_fail_streak()); /* 等锁失败 ≠ 选通失败 */ /* 1 号放掉之后一切照旧。 */ sim_lock_set_task(1); hal_lock_give(HAL_LOCK_I2C); sim_lock_set_task(0); TEST_ASSERT_EQUAL_INT32(k_mv[3], read_blade(3)); } /* ---- 参数与状态 ---------------------------------------------------------- */ /* 没 init 就调 → HAL_ERR_STATE,而且一个字节都不许发。 `hal_gpio_config()` 还没跑过,这时候碰总线等于在 `MUX_RST#` 悬空的情况下开工。 ⚠️ **这一条必须是 main() 里的第一条**:`i2cbus` 没有 deinit 接口, `s_inited` 是文件级 static,`sim_hal_reset()` 管不着它 —— 一旦前面有任何一条跑过 `i2cbus_init()`,「没 init」这个状态就再也造不出来了。 (真板上也一样:init 一次之后就没有回头路,所以这不是设计缺陷。) */ static void test_xact_before_init_is_a_state_error(void) { mount(); /* 挂好器件,但**不** i2cbus_init() */ sim_i2c_dev_t *mux = sim_i2c_dev(TCA9548A_ADDR); TEST_ASSERT_NOT_NULL(mux); ctx_t c = { BLADE_INA, 0, -1, HAL_OK }; TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, i2cbus_xact(0, 10, cb_read_mv, &c)); TEST_ASSERT_EQUAL_UINT32(0, c.calls); TEST_ASSERT_EQUAL_UINT32(0, mux->n_write); TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, i2cbus_recover()); } /* 通道号越界要在拿锁之前挡掉。 ⚠️ 特别是 `I2CBUS_CH_UNKNOWN`(-2):它是一个**内部状态**,不是一个能传进来的通道。 放过去的话 `1u << -2` 是未定义行为,而 x86 上多半"正常"跑出一个值来。 */ static void test_xact_rejects_bad_arguments_before_taking_the_lock(void) { mount_ready(); sim_i2c_dev_t *mux = sim_i2c_dev(TCA9548A_ADDR); TEST_ASSERT_NOT_NULL(mux); const uint32_t nw = mux->n_write; ctx_t c = { BLADE_INA, 0, -1, HAL_OK }; TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, i2cbus_xact(I2CBUS_CH_UNKNOWN, 10, cb_read_mv, &c)); TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, i2cbus_xact(-3, 10, cb_read_mv, &c)); TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, i2cbus_xact(TCA9548A_CH_COUNT, 10, cb_read_mv, &c)); TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, i2cbus_xact(0, 10, NULL, &c)); TEST_ASSERT_EQUAL_UINT32(0, c.calls); TEST_ASSERT_EQUAL_UINT32(nw, mux->n_write); TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C)); TEST_ASSERT_EQUAL_UINT32(0, i2cbus_lock_timeouts()); } /* `i2cbus_forget_channel()` 只是作废影子,**不碰器件** —— 它是给 recover 和测试用的。作废之后下一次事务必须重发选通。 */ static void test_forget_channel_only_drops_the_shadow(void) { mount_ready(); TEST_ASSERT_EQUAL_INT32(k_mv[2], read_blade(2)); TEST_ASSERT_EQUAL_UINT32(1, i2cbus_selects()); i2cbus_forget_channel(); TEST_ASSERT_EQUAL_INT(I2CBUS_CH_UNKNOWN, i2cbus_channel()); TEST_ASSERT_EQUAL_HEX8(0x04, sim_i2c_mux_mask()); /* 器件没被动 */ TEST_ASSERT_EQUAL_INT32(k_mv[2], read_blade(2)); TEST_ASSERT_EQUAL_UINT32(2, i2cbus_selects()); /* 重发了 */ } /* 巡检的形状:五路各一次 xact,来回跑 20 圈。 每一圈每一路读回来的都必须是自己那一片,而且**通道永远只开一路**。 这一条是前面所有断言的"耐久版"——单次对不代表跑久了还对 (影子写成 static 局部、计数溢出、掩码累加而不是覆盖,都要跑几圈才现形)。 */ static void test_a_full_sweep_stays_correct_for_many_rounds(void) { mount_ready(); for (int round = 0; round < 20; round++) { for (int ch = 0; ch < N_BLADE; ch++) { TEST_ASSERT_EQUAL_INT32(k_mv[ch], read_blade(ch)); TEST_ASSERT_EQUAL_INT(ch, sim_i2c_mux_ch()); /* 恰好一路,不是 -2 */ } } /* 一圈五次选通,20 圈 = 100 次;没有多也没有少。 */ TEST_ASSERT_EQUAL_UINT32(100, i2cbus_selects()); TEST_ASSERT_EQUAL_UINT32(0, i2cbus_select_fails()); TEST_ASSERT_EQUAL_UINT32(0, i2cbus_lock_timeouts()); } int main(void) { UNITY_BEGIN(); /* ⚠️ 这一条必须排在最前面,理由见它自己的注释(i2cbus 没有 deinit)。 */ RUN_TEST(test_xact_before_init_is_a_state_error); RUN_TEST(test_init_leaves_the_mux_closed_and_out_of_reset); RUN_TEST(test_init_survives_a_missing_mux_but_trusts_nothing); RUN_TEST(test_xact_reads_the_blade_you_asked_for); RUN_TEST(test_bypassing_i2cbus_silently_reads_the_wrong_phone); RUN_TEST(test_trunk_is_a_channel_too); RUN_TEST(test_a_second_xact_on_the_same_channel_skips_the_select); RUN_TEST(test_a_failed_select_invalidates_the_shadow); RUN_TEST(test_the_body_never_runs_on_a_channel_that_was_not_selected); RUN_TEST(test_fail_streak_is_consecutive_and_one_success_clears_it); RUN_TEST(test_a_shadow_hit_neither_counts_nor_clears); RUN_TEST(test_recover_pulses_mux_rst_and_forgets_the_channel); RUN_TEST(test_recover_is_rejected_inside_a_transaction); RUN_TEST(test_a_nested_xact_is_rejected_and_does_not_deadlock); RUN_TEST(test_another_tasks_xact_waits_instead_of_looking_nested); RUN_TEST(test_a_lock_timeout_changes_nothing_at_all); RUN_TEST(test_xact_rejects_bad_arguments_before_taking_the_lock); RUN_TEST(test_forget_channel_only_drops_the_shadow); RUN_TEST(test_a_full_sweep_stays_correct_for_many_rounds); return UNITY_END(); }