固件:R2 迁移 R-0 ~ R-6(底板 + 5 片同构刀片)

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]>
这个提交包含在:
zhou yang
2026-09-27 10:26:24 +08:00
共同撰写人 Claude Opus 5.5
父节点 5a80d8a872
当前提交 b8ad69681f
共修改 51 个文件,包含 10882 行新增和 4068 行删除
+104 -3
查看文件
@@ -1,12 +1,25 @@
/* A2 验收主项:能在假总线上挂一个器件并读回预置值。
顺带把「器件失联 / 偶发 NAK」这两种后面 A15 要用的故障注入也测掉。 */
顺带把「器件失联 / 偶发 NAK」这两种后面 A15 要用的故障注入也测掉。
---- R2 新增:假总线有了 `bus` 与一颗假 mux ------------------------------
五片刀片**完全同构**(同址 0x40 / 0x20),真板上区分它们的唯一手段是底板
TCA9548A 的通道。假总线如果只按地址查表,五片就塌成一片 ——
于是「选错通道」这一类 R2 最危险的静默错误在宿主机上一次也复现不出来。
所以器件带一个 `bus`(-1 主干 / 0~7 支路),本文件最后两条用例测的就是它:
**关着的支路一律 NAK**、**同时开两路会被记账**。
原有那几条用的仍是 `sim_i2c_add()`(挂主干),语义没变。 */
#include <string.h>
#include "sim_hal.h"
#include "unity.h"
#define INA226_CH1 0x40 /* 设计文档 §8.2 的地址表;A3 才会真的写驱动 */
#define PCA9555 0x20
#define INA226_RAIL 0x40 /* R2:五片刀片上的那片,五片同址(计划 §5.2) */
#define INA226_INPUT 0x43 /* R2:底板输入侧 U13,挂**主干** */
#define PCA9555 0x20
#define MUX 0x70 /* TCA9548A(底板 U35) */
#define MUX_RST_PIN 40 /* `MUX_RST#` 接的 GPIO,本文件用不到,给个真值免得误导 */
#define INA226_CH1 INA226_RAIL /* 原有用例沿用的名字 */
void setUp(void) { sim_hal_reset(); }
void tearDown(void) {}
@@ -120,6 +133,92 @@ static void test_address_out_of_range(void)
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hal_i2c_probe(0x80));
}
/* ---- R2:mux 支路 ---------------------------------------------------------
控制字是**掩码不是编号**,而且写 mux 是**一个裸字节**(不带寄存器号)。
这两件事都在 tca9548a.h 的文件头里;这里直接按线上的样子发。 */
static void select_mask(uint8_t mask)
{
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write(MUX, &mask, 1));
}
/* ⭐ 通道**关着**的时候,挂在那条支路上的器件一律 NAK —— 真器件就是这个行为:
支路被断开,从机根本收不到地址。
⚠️ 假总线要是不做这一层(只按地址查表),五片刀片就塌成一片:
「忘了选通道」「选错通道」「选通道失败却照读」这三种错在宿主机上**全都是绿的**,
而它们在真板上的现象是「读到的是另一台手机的电压电流」——
量纲、范围、变化趋势全部正常,没有任何一层会报错。 */
static void test_device_behind_a_closed_channel_naks(void)
{
init_bus();
sim_i2c_add(INA226_INPUT, 2); /* 主干上那片,永远看得见 */
sim_i2c_add_ch(2, INA226_RAIL, 2); /* 槽 3 的刀片 */
sim_i2c_add_ch(3, INA226_RAIL, 2); /* 槽 4 的刀片,**同一个地址** */
sim_i2c_add_mux(MUX, MUX_RST_PIN);
sim_i2c_set_reg_ch(2, INA226_RAIL, 0x02, 0x1111);
sim_i2c_set_reg_ch(3, INA226_RAIL, 0x02, 0x2222);
/* 上电全关:两片刀片谁也不应答,而主干那片照常。 */
TEST_ASSERT_EQUAL_INT(-1, sim_i2c_mux_ch());
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, hal_i2c_probe(INA226_RAIL));
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(INA226_INPUT));
/* 选中槽 3:同一个地址,读回来的必须是**那一片**的寄存器。 */
select_mask(1u << 2);
TEST_ASSERT_EQUAL_INT(2, sim_i2c_mux_ch());
const uint8_t ptr = 0x02;
uint8_t rd[2] = { 0, 0 };
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write_read(INA226_RAIL, &ptr, 1, rd, sizeof(rd)));
TEST_ASSERT_EQUAL_HEX16(0x1111, (uint16_t)((rd[0] << 8) | rd[1]));
/* 换成槽 4:地址一个字没改,读回来的却是另一片 —— 这就是「选错通道」的全貌。 */
select_mask(1u << 3);
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write_read(INA226_RAIL, &ptr, 1, rd, sizeof(rd)));
TEST_ASSERT_EQUAL_HEX16(0x2222, (uint16_t)((rd[0] << 8) | rd[1]));
/* 槽 3 那片这时**一次都没有**再被访问过。 */
TEST_ASSERT_EQUAL_UINT32(1, sim_i2c_dev_ch(2, INA226_RAIL)->n_read);
/* 关回全断:两片都不见了,主干那片还在。 */
select_mask(0);
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, hal_i2c_probe(INA226_RAIL));
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(INA226_INPUT));
/* 全程只开过一路,不许有「两片一起应答」。 */
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous());
}
/* ⭐ 同一个地址同时看得见两片 = **通道开重了**(控制字是掩码,写 0x03 不会报错)。
真板上那是两片一起应答,主机读回来的是两片的**线与** ——
数值在量程里、随负载变化、还特别稳,谁也看不出这是两台手机的数据与在一起了。
所以这个计数必须恒为 0;它非 0 的那一刻,就是所有电流读数开始悄悄偏小的那一刻。 */
static void test_two_channels_open_at_once_is_counted_as_ambiguous(void)
{
init_bus();
sim_i2c_add_ch(0, INA226_RAIL, 2);
sim_i2c_add_ch(1, INA226_RAIL, 2);
sim_i2c_add_mux(MUX, MUX_RST_PIN);
select_mask(1u << 0); /* 规规矩矩只开一路 */
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(INA226_RAIL));
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous());
select_mask(0x03); /* 两路一起开:写下去**不报错**,器件也不抗议 */
TEST_ASSERT_EQUAL_INT_MESSAGE(-2, sim_i2c_mux_ch(), "多于一路不是一种用法,是 bug");
TEST_ASSERT_EQUAL_HEX8(0x03, sim_i2c_mux_mask());
/* 事务照样「成功」—— 这正是它难查的地方。 */
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(INA226_RAIL));
TEST_ASSERT_EQUAL_UINT32(1, sim_i2c_ambiguous());
uint8_t rd[2];
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_read(INA226_RAIL, rd, sizeof(rd)));
TEST_ASSERT_EQUAL_UINT32_MESSAGE(2, sim_i2c_ambiguous(), "每一次事务都记一笔");
/* 收回成一路,计数就不再涨(它是「发生过几次」,不是一个状态位)。 */
select_mask(1u << 1);
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(INA226_RAIL));
TEST_ASSERT_EQUAL_UINT32(2, sim_i2c_ambiguous());
}
int main(void)
{
UNITY_BEGIN();
@@ -132,5 +231,7 @@ int main(void)
RUN_TEST(test_transient_nak_then_recovers);
RUN_TEST(test_partial_register_length_rejected);
RUN_TEST(test_address_out_of_range);
RUN_TEST(test_device_behind_a_closed_channel_naks);
RUN_TEST(test_two_channels_open_at_once_is_counted_as_ambiguous);
return UNITY_END();
}