2026-09-16 完成、当时未提交,这里补提交(原样,不含 09-27 上板的修改)。 - 器件层 / 拓扑层整体从 R1 换到 R2:TCA9548A 分通道、五路同址 INA226 0x40 + 输入侧 0x43、 刀片 PCA9555(VBUS_EN 挪到 P0.4)、CH224A 只读驱动、风扇闭环 t_thermal - 新增 i2cbus(mux 原子事务)、slot(刀片 I/O 门面)、host/qtest.sh - 74HC595 链退役(代码与测试保留) - 判据阈值与时间常量未动;19 个 ctest 全绿,idf.py build 通过 Co-Authored-By: Claude Opus 5.5 <[email protected]>
330 行
15 KiB
C
330 行
15 KiB
C
/* 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<<ch。
|
||
写成「发通道号」的实现在这里只有 ch=0 和 ch=1 侥幸对得上(0→0、1→1),
|
||
ch=2 开始就全错 —— 所以八个都要测,只测一个通道是测不出来的。 */
|
||
static void test_every_channel_is_one_bit_not_a_number(void)
|
||
{
|
||
sim_i2c_dev_t *mux = mount();
|
||
|
||
for (int ch = 0; ch < TCA9548A_CH_COUNT; ch++) {
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_select(TCA9548A_ADDR, ch));
|
||
TEST_ASSERT_EQUAL_UINT8(1, mux->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();
|
||
}
|