/* 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(); }