/* * 模块名称:屏板链路传输层(sb_link)主机侧单元测试 * 模块功能:在 PC 上对 Drivers/BSP/ScreenLink/sb_link.c 做离线验证。 * * 被测对象:真实源文件(以 #include 原样纳入,保证测的是生产代码而非副本): * ../../../Drivers/BSP/ScreenLink/sb_proto.c (sb_link 依赖它的帧 A 解析) * ../../../Drivers/BSP/ScreenLink/sb_link.c * 依赖面用桩替换:main.h(假 USART 寄存器 + 假 ReceiveToIdle_IT)、dbg_log.h(日志行缓冲)。 * * 为什么要主机侧单测(而不是只用台架): * 1. **ORE 清除序列**在台架上只能靠"拔插 TX 线"制造,且失败现象是"链路永久静默"—— * 与屏板没上电、线接反完全同形。主机侧能把"必须按 SR→DR 顺序读"这条钉成断言: * 桩按 RM0090 的语义实现(先读 SR 再读 DR 才清得掉 ORE),顺序反了清不掉 → * 下一次挂接收必然失败 → 用例立刻红。 * 2. **重挂接收漏一次 = 链路永久静默**:台架上要等 280 ms 才看得出来,且没有报错。 * 主机侧数 `sb_arm_calls` 即可。 * 3. 坏帧分支(长度不对 / 地址不对 / 功能码不对 / CRC 错)在台架上要造噪声才出现, * 一出现就被复位冲掉;主机侧是几行断言。 * * 覆盖点(对应 docs/任务规划/屏板链路.md §3 批 1 的 1-3 判据): * TC-SBLINK-001 init 参数守卫:空句柄 / 波特率不是 SB_LINK_BAUD(后者必须不挂接收) * TC-SBLINK-002 init 正常:挂接收一次、缓冲 = SB_LINK_RX_BUF_SIZE(16 B,不是 4208) * TC-SBLINK-003 未初始化就重挂接收:BSP_ERR_STATE,且不碰外设 * TC-SBLINK-004 非本链路的句柄:rx / error 两个入口都必须直接忽略 * TC-SBLINK-005 周期召唤帧(屏板实测向量 01 03 01 00 F0 48):计数、cmd、key、重挂 * TC-SBLINK-006 按键召唤帧(屏板实测向量 01 03 00 09 31 DE):key = 9 * TC-SBLINK-007 CRC 错帧:只累加 rx_crc_err,不污染 last_cmd / last_key * TC-SBLINK-008 长度不对:5 B / 16 B(缓冲满)/ 0 B 都记 rx_len_err * TC-SBLINK-009 地址 / 功能码 / 命令类型不符:记 rx_fmt_err * TC-SBLINK-010 错误中断(ORE,阻塞型):先读 SR 再读 DR 才清得掉,且必须重挂 * TC-SBLINK-015 错误中断(NE/FE,非阻塞型):接收还在跑,**不得**重挂(否则白记一次失败) * TC-SBLINK-011 重挂失败:HAL_BUSY / HAL_TIMEOUT / HAL_ERROR 三个映射 + rearm_fail 累计 * TC-SBLINK-012 统计上报:未到周期不出行;有变化才出;无变化不刷屏;首帧立刻单独报一次 * TC-SBLINK-013 统计上报的时基回绕:now_ms 跨 0xFFFFFFFF 仍按差值法工作 * TC-SBLINK-014 统计快照:空指针守卫、字段逐项对得上 * * 通过:全部 TC PASS(main 返回 0)。 * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 * * 创建日期:2026-10-29 */ #include #include #include /* ===================== 断言 ===================== */ static int s_total = 0, s_passed = 0, s_failed = 0; #define CHECK(cond, name) do { \ s_total++; \ if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \ else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \ } while (0) /* ===================== 被测代码 ===================== */ #include "sb_proto.c" #include "sb_link.c" /* ===================== 夹具 ===================== */ /* 本链路的句柄(地址唯一,用来区分"非本链路的句柄") */ static UART_HandleTypeDef s_uart; /* 另一个句柄:只当参数传进去,验证过滤 */ static UART_HandleTypeDef s_other; /* 屏板实测向量(出处:屏板固件工程 DTU-LCD 的实机抓包,同 test/host/sb_proto) */ static const uint8_t k_rt[SB_FRAME_A_LEN] = { 0x01U, 0x03U, 0x01U, 0x00U, 0xF0U, 0x48U }; static const uint8_t k_key[SB_FRAME_A_LEN] = { 0x01U, 0x03U, 0x00U, 0x09U, 0x31U, 0xDEU }; /* 复位桩 + 日志,然后以 700000 正常初始化一次;返回 init 的返回码 */ static bsp_err_t setup_link(void) { sb_fake_reset(); sb_log_reset(); s_uart.Instance = &g_sb_uart; s_uart.Init.BaudRate = SB_LINK_BAUD; s_uart.Init.WordLength = 8U; s_uart.Init.StopBits = 1U; s_uart.Init.Parity = 0U; s_uart.Init.Mode = 3U; s_uart.Init.OverSampling = 16U; s_uart.RxState = HAL_UART_STATE_READY; s_uart.ErrorCode = 0U; s_other.Instance = &g_sb_uart; s_other.Init.BaudRate = SB_LINK_BAUD; return sb_link_init(&s_uart); } /* 模拟"HAL 把 n 个字节搬进它拿到的那块缓冲,然后报一次 IDLE 事件" */ static void feed(const uint8_t *p_data, uint16_t len) { if ((sb_arm_buf != NULL) && (len > 0U)) { (void)memcpy(sb_arm_buf, p_data, len); } sb_link_on_rx_event(&s_uart, len); } /* 按 4 字节负载组一帧(CRC 低字节在前,与 sb_proto 口径一致),用来造反例帧 */ static void make_frame(uint8_t *p_frame, uint8_t addr, uint8_t func, uint8_t cmd, uint8_t key) { uint16_t crc; p_frame[0] = addr; p_frame[1] = func; p_frame[2] = cmd; p_frame[3] = key; crc = sb_crc16(p_frame, 4U); p_frame[4] = (uint8_t)(crc & 0xFFU); p_frame[5] = (uint8_t)((crc >> 8) & 0xFFU); } /* ===================== 用例 ===================== */ static void tc_init_guards(void) { bsp_err_t ret; sb_fake_reset(); sb_log_reset(); ret = sb_link_init(NULL); CHECK(ret == BSP_ERR_PARAM, "TC-SBLINK-001 a NULL handle is rejected"); CHECK(sb_arm_calls == 0U, "TC-SBLINK-001b nothing is armed for a NULL handle"); /* 波特率不是 700000:说明 Src/usart.c 的 USER CODE 覆盖丢了。 * 必须**不挂接收**并打一行 ERR —— 否则就是"115200 收 700 kbaud"的静默错配 */ s_uart.Instance = &g_sb_uart; s_uart.Init.BaudRate = 115200U; ret = sb_link_init(&s_uart); CHECK(ret == BSP_ERR_STATE, "TC-SBLINK-001c a wrong baud rate is rejected (BSP_ERR_STATE)"); CHECK(sb_arm_calls == 0U, "TC-SBLINK-001d nothing is armed when the baud rate is wrong"); CHECK(sb_log_n == 1U, "TC-SBLINK-001e exactly one ERR line is emitted"); CHECK(sb_log_has(0U, "baud=115200") == 1, "TC-SBLINK-001f the ERR line names the actual baud rate"); } static void tc_init_ok(void) { bsp_err_t ret = setup_link(); CHECK(ret == BSP_OK, "TC-SBLINK-002 init with 700000 returns BSP_OK"); CHECK(sb_arm_calls == 1U, "TC-SBLINK-002b reception is armed exactly once"); CHECK(sb_arm_size == SB_LINK_RX_BUF_SIZE, "TC-SBLINK-002c the receive buffer is SB_LINK_RX_BUF_SIZE"); CHECK(sb_arm_size == 16U, "TC-SBLINK-002d ... which is 16 B, not the board's 4208 B frame length"); CHECK(sb_arm_buf != NULL, "TC-SBLINK-002e a receive buffer is handed to HAL"); CHECK(sb_log_n == 0U, "TC-SBLINK-002f a clean init logs nothing"); } static void tc_not_initialised(void) { /* 先把模块打回未初始化态:sb_link_init 之前 s_huart 为 NULL。 * 本 TU 直接可见该静态变量(原样 include 生产源),这里显式还原一次。 */ UART_HandleTypeDef *saved = s_huart; sb_fake_reset(); sb_log_reset(); s_huart = NULL; CHECK(sb_link_receive_start() == BSP_ERR_STATE, "TC-SBLINK-003 re-arming before init returns BSP_ERR_STATE"); CHECK(sb_arm_calls == 0U, "TC-SBLINK-003b ... and never touches the peripheral"); s_huart = saved; } static void tc_foreign_handle(void) { sb_link_stats_t st; (void)setup_link(); sb_link_on_rx_event(&s_other, SB_FRAME_A_LEN); sb_link_on_error(&s_other); sb_link_get_stats(&st); CHECK(st.rx_events == 0U, "TC-SBLINK-004 an rx event on another handle is ignored"); CHECK(st.err_irq == 0U, "TC-SBLINK-004b an error event on another handle is ignored"); CHECK(sb_arm_calls == 1U, "TC-SBLINK-004c ... and does not re-arm (no extra arm call)"); CHECK(sb_reg_n == 0U, "TC-SBLINK-004d ... and does not touch the registers"); } static void tc_rt_frame(void) { sb_link_stats_t st; (void)setup_link(); feed(k_rt, SB_FRAME_A_LEN); sb_link_get_stats(&st); CHECK(st.rx_events == 1U, "TC-SBLINK-005 one IDLE event is counted"); CHECK(st.rx_ok == 1U, "TC-SBLINK-005b the board's real RT summon frame parses OK"); CHECK(st.last_size == SB_FRAME_A_LEN, "TC-SBLINK-005c last_size == 6"); CHECK(st.last_cmd == SB_CMD_RT, "TC-SBLINK-005d last_cmd == SB_CMD_RT"); CHECK(st.last_key == 0U, "TC-SBLINK-005e last_key == 0 for a periodic summon"); CHECK(sb_arm_calls == 2U, "TC-SBLINK-005f reception is re-armed after the event"); CHECK(st.rx_crc_err == 0U, "TC-SBLINK-005g no CRC error is counted"); CHECK(st.key_events == 0U, "TC-SBLINK-005h a periodic summon is not a key event"); } static void tc_key_frame(void) { sb_link_stats_t st; (void)setup_link(); feed(k_key, SB_FRAME_A_LEN); sb_link_get_stats(&st); CHECK(st.rx_ok == 1U, "TC-SBLINK-006 the board's real key summon frame parses OK"); CHECK(st.last_cmd == SB_CMD_KEY, "TC-SBLINK-006b last_cmd == SB_CMD_KEY"); CHECK(st.last_key == 9U, "TC-SBLINK-006c last_key == 9 (KEY_RESET on the board)"); CHECK(st.key_events == 1U, "TC-SBLINK-006d the key event counter advances (2-3 consumes this)"); } static void tc_crc_error(void) { uint8_t bad[SB_FRAME_A_LEN]; sb_link_stats_t st; (void)setup_link(); feed(k_key, SB_FRAME_A_LEN); /* 先来一帧好的,把 last_* 写成 9 */ (void)memcpy(bad, k_rt, SB_FRAME_A_LEN); bad[SB_FRAME_A_LEN - 1U] ^= 0xFFU; /* 打坏 CRC */ feed(bad, SB_FRAME_A_LEN); sb_link_get_stats(&st); CHECK(st.rx_events == 2U, "TC-SBLINK-007 both events are counted"); CHECK(st.rx_ok == 1U, "TC-SBLINK-007b only the good frame counts as OK"); CHECK(st.rx_crc_err == 1U, "TC-SBLINK-007c the corrupted frame is counted as a CRC error"); CHECK(st.last_key == 9U, "TC-SBLINK-007d a bad frame does not overwrite last_key"); CHECK(sb_arm_calls == 3U, "TC-SBLINK-007e reception is re-armed even after a bad frame"); } static void tc_length_error(void) { uint8_t five[5] = { 0x01U, 0x03U, 0x01U, 0x00U, 0xF0U }; uint8_t full[SB_LINK_RX_BUF_SIZE]; sb_link_stats_t st; (void)setup_link(); feed(five, 5U); /* 少一个字节 */ (void)memset(full, 0x00, sizeof(full)); feed(full, SB_LINK_RX_BUF_SIZE); /* 缓冲被塞满(噪声冲击的典型形态) */ sb_link_on_rx_event(&s_uart, 0U); /* 空事件(防御性) */ sb_link_get_stats(&st); CHECK(st.rx_events == 3U, "TC-SBLINK-008 every event is counted"); CHECK(st.rx_len_err == 3U, "TC-SBLINK-008b 5 B / 16 B / 0 B all count as length errors"); CHECK(st.rx_ok == 0U, "TC-SBLINK-008c none of them is treated as a valid frame"); CHECK(st.rx_crc_err == 0U, "TC-SBLINK-008d a wrong length is not reported as a CRC error"); CHECK(sb_arm_calls == 4U, "TC-SBLINK-008e reception is re-armed every time"); } static void tc_format_error(void) { uint8_t frame[SB_FRAME_A_LEN]; sb_link_stats_t st; (void)setup_link(); make_frame(frame, 0x02U, SB_FUNC_READ_HOLDING, SB_CMD_RT, 0U); /* 地址不对 */ feed(frame, SB_FRAME_A_LEN); make_frame(frame, SB_ADDR, 0x04U, SB_CMD_RT, 0U); /* 功能码不对 */ feed(frame, SB_FRAME_A_LEN); make_frame(frame, SB_ADDR, SB_FUNC_READ_HOLDING, 0x02U, 0U); /* 命令类型不对 */ feed(frame, SB_FRAME_A_LEN); sb_link_get_stats(&st); CHECK(st.rx_fmt_err == 3U, "TC-SBLINK-009 wrong address / function / command all count as format errors"); CHECK(st.rx_crc_err == 0U, "TC-SBLINK-009b ... and none of them is misreported as a CRC error"); CHECK(st.rx_ok == 0U, "TC-SBLINK-009c none of them is accepted"); } static void tc_error_clears_ore(void) { sb_link_stats_t st; (void)setup_link(); /* 造一次 ORE。ORE 是**阻塞型**错误:HAL 先把接收停掉(RxState 打回 READY)再回调, * 所以桩也先把 RxState 打回 READY —— 否则测的就不是 ORE 那条路 */ sb_ore_latched = 1U; s_uart.RxState = HAL_UART_STATE_READY; s_uart.ErrorCode = HAL_UART_ERROR_ORE; sb_link_on_error(&s_uart); CHECK(sb_fake_trace_is("SD") == 1, "TC-SBLINK-010 the error path reads SR then DR (RM0090 clear sequence)"); CHECK(sb_ore_latched == 0U, "TC-SBLINK-010b ORE is actually cleared"); CHECK(sb_arm_calls == 2U, "TC-SBLINK-010c a blocking error (ORE) re-arms reception"); CHECK(sb_arm_force == SB_FAKE_ARM_OK, "TC-SBLINK-010d (sanity) no forced failure is in play"); sb_link_get_stats(&st); CHECK(st.err_irq == 1U, "TC-SBLINK-010e the error interrupt is counted"); CHECK(st.err_bits == HAL_UART_ERROR_ORE, "TC-SBLINK-010f the error bits are recorded"); CHECK(st.rearm_fail == 0U, "TC-SBLINK-010g re-arming succeeded, so rearm_fail stays 0"); /* 反例:如果哪天有人把清除序列写成 DR→SR,ORE 就清不掉,下一次挂接收必失败。 * 这里直接模拟"ORE 没清掉"的后果,把判据钉住 */ sb_ore_latched = 1U; sb_sr_seen = 0U; CHECK(sb_link_receive_start() == BSP_ERR_IO, "TC-SBLINK-010h an uncleared ORE makes the next arm fail (this is why the clear matters)"); } /* * 非阻塞型错误(NE/FE/PE):HAL 不动接收(RxState 仍是 BUSY_RX),**不该**去重挂。 * 2026-10-29 台架实测踩的就是这条:每帧都伴随 NE/FE,旧写法每次都想重挂、每次拿到 * HAL_BUSY,于是 rearm_fail 跟着 err_irq 一起涨(160/160),看着像链路坏了。 */ static void tc_error_nonblocking(void) { sb_link_stats_t st; (void)setup_link(); s_uart.RxState = HAL_UART_STATE_BUSY_RX; /* 接收还在跑 */ s_uart.ErrorCode = HAL_UART_ERROR_NE; sb_link_on_error(&s_uart); CHECK(sb_arm_calls == 1U, "TC-SBLINK-015 a non-blocking error (NE) does not re-arm reception"); CHECK(sb_fake_trace_is("SD") == 1, "TC-SBLINK-015b ... but still clears the flags (SR then DR)"); sb_link_get_stats(&st); CHECK(st.err_irq == 1U, "TC-SBLINK-015c the error interrupt is counted"); CHECK(st.err_bits == HAL_UART_ERROR_NE, "TC-SBLINK-015d the NE bit is recorded"); CHECK(st.rearm_fail == 0U, "TC-SBLINK-015e ... and no phantom 're-arm failure' is booked"); /* 再来一次 FE:错误位按并集累计 */ s_uart.ErrorCode = HAL_UART_ERROR_FE; sb_link_on_error(&s_uart); sb_link_get_stats(&st); CHECK(st.err_bits == (HAL_UART_ERROR_NE | HAL_UART_ERROR_FE), "TC-SBLINK-015f error bits accumulate as a union"); } static void tc_rearm_failure(void) { sb_link_stats_t st; (void)setup_link(); sb_arm_force = SB_FAKE_ARM_BUSY; CHECK(sb_link_receive_start() == BSP_ERR_BUSY, "TC-SBLINK-011 HAL_BUSY maps to BSP_ERR_BUSY"); sb_arm_force = SB_FAKE_ARM_TIMEOUT; CHECK(sb_link_receive_start() == BSP_ERR_TIMEOUT, "TC-SBLINK-011b HAL_TIMEOUT maps to BSP_ERR_TIMEOUT"); sb_arm_force = SB_FAKE_ARM_ERROR; CHECK(sb_link_receive_start() == BSP_ERR_IO, "TC-SBLINK-011c HAL_ERROR maps to BSP_ERR_IO"); sb_link_get_stats(&st); CHECK(st.rearm_fail == 3U, "TC-SBLINK-011d every failed arm is counted in rearm_fail"); /* 中断里也要如实累计(不是只有任务侧那条路) */ sb_arm_force = SB_FAKE_ARM_BUSY; sb_link_on_rx_event(&s_uart, SB_FRAME_A_LEN); sb_link_get_stats(&st); CHECK(st.rearm_fail == 4U, "TC-SBLINK-011e a failed re-arm inside the rx callback is counted too"); } static void tc_poll_reporting(void) { sb_link_stats_t st; (void)setup_link(); /* 未到周期:一行都不出(哪怕计数已经变了) */ feed(k_rt, SB_FRAME_A_LEN); sb_link_poll(0U); CHECK(sb_log_n == 1U, "TC-SBLINK-012 the first valid frame is reported immediately"); CHECK(sb_log_has(0U, "first frame") == 1, "TC-SBLINK-012b ... by a dedicated 'first frame' line"); CHECK(sb_log_has(0U, "size=6") == 1, "TC-SBLINK-012c ... which carries size=6 (the 1-3 criterion)"); /* 到周期且计数有变化:出一行累计值 */ feed(k_rt, SB_FRAME_A_LEN); sb_link_poll(SB_LINK_REPORT_MS); CHECK(sb_log_n == 2U, "TC-SBLINK-012d a periodic report is emitted once the period elapses"); CHECK(sb_log_has(1U, "rx=2") == 1, "TC-SBLINK-012e the report carries the cumulative event count"); CHECK(sb_log_has(1U, "ok=2") == 1, "TC-SBLINK-012f ... and the OK count"); /* 到周期但计数没变:不出行(链路安静时不刷屏) */ sb_link_poll(2U * SB_LINK_REPORT_MS); CHECK(sb_log_n == 2U, "TC-SBLINK-012g nothing is emitted when no counter changed"); /* 有错帧:再出一行,且 err/crc 列可见 */ { uint8_t bad[SB_FRAME_A_LEN]; (void)memcpy(bad, k_rt, SB_FRAME_A_LEN); bad[5] ^= 0xFFU; feed(bad, SB_FRAME_A_LEN); } sb_link_poll(3U * SB_LINK_REPORT_MS); CHECK(sb_log_n == 3U, "TC-SBLINK-012h a CRC error triggers a new report"); CHECK(sb_log_has(2U, "crc=1") == 1, "TC-SBLINK-012i the report carries the CRC error count"); sb_link_get_stats(&st); CHECK(st.rx_events == 3U, "TC-SBLINK-012j (sanity) three events were fed in total"); } static void tc_poll_wraparound(void) { uint32_t base = 0xFFFFFFFFU - (SB_LINK_REPORT_MS / 2U); (void)setup_link(); /* 基准:距上次上报(0)已经远超一个周期 → 首帧行 + 周期行各一行 */ feed(k_rt, SB_FRAME_A_LEN); sb_link_poll(base); CHECK(sb_log_n == 2U, "TC-SBLINK-013 the baseline poll emits both the first-frame line and a report"); feed(k_rt, SB_FRAME_A_LEN); sb_link_poll(base + (SB_LINK_REPORT_MS / 2U)); /* 还没到周期 */ CHECK(sb_log_n == 2U, "TC-SBLINK-013b the period is not up yet"); /* 跨过 0xFFFFFFFF:差值法(now - last)必须仍然成立,且真的绕回了 */ CHECK((base + SB_LINK_REPORT_MS + 1U) < base, "TC-SBLINK-013c (sanity) this poll really wraps around"); sb_link_poll(base + SB_LINK_REPORT_MS + 1U); CHECK(sb_log_n == 3U, "TC-SBLINK-013d the report still fires across the tick wrap-around"); } static void tc_stats_snapshot(void) { sb_link_stats_t st; (void)setup_link(); feed(k_key, SB_FRAME_A_LEN); (void)memset(&st, 0, sizeof(st)); sb_link_get_stats(NULL); /* 空指针守卫:不得崩 */ sb_link_get_stats(&st); CHECK(st.rx_events == 1U, "TC-SBLINK-014 the snapshot carries rx_events"); CHECK(st.last_size == SB_FRAME_A_LEN, "TC-SBLINK-014b ... last_size"); CHECK(st.last_cmd == SB_CMD_KEY, "TC-SBLINK-014c ... last_cmd"); CHECK(st.last_key == 9U, "TC-SBLINK-014d ... last_key"); CHECK(st.rearm_fail == 0U, "TC-SBLINK-014e ... rearm_fail"); } int main(void) { printf("=== ScreenLink Link Tests ===\n"); printf("-- init: guards --\n"); tc_init_guards(); printf("-- init: normal --\n"); tc_init_ok(); printf("-- re-arm before init --\n"); tc_not_initialised(); printf("-- callback dispatch: foreign handle --\n"); tc_foreign_handle(); printf("-- rx event: periodic summon --\n"); tc_rt_frame(); printf("-- rx event: key summon --\n"); tc_key_frame(); printf("-- rx event: bad CRC --\n"); tc_crc_error(); printf("-- rx event: wrong length --\n"); tc_length_error(); printf("-- rx event: wrong address / function / command --\n"); tc_format_error(); printf("-- error event: ORE clearing --\n"); tc_error_clears_ore(); printf("-- error event: non-blocking (NE/FE) must not re-arm --\n"); tc_error_nonblocking(); printf("-- re-arm failure mapping --\n"); tc_rearm_failure(); printf("-- periodic reporting --\n"); tc_poll_reporting(); printf("-- periodic reporting: tick wrap-around --\n"); tc_poll_wraparound(); printf("-- stats snapshot --\n"); tc_stats_snapshot(); printf("\n=== ScreenLink Link Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); return (s_failed == 0) ? 0 : 1; }