/* R-0:TCA9548A 驱动(底板 U35,0x70)。 验收主项:**选通道发出去的是一个裸字节,不是「寄存器号 + 值」。** 按后者写,通道会被选成「寄存器号」那个字节的值,而且从 HAL 到驱动到应用 一路全是 HAL_OK —— 板子上的现象是「读到的电压电流全都正常,就是不对号」。 第二主项:控制字是**掩码**不是编号(同时开两路 = 两片同址 INA226 一起应答, 读回来的是线与,数值正常还很稳),以及 `probe()` 要能把 「0x70 上蹲着别的东西」和「0x70 根本不应答」分成两种错误码。 ⚠️ 本文件只测驱动,不挂 `MUX_RST#`(`rst_pin` 传 -1)—— 驱动本来就不该碰 GPIO,复位那条路径是 `i2cbus` 的事,在 test_i2cbus.c 里测。 */ #include "tca9548a.h" #include "hal_i2c.h" #include "sim_hal.h" #include "unity.h" /* 主干上的输入侧 INA226(0x43)——「0x70 不与主干撞车」那条要有个真东西做对照。 */ #define TRUNK_INA 0x43 /* 刀片支路上的 INA226。五片同址,正是 D8 的设计。 */ #define BLADE_INA 0x40 void setUp(void) { sim_hal_reset(); } /* 同一地址在当前可见范围里出现不止一次 = 通道开重了。 除了那条**故意**把通道开重的用例,全文件恒为 0。 */ void tearDown(void) {} /* 假板子:一颗 mux + 主干一片 0x43 + 通道 0 / 通道 1 各一片 0x40。 */ static sim_i2c_dev_t *mount(void) { TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(NULL)); sim_i2c_add_mux(TCA9548A_ADDR, -1); sim_i2c_add(TRUNK_INA, 2); sim_i2c_add_ch(0, BLADE_INA, 2); sim_i2c_add_ch(1, BLADE_INA, 2); sim_i2c_dev_t *mux = sim_i2c_dev(TCA9548A_ADDR); TEST_ASSERT_NOT_NULL(mux); return mux; } static int popcount8(unsigned v) { int n = 0; while (v != 0u) { n += (int)(v & 1u); v >>= 1; } return n; } /* ---- 验收主项一:一个裸字节 ---------------------------------------------- */ /* ★ 这一条就是文件头第 1 条的全部内容:线上必须**只有一个字节**, 而且那个字节就是掩码本身。 写错了会怎样:见下面 test_the_reg_plus_value_spelling_is_silently_wrong。 为什么功能测试看不出来:写法错了以后照样「选到了某个通道」, 五片刀片的读数全都在量程内、也会随负载变化,只是对应错了槽位。 */ static void test_select_puts_exactly_one_byte_on_the_wire(void) { sim_i2c_dev_t *mux = mount(); TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_select(TCA9548A_ADDR, 3)); TEST_ASSERT_EQUAL_UINT32(1, mux->n_write); TEST_ASSERT_EQUAL_UINT8(1, mux->last_write_len); /* ★ 不是 2 */ TEST_ASSERT_EQUAL_HEX8(0x08, mux->last_write[0]); /* ★ 掩码本身,不是通道号 3 */ TEST_ASSERT_EQUAL_HEX8(0x08, sim_i2c_mux_mask()); TEST_ASSERT_EQUAL_INT(3, sim_i2c_mux_ch()); TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous()); } /* ★★ 反证:按「寄存器号 + 值」写会怎样。 这是最像笔误、也最容易在 code review 里被放过去的一种写法 —— 绝大多数 I²C 器件就是这么写的。 结果:器件只看第一个字节,于是「选通道 3」变成了掩码 0x03 = **通道 0 和 1 同时开**, HAL 返回 HAL_OK,驱动返回 HAL_OK,上层拿到一个完全正常的读数。 真板上这时两片 0x40 一起应答,SDA 上是两片的线与:数值在量程内、还很稳定 —— 功能测试、甚至示波器看波形都发现不了。 */ static void test_the_reg_plus_value_spelling_is_silently_wrong(void) { mount(); const uint8_t wr[2] = { 0x03, 0x08 }; /* 「写寄存器 3,值 = 通道 3 的掩码」 */ TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write(TCA9548A_ADDR, wr, sizeof(wr))); /* ★ 不报错 */ TEST_ASSERT_EQUAL_HEX8(0x03, sim_i2c_mux_mask()); /* 想开 3,实际开了 0 和 1 */ TEST_ASSERT_EQUAL_INT(-2, sim_i2c_mux_ch()); /* 多于一路 */ /* 后果:0x40 这个地址现在同时看得见两片。 */ TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous()); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(BLADE_INA)); /* ★ 照样应答 */ TEST_ASSERT_EQUAL_UINT32(1, sim_i2c_ambiguous()); /* 对照:驱动的写法一次只开一路,同样一次 probe 不会再增加 ambiguous。 */ TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_select(TCA9548A_ADDR, 3)); TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, hal_i2c_probe(BLADE_INA)); /* 通道 3 上什么都没有 */ TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_select(TCA9548A_ADDR, 1)); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(BLADE_INA)); TEST_ASSERT_EQUAL_UINT32(1, sim_i2c_ambiguous()); /* 没有新增 */ } /* ---- 验收主项二:掩码不是编号 -------------------------------------------- */ /* 八个通道逐个对:掩码必须是 1<last_write_len); TEST_ASSERT_EQUAL_HEX8((uint8_t)(1u << ch), sim_i2c_mux_mask()); TEST_ASSERT_EQUAL_INT(ch, sim_i2c_mux_ch()); TEST_ASSERT_EQUAL_INT(1, popcount8(sim_i2c_mux_mask())); /* ★ 永远只开一路 */ } TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous()); } /* 全关 = 掩码 0:主干可见、所有支路断开。`i2cbus` 的「主干」就是这个状态。 */ static void test_none_closes_every_branch(void) { mount(); TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_select(TCA9548A_ADDR, 0)); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(BLADE_INA)); TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_select(TCA9548A_ADDR, TCA9548A_CH_NONE)); TEST_ASSERT_EQUAL_HEX8(0x00, sim_i2c_mux_mask()); TEST_ASSERT_EQUAL_INT(-1, sim_i2c_mux_ch()); TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, hal_i2c_probe(BLADE_INA)); /* 支路断了就是不应答 */ TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(TRUNK_INA)); /* 主干不受影响 */ } /* 越界的通道号必须在**发出任何字节之前**被挡掉。 不挡的话 `1u << 8` 溢出成 0 = 悄悄全关,现象是「五路一起失联」而不是报错。 */ static void test_out_of_range_channel_sends_nothing(void) { sim_i2c_dev_t *mux = mount(); TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_select(TCA9548A_ADDR, 2)); const uint32_t n = mux->n_write; TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, tca9548a_select(TCA9548A_ADDR, -2)); TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, tca9548a_select(TCA9548A_ADDR, TCA9548A_CH_COUNT)); TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, tca9548a_select(TCA9548A_ADDR, 99)); TEST_ASSERT_EQUAL_UINT32(n, mux->n_write); /* 一个字节都没发 */ TEST_ASSERT_EQUAL_HEX8(0x04, sim_i2c_mux_mask()); /* 通道也没被动过 */ } /* `mask_to_ch()` 的判据穷举:恰好一位 → 0~7;全 0 → NONE;**多于一位 → -2**。 -2 不是「另一种通道」,是「这块板上不该出现的状态」—— 把它当成合法返回值去用(比如拿去做数组下标)会越界。 */ static void test_mask_to_ch_is_exhaustive(void) { for (int m = 0; m <= 0xFF; m++) { const int got = tca9548a_mask_to_ch((uint8_t)m); if (m == 0) { TEST_ASSERT_EQUAL_INT(TCA9548A_CH_NONE, got); } else if (popcount8((unsigned)m) > 1) { TEST_ASSERT_EQUAL_INT(-2, got); } else { TEST_ASSERT_TRUE(got >= 0 && got < TCA9548A_CH_COUNT); TEST_ASSERT_EQUAL_HEX8((uint8_t)m, (uint8_t)(1u << got)); } } /* 几个最容易被写错的具体值 */ TEST_ASSERT_EQUAL_INT(0, tca9548a_mask_to_ch(0x01)); TEST_ASSERT_EQUAL_INT(7, tca9548a_mask_to_ch(0x80)); TEST_ASSERT_EQUAL_INT(-2, tca9548a_mask_to_ch(0x03)); /* 就是上面那个笔误的结果 */ TEST_ASSERT_EQUAL_INT(-2, tca9548a_mask_to_ch(0xFF)); TEST_ASSERT_EQUAL_INT(TCA9548A_CH_NONE, tca9548a_mask_to_ch(0x00)); } /* 选了就要能读回来 —— 读它同样是一个裸字节,不带寄存器指针。 这条是 `probe()` 的地基:写下去不报错但没生效(上拉太弱、支路被拉死) 只有靠读回来才认得出。 */ static void test_read_mask_reads_back_what_was_selected(void) { mount(); uint8_t m = 0xFF; TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_select(TCA9548A_ADDR, 4)); TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_read_mask(TCA9548A_ADDR, &m)); TEST_ASSERT_EQUAL_HEX8(0x10, m); TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_select(TCA9548A_ADDR, TCA9548A_CH_NONE)); TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_read_mask(TCA9548A_ADDR, &m)); TEST_ASSERT_EQUAL_HEX8(0x00, m); TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, tca9548a_read_mask(TCA9548A_ADDR, NULL)); } /* ---- 认件的三种结局 ------------------------------------------------------ */ /* 好器件:全关之后读回 0。 */ static void test_probe_accepts_a_fresh_mux(void) { mount(); TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_probe(TCA9548A_ADDR)); TEST_ASSERT_EQUAL_HEX8(0x00, sim_i2c_mux_mask()); /* probe 顺带把通道全关了 */ TEST_ASSERT_EQUAL_INT(-1, sim_i2c_mux_ch()); } /* ★ 地址上应答了、但读回来不是 0 → HAL_ERR_BUS,**不是** NAK。 两者要分开:NAK 是「没插 / 焊虚 / 地址错」,BUS 是「0x70 上蹲着另一颗器件, 或者写根本没生效」。后者在真板上的现象是「所有刀片都读不到」, 而这里能在启动自检那一刻就叫出来。 ⚠️ 这条用例里故意**不挂假 mux**(0x70 上换成一颗普通寄存器器件), 因为要模拟的正是「这不是一颗听话的 TCA9548A」。 */ static void test_probe_reports_bus_error_when_readback_is_not_zero(void) { TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(NULL)); sim_i2c_add(TCA9548A_ADDR, 1); /* 0x70 上的陌生人:会应答,但不听话 */ sim_i2c_set_reg(TCA9548A_ADDR, 0x00, 0x55); TEST_ASSERT_EQUAL_INT(HAL_ERR_BUS, tca9548a_probe(TCA9548A_ADDR)); } /* 器件不在(没焊、没插、地址错)→ NAK,而且是在**写**那一步就返回。 */ static void test_probe_naks_when_nobody_answers(void) { mount(); sim_i2c_set_present(TCA9548A_ADDR, false); TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, tca9548a_probe(TCA9548A_ADDR)); } /* 「全关」那一次写就 NAK 了,就不该再去读。 继续往下读的后果:读也 NAK,返回的错误码一样,但**总线上多了一次事务**; 更糟的是如果读侥幸成功(毛刺只打在写上),会拿一个陈旧的掩码判「认件通过」。 顺带钉住一件后面 i2cbus 要用到的事实:**失败的写不改变器件状态** —— 掩码还停在上一次选的那一路。固件这边却「以为」通道变了, 这正是 `i2cbus` 的通道影子必须在选通失败时作废的原因。 */ static void test_probe_stops_at_the_first_failure(void) { sim_i2c_dev_t *mux = mount(); TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_select(TCA9548A_ADDR, 2)); const uint32_t n_read = mux->n_read; sim_i2c_inject_nak(TCA9548A_ADDR, 1); /* 只吃掉「全关」那一次写 */ TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, tca9548a_probe(TCA9548A_ADDR)); TEST_ASSERT_EQUAL_UINT32(n_read, mux->n_read); /* 没有多发那次读 */ TEST_ASSERT_EQUAL_HEX8(0x04, sim_i2c_mux_mask()); /* 器件还停在通道 2 */ } /* ---- 地址表 -------------------------------------------------------------- */ /* 0x70 不与**主干上**的 0x43 / 0x4A / 0x4B 撞。 撞上的后果:选通道那一个字节会被写进 DAC63204 的寄存器里 —— 输出电压乱跳。 */ static void test_addr_does_not_collide_with_the_trunk(void) { TEST_ASSERT_TRUE(tca9548a_addr_ok()); TEST_ASSERT_NOT_EQUAL(0x43, TCA9548A_ADDR); /* 输入侧 INA226 */ TEST_ASSERT_NOT_EQUAL(0x4A, TCA9548A_ADDR); /* DAC63204 #1 */ TEST_ASSERT_NOT_EQUAL(0x4B, TCA9548A_ADDR); /* DAC63204 #2 */ TEST_ASSERT_TRUE(TCA9548A_ADDR <= 0x7f); } /* ★ 为什么 0x40 / 0x20 **不在**那张冲突表里 —— 这不是漏了,这是 D8 的全部意义。 它们在 mux 的支路上:主干上根本看不见,所以五片刀片可以完全同构 (每片都是 0x40 + 0x20、零 strap 电阻、五片可互换)。 把它们加进冲突表的后果比漏掉更糟:`addr_ok()` 会在一块好板子上返回 false, 启动自检直接把整机拦下来。 */ static void test_branch_addresses_are_absent_from_the_table_on_purpose(void) { mount(); /* 全关:主干上只有 0x43 和 0x70,没有 0x40。 */ TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_select(TCA9548A_ADDR, TCA9548A_CH_NONE)); TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, hal_i2c_probe(BLADE_INA)); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(TRUNK_INA)); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(TCA9548A_ADDR)); /* 选通道 0:0x40 出现,与主干的 0x43 / 0x70 同时可见、互不干扰。 */ TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_select(TCA9548A_ADDR, 0)); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(BLADE_INA)); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(TRUNK_INA)); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(TCA9548A_ADDR)); /* 换到通道 1:还是那个 0x40,但已经是**另一片**了。 */ TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_select(TCA9548A_ADDR, 1)); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(BLADE_INA)); TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous()); /* 全程没有两片一起应答 */ } /* 通道→去哪的那张表:槽 1 在通道 0、槽 5 在通道 4、CH224A 在通道 5。 ⚠️ 顺排是当下的事实不是不变量,所以业务代码要从 rail_cfg 查 —— 这里只钉住「这三个常量互不相同、且都在 0~7 内」。 */ static void test_channel_map_constants_are_sane(void) { TEST_ASSERT_TRUE(TCA9548A_CH_SLOT1 >= 0 && TCA9548A_CH_SLOT1 < TCA9548A_CH_COUNT); TEST_ASSERT_TRUE(TCA9548A_CH_SLOT5 >= 0 && TCA9548A_CH_SLOT5 < TCA9548A_CH_COUNT); TEST_ASSERT_TRUE(TCA9548A_CH_PD >= 0 && TCA9548A_CH_PD < TCA9548A_CH_COUNT); TEST_ASSERT_NOT_EQUAL(TCA9548A_CH_SLOT1, TCA9548A_CH_SLOT5); TEST_ASSERT_NOT_EQUAL(TCA9548A_CH_PD, TCA9548A_CH_SLOT1); TEST_ASSERT_NOT_EQUAL(TCA9548A_CH_PD, TCA9548A_CH_SLOT5); /* 五个槽正好占掉 SLOT1..SLOT5,PD 必须在这五个之外。 */ TEST_ASSERT_TRUE(TCA9548A_CH_PD < TCA9548A_CH_SLOT1 || TCA9548A_CH_PD > TCA9548A_CH_SLOT5); TEST_ASSERT_EQUAL_INT(-1, TCA9548A_CH_NONE); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_select_puts_exactly_one_byte_on_the_wire); RUN_TEST(test_the_reg_plus_value_spelling_is_silently_wrong); RUN_TEST(test_every_channel_is_one_bit_not_a_number); RUN_TEST(test_none_closes_every_branch); RUN_TEST(test_out_of_range_channel_sends_nothing); RUN_TEST(test_mask_to_ch_is_exhaustive); RUN_TEST(test_read_mask_reads_back_what_was_selected); RUN_TEST(test_probe_accepts_a_fresh_mux); RUN_TEST(test_probe_reports_bus_error_when_readback_is_not_zero); RUN_TEST(test_probe_naks_when_nobody_answers); RUN_TEST(test_probe_stops_at_the_first_failure); RUN_TEST(test_addr_does_not_collide_with_the_trunk); RUN_TEST(test_branch_addresses_are_absent_from_the_table_on_purpose); RUN_TEST(test_channel_map_constants_are_sane); return UNITY_END(); }