A6:PCA9555 驱动 + 「接地脚永远是输入」的硬约束

U23 @ 0x20,16 个脚全部是输入:5×EFUSE_FLT# + 5×EFUSE_PGOOD + BUCK_PG
+ INA_ALERT + 4 个焊死在地上的备用脚(P0_7 / P1_5~7)。位表与地址从
.claude/pcb1-parts.json 的真实焊盘网络逐脚核回来,与设计文档 §17.2 一致。

驱动根本不提供「把某个脚配成输出」的接口(和 sr595.h 不提供「抬 OE#」同一条
规矩),方向字一律过 pca9555_dir_safe(),不安全直接 abort。原因是 PCA9555 的
Output Port 寄存器复位值是 0xFF —— 把接地脚配成输出的那一刻它就输出高电平,
而真器件既不报错也不 NAK,只是发烫。

三种极性(FLT# 低有效 / PGOOD 高有效 / INA_ALERT 低有效)各自只在一个函数里
出现一次;上层问「有没有故障」,不许自己去 & 位。

sim 侧新增按 datasheet 建模的假 PCA9555:Output 复位 0xFF、输入按位合成、
接地脚被驱动时记短路、INT# 只被「读输入端口」清除。为此给假 I²C 总线加了一个
窃听钩子(POST_WRITE / PRE_READ)—— 通用寄存器表模拟不了「配成输出就对地
短路」这类事,以后再有这种器件照这个挂。

验收:8 条 ctest(81 用例)全绿;idf.py build 通过,pca9555.c.obj 在。
12 个「一定会被写出来的错」逐个注进去跑过,全部变红。

Co-Authored-By: Claude Opus 5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-07 06:28:11 +08:00
共同撰写人 Claude Opus 5
父节点 ab37f9dc49
当前提交 eb3df9ff49
共修改 12 个文件,包含 962 行新增和 59 行删除
+340
查看文件
@@ -0,0 +1,340 @@
/* A6:PCA9555 驱动。
验收主项:**方向寄存器永远不会把 Port0[7] / Port1[7:5] 配成输出** ——
那四个脚在板上直接接地,配成输出等于对地短路,而真器件既不报错也不 NAK。
第二主项:FLT / PG / BUCK_PG / INA_ALERT 三种极性一个都不许写反。 */
#include "pca9555.h"
#include "sim_hal.h"
#include "unity.h"
void setUp(void) { sim_hal_reset(); }
void tearDown(void) {}
static void mount(void)
{
sim_pca9555_attach(PCA9555_ADDR);
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(NULL));
}
/* 挂好并 init 完,外部电平停在静息值(全高 = 无故障、全部良好)。 */
static void mount_ready(void)
{
mount();
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_init());
}
/* ---- 验收主项一:接地脚永远是输入 ---------------------------------------- */
/* init 之后两个方向寄存器都必须是 0xFF —— 不是「大致全输入」,是逐位。 */
static void test_init_configures_every_pin_as_input(void)
{
mount_ready();
TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_config(0));
TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_config(1));
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_gnd_driven());
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_shorted());
}
/* dir_safe 的判据穷举:**当且仅当**接地位全为 1(输入)才算安全。
写成「dir != 0」「dir & 0x80」这类近似判据的,会在这 512 个组合里露馅。 */
static void test_dir_safe_is_exactly_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));
}
/* 几个会被真写出来的错值 */
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)); /* 端口号越界 */
}
/* 这条证明的是**假器件真的会照出短路**,否则上面那条「shorted == 0」是句空话。
手工绕过驱动,按「有人把 P0_7 配成输出」写一次方向寄存器:
PCA9555 的 Output Port 复位值是 0xFF,所以这一刻它就在对地灌电流。 */
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)));
TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_gnd_driven());
TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_shorted());
}
/* 同上,但先把输出锁存器写成 0 再配成输出:不冒烟,可仍然是在驱动一个接地脚。
两个计数分开就是为了让这一格不被漏掉。 */
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)));
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)
{
mount_ready();
TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_output(0));
TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_output(1));
}
/* ---- 极性反转寄存器 ------------------------------------------------------ */
/* 极性反转是「看门狗复位后残留」的重灾区:ESP32 重启了,PCA9555 没掉电。
init 必须无条件清零,而不是「复位值本来就是 0」。 */
static void test_init_clears_a_stale_polarity_inversion(void)
{
mount();
sim_i2c_set_reg(PCA9555_ADDR, PCA9555_REG_POLINV0, 0xFF); /* 上一条命的残留 */
sim_i2c_set_reg(PCA9555_ADDR, PCA9555_REG_POLINV1, 0x1F);
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_init());
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_polinv(0));
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_polinv(1));
}
/* ---- 验收主项二:三种极性 ------------------------------------------------ */
/* 静息态(两个端口都被上拉到 0xFF)= 五路无故障、五路电源良好、Buck 正常、无告警。
任何一处极性写反,这条立刻红。 */
static void test_idle_bus_means_everything_is_fine(void)
{
mount_ready();
pca9555_in_t in;
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
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_TRUE(pca9555_buck_pg(&in));
TEST_ASSERT_FALSE(pca9555_ina_alert(&in));
}
/* 全 0(比如整条总线被拉低、或者上拉没焊)= 五路全故障、五路全不良、Buck 不良、告警。
注意这**不是**「安全默认值」:它和「五路真的全炸了」长得一模一样,
§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);
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_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)
{
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))));
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));
}
}
/* 两根线或信号自己点名:拉低 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 要断言的那个数。 */
static void test_read_inputs_uses_one_transaction(void)
{
mount_ready();
const sim_i2c_dev_t *d = sim_i2c_dev(PCA9555_ADDR);
const uint32_t w0 = d->n_write, r0 = d->n_read;
pca9555_in_t in;
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
TEST_ASSERT_EQUAL_UINT32(1, d->n_write - w0);
TEST_ASSERT_EQUAL_UINT32(1, d->n_read - r0);
}
static void test_probe_accepts_a_fresh_device(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);
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(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 会用,这里只钉住它的行为) ----------------------------- */
/* 中断是加速路径不是主路径:**读走输入端口才释放**。
漏读一次就再也等不到下一个沿,所以 t_safety 必须无条件轮询。 */
static void test_int_is_only_cleared_by_reading_the_input_ports(void)
{
mount_ready();
TEST_ASSERT_FALSE(sim_pca9555_int_asserted());
sim_pca9555_drive(0, 0xFE); /* CH1 报故障 */
TEST_ASSERT_TRUE(sim_pca9555_int_asserted());
TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_int_asserts());
/* 读别的寄存器不清中断 */
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_probe());
TEST_ASSERT_TRUE(sim_pca9555_int_asserted());
pca9555_in_t in;
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
TEST_ASSERT_FALSE(sim_pca9555_int_asserted());
}
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_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_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_uses_one_transaction);
RUN_TEST(test_probe_accepts_a_fresh_device);
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);
return UNITY_END();
}