/* 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" /* ---- 脚手架 -------------------------------------------------------------- */ static void mount(void) { 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()); /* 顺带把通道全关 */ } 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) { for (int i = 0; i < PCA9555_SLOT_COUNT; i++) { sel(i); TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_init(i)); } } /* 绕开驱动往当前通道上那片写一个寄存器。用来演示「假器件真的会照出问题」。 */ static hal_err_t raw_write(uint8_t reg, uint8_t val) { 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)); } } /* 上一条的加强版: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 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_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)); /* 只错一位也是对地短路 */ } /* 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) { 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()); /* 真板子上这一刻开始冒烟 */ } /* 同上,但先把锁存器写成 0 再配成输出:不冒烟,可仍然是在驱动一个接地脚 (下一次写 Port0 就会把它拉高)。两个计数分开就是为了不漏掉这一格。 */ static void test_sim_counts_driving_a_grounded_pin_even_when_low(void) { 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()); } /* Port1 被配成输出:`gnd_driven` 只看 bit4~7,所以这里同时钉住 「dir_safe 把 bit0~3 也管住了」这件事 —— 它是判据,不是假器件能照出来的。 */ static void test_sim_catches_port1_configured_as_output(void) { 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()); } /* ---- 位分配与地址 -------------------------------------------------------- */ static void test_bit_map_is_self_consistent(void) { TEST_ASSERT_TRUE(pca9555_map_ok()); /* 五个输出位正好铺满允许配成输出的那五位,一位不多一位不少。 */ 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)); /* 没落到接地脚上 */ 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)); } /* 三个键的位号互不重复、都在输出白名单里。`pca9555_key_mask()` 是 `t_actuator` 的「防御性松键」用的那个掩码,少一位就是「有个键松不掉」。 */ static void test_key_bits_and_mask(void) { 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_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)); } /* 四个信号**两种极性**,一位一位单独拉低,四个判读函数各测一次。 极性写反的现象是「有故障被读成电源良好」—— 方向正好相反,是最坏的那一种: 系统不但不保护,还会更自信。所以每一格都要:拉低这一位时它变了, **而另外三个判读一动不动**(端口内串位的现象和极性写反长得一模一样)。 */ 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) { init_all(); sel(1); sim_pca9555_drive(1, 1, 0x00); pca9555_in_t in; TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in)); 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)); } /* 读的是**当前通道上那一片**。五片同址,读错通道 = 读到另一台手机的故障状态, 而量纲、范围、变化趋势全部正常。这条把「同址靠通道区分」真的跑一遍。 */ static void test_read_inputs_reads_the_blade_on_the_current_channel(void) { 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)); TEST_ASSERT_EQUAL_INT(i == 3, pca9555_efuse_fault(&in)); } TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous()); } /* §10.3 的总线预算是按**一次事务**算的(写命令字节 + 重复起始读 2 字节)。 拆成两次照样能跑,但 t_safety 一圈的 I²C 占用会对不上 §5.5 那张表。 */ static void test_read_inputs_uses_one_transaction(void) { 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; 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); } /* §10.10:这片不应答只是失去可见性,驱动如实返回错误码、什么动作都不做, **也不许改写调用方的缓冲** —— 半新半旧的一帧比明着失败危险得多。 */ static void test_read_inputs_propagates_nak_without_touching_the_buffer(void) { 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(0x55, in.port1); TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, pca9555_read_inputs(NULL)); } /* ---- probe ---------------------------------------------------------------- */ /* probe 的语义是「这是一片**刚上电**的 PCA9555 吗」:两个方向寄存器都得是 0xFF。 */ static void test_probe_accepts_a_fresh_blade(void) { for (int i = 0; i < PCA9555_SLOT_COUNT; i++) { sel(i); TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_probe()); } } /* ⚠️ 已经被 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()); } /* 这地址上蹲着别的东西(应答了,方向寄存器却不是复位值)。 */ 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()); } /* 刀片拔了 / 那条支路没选通 → 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_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_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_read_inputs_reads_the_blade_on_the_current_channel); RUN_TEST(test_read_inputs_uses_one_transaction); 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_naks_when_the_blade_is_not_reachable); return UNITY_END(); }