/* A7(**R2 版**):五路配置表 + 启动自检 + `EFUSE_EN`(需求 §10.4、计划 §5.7)。 验收主项一:**启动自检 —— 五路的地址 / 通道 / 引脚无重复、无越界**, 而且**自检逻辑本身查得出坏表**(真表是 const 的,只拿它跑一遍证明不了这件事)。 验收主项二:那张五路表和两个驱动自己的表**没有分家**(逐格比,不是抽查)。 验收主项三:⭐ **顺排的那几列也不许改成算式。** R2 里 `mux_ch` = rail−1、`EFUSE_EN` = IO(8+rail) 这两列**确实是顺排的**, 但 `PRSNT#` 那一列到第五路就断了(IO15、16、17、18、**IO7**)—— 它就是现成的反例:**顺排是当下的事实,不是不变量**。 ---- R1 → R2 这份测试换掉了什么 ----------------------------------------- · 595 那条链(按键位 / EDL 位)**整个没有了** —— 位号搬进 `pca9555.h`, 五路逐格相同,不再是「每路一个」,因此也不再进这张表; · `vbus_en` 那一列**没有了** —— 挪到刀片 PCA9555 的 P0.4(计划 §5.4), 于是 `rail_all_off()` 的前半段从 GPIO 变成了 I²C,**可能失败**; · 新增 `mux_ch` 与 `PRSNT#` 两列,`RAIL_SELFTEST_SR595` 换成 `RAIL_SELFTEST_MUX`; · INA226 五路同址 0x40 —— R1 那条「六片六地址」的用例整个作废。 */ #include #include "board_pins.h" #include "dac63204.h" #include "hal_gpio.h" #include "hal_i2c.h" #include "hal_lock.h" #include "i2cbus.h" #include "ina226.h" #include "pca9555.h" #include "phonerail.h" #include "rail_cfg.h" #include "sim_hal.h" #include "slot.h" #include "tca9548a.h" #include "unity.h" #define N PHONERAIL_RAIL_COUNT void setUp(void) { sim_hal_reset(); /* 驱动里的静态影子不归 sim 管,不清会串到下一条用例。 */ pca9555_shadow_reset(); } void tearDown(void) {} /* ---- 验收主项一:自检 ----------------------------------------------------- */ static void test_selftest_passes(void) { TEST_ASSERT_EQUAL_HEX32(0, rail_selftest()); } /* 自检背后的每一个谓词都要真的被算过。`rail_selftest()` 哪天被改成 `return 0;`, 上一条照样绿 —— 这条不会。 */ static void test_selftest_covers_every_driver_table(void) { TEST_ASSERT_TRUE(ina226_addr_table_ok()); TEST_ASSERT_TRUE(dac63204_map_ok()); TEST_ASSERT_TRUE(pca9555_map_ok()); TEST_ASSERT_TRUE(pca9555_addr_ok()); TEST_ASSERT_TRUE(tca9548a_addr_ok()); /* R2 新增的那颗,漏了它自检就少一项 */ } /* 每个失败位都得有人话名字:打日志时打出一个 "?" 等于没打, 而这条日志是上电自检失败时**唯一**的线索(那时串口以外什么都还没起来)。 */ static void test_selftest_str_names_every_bit(void) { static const uint32_t bits[] = { RAIL_SELFTEST_INA, RAIL_SELFTEST_DAC, RAIL_SELFTEST_MUX, RAIL_SELFTEST_PCA9555, RAIL_SELFTEST_TABLE, RAIL_SELFTEST_PINS, }; for (unsigned i = 0; i < sizeof(bits) / sizeof(bits[0]); i++) { TEST_ASSERT_TRUE(rail_selftest_str(bits[i])[0] != '?'); } /* R2 新增的那一位不许沿用 595 的名字 —— 名字对不上现象,等于把人往沟里带。 */ TEST_ASSERT_EQUAL_HEX32(1u << 2, (uint32_t)RAIL_SELFTEST_MUX); TEST_ASSERT_NULL(strstr(rail_selftest_str(RAIL_SELFTEST_MUX), "595")); TEST_ASSERT_EQUAL_STRING("?", rail_selftest_str(0)); TEST_ASSERT_EQUAL_STRING("?", rail_selftest_str(1u << 31)); /* 两位一起传进来也是 "?":调用方该逐位拆。 */ TEST_ASSERT_EQUAL_STRING("?", rail_selftest_str(RAIL_SELFTEST_INA | RAIL_SELFTEST_DAC)); } /* ---- 自检逻辑本身:喂坏表进去,它得认出来 -------------------------------- 拿真表跑一遍只能证明「现在没事」,证明不了「真有事的时候查得出来」—— 而真表是 const 的,测试改不动它。所以复制一份出来逐样改坏。 */ static void copy_real_table(rail_cfg_t *dst) { for (int rail = 1; rail <= N; rail++) { dst[rail - 1] = *rail_cfg(rail); } } static void test_a_faithful_copy_of_the_real_table_passes(void) { rail_cfg_t t[N]; copy_real_table(t); TEST_ASSERT_EQUAL_HEX32(0, rail_selftest_table(t)); /* 真表本身走的也是这条路 —— rail_cfg(1) 就是那张表的首行。 */ TEST_ASSERT_EQUAL_HEX32(0, rail_selftest_table(rail_cfg(1))); /* NULL 不许悄悄当成「过了」。 */ TEST_ASSERT_TRUE((rail_selftest_table(NULL) & RAIL_SELFTEST_TABLE) != 0); } /* 表和驱动分家:只改一格,而且改成一个**合法**的值。 这一手光看「地址是不是合法、有没有重复」是查不出来的 —— 板子照样跑,只是「读 CH3 的电流」读的是输入侧那片。 */ static void test_a_cell_that_drifted_from_the_driver_is_caught(void) { rail_cfg_t t[N]; copy_real_table(t); t[2].ina_addr = INA226_ADDR_INPUT; /* 0x43 合法、板上真有这片 */ TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_TABLE, rail_selftest_table(t)); copy_real_table(t); t[0].dac_ch = 0; /* CH1 从 U6.OUT2 挪到 OUT0 —— 那是 CH2 的通道 */ TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_TABLE, rail_selftest_table(t)); copy_real_table(t); t[2].dac_addr = DAC63204_ADDR_U6; /* 「CH3 当然在 U6」—— R1 就踩过的那一脚 */ TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_TABLE, rail_selftest_table(t)); } /* ⭐ mux 通道的三种坏法。R2 里选错通道 = 读到另一台手机, 量纲、范围、变化趋势全都正常,没有任何一层会报错(计划 §4.3 的 C-d)。 */ static void test_every_kind_of_bad_mux_channel_is_caught(void) { rail_cfg_t t[N]; /* ① 两路指向同一片刀片:CH4 的命令发给 CH1,CH4 读到的是 CH1 的电压电流。 五台手机全在跑,五路数字全都「正常」—— 只是有两路是同一份。 */ copy_real_table(t); t[3].mux_ch = t[0].mux_ch; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_MUX, rail_selftest_table(t)); /* ② 越界:TCA9548A 只有 8 路,控制字是掩码,写 8 进去移位就溢出了。 */ copy_real_table(t); t[4].mux_ch = TCA9548A_CH_COUNT; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_MUX, rail_selftest_table(t)); copy_real_table(t); t[0].mux_ch = 200; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_MUX, rail_selftest_table(t)); /* ③ ⭐ 通道 5 —— 那是 CH224A(PD 受电芯片)那一路,不是刀片。 选到它再去读 0x40 读不到任何东西,现象是「这一路的电压电流突然全是 0」, 看上去像刀片被拔了,于是人会去拔插刀片、换刀片,而板子和刀片都没毛病。 */ copy_real_table(t); t[2].mux_ch = TCA9548A_CH_PD; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_MUX, rail_selftest_table(t)); TEST_ASSERT_EQUAL_INT(5, TCA9548A_CH_PD); /* 把「5 是哪一路」也钉住 */ } /* 引脚:重复、撞上别的信号、strapping、PSRAM、不存在的号、UART0、USB、越界。 每一种在真板上的后果都不一样,但共同点是**都不报错**。 */ static void test_every_kind_of_bad_pin_is_caught(void) { rail_cfg_t t[N]; /* 同一路的两根脚撞在一起:`PRSNT#` 是输入、`EFUSE_EN` 是输出, 配成输出之后 `slot_present()` 读回来的是固件自己写下去的值 —— 恒「在位」。 */ copy_real_table(t); t[1].prsnt_n = t[1].efuse_en; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); /* 两路共用一根 `EFUSE_EN`:关 CH1 把 CH2 也关了。 */ copy_real_table(t); t[1].efuse_en = t[0].efuse_en; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); /* 撞上 SDA / SCL:整条 I²C 哑掉,五路的电压电流、五片 PCA9555 一起失联。 */ copy_real_table(t); t[2].efuse_en = BOARD_PIN_I2C_SDA; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); copy_real_table(t); t[2].prsnt_n = BOARD_PIN_I2C_SCL; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); /* strapping(0/3/45/46):挂负载会改上电模式,现象是**有时候起不来**。 */ copy_real_table(t); t[0].efuse_en = 45; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); copy_real_table(t); t[0].prsnt_n = 0; /* IO0 在 R2 上还接着 J33 应急口的下载脚 */ TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); /* PSRAM(35/36/37,N16R8 的八线 PSRAM 占着):现象是跑着跑着内存错乱。 */ copy_real_table(t); t[4].efuse_en = 36; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); /* 不存在的脚。IO22~25 在 S3 上**根本没有**,IO26~32 是模组内部 flash。 `gpio_config()` 对它们不报错,只是无声无息什么也不做 —— 于是「这一路的 eFuse 既开不了也**关不掉**」,而固件一路 HAL_OK。 */ copy_real_table(t); t[4].efuse_en = 24; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); copy_real_table(t); t[4].efuse_en = 30; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); /* ⭐ UART0(43/44)= J33 应急口。Hub 挂掉(或者 `HUB_RST` 被误写)之后, 它是**唯一**还能进得来的路;占掉它 = 出事那天连日志都读不出来。 */ copy_real_table(t); t[3].efuse_en = 43; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); copy_real_table(t); t[3].prsnt_n = 44; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); /* 原生 USB 的 D±(19/20)是硬连线,谁占了谁就断了下载与上位机 CDC。 */ copy_real_table(t); t[4].prsnt_n = BOARD_PIN_USB_DM; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); copy_real_table(t); t[4].prsnt_n = BOARD_PIN_USB_DP; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); /* 纯越界:上界是 48(IO48 存在,`FAN_EN` 就在上面),负数同理。 */ copy_real_table(t); t[3].efuse_en = BOARD_PIN_NUM_MAX + 1; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); copy_real_table(t); t[3].efuse_en = -1; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_PINS, rail_selftest_table(t)); } /* 三样一起坏,三位一起报 —— 别在第一个错上就 return。 自检只报第一项的话,修完一项还得重跑一次上电才能看见下一项。 */ static void test_three_kinds_of_breakage_report_three_bits(void) { rail_cfg_t t[N]; copy_real_table(t); t[2].ina_addr = INA226_ADDR_INPUT; t[0].efuse_en = 45; t[1].mux_ch = TCA9548A_CH_PD; TEST_ASSERT_EQUAL_HEX32(RAIL_SELFTEST_TABLE | RAIL_SELFTEST_PINS | RAIL_SELFTEST_MUX, rail_selftest_table(t)); } /* ---- 验收主项三:顺排也得查表 --------------------------------------------- */ /* 「起点 + 步长 × ch」形式的算式一次拦掉,不去猜别人会写哪一个。 */ static bool is_arithmetic(const int *v, int n) { const int step = v[1] - v[0]; for (int i = 2; i < n; i++) { if (v[i] - v[i - 1] != step) { return false; } } return true; } /* ⭐⭐ 这条是本文件的核心。 R2 的 `mux_ch`(0 1 2 3 4)与 `EFUSE_EN`(IO9~IO13)**现在确实是顺排的**, 这条用例把这个事实写下来 —— 但写下来的是「事实」,不是「不变量」: 下面紧跟着的 `PRSNT#` 那一列,四路顺排、第五路是 IO7,就是现成的反例。 ⚠️ 把 `PRSNT#` 写成 IO(14+rail) 会怎样:CH5 去读 **IO19**,那是原生 USB 的 D−。 读回来的电平和「刀片在不在位」毫无关系,于是 CH5 空槽照样允许上电 —— 而空槽上电意味着金手指裸露带 12V(计划 §5.8)。 ⚠️ 为什么功能测试看不出来:五片刀片插满的时候,IO19 读成什么都不影响运行; 只有「CH5 槽空着、又下了上电命令」这一种组合才会现形,而那正是没人会去测的组合。 */ static void test_sequential_columns_are_facts_not_invariants(void) { int mux[N], efuse[N], prsnt[N]; for (int rail = 1; rail <= N; rail++) { const rail_cfg_t *c = rail_cfg(rail); mux[rail - 1] = (int)c->mux_ch; efuse[rail - 1] = c->efuse_en; prsnt[rail - 1] = c->prsnt_n; } /* 两列「现在是顺排」的事实。改了先来改这条,别默默改表。 */ for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_EQUAL_INT_MESSAGE(rail - 1, mux[rail - 1], "mux_ch 当下就是 rail−1"); TEST_ASSERT_EQUAL_INT_MESSAGE(8 + rail, efuse[rail - 1], "EFUSE_EN 当下就是 IO(8+rail)"); } TEST_ASSERT_TRUE(is_arithmetic(mux, N)); TEST_ASSERT_TRUE(is_arithmetic(efuse, N)); /* ⭐ 反例:`PRSNT#` 不是等差数列,任何 `起点 + 步长 × rail` 都描述不了它。 */ TEST_ASSERT_FALSE_MESSAGE(is_arithmetic(prsnt, N), "PRSNT# 到第五路就断了,不许算"); /* 而且那个算式**只有一路错** —— 「拿 CH1 试一下没问题」正是它会被放过的原因。 */ int wrong = 0; for (int rail = 1; rail <= N; rail++) { if (prsnt[rail - 1] != 14 + rail) { wrong++; } } TEST_ASSERT_EQUAL_INT_MESSAGE(1, wrong, "IO(14+rail) 只在 CH5 上是错的"); TEST_ASSERT_EQUAL_INT_MESSAGE(7, prsnt[4], "PRSNT5# 是 IO7(方案 B 重排)"); /* 算错之后落到哪儿:IO19 = 原生 USB 的 D−。把这件事写进断言, 免得哪天有人觉得「不过是错了一根脚」。 */ TEST_ASSERT_EQUAL_INT_MESSAGE(BOARD_PIN_USB_DM, 14 + 5, "算错的 PRSNT5# 正好落在 USB D− 上"); } /* DAC 那两列 **R2 一个字没改**,坑原样还在:「前三路在 U6、通道号 = rail−1」 这句话五路里有四路不成立。接错了板子照样正常工作,只是调 CH1 的命令调了 CH2。 */ static void test_dac_mapping_is_not_the_obvious_one(void) { int wrong = 0; for (int rail = 1; rail <= N; rail++) { const rail_cfg_t *c = rail_cfg(rail); if (c->dac_addr != ((rail <= 3) ? DAC63204_ADDR_U6 : DAC63204_ADDR_U7) || c->dac_ch != (uint8_t)((rail - 1) % 3)) { wrong++; } } TEST_ASSERT_EQUAL_INT_MESSAGE(4, wrong, "只有 CH5 正好撞上那个算式"); TEST_ASSERT_EQUAL_HEX8_MESSAGE(DAC63204_ADDR_U7, rail_cfg(3)->dac_addr, "CH3 的 DAC 在 U7"); TEST_ASSERT_EQUAL_UINT8_MESSAGE(2, rail_cfg(1)->dac_ch, "CH1 挂在 U6.OUT2,不是 OUT0"); } /* ⭐ 五路的 INA226 **全是 0x40**,这不是 bug(五片刀片完全同构,D8)。 ⚠️ 于是「拿到地址就直接读」在 R2 上读的是**上一次选中的那一路** —— 真正区分是哪一路的是 `mux_ch`。这条把两件事一起钉住。 */ static void test_all_five_ina226_share_one_address_and_mux_is_what_separates_them(void) { for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_EQUAL_HEX8_MESSAGE(INA226_ADDR_RAIL, rail_cfg(rail)->ina_addr, "五路同址是设计,不是笔误"); } /* 输入侧那片必须不同址:它挂主干,和刀片那片**同时可见**。 */ TEST_ASSERT_NOT_EQUAL(INA226_ADDR_RAIL, INA226_ADDR_INPUT); /* 区分五路的是通道,而通道两两不同 —— 这一句才是「同址无害」的前提。 */ for (int a = 1; a <= N; a++) { for (int b = a + 1; b <= N; b++) { TEST_ASSERT_NOT_EQUAL(rail_cfg(a)->mux_ch, rail_cfg(b)->mux_ch); } } } /* 抽几个最容易被「顺手改顺」的格子,直接钉死实测值 (出处:`.claude/r2/_base_pcb_nets.json` 的 `U35`/`U20` 与 §5.7,2026-09-16 逐格核过)。 */ static void test_the_most_surprising_cells(void) { TEST_ASSERT_EQUAL_UINT8_MESSAGE(4, rail_cfg(5)->mux_ch, "CH5 在通道 4,通道 5 是 CH224A"); TEST_ASSERT_EQUAL_INT_MESSAGE(7, rail_cfg(5)->prsnt_n, "PRSNT5# 是 IO7 不是 IO19"); TEST_ASSERT_EQUAL_INT_MESSAGE(9, rail_cfg(1)->efuse_en, "EFUSE_EN1 是 IO9(R1 是 IO39)"); TEST_ASSERT_EQUAL_HEX8_MESSAGE(DAC63204_ADDR_U7, rail_cfg(5)->dac_addr, "CH5 的 DAC 也在 U7"); } /* ---- 表与驱动没有分家(外面独立比一遍) ----------------------------------- */ /* `rail_selftest()` 内部也比一遍,但那是**被测代码自己**在比。 这一条是外面独立比的:内部那段哪天被删了,这里照样红。 */ static void test_table_matches_every_driver_table(void) { for (int rail = 1; rail <= N; rail++) { const rail_cfg_t *c = rail_cfg(rail); TEST_ASSERT_EQUAL_HEX8(ina226_rail_addr(rail), c->ina_addr); TEST_ASSERT_EQUAL_HEX8(dac63204_rail_addr(rail), c->dac_addr); TEST_ASSERT_EQUAL_UINT8(dac63204_rail_ch(rail), c->dac_ch); /* 路号 → 通道的唯一换算点在 `slot_mux_ch()`,它必须查的是同一张表。 */ TEST_ASSERT_EQUAL_INT(slot_mux_ch(rail), (int)c->mux_ch); } } /* 两路共用一个东西的现象:共用 (DAC 片, 通道) = 调这路那路也动; 共用 mux 通道 = 两路读到同一台手机;共用 GPIO = 关这路那路也关。 */ static void test_no_two_rails_share_anything(void) { for (int a = 1; a <= N; a++) { const rail_cfg_t *x = rail_cfg(a); for (int b = a + 1; b <= N; b++) { const rail_cfg_t *y = rail_cfg(b); TEST_ASSERT_FALSE(x->dac_addr == y->dac_addr && x->dac_ch == y->dac_ch); TEST_ASSERT_NOT_EQUAL(x->mux_ch, y->mux_ch); TEST_ASSERT_NOT_EQUAL(x->efuse_en, y->efuse_en); TEST_ASSERT_NOT_EQUAL(x->prsnt_n, y->prsnt_n); TEST_ASSERT_NOT_EQUAL(x->efuse_en, y->prsnt_n); TEST_ASSERT_NOT_EQUAL(x->prsnt_n, y->efuse_en); } /* 一路自己的输出脚与输入脚更不能是同一个。 */ TEST_ASSERT_NOT_EQUAL(x->efuse_en, x->prsnt_n); } } static void test_every_field_is_in_range(void) { for (int rail = 1; rail <= N; rail++) { const rail_cfg_t *c = rail_cfg(rail); TEST_ASSERT_TRUE(c->ina_addr <= 0x7f); TEST_ASSERT_TRUE(c->dac_addr == DAC63204_ADDR_U6 || c->dac_addr == DAC63204_ADDR_U7); TEST_ASSERT_TRUE(c->dac_ch <= 3); /* 该通道必须在那片「真的接了线」的位图里 —— 没接线的通道 FBx 悬空, 上电就是开环(datasheet 表 5-1)。 */ TEST_ASSERT_TRUE((dac63204_used_mask(c->dac_addr) & (1u << c->dac_ch)) != 0); TEST_ASSERT_TRUE(c->mux_ch < TCA9548A_CH_COUNT); TEST_ASSERT_NOT_EQUAL(TCA9548A_CH_PD, c->mux_ch); } } /* ---- ⭐ 全板引脚不重复 ---------------------------------------------------- 查的是**全板**,不只是五路那十个脚:重复的两个脚一定分属两个模块, 只查自己那十个的话,「`PRSNT3#` 写成了 IO41」(撞上 SDA)这种错查不出来。 R2 的宏名与值和 R1 **一个都不一样**,所以这张清单是照着 `board_pins.h` 重列的,不是从 R1 那版改出来的。 */ static bool in_list(const int *v, size_t n, int pin) { for (size_t i = 0; i < n; i++) { if (v[i] == pin) { return true; } } return false; } static void test_no_pin_is_used_twice_on_the_whole_board(void) { static const int fixed[] = { BOARD_PIN_I2C_SDA, BOARD_PIN_I2C_SCL, BOARD_PIN_MUX_RST_N, BOARD_PIN_IOEXP_INT_N, BOARD_PIN_INA_ALERT_N, BOARD_PIN_HUB_RST_N, BOARD_PIN_UP_VBUS_DET, BOARD_PIN_NTC1, BOARD_PIN_NTC2, BOARD_PIN_FAN_PWM, BOARD_PIN_FAN_EN, BOARD_PIN_FAN_TACH_IN, BOARD_PIN_FAN_TACH_OUT, BOARD_PIN_USB_DM, BOARD_PIN_USB_DP, }; static const int absent[] = BOARD_PINS_ABSENT; static const int strapping[] = BOARD_PINS_STRAPPING; static const int reserved[] = BOARD_PINS_RESERVED; static const int uart0[] = BOARD_PINS_UART0; int all[sizeof(fixed) / sizeof(fixed[0]) + 2 * N]; size_t n = 0; for (size_t i = 0; i < sizeof(fixed) / sizeof(fixed[0]); i++) { all[n++] = fixed[i]; } for (int rail = 1; rail <= N; rail++) { all[n++] = rail_cfg(rail)->efuse_en; all[n++] = rail_cfg(rail)->prsnt_n; } TEST_ASSERT_EQUAL_UINT32(sizeof(all) / sizeof(all[0]), (uint32_t)n); for (size_t i = 0; i < n; i++) { TEST_ASSERT_TRUE(all[i] >= 0 && all[i] <= BOARD_PIN_NUM_MAX); TEST_ASSERT_FALSE(in_list(absent, sizeof(absent) / sizeof(absent[0]), all[i])); TEST_ASSERT_FALSE(in_list(strapping, sizeof(strapping) / sizeof(strapping[0]), all[i])); TEST_ASSERT_FALSE(in_list(reserved, sizeof(reserved) / sizeof(reserved[0]), all[i])); /* USB 的 D± 本来就在 `fixed` 里,所以 UART0 那条单独查五路那十个 —— `fixed` 里没有 43/44,它们是**留给应急口的空位**,不是已分配的脚。 */ TEST_ASSERT_FALSE(in_list(uart0, sizeof(uart0) / sizeof(uart0[0]), all[i])); for (size_t j = i + 1; j < n; j++) { TEST_ASSERT_NOT_EQUAL(all[i], all[j]); } } } /* 十根脚是**全固件唯一一处**出处 —— 没有第二张表能和它对账 (INA / DAC 两列有驱动那边兜着,这两列没有)。 出处是 `.claude/r2/_base_pcb_nets.json` 里 `U20`(WROOM-1)的真实焊盘网络 (2026-09-16 逐脚核过),改这条之前先回去重核一遍。 ⚠️ 光靠自检查不出「`EFUSE_EN1` 和 `PRSNT1#` 写反了」:两个都是合法的空闲脚、 不重复、不越界。那种错只有这条和板上实测能拦。 */ static void test_rail_pins_match_the_pcb_netlist(void) { /* EFUSE_EN PRSNT# (模组脚号写在注释里) */ static const int want[N][2] = { { 9, 15 }, /* CH1: 脚17 / 脚8 */ { 10, 16 }, /* CH2: 脚18 / 脚9 */ { 11, 17 }, /* CH3: 脚19 / 脚10 */ { 12, 18 }, /* CH4: 脚20 / 脚11 */ { 13, 7 }, /* CH5: 脚21 / 脚7 ← 不接着顺排 */ }; for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_EQUAL_INT(want[rail - 1][0], rail_cfg(rail)->efuse_en); TEST_ASSERT_EQUAL_INT(want[rail - 1][1], rail_cfg(rail)->prsnt_n); } } /* 两路 NTC 必须落在 ADC1(IO1~IO10):ADC2 在 WiFi 开着的时候读不了, 而本板 WiFi 常开(§10.8 传输 A)。 ⚠️ 挪到 ADC2 **不会报错**,只会在连上 WiFi 之后开始读失败 —— 台架上不连 WiFi 一切正常,很难复现。 */ static void test_ntc_pins_are_on_adc1(void) { TEST_ASSERT_TRUE_MESSAGE(BOARD_PIN_NTC1 >= 1 && BOARD_PIN_NTC1 <= 10, "NTC1 必须在 ADC1"); TEST_ASSERT_TRUE_MESSAGE(BOARD_PIN_NTC2 >= 1 && BOARD_PIN_NTC2 <= 10, "NTC2 必须在 ADC1"); /* 自检也得认这件事 —— 上面两句只是在核对头文件,这句核对判据还在。 */ TEST_ASSERT_EQUAL_HEX32(0, rail_selftest_table(rail_cfg(1)) & RAIL_SELFTEST_PINS); } /* ⚠️ 回归拦网:ESP32-S3 的编号是 0~48,而 `FAN_EN` 就在 **IO48** 上。 HAL 两边的边界原来写的是 `pin < 48`,正好把它关在门外 —— 现象是风扇既开不了也关不掉,而固件一路 HAL_OK。 */ static void test_io48_is_a_usable_pin(void) { TEST_ASSERT_EQUAL_INT(48, BOARD_PIN_FAN_EN); TEST_ASSERT_EQUAL_INT(48, BOARD_PIN_NUM_MAX); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_config(48, HAL_GPIO_OUTPUT)); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_write(48, 1)); TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(48)); TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hal_gpio_config(49, HAL_GPIO_OUTPUT)); /* 49 才是越界 */ } /* ---- `EFUSE_EN`:表驱动遍历 ----------------------------------------------- */ static void assert_all_efuse_off(void) { for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(rail)->efuse_en)); TEST_ASSERT_FALSE(rail_efuse_is_on(rail)); } } /* 上电第一件事:五个脚都配成输出、都是低。「五路全关」不许靠某个默认值 —— DAC 上电那一刻输出就有电压了,eFuse 的 EN 这时必须已经确定是低的。 */ static void test_en_init_leaves_all_five_pins_driven_low(void) { TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); assert_all_efuse_off(); for (int rail = 1; rail <= N; rail++) { /* sim_gpio_level() 对没配过的脚返回 −1,所以上面那句已经证明配过了; 这里再确认「真的写过一次 0」,而不只是配置时的默认值。 */ TEST_ASSERT_TRUE(sim_gpio_writes(rail_cfg(rail)->efuse_en) >= 1); } } /* 开一路只动那一路的那一个脚。拦的是「rail−1 当下标」那类错位: 错位之后打开的是别人,而功能上「确实有一路开了」。 */ static void test_enabling_one_rail_touches_exactly_one_pin(void) { TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); for (int on = 1; on <= N; on++) { TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(on, true)); for (int rail = 1; rail <= N; rail++) { const int want = (rail == on) ? 1 : 0; TEST_ASSERT_EQUAL_INT(want, sim_gpio_level(rail_cfg(rail)->efuse_en)); TEST_ASSERT_EQUAL_INT(want, rail_efuse_is_on(rail) ? 1 : 0); /* `PRSNT#` 是输入,一根都不许被当成输出写 —— 写了就把上拉打死了。 */ TEST_ASSERT_EQUAL_UINT32(0, sim_gpio_writes(rail_cfg(rail)->prsnt_n)); } TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(on, false)); } assert_all_efuse_off(); } /* 高有效:true = 导通。写反了「命令开电结果全关」一眼看得出来, 但**关电命令会变成开电**,那才是危险的一半。 */ static void test_enable_is_active_high(void) { TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(1, true)); TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(rail_cfg(1)->efuse_en)); TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(1, false)); TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(1)->efuse_en)); } /* 影子**只在写成功时更新**。A10 的判据靠它决定「这一路现在该不该有电」: 影子说 on、线上其实没写进去,判据就会武装在一条关着的路上, 于是每一圈都得出「偏低 3900 mV」并报过压/欠压故障。 */ static void test_shadow_only_moves_when_the_write_succeeded(void) { TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(3, true)); TEST_ASSERT_TRUE(rail_efuse_is_on(3)); /* 把那个脚改回输入 —— 之后写它必然失败(HAL 会拦)。 */ const int pin = rail_cfg(3)->efuse_en; TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_config(pin, HAL_GPIO_INPUT)); TEST_ASSERT_NOT_EQUAL(HAL_OK, rail_efuse_enable(3, false)); TEST_ASSERT_TRUE_MESSAGE(rail_efuse_is_on(3), "关失败 ⇒ 影子留在 on ⇒ 判据继续武装"); /* 越界的路号既不许崩,也不许把别人的影子改掉。 */ TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, rail_efuse_enable(0, true)); TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, rail_efuse_enable(N + 1, true)); TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, rail_efuse_enable(-1, false)); TEST_ASSERT_FALSE(rail_efuse_is_on(0)); TEST_ASSERT_FALSE(rail_efuse_is_on(N + 1)); for (int rail = 1; rail <= N; rail++) { if (rail != 3) { TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(rail)->efuse_en)); } } } /* ---- `rail_all_off()`:先 I²C 后 GPIO ------------------------------------- */ /* 一块「底板 + 五片刀片」的假板子:假 TCA9548A 挂主干,五片假 PCA9555 各在自己那条支路上(五片同址 0x20,正是 R2 的设计)。 */ static void mount_board(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); for (int rail = 1; rail <= N; rail++) { sim_pca9555_attach_ch(rail - 1, slot_mux_ch(rail), PCA9555_ADDR); } TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_init()); for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_EQUAL_INT(HAL_OK, slot_init(rail)); } } /* 五路全开:eFuse(GPIO)+ VBUS(I²C)。 */ static void power_everything_up(void) { for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(rail, true)); TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(rail, true)); TEST_ASSERT_EQUAL_INT(1, sim_pca9555_pin(rail - 1, 0, PCA9555_BIT_VBUS_EN)); } } /* 「先断 VBUS 再关 eFuse」在 sim 上**没有现成的观测面**:VBUS 走 I²C、 eFuse 走 GPIO,两个计数器不在同一个序列里(`sim_gpio_write_seq()` 只数 GPIO)。 所以这里在假 PCA9555 的写钩子上**串一层**:每当有人写那片 PCA9555 (断 VBUS 就是这么断的),顺手抄一眼五根 `EFUSE_EN` 的电平。 进 `rail_all_off()` 之前五根都是高,所以只要抄到一个 0, 就说明 eFuse 已经被提前关掉了 —— 那正是要拦的顺序反转。 */ static sim_i2c_hook_t s_orig_hook; static uint32_t s_efuse_low_seen; static void order_spy(sim_i2c_dev_t *d, sim_i2c_ev_t ev) { if (ev == SIM_I2C_EV_POST_WRITE) { for (int rail = 1; rail <= N; rail++) { if (sim_gpio_level(rail_cfg(rail)->efuse_en) == 0) { s_efuse_low_seen++; } } } if (s_orig_hook != NULL) { s_orig_hook(d, ev); /* 假 PCA9555 自己那一层不能被顶掉 */ } } static void install_order_spy(void) { s_efuse_low_seen = 0; s_orig_hook = NULL; for (int rail = 1; rail <= N; rail++) { sim_i2c_dev_t *d = sim_i2c_dev_ch(slot_mux_ch(rail), PCA9555_ADDR); TEST_ASSERT_NOT_NULL(d); if (s_orig_hook == NULL) { s_orig_hook = d->hook; /* 五片挂的是同一个函数 */ } TEST_ASSERT_EQUAL_PTR(s_orig_hook, d->hook); d->hook = order_spy; } } /* 全关:十次操作全做完,而且**五路 VBUS 全部早于任何一根 eFuse**。 反过来的话中间有一小段「手机只靠 USB 供着电」,那既不是开也不是关 —— 手机会在那一瞬间掉到 USB 限流上,日志里看不出任何异常。 */ static void test_all_off_cuts_vbus_before_efuse(void) { mount_board(); power_everything_up(); install_order_spy(); TEST_ASSERT_EQUAL_INT(HAL_OK, rail_all_off()); assert_all_efuse_off(); for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_EQUAL_INT(0, sim_pca9555_pin(rail - 1, 0, PCA9555_BIT_VBUS_EN)); TEST_ASSERT_FALSE(slot_vbus_is_on(rail)); } TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, s_efuse_low_seen, "断 VBUS 的时候 eFuse 还不许关"); } /* ⭐ R2 的**已知降级**(计划 §5.4,是决定不是 bug): 前半段断 VBUS 要走 I²C,mux / PCA9555 失联时**必然失败**; 后半段关 eFuse 是 GPIO,永远成功。 所以最坏情况下 `rail_all_off()` 的语义是 「五路直供确实断了,但 VBUS 可能还通着」—— 手机仍靠 USB 的 5 V 吊着命。 这是 R2 用一根跨板针换来的代价。这条用例把它钉住: 哪天有人「顺手」让 VBUS 失败就提前 return,五路直供就再也关不掉了。 */ static void test_all_off_still_cuts_every_efuse_when_i2c_is_dead(void) { mount_board(); power_everything_up(); /* 五片 PCA9555 一起失联(真板上的对应情形:mux 挂了 / 刀片供电塌了)。 */ for (int rail = 1; rail <= N; rail++) { sim_i2c_inject_nak_ch(slot_mux_ch(rail), PCA9555_ADDR, -1); } TEST_ASSERT_EQUAL_INT_MESSAGE(HAL_ERR_NAK, rail_all_off(), "如实报第一个错误码"); /* 直供全断 —— 这一半是保证的。 */ assert_all_efuse_off(); /* VBUS **还通着** —— 这一半不保证,正是那笔交易的代价。 写成断言是为了让「哪天它变了」必须先来改这条注释。 */ for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_EQUAL_INT_MESSAGE(1, sim_pca9555_pin(rail - 1, 0, PCA9555_BIT_VBUS_EN), "I²C 死了 VBUS 就关不掉,这是 R2 的已知代价"); } } /* 「十次操作全部试一遍」:只让**第一路**的 PCA9555 失联。 在第一个错上就 return 的写法,会让另外四路的 VBUS 与全部五路的 eFuse 都留着。 */ static void test_all_off_keeps_going_after_the_first_failure(void) { mount_board(); power_everything_up(); sim_i2c_inject_nak_ch(slot_mux_ch(1), PCA9555_ADDR, -1); TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, rail_all_off()); assert_all_efuse_off(); /* 五根 eFuse 一根都不许剩 */ TEST_ASSERT_EQUAL_INT(1, sim_pca9555_pin(0, 0, PCA9555_BIT_VBUS_EN)); /* 这一路关不掉 */ for (int rail = 2; rail <= N; rail++) { TEST_ASSERT_EQUAL_INT_MESSAGE(0, sim_pca9555_pin(rail - 1, 0, PCA9555_BIT_VBUS_EN), "第一路失败不许连累后面四路"); } } /* 没 `rail_en_init()`(也没有总线)就调:返回错误码,不崩、不乱写。 这是复位路径上真会发生的情形 —— 早期自检失败就直接走收尾。 */ static void test_all_off_before_init_reports_an_error(void) { TEST_ASSERT_NOT_EQUAL(HAL_OK, rail_all_off()); for (int rail = 1; rail <= N; rail++) { TEST_ASSERT_EQUAL_INT(-1, sim_gpio_level(rail_cfg(rail)->efuse_en)); /* 没配过 */ } } int main(void) { UNITY_BEGIN(); RUN_TEST(test_selftest_passes); RUN_TEST(test_selftest_covers_every_driver_table); RUN_TEST(test_selftest_str_names_every_bit); RUN_TEST(test_a_faithful_copy_of_the_real_table_passes); RUN_TEST(test_a_cell_that_drifted_from_the_driver_is_caught); RUN_TEST(test_every_kind_of_bad_mux_channel_is_caught); RUN_TEST(test_every_kind_of_bad_pin_is_caught); RUN_TEST(test_three_kinds_of_breakage_report_three_bits); RUN_TEST(test_sequential_columns_are_facts_not_invariants); RUN_TEST(test_dac_mapping_is_not_the_obvious_one); RUN_TEST(test_all_five_ina226_share_one_address_and_mux_is_what_separates_them); RUN_TEST(test_the_most_surprising_cells); RUN_TEST(test_table_matches_every_driver_table); RUN_TEST(test_no_two_rails_share_anything); RUN_TEST(test_every_field_is_in_range); RUN_TEST(test_no_pin_is_used_twice_on_the_whole_board); RUN_TEST(test_rail_pins_match_the_pcb_netlist); RUN_TEST(test_ntc_pins_are_on_adc1); RUN_TEST(test_io48_is_a_usable_pin); RUN_TEST(test_en_init_leaves_all_five_pins_driven_low); RUN_TEST(test_enabling_one_rail_touches_exactly_one_pin); RUN_TEST(test_enable_is_active_high); RUN_TEST(test_shadow_only_moves_when_the_write_succeeded); RUN_TEST(test_all_off_cuts_vbus_before_efuse); RUN_TEST(test_all_off_still_cuts_every_efuse_when_i2c_is_dead); RUN_TEST(test_all_off_keeps_going_after_the_first_failure); RUN_TEST(test_all_off_before_init_reports_an_error); return UNITY_END(); }