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]>
这个提交包含在:
@@ -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();
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户