固件: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 行删除
+321
查看文件
@@ -0,0 +1,321 @@
/* R-0:CH224A 驱动(底板 U36,USB-PD 受电芯片,挂在 mux 通道 5 上)。
板上它只干一件事:`R7` 120 k 硬件请求 20 V,固件**不参与协商**,
只读出「当前这个充电器在 20 V 档最多能给多少电流」,换算成整板功率上限
(计划 §5.9 / R1 §8.3 的降载)。
这份测试守三件事:
1. `0x50` 的单位是 **50 mA/LSB** —— 把单位记错,60 W 的充电器会被当成 6 W
(整板立刻降到只能开一路)或者 600 W(降载形同虚设,输入侧过流跳闸);
2. **读失败时不许动调用方的 `*ma`** —— 旧的上限比一个 0 可信;
3. ⛔ **驱动永远不写 `0x0A`**(`0x0A = 5` 是请求 28 V EPR,
板上 `D3` 是 SMBJ22A、VRWM 22 V,会把 TVS 打到雪崩)。
⚠️ `CH224A_ADDR_DEFAULT`(0x23)**还没有被实物证实**(计划 §11 的待定项),
所以驱动的每个接口都收 addr 参数,这份测试也从不把 0x23 当成已知事实 ——
见 test_every_entry_point_takes_the_address_because_it_is_unconfirmed。 */
#include "ch224a.h"
#include "hal_i2c.h"
#include "sim_hal.h"
#include "unity.h"
/* B1 上板扫描之前,测试里用哪个地址都行 —— 这正是要证明的事之一。
这里**故意不用** CH224A_ADDR_DEFAULT,免得哪天 0x23 被证伪时
这一整份测试跟着一起红。 */
#define TEST_ADDR 0x22
/* ---- 窃听口:这颗器件到底被写过什么 -------------------------------------- */
/* `hal_i2c_write_read()` 的「寄存器指针」那一段在假总线上也算一次 write,
所以断言 `n_write == 0` 是做不到的(只读驱动也得把指针写出去)。
退而求其次,但更严:
· `s_touched[r]` —— 寄存器指针曾经指向过 r;
· `s_data_bytes` —— 指针后面**跟过几个数据字节**(只读驱动必须恒为 0)。
「写 0x0A = 5」这个动作会同时踩中两条:s_touched[0x0A] 和 s_data_bytes。 */
static uint8_t s_touched[256];
static uint32_t s_data_bytes;
static void spy(sim_i2c_dev_t *d, sim_i2c_ev_t ev)
{
if (ev != SIM_I2C_EV_POST_WRITE) {
return;
}
if (d->last_write_len >= 1) {
s_touched[d->last_write[0]] = 1;
}
if (d->last_write_len > 1) {
s_data_bytes += (uint32_t)d->last_write_len - 1u;
}
}
void setUp(void)
{
sim_hal_reset();
for (int i = 0; i < 256; i++) {
s_touched[i] = 0;
}
s_data_bytes = 0;
}
void tearDown(void) {}
static sim_i2c_dev_t *mount(uint8_t addr)
{
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(NULL));
sim_i2c_dev_t *d = sim_i2c_add(addr, 1);
TEST_ASSERT_NOT_NULL(d);
d->hook = spy;
return d;
}
/* ---- 单位换算:50 mA/LSB ------------------------------------------------- */
/* ★ 三个真实档位。20 V 档下 60 W / 65 W / 100 W 分别是 3000 / 3250 / 5000 mA,
除以 50 正好是 60 / 65 / 100 —— **寄存器值的数字恰好等于瓦数**,
这个巧合很容易让人误以为「这个寄存器读出来就是瓦数」而把 ×50 省掉。
省掉的后果:60 W 充电器被当成 60 mA,整板功率上限 1.2 W,五路一个都开不起来;
反过来把单位当成 10 mA/LSB,100 W 的充电器会被算成 20 W —— 同样是静默的,
现象只是「插了 100 W 的充电器也只敢开两路」,没有任何报错。 */
static void test_current_register_is_50ma_per_lsb(void)
{
TEST_ASSERT_EQUAL_UINT32(3000, ch224a_ma_from_reg(60)); /* 60 W @20 V */
TEST_ASSERT_EQUAL_UINT32(3250, ch224a_ma_from_reg(65)); /* 65 W @20 V */
TEST_ASSERT_EQUAL_UINT32(5000, ch224a_ma_from_reg(100)); /* 100 W @20 V,要 e-marker 线 */
TEST_ASSERT_EQUAL_UINT32(0, ch224a_ma_from_reg(0));
TEST_ASSERT_EQUAL_UINT32(50, ch224a_ma_from_reg(1)); /* 1 LSB = 50 mA */
TEST_ASSERT_EQUAL_UINT32(12750, ch224a_ma_from_reg(255)); /* 满量程不溢出 */
/* 逐点线性,没有偏移:写成 (reg+1)*50 之类的实现在 reg=0 上就露馅。 */
for (unsigned r = 0; r <= 255u; r++) {
TEST_ASSERT_EQUAL_UINT32(r * 50u, ch224a_ma_from_reg((uint8_t)r));
}
}
/* ★ 功率上限按 **20 V** 档换算,不是 5 V 也不是 12 V。
板上是 `R7` 一颗电阻把档位定死在 20 V,降压 `U37` 再变 12 V ——
拿 12 V(母线电压)去乘是最自然的一个错,而且算出来的数看着也很合理
(100 W 的充电器会被算成 60 W),于是最坏工况下真的会过载跳闸。 */
static void test_power_limit_is_twenty_volts_times_the_current(void)
{
TEST_ASSERT_EQUAL_UINT32(60000, ch224a_power_limit_mw(3000));
TEST_ASSERT_EQUAL_UINT32(65000, ch224a_power_limit_mw(3250));
TEST_ASSERT_EQUAL_UINT32(100000, ch224a_power_limit_mw(5000));
TEST_ASSERT_EQUAL_UINT32(0, ch224a_power_limit_mw(0));
/* 档位是 20 000 mV,不是 12 000 / 5 000。 */
TEST_ASSERT_EQUAL_UINT32(20000, CH224A_FIXED_MV);
TEST_ASSERT_EQUAL_UINT32(20u * 1234u, ch224a_power_limit_mw(1234));
/* 满量程 12750 mA = 255 W。中间过程若用 32 位相乘
(20000 × 12750 = 255 000 000)还没溢出,但离 uint32 上限只剩一个数量级 ——
驱动里用 uint64 中转就是为这条。 */
TEST_ASSERT_EQUAL_UINT32(255000, ch224a_power_limit_mw(ch224a_ma_from_reg(255)));
/* 两个函数串起来:寄存器 → 功率,整条链一次对完。 */
TEST_ASSERT_EQUAL_UINT32(60000, ch224a_power_limit_mw(ch224a_ma_from_reg(60)));
TEST_ASSERT_EQUAL_UINT32(100000, ch224a_power_limit_mw(ch224a_ma_from_reg(100)));
}
/* ---- 总线上的行为 -------------------------------------------------------- */
/* 读到什么就换算什么,而且只发一次事务(写指针 + 读一个字节)。 */
static void test_read_max_ma_converts_what_the_chip_says(void)
{
sim_i2c_dev_t *d = mount(TEST_ADDR);
sim_i2c_set_reg(TEST_ADDR, CH224A_REG_CURRENT, 65); /* 65 W 的砖 */
uint32_t ma = 0;
TEST_ASSERT_EQUAL_INT(HAL_OK, ch224a_read_max_ma(TEST_ADDR, &ma));
TEST_ASSERT_EQUAL_UINT32(3250, ma);
TEST_ASSERT_EQUAL_UINT32(1, d->n_read);
TEST_ASSERT_EQUAL_HEX8(CH224A_REG_CURRENT, d->last_write[0]); /* 读的是 0x50 */
/* 换一块 100 W 的:同一个接口,不需要重新 init。 */
sim_i2c_set_reg(TEST_ADDR, CH224A_REG_CURRENT, 100);
TEST_ASSERT_EQUAL_INT(HAL_OK, ch224a_read_max_ma(TEST_ADDR, &ma));
TEST_ASSERT_EQUAL_UINT32(5000, ma);
TEST_ASSERT_EQUAL_UINT32(100000, ch224a_power_limit_mw(ma));
}
/* ★ 读失败时 `*ma` **一个字节都不许动**。
写成「先清零再读」的后果:总线上一次毛刺 → 上限变成 0 mA → 降载逻辑认为
这块砖一点电都给不了 → 五路全部降载甚至停机。而下一个周期读成功了又恢复,
现象是「偶尔全部掉一下」,日志里除了一条 NAK 什么线索都没有。
旧的上限即使过期了也比一个 0 可信 —— 充电器不会在运行期被换掉。 */
static void test_a_failed_read_leaves_the_previous_limit_untouched(void)
{
mount(TEST_ADDR);
sim_i2c_set_reg(TEST_ADDR, CH224A_REG_CURRENT, 100);
uint32_t ma = 3000; /* 上一轮读到的 60 W */
sim_i2c_inject_nak(TEST_ADDR, 1);
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, ch224a_read_max_ma(TEST_ADDR, &ma));
TEST_ASSERT_EQUAL_UINT32(3000, ma); /* ★ 没被改成 0,也没被改成 5000 */
/* 器件失联(芯片挂了 / PD 口拔了)同样如此。 */
sim_i2c_set_present(TEST_ADDR, false);
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, ch224a_read_max_ma(TEST_ADDR, &ma));
TEST_ASSERT_EQUAL_UINT32(3000, ma);
/* 恢复之后才更新。 */
sim_i2c_set_present(TEST_ADDR, true);
TEST_ASSERT_EQUAL_INT(HAL_OK, ch224a_read_max_ma(TEST_ADDR, &ma));
TEST_ASSERT_EQUAL_UINT32(5000, ma);
}
/* 状态寄存器是 0x09,而且**不校验内容** —— 这颗芯片没有 ID 寄存器,
状态字的每一个值都是合法的。拿状态字当 ID 校验的后果是
「协商到某个档位时启动自检就过不去」,而那完全取决于插的是哪块砖。 */
static void test_probe_accepts_any_status_byte(void)
{
sim_i2c_dev_t *d = mount(TEST_ADDR);
for (unsigned v = 0; v <= 255u; v++) {
sim_i2c_set_reg(TEST_ADDR, CH224A_REG_STATUS, (uint16_t)v);
TEST_ASSERT_EQUAL_INT(HAL_OK, ch224a_probe(TEST_ADDR));
uint8_t st = 0xAA;
TEST_ASSERT_EQUAL_INT(HAL_OK, ch224a_read_status(TEST_ADDR, &st));
TEST_ASSERT_EQUAL_HEX8((uint8_t)v, st);
}
TEST_ASSERT_EQUAL_HEX8(CH224A_REG_STATUS, d->last_write[0]);
sim_i2c_set_present(TEST_ADDR, false);
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, ch224a_probe(TEST_ADDR));
}
/* NULL 出参一律 HAL_ERR_ARG,而且**在碰总线之前**就返回。 */
static void test_null_outputs_are_rejected_before_touching_the_bus(void)
{
sim_i2c_dev_t *d = mount(TEST_ADDR);
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, ch224a_read_status(TEST_ADDR, NULL));
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, ch224a_read_max_ma(TEST_ADDR, NULL));
TEST_ASSERT_EQUAL_UINT32(0, d->n_read);
TEST_ASSERT_EQUAL_UINT32(0, d->n_write);
}
/* ---- ⛔ 那条禁令 --------------------------------------------------------- */
/* ⭐⭐ **社会性用例:驱动里不存在任何写 `0x0A` 的接口。**
C 里没法断言「某个函数不存在」,所以这里从**线上**反过来证:
把驱动的每一个公开接口都跑一遍(而且是反复跑),然后断言
· 寄存器指针**从来没有指向过 0x0A**;
· 指针后面**从来没有跟过数据字节**(`s_data_bytes == 0`)—— 也就是说
这颗器件在整个测试跑完之后,一次真正的「写寄存器」都没有发生过;
· 驱动碰过的寄存器只有 `0x09` 和 `0x50` 两个,一个不多。
⚠️ 为什么不是直接断言 `n_write == 0`:`hal_i2c_write_read()` 的
「发寄存器指针」那一段在假总线上也计一次 write,只读驱动也躲不掉。
`n_write == 0` 在这颗器件上永远做不到,所以换成上面三条 —— 它们更严:
`n_write == 0` 拦不住「写 0x0A 用的是 write_read」,`s_data_bytes == 0` 能。
为什么这件事值得单列一条用例:`0x0A = 5` 是「请求 28 V EPR」。
板上 `D3` 是 SMBJ22A(VRWM 22 V),`U37` 的 EN 分压按 20 V 设计 ——
28 V 会把 TVS 打到雪崩,一次就报废。这个动作**不会报错**,
它是 CH224A 完全合法的一条命令,芯片会老老实实去协商。
所以防线不是「叮嘱别用」,是**根本不提供接口**(和 `sr595` 不提供抬 `OE#`、
`pca9555` 不提供把接地脚配成输出,是同一条规矩)。
这条用例的作用是:哪天有人「顺手加个 ch224a_request_mv()」,这里立刻红。 */
static void test_the_driver_never_writes_a_single_data_byte(void)
{
mount(TEST_ADDR);
sim_i2c_set_reg(TEST_ADDR, 0x0A, 0x5A); /* 哨兵:谁动了它谁露馅 */
sim_i2c_set_reg(TEST_ADDR, CH224A_REG_CURRENT, 60);
/* 把公开接口全跑一遍,跑够多次(万一有人写成「第 N 次才配置一下」)。 */
for (int i = 0; i < 16; i++) {
uint8_t st = 0;
uint32_t ma = 0;
TEST_ASSERT_EQUAL_INT(HAL_OK, ch224a_probe(TEST_ADDR));
TEST_ASSERT_EQUAL_INT(HAL_OK, ch224a_read_status(TEST_ADDR, &st));
TEST_ASSERT_EQUAL_INT(HAL_OK, ch224a_read_max_ma(TEST_ADDR, &ma));
TEST_ASSERT_EQUAL_UINT32(3000, ma);
TEST_ASSERT_TRUE(ch224a_addr_ok(TEST_ADDR));
(void)ch224a_ma_from_reg(60);
(void)ch224a_power_limit_mw(3000);
}
/* ★ 一个数据字节都没写出去 = 这是一颗只读器件。 */
TEST_ASSERT_EQUAL_UINT32(0, s_data_bytes);
/* ★ 指针从没指向过 0x0A。 */
TEST_ASSERT_EQUAL_UINT8(0, s_touched[0x0A]);
/* ★ 哨兵没被动过。 */
TEST_ASSERT_EQUAL_HEX16(0x5A, sim_i2c_get_reg(TEST_ADDR, 0x0A));
/* ★ 碰过的寄存器只有这两个。 */
for (int r = 0; r < 256; r++) {
const uint8_t want = (r == CH224A_REG_STATUS || r == CH224A_REG_CURRENT) ? 1u : 0u;
TEST_ASSERT_EQUAL_UINT8(want, s_touched[r]);
}
/* 这条检查本身不是句空话:手工写一次 0x0A = 5(也就是那个禁令动作),
窃听口必须立刻照出来。抓不到的话上面那四条断言全是摆设。 */
const uint8_t evil[2] = { 0x0A, 5 };
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write(TEST_ADDR, evil, sizeof(evil))); /* 器件不报错 */
TEST_ASSERT_EQUAL_UINT32(1, s_data_bytes);
TEST_ASSERT_EQUAL_UINT8(1, s_touched[0x0A]);
TEST_ASSERT_EQUAL_HEX16(5, sim_i2c_get_reg(TEST_ADDR, 0x0A)); /* 28 V 就是这么请求的 */
}
/* ---- 地址 ---------------------------------------------------------------- */
/* ⚠️ **0x23 还没有被实物证实**(计划 §11)。这条用例钉住的是
「驱动不把地址写死」这件事,而不是 0x23 本身:
所有接口都收 addr 参数,换一个地址照样跑得通。
B1 上板扫描时把通道 5 上真正扫到的那个地址填回 `CH224A_ADDR_DEFAULT`
并同步改计划 §5.2 —— 在那之前,用 0x23 扫不到是**预期结果,不是固件 bug**。 */
static void test_every_entry_point_takes_the_address_because_it_is_unconfirmed(void)
{
/* 器件蹲在 0x22,`CH224A_ADDR_DEFAULT` 上什么也没有。 */
mount(0x22);
sim_i2c_set_reg(0x22, CH224A_REG_CURRENT, 60);
uint8_t st = 0;
uint32_t ma = 0;
TEST_ASSERT_EQUAL_INT(HAL_OK, ch224a_probe(0x22));
TEST_ASSERT_EQUAL_INT(HAL_OK, ch224a_read_status(0x22, &st));
TEST_ASSERT_EQUAL_INT(HAL_OK, ch224a_read_max_ma(0x22, &ma));
TEST_ASSERT_EQUAL_UINT32(3000, ma);
/* 默认值上扫不到 —— 这就是 B1 之前应当看到的现象。 */
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, ch224a_probe(CH224A_ADDR_DEFAULT));
/* 顺带确认默认值还是那个待确认的 0x23(改了就该同时改计划 §5.2)。 */
TEST_ASSERT_EQUAL_HEX8(0x23, CH224A_ADDR_DEFAULT);
TEST_ASSERT_TRUE(ch224a_addr_ok(CH224A_ADDR_DEFAULT));
}
/* 地址冲突表:主干上的 4 个 + mux 自己 + 刀片支路上的 2 个。
⚠️ 与 `tca9548a_addr_ok()` 不同,这里**连 0x20 / 0x40 也要查** ——
CH224A 挂在通道 5 上,选到通道 5 的那一刻它和主干是连通的;
要是它和某片刀片同址,那更是直接把刀片的 INA226 盖住。 */
static void test_addr_ok_rejects_every_address_already_on_the_board(void)
{
static const uint8_t taken[] = { 0x20, 0x40, 0x43, 0x4A, 0x4B, 0x70 };
for (unsigned i = 0; i < sizeof(taken); i++) {
TEST_ASSERT_FALSE(ch224a_addr_ok(taken[i]));
}
TEST_ASSERT_TRUE(ch224a_addr_ok(0x22));
TEST_ASSERT_TRUE(ch224a_addr_ok(0x23));
TEST_ASSERT_TRUE(ch224a_addr_ok(0x7f));
TEST_ASSERT_FALSE(ch224a_addr_ok(0x80)); /* 7 位地址,0x80 起就不是地址了 */
TEST_ASSERT_FALSE(ch224a_addr_ok(0xFF));
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_current_register_is_50ma_per_lsb);
RUN_TEST(test_power_limit_is_twenty_volts_times_the_current);
RUN_TEST(test_read_max_ma_converts_what_the_chip_says);
RUN_TEST(test_a_failed_read_leaves_the_previous_limit_untouched);
RUN_TEST(test_probe_accepts_any_status_byte);
RUN_TEST(test_null_outputs_are_rejected_before_touching_the_bus);
RUN_TEST(test_the_driver_never_writes_a_single_data_byte);
RUN_TEST(test_every_entry_point_takes_the_address_because_it_is_unconfirmed);
RUN_TEST(test_addr_ok_rejects_every_address_already_on_the_board);
return UNITY_END();
}