/* A5:74HC595 24 位链 + 影子寄存器单一写入点。 两条验收断言(固件开发计划 §5 A5 那一行)都打了 ⭐: ① 改 CH1 的按键**不动** CH2~5 的位 ② 字节顺序 **U33 → U22 → U21** 全部断言都对着 `sim_sr595_*`(按 datasheet 建模的假移位链,只看线上真实发生的 边),不对着驱动自己的影子变量 —— 后者是自己证明自己。 */ #include "hal_gpio.h" #include "phonerail.h" #include "sim_hal.h" #include "sr595.h" #include "unity.h" void setUp(void) { sim_hal_reset(); } void tearDown(void) {} static void attach(void) { sim_sr595_attach(SR595_PIN_SER, SR595_PIN_SRCLK, SR595_PIN_RCLK, SR595_PIN_OE_N); } static void up(void) { attach(); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_init()); } /* 链上某一位现在是什么(输出锁存器,不是影子)。 */ static int out_bit(int bit) { return (int)((sim_sr595_outputs() >> bit) & 1u); } /* ---- 位表:只能查,不能算 ------------------------------------------------- */ /* 位表逐条对着 `.claude/pcb1-parts.json` 里的真实焊盘网络(设计文档 §26.18 C)。 这张表**不是任何公式**:CH2/CH3 在链尾那片 U33 上,CH5 的三个键还是倒着排的。 */ static void test_bit_table_matches_the_board(void) { /* CH1 -> U21.QB QC QD */ TEST_ASSERT_EQUAL_INT(1, sr595_key_bit(1, SR595_KEY_PWR)); TEST_ASSERT_EQUAL_INT(2, sr595_key_bit(1, SR595_KEY_VUP)); TEST_ASSERT_EQUAL_INT(3, sr595_key_bit(1, SR595_KEY_VDN)); /* CH2 -> U33.QD QB QC */ TEST_ASSERT_EQUAL_INT(19, sr595_key_bit(2, SR595_KEY_PWR)); TEST_ASSERT_EQUAL_INT(17, sr595_key_bit(2, SR595_KEY_VUP)); TEST_ASSERT_EQUAL_INT(18, sr595_key_bit(2, SR595_KEY_VDN)); /* CH3 -> U33.QG QF QE */ TEST_ASSERT_EQUAL_INT(22, sr595_key_bit(3, SR595_KEY_PWR)); TEST_ASSERT_EQUAL_INT(21, sr595_key_bit(3, SR595_KEY_VUP)); TEST_ASSERT_EQUAL_INT(20, sr595_key_bit(3, SR595_KEY_VDN)); /* CH4 -> U22.QB QC QD */ TEST_ASSERT_EQUAL_INT(9, sr595_key_bit(4, SR595_KEY_PWR)); TEST_ASSERT_EQUAL_INT(10, sr595_key_bit(4, SR595_KEY_VUP)); TEST_ASSERT_EQUAL_INT(11, sr595_key_bit(4, SR595_KEY_VDN)); /* CH5 -> U22.QH QG QF —— 倒着的 */ TEST_ASSERT_EQUAL_INT(15, sr595_key_bit(5, SR595_KEY_PWR)); TEST_ASSERT_EQUAL_INT(14, sr595_key_bit(5, SR595_KEY_VUP)); TEST_ASSERT_EQUAL_INT(13, sr595_key_bit(5, SR595_KEY_VDN)); TEST_ASSERT_EQUAL_INT(16, sr595_edl_bit(1)); /* U33.QA —— 只剩这一路还在 U33 */ TEST_ASSERT_EQUAL_INT(7, sr595_edl_bit(2)); /* U21.QH */ TEST_ASSERT_EQUAL_INT(6, sr595_edl_bit(3)); /* U21.QG */ TEST_ASSERT_EQUAL_INT(0, sr595_edl_bit(4)); /* U21.QA */ TEST_ASSERT_EQUAL_INT(5, sr595_edl_bit(5)); /* U21.QF */ } /* 把「一定会被写出来的那个算式」按死:三键顺排 `bit = 3(rail−1) + key`。 ⚠️ CH4 **碰巧**就是 9/10/11,和公式一模一样 —— 所以这里只能整体断言 「15 位里只有 CH4 那三位对得上」,不能逐路断言「都不等于公式」。 写成算式的症状是**按 CH1 的电源键按到了别路的音量键**:板子照样正常工作, 功能测试测不出来(§26.18)。 */ static void test_bit_table_is_not_a_formula(void) { int naive_hits = 0; for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) { for (int k = 0; k < SR595_KEY_COUNT; k++) { if (sr595_key_bit(rail, (sr595_key_t)k) == 3 * (rail - 1) + k) { naive_hits++; } } } TEST_ASSERT_EQUAL_INT_MESSAGE(3, naive_hits, "15 位里只有 CH4 的三位碰巧和顺排公式重合"); for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) { /* 每一路的三个键在表里**未必**是 PWR sr595_key_bit(rail, SR595_KEY_VDN)); } /* 「U33 是 EDL 专用片、EDL_ENn 在它的第 n−1 位」—— 只剩 EDL_EN1 还对 */ if (rail >= 2) { TEST_ASSERT_NOT_EQUAL_INT(16 + (rail - 1), sr595_edl_bit(rail)); } } /* CH2 / CH3 的六个键位全在链尾那片 U33(bit16~23)上,一个都不在 U21 */ for (int k = 0; k < SR595_KEY_COUNT; k++) { TEST_ASSERT_TRUE(sr595_key_bit(2, (sr595_key_t)k) >= 16); TEST_ASSERT_TRUE(sr595_key_bit(3, (sr595_key_t)k) >= 16); } } /* 21 个信号占满 21 个有效位,一位不多一位不少,空脚一个都没被占。启动自检用。 */ static void test_map_ok(void) { TEST_ASSERT_TRUE(sr595_map_ok()); TEST_ASSERT_EQUAL_HEX32(0x00FFEEEFu, SR595_MASK_VALID); /* 去掉 bit4/8/12 */ uint32_t keys = sr595_all_keys_mask(); int n = 0; for (int b = 0; b < SR595_BITS; b++) { n += (int)((keys >> b) & 1u); } TEST_ASSERT_EQUAL_INT_MESSAGE(15, n, "15 路侧键"); /* 按键掩码里不许混进 EDL 与 HUB_RST */ TEST_ASSERT_EQUAL_HEX32(0, keys & ((uint32_t)1u << SR595_BIT_HUB_RST)); for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) { TEST_ASSERT_EQUAL_HEX32(0, keys & ((uint32_t)1u << sr595_edl_bit(rail))); } } /* ---- ② 字节顺序 ---------------------------------------------------------- */ /* ⭐ 验收主项二:一帧的字节顺序是 **U33 → U22 → U21**。 先移入的位走得最远,所以链尾那片的字节要先发。发反了 U21 的字节会停在 U33 上 —— 现象是「按 CH1 的电源键,CH3 的手机弹出了 EDL」。 这里靠的是假移位链自己按 datasheet 移的结果,不是驱动的影子变量。 */ static void test_byte_order_is_u33_u22_u21(void) { up(); /* 三片各给一个只有该片认得的图案,这样字节走错片一眼就看出来。 */ const uint32_t pattern = 0x00A1B2C3u & SR595_MASK_VALID; sr595_modify(SR595_MASK_VALID, pattern); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_flush()); TEST_ASSERT_EQUAL_HEX32(pattern, sim_sr595_outputs()); TEST_ASSERT_EQUAL_HEX32_MESSAGE(pattern & 0xFFu, sim_sr595_outputs() & 0xFFu, "U21 = 影子的 bit0~7"); TEST_ASSERT_EQUAL_HEX32_MESSAGE((pattern >> 8) & 0xFFu, (sim_sr595_outputs() >> 8) & 0xFFu, "U22 = 影子的 bit8~15"); TEST_ASSERT_EQUAL_HEX32_MESSAGE((pattern >> 16) & 0xFFu, (sim_sr595_outputs() >> 16) & 0xFFu, "U33 = 影子的 bit16~23(链尾,字节要先发)"); /* 字节顺序反过来发的话,输出会变成字节序倒过来的那个值 —— 图案不对称,认得出来。 */ uint32_t reversed = ((pattern & 0xFFu) << 16) | (pattern & 0xFF00u) | ((pattern >> 16) & 0xFFu); TEST_ASSERT_NOT_EQUAL_HEX32(reversed, sim_sr595_outputs()); } /* 一帧正好 24 个 SRCLK 上升沿 + 1 个 RCLK 上升沿。 多打一个时钟整条链就转一格(**每一位都错开一位**,症状是十五个按键全乱套), 而只看最终电压电流是完全正常的。 */ static void test_one_frame_is_24_clocks_and_1_latch(void) { up(); uint32_t c0 = sim_sr595_clocks(), l0 = sim_sr595_latches(); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_write(sr595_key_mask(2), sr595_key_mask(2))); TEST_ASSERT_EQUAL_UINT32(c0 + 24, sim_sr595_clocks()); TEST_ASSERT_EQUAL_UINT32(l0 + 1, sim_sr595_latches()); } /* RCLK 打点之前输出一点不变 ⇒ 整帧是原子的,组合键不会先单按一个。 假移位链把「移位寄存器」和「输出锁存器」分开建模,正是为了能断言这一条。 */ static void test_frame_is_atomic(void) { up(); uint32_t before = sim_sr595_outputs(); /* 手动把一帧发一半:只移 8 位,不打 RCLK。 */ for (int i = 0; i < 8; i++) { TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_write(SR595_PIN_SER, 1)); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_write(SR595_PIN_SRCLK, 1)); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_write(SR595_PIN_SRCLK, 0)); } TEST_ASSERT_NOT_EQUAL_HEX32(before, sim_sr595_shift()); /* 移位寄存器动了 */ TEST_ASSERT_EQUAL_HEX32(before, sim_sr595_outputs()); /* 输出一点没动 */ } /* ---- ① 改一路不动别路 ---------------------------------------------------- */ /* ⭐ 验收主项一:改 CH1 的按键,CH2~5 的 12 位一位都不许动。 这是「全固件只有一个函数能写 595」的核心理由 —— 多处直接操作移位链的症状是 **另一台手机的按键被顺手清掉**,要两个动作正好撞在一起才复现。 */ static void test_changing_ch1_leaves_ch2_to_ch5_alone(void) { up(); /* 先让 CH2~5 各按住一个不同的键,作为「别人的现场」。 */ uint32_t others = 0; others |= (uint32_t)1u << sr595_key_bit(2, SR595_KEY_PWR); others |= (uint32_t)1u << sr595_key_bit(3, SR595_KEY_VUP); others |= (uint32_t)1u << sr595_key_bit(4, SR595_KEY_VDN); others |= (uint32_t)1u << sr595_key_bit(5, SR595_KEY_PWR); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_write(others, others)); TEST_ASSERT_EQUAL_HEX32(others, sim_sr595_outputs() & sr595_all_keys_mask()); /* CH1 按下电源键 -> 松开。两次之后,别路的四位必须原封不动。 */ uint32_t k1_pwr = (uint32_t)1u << sr595_key_bit(1, SR595_KEY_PWR); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_write(k1_pwr, k1_pwr)); TEST_ASSERT_EQUAL_HEX32_MESSAGE(others, sim_sr595_outputs() & ~sr595_key_mask(1) & sr595_all_keys_mask(), "按下 CH1 动了别路的位"); TEST_ASSERT_EQUAL_INT(1, out_bit(sr595_key_bit(1, SR595_KEY_PWR))); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_write(k1_pwr, 0)); TEST_ASSERT_EQUAL_HEX32_MESSAGE(others, sim_sr595_outputs() & ~sr595_key_mask(1) & sr595_all_keys_mask(), "松开 CH1 动了别路的位"); TEST_ASSERT_EQUAL_INT(0, out_bit(sr595_key_bit(1, SR595_KEY_PWR))); /* 顺带:HUB_RST 从头到尾没被别人顺手清掉。 */ TEST_ASSERT_EQUAL_INT(1, out_bit(SR595_BIT_HUB_RST)); } /* 「一路全松开」用 sr595_key_mask(),同样不许碰别路,也不许碰本路的 EDL_EN。 */ static void test_releasing_one_rail_keeps_edl_and_others(void) { up(); uint32_t all_keys = sr595_all_keys_mask(); uint32_t edl3 = (uint32_t)1u << sr595_edl_bit(3); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_write(all_keys | edl3, all_keys | edl3)); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_write(sr595_key_mask(3), 0)); TEST_ASSERT_EQUAL_HEX32(0, sim_sr595_outputs() & sr595_key_mask(3)); TEST_ASSERT_EQUAL_HEX32(all_keys & ~sr595_key_mask(3), sim_sr595_outputs() & all_keys); TEST_ASSERT_EQUAL_INT_MESSAGE(1, out_bit(sr595_edl_bit(3)), "松开按键不该碰 EDL_EN3"); } /* 组合键(VOL− + PWR)必须落在**同一帧**里:一次 sr595_write() 两位一起给。 分两次写会先单按一个键 —— EDL 的组合键对顺序是敏感的(需求 §10.6)。 */ static void test_combo_lands_in_one_frame(void) { up(); uint32_t l0 = sim_sr595_latches(); uint32_t combo = ((uint32_t)1u << sr595_key_bit(4, SR595_KEY_PWR)) | ((uint32_t)1u << sr595_key_bit(4, SR595_KEY_VDN)); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_write(combo, combo)); TEST_ASSERT_EQUAL_UINT32_MESSAGE(l0 + 1, sim_sr595_latches(), "组合键必须一帧发完"); TEST_ASSERT_EQUAL_HEX32(combo, sim_sr595_outputs() & sr595_key_mask(4)); } /* ---- 上电顺序与 HUB_RST -------------------------------------------------- */ /* `HUB_RST` 是**低有效**(USB2517I 的 RESET_N,设计文档 §21.7)。 影子全 0 会把 Hub 一直摁在复位里 —— 现象是「插上枚举不出来」, 而五路手机的电压电流全都正常。所以上电安全值不是 0。 */ static void test_hub_rst_is_active_low_and_released_after_init(void) { up(); TEST_ASSERT_EQUAL_HEX32(SR595_SHADOW_SAFE, sr595_shadow()); TEST_ASSERT_EQUAL_INT_MESSAGE(1, out_bit(SR595_BIT_HUB_RST), "上电后 Hub 不能被摁在复位里"); sr595_set_hub_reset(true); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_flush()); TEST_ASSERT_EQUAL_INT(0, out_bit(SR595_BIT_HUB_RST)); sr595_set_hub_reset(false); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_flush()); TEST_ASSERT_EQUAL_INT(1, out_bit(SR595_BIT_HUB_RST)); /* 复位 Hub 期间 15 个按键一个都不许被按下去。 */ sr595_set_hub_reset(true); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_flush()); TEST_ASSERT_EQUAL_HEX32(0, sim_sr595_outputs() & sr595_all_keys_mask()); } /* 上电顺序:先锁存安全值,**再**使能 OE#。 换个个儿 = 把移位寄存器里的随机值放到 15 个栅极上,也就是五台手机的电源键 可能一起被按下去(设计文档 §8.1「复位安全三条」)。 假移位链 attach 时被塞了 0xA5A5A5,正是用来照出这个错的。 */ static void test_outputs_are_enabled_only_after_a_safe_latch(void) { attach(); TEST_ASSERT_EQUAL_INT_MESSAGE(1, sim_sr595_oe(), "上电时 OE# 由 R609 拉高"); TEST_ASSERT_FALSE(sim_sr595_enabled()); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_init()); TEST_ASSERT_TRUE_MESSAGE(sim_sr595_enabled(), "init 之后 OE# 必须被拉低"); TEST_ASSERT_EQUAL_INT(0, sim_sr595_oe()); TEST_ASSERT_TRUE_MESSAGE(sim_sr595_enable_latches() >= 1, "OE# 拉低之前一帧都还没锁存过 —— 15 个栅极上是随机值"); TEST_ASSERT_EQUAL_HEX32_MESSAGE(SR595_SHADOW_SAFE, sim_sr595_enable_outputs(), "使能那一刻链上不是安全值"); TEST_ASSERT_EQUAL_HEX32(0, sim_sr595_outputs() & sr595_all_keys_mask()); } /* ---- 影子与刷新 ---------------------------------------------------------- */ /* 「改影子 -> 请求刷新」是两步:`sr595_modify()` 一个字节都不许发出去。 */ static void test_modify_does_not_touch_the_chain(void) { up(); uint32_t c0 = sim_sr595_clocks(), out0 = sim_sr595_outputs(); sr595_modify(sr595_key_mask(5), sr595_key_mask(5)); TEST_ASSERT_TRUE(sr595_dirty()); TEST_ASSERT_EQUAL_UINT32_MESSAGE(c0, sim_sr595_clocks(), "modify 不许碰总线"); TEST_ASSERT_EQUAL_HEX32(out0, sim_sr595_outputs()); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_flush()); TEST_ASSERT_FALSE(sr595_dirty()); TEST_ASSERT_EQUAL_HEX32(sr595_shadow(), sim_sr595_outputs()); } /* 不脏就不空发一帧:24 个时钟 + 一次锁存不是免费的,而且真板子上每发一帧 都要占住那三根线。写一个和现在一样的值同样不算脏。 */ static void test_flush_is_a_noop_when_clean(void) { up(); uint32_t c0 = sim_sr595_clocks(), l0 = sim_sr595_latches(); TEST_ASSERT_FALSE(sr595_dirty()); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_flush()); TEST_ASSERT_EQUAL_UINT32(c0, sim_sr595_clocks()); TEST_ASSERT_EQUAL_UINT32(l0, sim_sr595_latches()); sr595_set_hub_reset(false); /* 已经是这个值了 */ TEST_ASSERT_FALSE(sr595_dirty()); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_flush()); TEST_ASSERT_EQUAL_UINT32(l0, sim_sr595_latches()); } /* value 里 mask 以外的位不许漏进影子(`sr595_write(bit, 0xFFFFFFFF)` 这种写法)。 */ static void test_modify_only_touches_the_masked_bits(void) { up(); uint32_t k2 = (uint32_t)1u << sr595_key_bit(2, SR595_KEY_VUP); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_write(k2, 0xFFFFFFFFu)); TEST_ASSERT_EQUAL_HEX32(SR595_SHADOW_SAFE | k2, sim_sr595_outputs()); } /* 发送失败(把 SER 那根脚踢回输入,模拟「没配成输出就用」)要留着脏, 下一次 flush 重发**整帧**。丢了脏标志 = 链上和影子永久对不上, 而后面每一次 flush 都会「成功」。 */ static void test_failed_flush_stays_dirty(void) { up(); uint32_t k1 = (uint32_t)1u << sr595_key_bit(1, SR595_KEY_VDN); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_config(SR595_PIN_SER, HAL_GPIO_INPUT)); sr595_modify(k1, k1); TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, sr595_flush()); TEST_ASSERT_TRUE_MESSAGE(sr595_dirty(), "发失败必须留着脏"); TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_config(SR595_PIN_SER, HAL_GPIO_OUTPUT)); TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_flush()); TEST_ASSERT_EQUAL_HEX32(SR595_SHADOW_SAFE | k1, sim_sr595_outputs()); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_bit_table_matches_the_board); RUN_TEST(test_bit_table_is_not_a_formula); RUN_TEST(test_map_ok); RUN_TEST(test_byte_order_is_u33_u22_u21); RUN_TEST(test_one_frame_is_24_clocks_and_1_latch); RUN_TEST(test_frame_is_atomic); RUN_TEST(test_changing_ch1_leaves_ch2_to_ch5_alone); RUN_TEST(test_releasing_one_rail_keeps_edl_and_others); RUN_TEST(test_combo_lands_in_one_frame); RUN_TEST(test_hub_rst_is_active_low_and_released_after_init); RUN_TEST(test_outputs_are_enabled_only_after_a_safe_latch); RUN_TEST(test_modify_does_not_touch_the_chain); RUN_TEST(test_flush_is_a_noop_when_clean); RUN_TEST(test_modify_only_touches_the_masked_bits); RUN_TEST(test_failed_flush_stays_dirty); return UNITY_END(); }