固件: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]>
这个提交包含在:
+578
-233
@@ -1,230 +1,578 @@
|
||||
/* A6:PCA9555 驱动。
|
||||
验收主项:**方向寄存器永远不会把 Port0[7] / Port1[7:5] 配成输出** ——
|
||||
那四个脚在板上直接接地,配成输出等于对地短路,而真器件既不报错也不 NAK。
|
||||
第二主项:FLT / PG / BUCK_PG / INA_ALERT 三种极性一个都不许写反。 */
|
||||
/* A6(**R2 版**):刀片 PCA9555 驱动 —— 每片刀片一颗,五片同址 0x20,
|
||||
靠底板 TCA9548A 分通道;**P0.0~P0.4 是输出**。
|
||||
|
||||
R1 那版测试的前提(一颗、16 脚全是输入)一条都不成立了,所以整篇重写。
|
||||
|
||||
本文件要证明的三件「真会出事」的事,按危险程度排:
|
||||
|
||||
⭐① **初始化顺序就是安全本身。** Output Port 复位值是 0xFF,Config 复位值
|
||||
也是 0xFF(全输入)。只要先改方向、后写锁存器,那一刻 P0.0~P0.4 一起
|
||||
输出 1 = 三个键全按下 + EDL 短接 + VBUS 上电,五台手机一起被按住电源键,
|
||||
**而 I²C 从头到尾一切正常**:不 NAK、不超时、回读也对。
|
||||
拦网是 `sim_pca9555_unsafe_enable()`,必须恒为 0。
|
||||
⭐② **接地脚永远不许被配成输出。** P0.5~7 / P1.4~7 在刀片上直接接地、不串
|
||||
限流电阻,配成输出就是对地短路 —— 真器件不报错、不 NAK,只会发烫。
|
||||
Port1 更严:bit0~3 接的是 eFuse / Buck / INA 的**开漏输出**,配成输出
|
||||
是两个驱动器对打,一位都不许。
|
||||
⭐③ **五片的影子互不串。** 这是 R2 新增的失败形态:影子记到了另一片上,
|
||||
下一次写会把那一片的键"顺手"改掉,而五路电压电流全部正常。
|
||||
|
||||
断言一律打在假器件的可观测面(`sim_pca9555_*`)上。对着 `pca9555_shadow()`
|
||||
断言等于自己证明自己 —— 影子和线上真实发生的事分家,正是这里要抓的错。
|
||||
|
||||
⚠️ 本驱动**不碰 mux**(契约:通道由调用方先选好),所以测试自己拿
|
||||
`tca9548a_select()` 选通道。约定 idx i ↔ 通道 i(只是本文件的约定,
|
||||
路号 → 通道的真表在 `rail_cfg.c`,那是 `test_slot.c` 的事)。 */
|
||||
#include "pca9555.h"
|
||||
|
||||
#include "board_pins.h"
|
||||
#include "i2cbus.h"
|
||||
#include "sim_hal.h"
|
||||
#include "tca9548a.h"
|
||||
#include "unity.h"
|
||||
|
||||
void setUp(void) { sim_hal_reset(); }
|
||||
void tearDown(void) {}
|
||||
/* ---- 脚手架 -------------------------------------------------------------- */
|
||||
|
||||
static void mount(void)
|
||||
{
|
||||
sim_pca9555_attach(PCA9555_ADDR);
|
||||
sim_hal_reset();
|
||||
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 i = 0; i < PCA9555_SLOT_COUNT; i++) {
|
||||
sim_pca9555_attach_ch(i, i, PCA9555_ADDR);
|
||||
}
|
||||
/* 驱动的影子是文件级静态,跨用例不会自己清 —— 每条用例都从「刚上电」开始。 */
|
||||
pca9555_shadow_reset();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_init()); /* 顺带把通道全关 */
|
||||
}
|
||||
|
||||
/* 挂好并 init 完,外部电平停在静息值(全高 = 无故障、全部良好)。 */
|
||||
static void mount_ready(void)
|
||||
void setUp(void) { mount(); }
|
||||
void tearDown(void) {}
|
||||
|
||||
/* 选到第 idx 片。驱动的契约就是「调用方先选好」,这里代的是 `slot.c` 的班。 */
|
||||
static void sel(int idx) { TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_select(TCA9548A_ADDR, idx)); }
|
||||
|
||||
static void init_all(void)
|
||||
{
|
||||
mount();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_init());
|
||||
for (int i = 0; i < PCA9555_SLOT_COUNT; i++) {
|
||||
sel(i);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_init(i));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 验收主项一:接地脚永远是输入 ---------------------------------------- */
|
||||
|
||||
/* init 之后两个方向寄存器都必须是 0xFF —— 不是「大致全输入」,是逐位。 */
|
||||
static void test_init_configures_every_pin_as_input(void)
|
||||
/* 绕开驱动往当前通道上那片写一个寄存器。用来演示「假器件真的会照出问题」。 */
|
||||
static hal_err_t raw_write(uint8_t reg, uint8_t val)
|
||||
{
|
||||
mount_ready();
|
||||
TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_config(0));
|
||||
TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_config(1));
|
||||
const uint8_t wr[2] = { reg, val };
|
||||
return hal_i2c_write(PCA9555_ADDR, wr, sizeof(wr));
|
||||
}
|
||||
|
||||
/* ---- ⭐① 初始化顺序 ------------------------------------------------------ */
|
||||
|
||||
/* 五片全部 init 完,**一次都不许出现「锁存器还是 0xFF 就把 Port0 配成输出」**。
|
||||
写错的现象:五台手机一起被按住电源键 + VBUS 全部上电,持续到下一次写锁存器
|
||||
(几十 µs~几 ms),足够让手机认到一次长按。I²C 侧一切正常,日志里什么都没有。
|
||||
把 `pca9555_init()` 里的 CONFIG0 那两句挪到 OUTPUT0 之前,这条立刻红。 */
|
||||
static void test_init_never_enables_outputs_while_the_latch_is_still_ff(void)
|
||||
{
|
||||
init_all();
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_unsafe_enable());
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_gnd_driven());
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_shorted());
|
||||
|
||||
for (int i = 0; i < PCA9555_SLOT_COUNT; i++) {
|
||||
TEST_ASSERT_EQUAL_HEX8(PCA9555_CONFIG0_INIT, sim_pca9555_config(i, 0));
|
||||
TEST_ASSERT_EQUAL_HEX8(PCA9555_CONFIG1_INIT, sim_pca9555_config(i, 1));
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_output(i, 0)); /* 复位值 0xFF 已被清掉 */
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_output(i, 1));
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_polinv(i, 0));
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_polinv(i, 1));
|
||||
}
|
||||
}
|
||||
|
||||
/* dir_safe 的判据穷举:**当且仅当**接地位全为 1(输入)才算安全。
|
||||
写成「dir != 0」「dir & 0x80」这类近似判据的,会在这 512 个组合里露馅。 */
|
||||
static void test_dir_safe_is_exactly_the_grounded_pins(void)
|
||||
/* 上一条的加强版:init 完**每一个输出脚在栅极上的实际电平**都得是 0。
|
||||
三个键松开、EDL 不导通、VBUS 断。接地脚必须是高阻(-1),不是 0 —— 这两者
|
||||
在寄存器上长得一样,在栅极上差着一条对地通路。 */
|
||||
static void test_after_init_every_output_pin_is_released(void)
|
||||
{
|
||||
init_all();
|
||||
|
||||
for (int i = 0; i < PCA9555_SLOT_COUNT; i++) {
|
||||
for (int bit = 0; bit <= PCA9555_BIT_VBUS_EN; bit++) {
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_pca9555_pin(i, 0, bit));
|
||||
}
|
||||
for (int bit = 5; bit < 8; bit++) {
|
||||
TEST_ASSERT_EQUAL_INT(-1, sim_pca9555_pin(i, 0, bit)); /* 接地脚 = 输入 = 高阻 */
|
||||
}
|
||||
for (int bit = 0; bit < 8; bit++) {
|
||||
TEST_ASSERT_EQUAL_INT(-1, sim_pca9555_pin(i, 1, bit)); /* Port1 一位都不输出 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ⛔ 这条证明上面那道拦网**不是句空话**。手工按「有人把顺序调过来了」写一次
|
||||
CONFIG0:此刻锁存器还是复位值 0xFF ⇒ 五个输出脚一起输出高。
|
||||
注意 `gnd_driven` 仍然是 0 —— 方向字本身是对的,错的只有**时机**。
|
||||
两个计数分开,就是为了让「顺序错」和「方向错」不互相掩护。 */
|
||||
static void test_sim_catches_the_reversed_init_order(void)
|
||||
{
|
||||
sel(0);
|
||||
TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_output(0, 0)); /* 复位值,这才是要命的地方 */
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, raw_write(PCA9555_REG_CONFIG0, PCA9555_CONFIG0_INIT));
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_unsafe_enable());
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_gnd_driven());
|
||||
/* 这一刻线上真实发生的事:三个键被按住 + EDL 短接 + VBUS 上电。 */
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_pca9555_pin(0, 0, PCA9555_BIT_KEY_PWR));
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_pca9555_pin(0, 0, PCA9555_BIT_EDL_EN));
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_pca9555_pin(0, 0, PCA9555_BIT_VBUS_EN));
|
||||
}
|
||||
|
||||
/* 极性反转是「看门狗复位后残留」的重灾区:ESP32 重启了,刀片上的 PCA9555 没掉电,
|
||||
上一条命写进去的值原样留着。init 必须**无条件清零**,不是「复位值本来就是 0」。
|
||||
写成非 0 的现象是「故障全都看不见」,而 I²C 一切正常。 */
|
||||
static void test_init_clears_a_stale_polarity_inversion(void)
|
||||
{
|
||||
sel(2);
|
||||
sim_i2c_set_reg_ch(2, PCA9555_ADDR, PCA9555_REG_POLINV0, 0xFF); /* 上一条命的残留 */
|
||||
sim_i2c_set_reg_ch(2, PCA9555_ADDR, PCA9555_REG_POLINV1, 0x0F);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_init(2));
|
||||
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_polinv(2, 0));
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_polinv(2, 1));
|
||||
}
|
||||
|
||||
/* 半路失败必须立刻停 —— 尤其不许「输出锁存器没写成,方向照样改」:
|
||||
那正好凑成了顺序反过来的那一幕。这里让头两次事务 NAK(写 OUTPUT0 + 回读),
|
||||
之后总线恢复正常,看驱动会不会接着把方向写下去。 */
|
||||
static void test_init_stops_at_the_first_failure_and_leaves_the_pins_as_inputs(void)
|
||||
{
|
||||
sel(1);
|
||||
sim_i2c_inject_nak_ch(1, PCA9555_ADDR, 1);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_init(1));
|
||||
|
||||
TEST_ASSERT_EQUAL_HEX8(PCA9555_DIR_ALL_INPUT, sim_pca9555_config(1, 0)); /* 方向没被碰 */
|
||||
TEST_ASSERT_EQUAL_HEX8(PCA9555_DIR_ALL_INPUT, sim_pca9555_config(1, 1));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_unsafe_enable());
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_gnd_driven());
|
||||
}
|
||||
|
||||
/* 回读校验:写下去了、读回来不是那个值 = 这地址上蹲着的不是我们以为的那片。
|
||||
必须失败而不是接着往下配方向。用「写成功但回读时 NAK」造这条错路。 */
|
||||
static void test_init_fails_when_the_readback_does_not_come_back(void)
|
||||
{
|
||||
sel(3);
|
||||
sim_i2c_inject_nak_ch(3, PCA9555_ADDR, 2); /* 第 1 次写过、第 2 次(回读)起 NAK */
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_init(3));
|
||||
TEST_ASSERT_EQUAL_HEX8(PCA9555_DIR_ALL_INPUT, sim_pca9555_config(3, 0));
|
||||
}
|
||||
|
||||
static void test_init_rejects_a_bad_slot(void)
|
||||
{
|
||||
sel(0);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, pca9555_init(-1));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, pca9555_init(PCA9555_SLOT_COUNT));
|
||||
TEST_ASSERT_EQUAL_HEX8(PCA9555_DIR_ALL_INPUT, sim_pca9555_config(0, 0)); /* 什么都没发生 */
|
||||
}
|
||||
|
||||
/* ---- ⭐② 方向白名单 ------------------------------------------------------ */
|
||||
|
||||
/* Port0 的判据穷举 256 个方向字:**当且仅当 bit5~7 全是 1(输入)才安全**。
|
||||
写成「dir != 0」「dir & 0x80」这类近似判据的,会在这 256 个组合里露馅。
|
||||
⚠️ bit0~4 随意 —— 它们本来就是要当输出的,多配少配是功能问题不是安全问题。 */
|
||||
static void test_dir_safe_port0_only_guards_the_grounded_pins(void)
|
||||
{
|
||||
for (int dir = 0; dir <= 0xFF; dir++) {
|
||||
const bool p0_want = ((uint8_t)dir & PCA9555_P0_GND_MASK) == PCA9555_P0_GND_MASK;
|
||||
const bool p1_want = ((uint8_t)dir & PCA9555_P1_GND_MASK) == PCA9555_P1_GND_MASK;
|
||||
TEST_ASSERT_EQUAL_INT(p0_want, pca9555_dir_safe(0, (uint8_t)dir));
|
||||
TEST_ASSERT_EQUAL_INT(p1_want, pca9555_dir_safe(1, (uint8_t)dir));
|
||||
const bool want = ((uint8_t)dir & PCA9555_P0_GND_MASK) == PCA9555_P0_GND_MASK;
|
||||
TEST_ASSERT_EQUAL_INT(want, pca9555_dir_safe(0, (uint8_t)dir));
|
||||
}
|
||||
/* 几个会被真写出来的错值 */
|
||||
TEST_ASSERT_FALSE(pca9555_dir_safe(0, 0x7F)); /* 只把 P0_7 配成输出:最像笔误的一个 */
|
||||
TEST_ASSERT_FALSE(pca9555_dir_safe(1, 0x1F)); /* 「低五位是 PG,高三位不用」 */
|
||||
TEST_ASSERT_FALSE(pca9555_dir_safe(1, 0x00)); /* 全输出 */
|
||||
TEST_ASSERT_TRUE(pca9555_dir_safe(0, PCA9555_DIR_ALL_INPUT));
|
||||
TEST_ASSERT_TRUE(pca9555_dir_safe(1, PCA9555_DIR_ALL_INPUT));
|
||||
TEST_ASSERT_FALSE(pca9555_dir_safe(2, PCA9555_DIR_ALL_INPUT)); /* 端口号越界 */
|
||||
TEST_ASSERT_TRUE(pca9555_dir_safe(0, PCA9555_CONFIG0_INIT)); /* 定稿值 0xE0 */
|
||||
TEST_ASSERT_TRUE(pca9555_dir_safe(0, PCA9555_DIR_ALL_INPUT)); /* 全输入永远安全 */
|
||||
TEST_ASSERT_FALSE(pca9555_dir_safe(0, 0x60)); /* 顺手把 bit7 也配成输出:最像笔误的一个 */
|
||||
TEST_ASSERT_FALSE(pca9555_dir_safe(0, 0x00)); /* 「全配成输出反正用不着」 */
|
||||
TEST_ASSERT_FALSE(pca9555_dir_safe(0, 0xE0u & ~0x20u)); /* 只错一位也是对地短路 */
|
||||
}
|
||||
|
||||
/* 这条证明的是**假器件真的会照出短路**,否则上面那条「shorted == 0」是句空话。
|
||||
手工绕过驱动,按「有人把 P0_7 配成输出」写一次方向寄存器:
|
||||
PCA9555 的 Output Port 复位值是 0xFF,所以这一刻它就在对地灌电流。 */
|
||||
/* Port1 **一位都不许输出**,所以只有 0xFF 一个合法值。
|
||||
bit0~3 接的是 eFuse `FLT#` / Buck `PWRGD` / INA `ALERT` 的开漏输出:配成输出
|
||||
= 我们的推挽和对方的开漏对着灌,现象是「故障永远读不到」+ 那颗芯片过热;
|
||||
bit4~7 焊死在地上,配成输出直接对地短路。两种后果都不 NAK、不报错。 */
|
||||
static void test_dir_safe_port1_allows_nothing_to_be_an_output(void)
|
||||
{
|
||||
for (int dir = 0; dir <= 0xFF; dir++) {
|
||||
TEST_ASSERT_EQUAL_INT(dir == PCA9555_DIR_ALL_INPUT, pca9555_dir_safe(1, (uint8_t)dir));
|
||||
}
|
||||
TEST_ASSERT_FALSE(pca9555_dir_safe(1, 0xF0)); /* 「低四位是信号,配成输出读得快」 */
|
||||
TEST_ASSERT_FALSE(pca9555_dir_safe(1, 0x0F)); /* 「高四位不用,随便配」 */
|
||||
TEST_ASSERT_TRUE(pca9555_dir_safe(1, PCA9555_CONFIG1_INIT));
|
||||
}
|
||||
|
||||
/* 端口号越界一律 false(不是「当成 0 号口」)。 */
|
||||
static void test_dir_safe_rejects_a_bad_port(void)
|
||||
{
|
||||
TEST_ASSERT_FALSE(pca9555_dir_safe(2, PCA9555_DIR_ALL_INPUT));
|
||||
TEST_ASSERT_FALSE(pca9555_dir_safe(-1, PCA9555_DIR_ALL_INPUT));
|
||||
}
|
||||
|
||||
/* 整篇跑下来一次都不许碰接地脚:init 五片 + 各种输出写 + 读输入。 */
|
||||
static void test_grounded_pins_are_never_driven_on_any_blade(void)
|
||||
{
|
||||
init_all();
|
||||
for (int i = 0; i < PCA9555_SLOT_COUNT; i++) {
|
||||
sel(i);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK,
|
||||
pca9555_write_out0(i, PCA9555_P0_OUT_MASK, PCA9555_P0_OUT_MASK));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_write_out0(i, PCA9555_P0_OUT_MASK, 0x00));
|
||||
pca9555_in_t in;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
}
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_gnd_driven());
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_shorted());
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous()); /* 通道从没开重过 */
|
||||
}
|
||||
|
||||
/* 证明上面那两个计数不是空话:手工把 P0.7(接地)配成输出。
|
||||
锁存器还是 0xFF ⇒ 立刻对地短路(`shorted` 也 +1)。 */
|
||||
static void test_sim_catches_a_grounded_pin_driven_high(void)
|
||||
{
|
||||
mount_ready();
|
||||
|
||||
TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_output(0)); /* 复位值就是全高,这才是要命的地方 */
|
||||
|
||||
const uint8_t wr[2] = { PCA9555_REG_CONFIG0, 0x7F };
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write(PCA9555_ADDR, wr, sizeof(wr)));
|
||||
sel(4);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, raw_write(PCA9555_REG_CONFIG0, 0x7F));
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_gnd_driven());
|
||||
TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_shorted());
|
||||
TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_shorted()); /* 真板子上这一刻开始冒烟 */
|
||||
}
|
||||
|
||||
/* 同上,但先把输出锁存器写成 0 再配成输出:不冒烟,可仍然是在驱动一个接地脚。
|
||||
两个计数分开就是为了让这一格不被漏掉。 */
|
||||
/* 同上,但先把锁存器写成 0 再配成输出:不冒烟,可仍然是在驱动一个接地脚
|
||||
(下一次写 Port0 就会把它拉高)。两个计数分开就是为了不漏掉这一格。 */
|
||||
static void test_sim_counts_driving_a_grounded_pin_even_when_low(void)
|
||||
{
|
||||
mount_ready();
|
||||
|
||||
const uint8_t out0[2] = { PCA9555_REG_OUTPUT0, 0x00 };
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write(PCA9555_ADDR, out0, sizeof(out0)));
|
||||
const uint8_t cfg0[2] = { PCA9555_REG_CONFIG0, 0x7F };
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write(PCA9555_ADDR, cfg0, sizeof(cfg0)));
|
||||
sel(4);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, raw_write(PCA9555_REG_OUTPUT0, 0x00));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, raw_write(PCA9555_REG_CONFIG0, 0x7F));
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_gnd_driven());
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_shorted());
|
||||
}
|
||||
|
||||
/* 驱动全程一个字节都不许写 Output Port:没有输出脚,写它只可能是写错寄存器号。 */
|
||||
static void test_init_never_touches_the_output_registers(void)
|
||||
/* Port1 被配成输出:`gnd_driven` 只看 bit4~7,所以这里同时钉住
|
||||
「dir_safe 把 bit0~3 也管住了」这件事 —— 它是判据,不是假器件能照出来的。 */
|
||||
static void test_sim_catches_port1_configured_as_output(void)
|
||||
{
|
||||
mount_ready();
|
||||
TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_output(0));
|
||||
TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_output(1));
|
||||
sel(0);
|
||||
TEST_ASSERT_FALSE(pca9555_dir_safe(1, 0x0F));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, raw_write(PCA9555_REG_CONFIG1, 0x0F)); /* bit4~7 输出 */
|
||||
TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_gnd_driven());
|
||||
}
|
||||
|
||||
/* ---- 极性反转寄存器 ------------------------------------------------------ */
|
||||
/* ---- 位分配与地址 -------------------------------------------------------- */
|
||||
|
||||
/* 极性反转是「看门狗复位后残留」的重灾区:ESP32 重启了,PCA9555 没掉电。
|
||||
init 必须无条件清零,而不是「复位值本来就是 0」。 */
|
||||
static void test_init_clears_a_stale_polarity_inversion(void)
|
||||
static void test_bit_map_is_self_consistent(void)
|
||||
{
|
||||
mount();
|
||||
sim_i2c_set_reg(PCA9555_ADDR, PCA9555_REG_POLINV0, 0xFF); /* 上一条命的残留 */
|
||||
sim_i2c_set_reg(PCA9555_ADDR, PCA9555_REG_POLINV1, 0x1F);
|
||||
TEST_ASSERT_TRUE(pca9555_map_ok());
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_init());
|
||||
/* 五个输出位正好铺满允许配成输出的那五位,一位不多一位不少。 */
|
||||
const uint8_t out_bits = (uint8_t)((1u << PCA9555_BIT_KEY_PWR) | (1u << PCA9555_BIT_KEY_VUP) |
|
||||
(1u << PCA9555_BIT_KEY_VDN) | (1u << PCA9555_BIT_EDL_EN) |
|
||||
(1u << PCA9555_BIT_VBUS_EN));
|
||||
TEST_ASSERT_EQUAL_HEX8(PCA9555_P0_OUT_MASK, out_bits);
|
||||
TEST_ASSERT_EQUAL_HEX8(0, (uint8_t)(out_bits & PCA9555_P0_GND_MASK)); /* 没落到接地脚上 */
|
||||
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_polinv(0));
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_polinv(1));
|
||||
const uint8_t in_bits = (uint8_t)((1u << PCA9555_BIT_EFUSE_FLT) | (1u << PCA9555_BIT_BUCK_PG) |
|
||||
(1u << PCA9555_BIT_INA_ALERT) | (1u << PCA9555_BIT_EFUSE_PG));
|
||||
TEST_ASSERT_EQUAL_HEX8(PCA9555_P1_IN_MASK, in_bits);
|
||||
TEST_ASSERT_EQUAL_HEX8(0, (uint8_t)(in_bits & PCA9555_P1_GND_MASK));
|
||||
|
||||
/* 方向字与位分配自洽:Port0 上「不是输入」的正好是那五个输出位。 */
|
||||
TEST_ASSERT_EQUAL_HEX8(PCA9555_P0_OUT_MASK, (uint8_t)~PCA9555_CONFIG0_INIT);
|
||||
TEST_ASSERT_EQUAL_HEX8(PCA9555_DIR_ALL_INPUT, PCA9555_CONFIG1_INIT);
|
||||
|
||||
/* 两个端口的掩码各自铺满 8 位 —— 漏一位就是「有个脚没人管」。 */
|
||||
TEST_ASSERT_EQUAL_HEX8(0xFF, (uint8_t)(PCA9555_P0_OUT_MASK | PCA9555_P0_GND_MASK));
|
||||
TEST_ASSERT_EQUAL_HEX8(0xFF, (uint8_t)(PCA9555_P1_IN_MASK | PCA9555_P1_GND_MASK));
|
||||
}
|
||||
|
||||
/* ---- 验收主项二:三种极性 ------------------------------------------------ */
|
||||
|
||||
/* 静息态(两个端口都被上拉到 0xFF)= 五路无故障、五路电源良好、Buck 正常、无告警。
|
||||
任何一处极性写反,这条立刻红。 */
|
||||
static void test_idle_bus_means_everything_is_fine(void)
|
||||
/* 三个键的位号互不重复、都在输出白名单里。`pca9555_key_mask()` 是
|
||||
`t_actuator` 的「防御性松键」用的那个掩码,少一位就是「有个键松不掉」。 */
|
||||
static void test_key_bits_and_mask(void)
|
||||
{
|
||||
mount_ready();
|
||||
TEST_ASSERT_EQUAL_INT(PCA9555_BIT_KEY_PWR, pca9555_key_bit(PCA9555_KEY_PWR));
|
||||
TEST_ASSERT_EQUAL_INT(PCA9555_BIT_KEY_VUP, pca9555_key_bit(PCA9555_KEY_VUP));
|
||||
TEST_ASSERT_EQUAL_INT(PCA9555_BIT_KEY_VDN, pca9555_key_bit(PCA9555_KEY_VDN));
|
||||
|
||||
const uint8_t m = pca9555_key_mask();
|
||||
TEST_ASSERT_EQUAL_HEX8(0x07, m);
|
||||
TEST_ASSERT_EQUAL_HEX8(0, (uint8_t)(m & PCA9555_P0_GND_MASK)); /* 松键不许碰接地脚 */
|
||||
TEST_ASSERT_EQUAL_HEX8(0, (uint8_t)(m & (1u << PCA9555_BIT_VBUS_EN))); /* 也不许碰 VBUS */
|
||||
TEST_ASSERT_EQUAL_HEX8(0, (uint8_t)(m & (1u << PCA9555_BIT_EDL_EN)));
|
||||
}
|
||||
|
||||
/* 地址撞车 = 两片一起应答,读回来是线与(数值正常、还很稳),一层都不报错。
|
||||
⚠️ 刀片支路上的 INA226 是 **0x40**,与 0x20 本来就不是同一个地址,所以它同在
|
||||
一条支路上没问题 —— 真正要防的是「有人把 PCA9555 的 A0 焊成 1 变 0x21」,
|
||||
或者哪天往主干上加器件时选了 0x20。 */
|
||||
static void test_address_does_not_collide(void)
|
||||
{
|
||||
TEST_ASSERT_TRUE(pca9555_addr_ok());
|
||||
TEST_ASSERT_EQUAL_HEX8(0x20, PCA9555_ADDR); /* A0=A1=A2=GND(U4 的 pin21/2/3) */
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(TCA9548A_ADDR, PCA9555_ADDR); /* 0x70:撞了 = 选通道变成写 IO */
|
||||
TEST_ASSERT_NOT_EQUAL(0x43, PCA9555_ADDR); /* 输入侧 INA226 */
|
||||
TEST_ASSERT_NOT_EQUAL(0x4A, PCA9555_ADDR); /* DAC63204 ×2 */
|
||||
TEST_ASSERT_NOT_EQUAL(0x4B, PCA9555_ADDR);
|
||||
TEST_ASSERT_NOT_EQUAL(0x40, PCA9555_ADDR); /* 同支路的刀片 INA226 —— 也不撞 */
|
||||
|
||||
/* 五片同址是**设计**不是笔误:假总线允许它,靠通道区分(sim_i2c_add_ch 的 assert
|
||||
只管同一条支路)。这一条把「哪天有人想给五片分配不同地址」钉死。 */
|
||||
for (int i = 0; i < PCA9555_SLOT_COUNT; i++) {
|
||||
sel(i);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_probe()); /* 五个通道上都是同一个地址 */
|
||||
}
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous());
|
||||
}
|
||||
|
||||
/* ---- ⭐③ 输出写:读改写 + 五片互不串 ------------------------------------ */
|
||||
|
||||
/* 读改写语义:只动 mask 里的位,其余位保持影子里的值。
|
||||
组合键(VUP+PWR)必须落在**同一次写**里 —— 分两次写会先单按一个键,
|
||||
小米 8 在那几毫秒里可能真的响应了单键(§10.6)。 */
|
||||
static void test_write_out0_only_touches_the_masked_bits(void)
|
||||
{
|
||||
init_all();
|
||||
sel(0);
|
||||
|
||||
/* 先按住 VBUS(它在后面每一步里都必须纹丝不动)。 */
|
||||
const uint8_t vbus = (uint8_t)(1u << PCA9555_BIT_VBUS_EN);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_write_out0(0, vbus, vbus));
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_pca9555_pin(0, 0, PCA9555_BIT_VBUS_EN));
|
||||
|
||||
/* 组合键:两位一次写下去。 */
|
||||
const uint8_t combo = (uint8_t)((1u << PCA9555_BIT_KEY_PWR) | (1u << PCA9555_BIT_KEY_VUP));
|
||||
const sim_i2c_dev_t *d = sim_i2c_dev_ch(0, PCA9555_ADDR);
|
||||
const uint32_t w0 = d->n_write;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_write_out0(0, combo, combo));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, d->n_write - w0); /* 一次事务,不是两次 */
|
||||
|
||||
TEST_ASSERT_EQUAL_HEX8((uint8_t)(combo | vbus), sim_pca9555_output(0, 0));
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_pca9555_pin(0, 0, PCA9555_BIT_VBUS_EN)); /* 没被顺手清掉 */
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_pca9555_pin(0, 0, PCA9555_BIT_KEY_VDN));
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_pca9555_pin(0, 0, PCA9555_BIT_EDL_EN));
|
||||
|
||||
/* 松键:只清这两位,VBUS 还在。 */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_write_out0(0, pca9555_key_mask(), 0x00));
|
||||
TEST_ASSERT_EQUAL_HEX8(vbus, sim_pca9555_output(0, 0));
|
||||
}
|
||||
|
||||
/* mask 碰到输出白名单以外的位 → `HAL_ERR_ARG`,而且**总线上什么都没发生**。
|
||||
悄悄把它写下去 = 把接地脚拉高(如果方向也错了)或者白写一次;
|
||||
这里返回错误码而不是 abort,是因为那多半是上层算错了掩码。 */
|
||||
static void test_write_out0_rejects_a_mask_that_touches_a_grounded_pin(void)
|
||||
{
|
||||
init_all();
|
||||
sel(2);
|
||||
const sim_i2c_dev_t *d = sim_i2c_dev_ch(2, PCA9555_ADDR);
|
||||
const uint32_t w0 = d->n_write;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, pca9555_write_out0(2, 0x20, 0x20)); /* bit5 接地 */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, pca9555_write_out0(2, 0xFF, 0x00)); /* 「全清一遍」 */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, pca9555_write_out0(2, 0x80, 0x00));
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(0, d->n_write - w0); /* 一个字节都没发出去 */
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_output(2, 0));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_gnd_driven());
|
||||
}
|
||||
|
||||
static void test_write_out0_rejects_a_bad_slot(void)
|
||||
{
|
||||
init_all();
|
||||
sel(0);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, pca9555_write_out0(-1, 0x01, 0x01));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, pca9555_write_out0(PCA9555_SLOT_COUNT, 0x01, 0x01));
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_output(0, 0));
|
||||
}
|
||||
|
||||
/* ⭐ **写失败时影子不更新。** 更新了的后果不是「这一次没生效」,而是**下一次**
|
||||
写会带着一个从没落到线上的位:比如这次「按 PWR」失败了却记了影子,下一次
|
||||
「按 VUP」就会把 PWR 一起按下去 —— 而那一次是成功的,日志里干干净净。
|
||||
所以判据打在**下一次写真正落到线上的字节**上,不是打在影子上。 */
|
||||
static void test_write_out0_keeps_the_shadow_when_the_write_fails(void)
|
||||
{
|
||||
init_all();
|
||||
sel(1);
|
||||
const uint8_t pwr = (uint8_t)(1u << PCA9555_BIT_KEY_PWR);
|
||||
const uint8_t vup = (uint8_t)(1u << PCA9555_BIT_KEY_VUP);
|
||||
|
||||
sim_i2c_inject_nak_ch(1, PCA9555_ADDR, 1);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_write_out0(1, pwr, pwr));
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_output(1, 0)); /* 线上确实没变 */
|
||||
|
||||
/* 下一次写(成功)只许出现 VUP 一位。带上 PWR = 影子被污染了。 */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_write_out0(1, vup, vup));
|
||||
TEST_ASSERT_EQUAL_HEX8(vup, sim_pca9555_output(1, 0));
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_pca9555_pin(1, 0, PCA9555_BIT_KEY_PWR));
|
||||
}
|
||||
|
||||
/* ⭐ R2 新增的失败形态:**五片的影子串了**。
|
||||
R1 只有一颗芯片,这类错根本不存在;R2 里影子是个五格数组,写成
|
||||
`s_out0[0]` 或者少减一个 1,现象是「改 CH1 的键把 CH4 的键也改了」,
|
||||
而五路电压电流全部正常、I²C 也没报错。 */
|
||||
static void test_the_five_shadows_do_not_bleed_into_each_other(void)
|
||||
{
|
||||
init_all();
|
||||
|
||||
/* 每一片按一个不同的键,通道来回切,模拟 t_actuator 交错操作五路。 */
|
||||
const uint8_t want[PCA9555_SLOT_COUNT] = {
|
||||
(uint8_t)(1u << PCA9555_BIT_KEY_PWR),
|
||||
(uint8_t)(1u << PCA9555_BIT_KEY_VUP),
|
||||
(uint8_t)(1u << PCA9555_BIT_KEY_VDN),
|
||||
(uint8_t)(1u << PCA9555_BIT_EDL_EN),
|
||||
(uint8_t)(1u << PCA9555_BIT_VBUS_EN),
|
||||
};
|
||||
for (int i = 0; i < PCA9555_SLOT_COUNT; i++) {
|
||||
sel(i);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_write_out0(i, PCA9555_P0_OUT_MASK, want[i]));
|
||||
}
|
||||
for (int i = 0; i < PCA9555_SLOT_COUNT; i++) {
|
||||
TEST_ASSERT_EQUAL_HEX8(want[i], sim_pca9555_output(i, 0));
|
||||
}
|
||||
|
||||
/* 再回到第 0 片改一位:另外四片必须一位都不动。 */
|
||||
sel(0);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_write_out0(0, pca9555_key_mask(), 0x00));
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_output(0, 0));
|
||||
for (int i = 1; i < PCA9555_SLOT_COUNT; i++) {
|
||||
TEST_ASSERT_EQUAL_HEX8(want[i], sim_pca9555_output(i, 0));
|
||||
}
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous());
|
||||
}
|
||||
|
||||
/* `pca9555_shadow_reset()` 只动影子、**不碰总线**(A15 的降级路径靠它说
|
||||
「现在生效的是上电默认值」)。碰了总线就是在一条已经失联的总线上瞎写。 */
|
||||
static void test_shadow_reset_does_not_touch_the_bus(void)
|
||||
{
|
||||
init_all();
|
||||
sel(3);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_write_out0(3, PCA9555_P0_OUT_MASK, 0x1F));
|
||||
const sim_i2c_dev_t *d = sim_i2c_dev_ch(3, PCA9555_ADDR);
|
||||
const uint32_t w0 = d->n_write;
|
||||
|
||||
pca9555_shadow_reset();
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(0, d->n_write - w0);
|
||||
TEST_ASSERT_EQUAL_HEX8(0x1F, sim_pca9555_output(3, 0)); /* 器件上的值没变 */
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, pca9555_shadow(3));
|
||||
}
|
||||
|
||||
/* ---- 输入与四个语义判读 -------------------------------------------------- */
|
||||
|
||||
/* 静息态:Port1 四根开漏线全被上拉 ⇒ 无故障、eFuse 良好、Buck 正常、无告警。
|
||||
任何一处极性写反,这条立刻红。
|
||||
⚠️ Port0 读回来必须是 0x00:bit0~4 是输出(锁存器 0),bit5~7 接地。 */
|
||||
static void test_idle_inputs_mean_everything_is_fine(void)
|
||||
{
|
||||
init_all();
|
||||
sel(0);
|
||||
|
||||
pca9555_in_t in;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, in.port0);
|
||||
TEST_ASSERT_EQUAL_HEX8(PCA9555_P1_IN_MASK, in.port1); /* bit4~7 接地,恒 0 */
|
||||
|
||||
TEST_ASSERT_EQUAL_HEX8(0x7F, in.port0); /* P0_7 接地,读回来必是 0 */
|
||||
TEST_ASSERT_EQUAL_HEX8(0x1F, in.port1); /* P1_5~7 接地 */
|
||||
|
||||
for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) {
|
||||
TEST_ASSERT_FALSE(pca9555_rail_fault(&in, rail));
|
||||
TEST_ASSERT_TRUE(pca9555_rail_pg(&in, rail));
|
||||
}
|
||||
TEST_ASSERT_FALSE(pca9555_efuse_fault(&in));
|
||||
TEST_ASSERT_TRUE(pca9555_efuse_pg(&in));
|
||||
TEST_ASSERT_TRUE(pca9555_buck_pg(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_ina_alert(&in));
|
||||
}
|
||||
|
||||
/* 全 0(比如整条总线被拉低、或者上拉没焊)= 五路全故障、五路全不良、Buck 不良、告警。
|
||||
注意这**不是**「安全默认值」:它和「五路真的全炸了」长得一模一样,
|
||||
/* 四个信号**两种极性**,一位一位单独拉低,四个判读函数各测一次。
|
||||
极性写反的现象是「有故障被读成电源良好」—— 方向正好相反,是最坏的那一种:
|
||||
系统不但不保护,还会更自信。所以每一格都要:拉低这一位时它变了,
|
||||
**而另外三个判读一动不动**(端口内串位的现象和极性写反长得一模一样)。 */
|
||||
static void test_each_polarity_exactly_once(void)
|
||||
{
|
||||
init_all();
|
||||
sel(2);
|
||||
pca9555_in_t in;
|
||||
|
||||
/* ① EFUSE_FLT# 低有效 ⇒ 拉低 = 有故障 */
|
||||
sim_pca9555_drive(2, 1, (uint8_t)(0xFFu & ~(1u << PCA9555_BIT_EFUSE_FLT)));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
TEST_ASSERT_TRUE(pca9555_efuse_fault(&in));
|
||||
TEST_ASSERT_TRUE(pca9555_efuse_pg(&in)); /* 另外三个不许跟着动 */
|
||||
TEST_ASSERT_TRUE(pca9555_buck_pg(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_ina_alert(&in));
|
||||
|
||||
/* ② EFUSE_PG 高 = 良好 ⇒ 拉低 = 不良 */
|
||||
sim_pca9555_drive(2, 1, (uint8_t)(0xFFu & ~(1u << PCA9555_BIT_EFUSE_PG)));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_efuse_pg(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_efuse_fault(&in));
|
||||
TEST_ASSERT_TRUE(pca9555_buck_pg(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_ina_alert(&in));
|
||||
|
||||
/* ③ BUCK_PG 高 = 正常 ⇒ 拉低 = 本路 Buck 不良(R2 里只代表本路!) */
|
||||
sim_pca9555_drive(2, 1, (uint8_t)(0xFFu & ~(1u << PCA9555_BIT_BUCK_PG)));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_buck_pg(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_efuse_fault(&in));
|
||||
TEST_ASSERT_TRUE(pca9555_efuse_pg(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_ina_alert(&in));
|
||||
|
||||
/* ④ INA_ALERT 低有效 ⇒ 拉低 = 在告警 */
|
||||
sim_pca9555_drive(2, 1, (uint8_t)(0xFFu & ~(1u << PCA9555_BIT_INA_ALERT)));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
TEST_ASSERT_TRUE(pca9555_ina_alert(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_efuse_fault(&in));
|
||||
TEST_ASSERT_TRUE(pca9555_efuse_pg(&in));
|
||||
TEST_ASSERT_TRUE(pca9555_buck_pg(&in));
|
||||
}
|
||||
|
||||
/* 全 0(整条支路被拉低、或者上拉没焊)= 全故障 + 全不良 + 告警。
|
||||
这**不是**安全默认值:它和「这一路真的全炸了」长得一模一样,
|
||||
§10.10 因此规定这片不应答时改用 INA226 的电压判据兜底,而不是信这一帧。 */
|
||||
static void test_all_low_means_everything_is_bad(void)
|
||||
{
|
||||
mount_ready();
|
||||
sim_pca9555_drive(0, 0x00);
|
||||
sim_pca9555_drive(1, 0x00);
|
||||
init_all();
|
||||
sel(1);
|
||||
sim_pca9555_drive(1, 1, 0x00);
|
||||
|
||||
pca9555_in_t in;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) {
|
||||
TEST_ASSERT_TRUE(pca9555_rail_fault(&in, rail));
|
||||
TEST_ASSERT_FALSE(pca9555_rail_pg(&in, rail));
|
||||
}
|
||||
TEST_ASSERT_TRUE(pca9555_efuse_fault(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_efuse_pg(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_buck_pg(&in));
|
||||
TEST_ASSERT_TRUE(pca9555_ina_alert(&in));
|
||||
}
|
||||
|
||||
/* 逐路点名:只让一路报故障,其余四路必须纹丝不动。
|
||||
FLT 和 PG 两张表在两个端口上长得一模一样(都是 bit0~4),把端口搞反之后
|
||||
「有故障」会被读成「电源良好」,所以这里一路一路地把两个端口错开来验。 */
|
||||
static void test_one_rail_at_a_time(void)
|
||||
/* 读的是**当前通道上那一片**。五片同址,读错通道 = 读到另一台手机的故障状态,
|
||||
而量纲、范围、变化趋势全部正常。这条把「同址靠通道区分」真的跑一遍。 */
|
||||
static void test_read_inputs_reads_the_blade_on_the_current_channel(void)
|
||||
{
|
||||
mount_ready();
|
||||
|
||||
for (int hurt = 1; hurt <= PCA9555_RAIL_COUNT; hurt++) {
|
||||
/* FLT 只拉低 hurt 那一路;PG 只拉低**另一路**(hurt 的下一路,绕回来)。 */
|
||||
const int other = (hurt % PCA9555_RAIL_COUNT) + 1;
|
||||
sim_pca9555_drive(0, (uint8_t)(0xFFu & ~(1u << pca9555_flt_bit(hurt))));
|
||||
sim_pca9555_drive(1, (uint8_t)(0xFFu & ~(1u << pca9555_pg_bit(other))));
|
||||
init_all();
|
||||
/* 只让第 3 片报 eFuse 故障。 */
|
||||
sim_pca9555_drive(3, 1, (uint8_t)(0xFFu & ~(1u << PCA9555_BIT_EFUSE_FLT)));
|
||||
|
||||
for (int i = 0; i < PCA9555_SLOT_COUNT; i++) {
|
||||
sel(i);
|
||||
pca9555_in_t in;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
|
||||
for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) {
|
||||
TEST_ASSERT_EQUAL_INT(rail == hurt, pca9555_rail_fault(&in, rail));
|
||||
TEST_ASSERT_EQUAL_INT(rail != other, pca9555_rail_pg(&in, rail));
|
||||
}
|
||||
/* 两根线或信号不受任何一路 FLT 影响(它们在 P0_5 / P0_6 上)。 */
|
||||
TEST_ASSERT_TRUE(pca9555_buck_pg(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_ina_alert(&in));
|
||||
TEST_ASSERT_EQUAL_INT(i == 3, pca9555_efuse_fault(&in));
|
||||
}
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous());
|
||||
}
|
||||
|
||||
/* 两根线或信号自己点名:拉低 BUCK_PG 不许把任何一路读成故障,反之亦然。 */
|
||||
static void test_wire_or_signals_do_not_alias_a_rail(void)
|
||||
{
|
||||
mount_ready();
|
||||
pca9555_in_t in;
|
||||
|
||||
sim_pca9555_drive(0, (uint8_t)(0xFFu & ~(1u << PCA9555_BIT_BUCK_PG)));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_buck_pg(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_ina_alert(&in));
|
||||
for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) {
|
||||
TEST_ASSERT_FALSE(pca9555_rail_fault(&in, rail));
|
||||
}
|
||||
|
||||
sim_pca9555_drive(0, (uint8_t)(0xFFu & ~(1u << PCA9555_BIT_INA_ALERT)));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
TEST_ASSERT_TRUE(pca9555_buck_pg(&in));
|
||||
TEST_ASSERT_TRUE(pca9555_ina_alert(&in));
|
||||
for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) {
|
||||
TEST_ASSERT_FALSE(pca9555_rail_fault(&in, rail));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 位表与地址 ---------------------------------------------------------- */
|
||||
|
||||
static void test_bit_map_is_consistent(void)
|
||||
{
|
||||
TEST_ASSERT_TRUE(pca9555_map_ok());
|
||||
TEST_ASSERT_TRUE(pca9555_addr_ok());
|
||||
TEST_ASSERT_EQUAL_HEX8(0x20, PCA9555_ADDR); /* A0=A1=A2=GND,U23 的 pin21/2/3 */
|
||||
|
||||
for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) {
|
||||
TEST_ASSERT_EQUAL_INT(rail - 1, pca9555_flt_bit(rail));
|
||||
TEST_ASSERT_EQUAL_INT(rail - 1, pca9555_pg_bit(rail));
|
||||
/* 一路都不许落到接地脚或两根线或信号上 */
|
||||
TEST_ASSERT_TRUE(pca9555_flt_bit(rail) < PCA9555_BIT_BUCK_PG);
|
||||
TEST_ASSERT_EQUAL_HEX8(0, (uint8_t)((1u << pca9555_flt_bit(rail)) & PCA9555_P0_GND_MASK));
|
||||
TEST_ASSERT_EQUAL_HEX8(0, (uint8_t)((1u << pca9555_pg_bit(rail)) & PCA9555_P1_GND_MASK));
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT(5, PCA9555_BIT_BUCK_PG);
|
||||
TEST_ASSERT_EQUAL_INT(6, PCA9555_BIT_INA_ALERT);
|
||||
}
|
||||
|
||||
/* ---- 总线行为 ------------------------------------------------------------ */
|
||||
|
||||
/* §10.3 的 0.12 ms 预算是按**一次事务**算的(写命令字节 + 重复起始读 2 字节)。
|
||||
拆成两次事务照样能跑,但 t_safety 的 I²C 占用会对不上 A8 要断言的那个数。 */
|
||||
/* §10.3 的总线预算是按**一次事务**算的(写命令字节 + 重复起始读 2 字节)。
|
||||
拆成两次照样能跑,但 t_safety 一圈的 I²C 占用会对不上 §5.5 那张表。 */
|
||||
static void test_read_inputs_uses_one_transaction(void)
|
||||
{
|
||||
mount_ready();
|
||||
const sim_i2c_dev_t *d = sim_i2c_dev(PCA9555_ADDR);
|
||||
init_all();
|
||||
sel(0);
|
||||
const sim_i2c_dev_t *d = sim_i2c_dev_ch(0, PCA9555_ADDR);
|
||||
const uint32_t w0 = d->n_write, r0 = d->n_read;
|
||||
|
||||
pca9555_in_t in;
|
||||
@@ -234,107 +582,104 @@ static void test_read_inputs_uses_one_transaction(void)
|
||||
TEST_ASSERT_EQUAL_UINT32(1, d->n_read - r0);
|
||||
}
|
||||
|
||||
static void test_probe_accepts_a_fresh_device(void)
|
||||
/* §10.10:这片不应答只是失去可见性,驱动如实返回错误码、什么动作都不做,
|
||||
**也不许改写调用方的缓冲** —— 半新半旧的一帧比明着失败危险得多。 */
|
||||
static void test_read_inputs_propagates_nak_without_touching_the_buffer(void)
|
||||
{
|
||||
mount();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_probe());
|
||||
}
|
||||
|
||||
/* 这个地址上蹲着别的东西(应答了,但方向寄存器不是复位值)。 */
|
||||
static void test_probe_rejects_a_stranger(void)
|
||||
{
|
||||
mount();
|
||||
sim_i2c_set_reg(PCA9555_ADDR, PCA9555_REG_CONFIG1, 0x00);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_BUS, pca9555_probe());
|
||||
}
|
||||
|
||||
static void test_probe_absent_device_naks(void)
|
||||
{
|
||||
mount();
|
||||
sim_i2c_set_present(PCA9555_ADDR, false);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_probe());
|
||||
}
|
||||
|
||||
/* §10.10:这片不应答只是失去可见性,驱动如实返回 NAK,什么动作都不做。 */
|
||||
static void test_read_propagates_nak(void)
|
||||
{
|
||||
mount_ready();
|
||||
sim_i2c_inject_nak(PCA9555_ADDR, -1);
|
||||
init_all();
|
||||
sel(4);
|
||||
sim_i2c_inject_nak_ch(4, PCA9555_ADDR, -1);
|
||||
|
||||
pca9555_in_t in = { 0xAA, 0x55 };
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_read_inputs(&in));
|
||||
TEST_ASSERT_EQUAL_HEX8(0xAA, in.port0); /* 失败时不许改写调用方的缓冲 */
|
||||
TEST_ASSERT_EQUAL_HEX8(0xAA, in.port0);
|
||||
TEST_ASSERT_EQUAL_HEX8(0x55, in.port1);
|
||||
}
|
||||
|
||||
static void test_init_propagates_nak(void)
|
||||
{
|
||||
mount();
|
||||
sim_i2c_inject_nak(PCA9555_ADDR, -1);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_init());
|
||||
}
|
||||
|
||||
/* 回读对不上 = 总线上有东西但不是我们以为的那片,init 必须失败而不是接着往下跑。
|
||||
这里用「写第二个方向寄存器时 NAK」来造回读失败之外的另一条错路。 */
|
||||
static void test_init_stops_at_the_first_failure(void)
|
||||
{
|
||||
mount();
|
||||
sim_i2c_inject_nak(PCA9555_ADDR, 3); /* 头三次事务 NAK,之后恢复 */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_init());
|
||||
/* 失败了就别把剩下的寄存器也写一遍 —— 极性反转还停在没被碰过的状态。 */
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_polinv(1));
|
||||
}
|
||||
|
||||
static void test_null_output_rejected(void)
|
||||
{
|
||||
mount_ready();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, pca9555_read_inputs(NULL));
|
||||
}
|
||||
|
||||
/* ---- INT#(A8/A10 会用,这里只钉住它的行为) ----------------------------- */
|
||||
/* ---- probe ---------------------------------------------------------------- */
|
||||
|
||||
/* 中断是加速路径不是主路径:**读走输入端口才释放**。
|
||||
漏读一次就再也等不到下一个沿,所以 t_safety 必须无条件轮询。 */
|
||||
static void test_int_is_only_cleared_by_reading_the_input_ports(void)
|
||||
/* probe 的语义是「这是一片**刚上电**的 PCA9555 吗」:两个方向寄存器都得是 0xFF。 */
|
||||
static void test_probe_accepts_a_fresh_blade(void)
|
||||
{
|
||||
mount_ready();
|
||||
TEST_ASSERT_FALSE(sim_pca9555_int_asserted());
|
||||
for (int i = 0; i < PCA9555_SLOT_COUNT; i++) {
|
||||
sel(i);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_probe());
|
||||
}
|
||||
}
|
||||
|
||||
sim_pca9555_drive(0, 0xFE); /* CH1 报故障 */
|
||||
TEST_ASSERT_TRUE(sim_pca9555_int_asserted());
|
||||
TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_int_asserts());
|
||||
/* ⚠️ 已经被 init 过的片子返回 HAL_ERR_BUS —— 这是对的,不是 bug。
|
||||
所以重连之后不能拿 probe 当「它还在不在」的判据(那是 read_inputs 的活)。 */
|
||||
static void test_probe_rejects_an_already_initialised_blade(void)
|
||||
{
|
||||
sel(0);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_init(0));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_BUS, pca9555_probe());
|
||||
}
|
||||
|
||||
/* 读别的寄存器不清中断 */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_probe());
|
||||
TEST_ASSERT_TRUE(sim_pca9555_int_asserted());
|
||||
/* 这地址上蹲着别的东西(应答了,方向寄存器却不是复位值)。 */
|
||||
static void test_probe_rejects_a_stranger(void)
|
||||
{
|
||||
sel(1);
|
||||
sim_i2c_set_reg_ch(1, PCA9555_ADDR, PCA9555_REG_CONFIG1, 0x00);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_BUS, pca9555_probe());
|
||||
}
|
||||
|
||||
pca9555_in_t in;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
TEST_ASSERT_FALSE(sim_pca9555_int_asserted());
|
||||
/* 刀片拔了 / 那条支路没选通 → NAK。R2 特有:**通道没开**和**器件没了**在总线上
|
||||
长得一模一样,两者都只能得到 NAK —— 区分它们是 `t_safety` 的事(PRSNT# + mux
|
||||
失联判据),不是驱动的事。 */
|
||||
static void test_probe_naks_when_the_blade_is_not_reachable(void)
|
||||
{
|
||||
sel(2);
|
||||
sim_i2c_set_present_ch(2, PCA9555_ADDR, false);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_probe());
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, tca9548a_select(TCA9548A_ADDR, TCA9548A_CH_NONE));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_probe()); /* 通道全关 = 谁也够不着 */
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_init_configures_every_pin_as_input);
|
||||
RUN_TEST(test_dir_safe_is_exactly_the_grounded_pins);
|
||||
/* ① 初始化顺序 */
|
||||
RUN_TEST(test_init_never_enables_outputs_while_the_latch_is_still_ff);
|
||||
RUN_TEST(test_after_init_every_output_pin_is_released);
|
||||
RUN_TEST(test_sim_catches_the_reversed_init_order);
|
||||
RUN_TEST(test_init_clears_a_stale_polarity_inversion);
|
||||
RUN_TEST(test_init_stops_at_the_first_failure_and_leaves_the_pins_as_inputs);
|
||||
RUN_TEST(test_init_fails_when_the_readback_does_not_come_back);
|
||||
RUN_TEST(test_init_rejects_a_bad_slot);
|
||||
/* ② 方向白名单 */
|
||||
RUN_TEST(test_dir_safe_port0_only_guards_the_grounded_pins);
|
||||
RUN_TEST(test_dir_safe_port1_allows_nothing_to_be_an_output);
|
||||
RUN_TEST(test_dir_safe_rejects_a_bad_port);
|
||||
RUN_TEST(test_grounded_pins_are_never_driven_on_any_blade);
|
||||
RUN_TEST(test_sim_catches_a_grounded_pin_driven_high);
|
||||
RUN_TEST(test_sim_counts_driving_a_grounded_pin_even_when_low);
|
||||
RUN_TEST(test_init_never_touches_the_output_registers);
|
||||
RUN_TEST(test_init_clears_a_stale_polarity_inversion);
|
||||
RUN_TEST(test_idle_bus_means_everything_is_fine);
|
||||
RUN_TEST(test_sim_catches_port1_configured_as_output);
|
||||
/* 位分配与地址 */
|
||||
RUN_TEST(test_bit_map_is_self_consistent);
|
||||
RUN_TEST(test_key_bits_and_mask);
|
||||
RUN_TEST(test_address_does_not_collide);
|
||||
/* ③ 输出写 */
|
||||
RUN_TEST(test_write_out0_only_touches_the_masked_bits);
|
||||
RUN_TEST(test_write_out0_rejects_a_mask_that_touches_a_grounded_pin);
|
||||
RUN_TEST(test_write_out0_rejects_a_bad_slot);
|
||||
RUN_TEST(test_write_out0_keeps_the_shadow_when_the_write_fails);
|
||||
RUN_TEST(test_the_five_shadows_do_not_bleed_into_each_other);
|
||||
RUN_TEST(test_shadow_reset_does_not_touch_the_bus);
|
||||
/* 输入与极性 */
|
||||
RUN_TEST(test_idle_inputs_mean_everything_is_fine);
|
||||
RUN_TEST(test_each_polarity_exactly_once);
|
||||
RUN_TEST(test_all_low_means_everything_is_bad);
|
||||
RUN_TEST(test_one_rail_at_a_time);
|
||||
RUN_TEST(test_wire_or_signals_do_not_alias_a_rail);
|
||||
RUN_TEST(test_bit_map_is_consistent);
|
||||
RUN_TEST(test_read_inputs_reads_the_blade_on_the_current_channel);
|
||||
RUN_TEST(test_read_inputs_uses_one_transaction);
|
||||
RUN_TEST(test_probe_accepts_a_fresh_device);
|
||||
RUN_TEST(test_read_inputs_propagates_nak_without_touching_the_buffer);
|
||||
/* probe */
|
||||
RUN_TEST(test_probe_accepts_a_fresh_blade);
|
||||
RUN_TEST(test_probe_rejects_an_already_initialised_blade);
|
||||
RUN_TEST(test_probe_rejects_a_stranger);
|
||||
RUN_TEST(test_probe_absent_device_naks);
|
||||
RUN_TEST(test_read_propagates_nak);
|
||||
RUN_TEST(test_init_propagates_nak);
|
||||
RUN_TEST(test_init_stops_at_the_first_failure);
|
||||
RUN_TEST(test_null_output_rejected);
|
||||
RUN_TEST(test_int_is_only_cleared_by_reading_the_input_ports);
|
||||
RUN_TEST(test_probe_naks_when_the_blade_is_not_reachable);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户