Files
STM32F4-Base/test/host/sb_link/sb_link_test.c
T
wandering1 7708f12c10 fix(ui/screenlink): 按键应答扣到按键后画面,反馈典型 250-450 ms 降至 70-135 ms
按键召唤答的是按键前的旧帧:uiTask 还没消化键答案就发走了,新画面
只能等下一次周期召唤(280 ms/轮)才带得出——真机反馈 250-450 ms 的
主构成(根因账本 docs/子系统设计/屏板链路设计说明.md §7.1)。

改动(用户批准范围 A+B+C;屏板固件未动,决策三不变):
- sb_link:按键召唤改为只入队不起发;uiTask 同拍重绘并组帧成功后交
  凭据(sb_link_key_rendered),TIM3 1 kHz 的 sb_link_ms_tick 以 ≤1 ms
  放行新帧。140 ms 截止(SB_KEY_RESP_MAX_MS,+59.6 ms 线上 <205 ms
  屏板放弃线)保底发旧帧,"一召一答"契约不破;线忙时同旧丢单、
  不留扣件。起发两中断源(USART1 IDLE/TIM3)共用 sb_tx_kick,
  "判忙→占位"在 PRIMASK 临界区原子完成。
- ui_task:排空注键挪到组帧之前,注键→即派即绘→组帧同拍闭环;
  组帧返回 BSP_OK 才交重绘凭据(BUSY 拍帧还没反映按键)。
- ui_port:注键单槽改 8 深环(连按不再互相覆盖,满丢最旧+计数);
  新增 ui_port_service_keys:同拍 lv_indev_read + lv_refr_now,
  省掉 indev ≤20 ms + 刷新 ≤20 ms 两段等待。
- ui_menu_view:菜单移动选中只失效新旧两行带(黄金参考零像素变化),
  滚动/异常回退整框失效。
- simulator:窗口循环 sleep 扣除本轮已耗时(原先无条件睡满
  lv_timer_handler 返回值,叠加 Windows Sleep 15.6 ms 粒度放大观感)。
- 串口 TX 上报新增 dfok/dfold(凭据放行新帧数/超时保底旧帧数):
  dfold 连涨 = uiTask 被饿 ≥140 ms 的直接现场证据。

验证:test/host/sb_link 164/164(新增 18 条:不立刻起发/凭据放行/
140 ms 截止保底/忙时不留扣件/陈旧凭据不放行等边界);菜单 8 状态 +
内容页 66 张黄金像素 PASS;Keil @build 0 错误 0 警告(两次);
BOM 99 文件无漂移、代码规范、doc-links 本次文件零问题、cppcheck PASS。
文档:屏板规划文档已被主干收编进设计说明,延迟账本落
docs/子系统设计/屏板链路设计说明.md §7.1,并同步 §4 口径、§8 接口、
§9.1 单测台账 146→164、§10 坑账本补条。

RAM 台账(分支不跑 --write,并行分支提交约定):本笔盖戳行
RW_IRAM1 94,336 B (72.0%) / RW_CCM 57,376 B (87.9%),较上一台账行
+4,400 B——为距上次台账构建以来分支累计差(含批 3 状态灯等未盖戳
提交),非全部本笔引入;本次自身增量按对象计为 ui_port.o +12 B
(8 深键环+丢计数+indev 指针)与 sb_link/o 数十字节级。
gate: only mem-budget/mem-numbers pending lead ledger backfill
(合并落主干后 Lead 统一 --write 回填受管区)。
2026-10-30 09:50:31 +08:00

1018 lines
42 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:屏板链路传输层(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 §2 的收中断与错误处理判据):
* 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 统计快照:空指针守卫、字段逐项对得上
*
* 覆盖点(对应 docs/子系统设计/屏板链路设计说明.md §4 的组帧与时序判据):
* TC-SBLINK-016 组帧入口守卫:未初始化 / 空显存指针
* TC-SBLINK-017 帧 B 内容:点阵逐字节、info[0..3] 小端掩码、info[4..7] 非 LOGO、
* 998 B 填充全 0、CRC 覆盖 [0, 4205]
* TC-SBLINK-018 变化检测:内容不变不重组;点阵改一字节要重组;只改掩码也要重组
* TC-SBLINK-019 中断起发:未组帧不发;一召唤一帧 4208 B;TX 期间不叠发
* TC-SBLINK-020 TX 占用期间帧缓冲逐字节冻结,TxCplt 后补组(防"脏帧")
* TC-SBLINK-021 起发失败要清 busy,否则链路此后永不发送
* TC-SBLINK-022 错误中断里清残留 busy;但 TX 真的还在发时不得判死(防残留 busy)
* TC-SBLINK-023 按键队列:单次交付、周期帧不带键、溢出丢新并计数、FIFO 顺序
* TC-SBLINK-024 离线判定:9.999 s 不判、10 s 判、不重复刷屏、一帧即恢复
* TC-SBLINK-025 组帧耗时换算:周期数 → µs 的除数(SystemCoreClock/1e6)
* TC-SBLINK-026 上报行:fb/tx 列可见;离线行排在周期行之前且不重复
*
* 覆盖点(2026-10-30 按键延迟整治:按键召唤延迟应答,
* docs/子系统设计/屏板链路设计说明.md §7.1):
* TC-SBLINK-030 按键召唤不立刻起发:扣住本答、键照常进队、不误记丢单
* TC-SBLINK-031 放行链:画面更新 + 组帧 OK + key_rendered 凭据 → 下一拍发一帧、
* dfok 计数、放行后不再复发的
* TC-SBLINK-032 截止保底:uiTask 饿死时满 SB_KEY_RESP_MAX_MS 发旧帧、dfold 计数、
* 窗口随即关闭
* TC-SBLINK-033 边界四条:周期召唤不受影响 / 线忙丢单不留扣件 /
* 新召唤清陈旧凭据 / 首帧未备时同旧丢单
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-29
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
/* ===================== 断言 ===================== */
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.gState = 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);
}
/* 假显存:组帧的输入(内容按 i 变化,方便"改一个字节"的用例) */
static uint8_t s_bmp[SB_FRAME_B_BMP_LEN];
static void fill_bmp(uint8_t seed)
{
uint32_t i;
for (i = 0U; i < SB_FRAME_B_BMP_LEN; i++) {
s_bmp[i] = (uint8_t)(seed + (uint8_t)(i & 0xFFU));
}
}
/*
* 模拟"HAL 的 DMA 传完 → gState 打回 READY → 调 TxCplt 回调"。
* 真机这两步在 HAL_DMA_IRQHandler 里连着做(UART_DMATransmitCplt 里先改 gState
* 再回调),所以测试也必须按这个顺序——否则"错误中断里靠 gState 判残留 busy"
* 那条用例的前提就不成立了。
*/
static void tx_complete(void)
{
s_uart.gState = HAL_UART_STATE_READY;
sb_link_on_tx_cplt(&s_uart);
}
/* ===================== 用例 ===================== */
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");
/* 有错帧:再出一行,且 crc 列可见。
* 同时喂一帧正常召唤:本用例的时基是每步 +5 s,单靠错帧会让"连续 10 s 无**有效**
* 召唤帧"在第三步就成立(那是 2-5 的离线判定,由 TC-SBLINK-027 专测),
* 那行 OFFLINE 会插进来把行号错开,让本用例悄悄变成在测别的东西。 */
{
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);
feed(k_rt, 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 == 4U, "TC-SBLINK-012j (sanity) four 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");
}
/* ===================== 批 2 用例(帧 B 组帧 / 起发 / 收尾 / 按键 / 离线) ===================== */
static void tc_frame_update_guards(void)
{
fill_bmp(0x11U);
/* 未初始化:STATE。直接清 s_huart(本用例白盒:测试 TU 里 include 了 sb_link.c,
* 静态量可见)——否则本用例能否通过就取决于"有没有人先跑过 setup_link",
* 那是用例顺序依赖,加一条用例就可能把它弄红。 */
s_huart = NULL;
sb_fake_reset();
sb_log_reset();
CHECK(sb_link_frame_update(s_bmp, 0U) == BSP_ERR_STATE,
"TC-SBLINK-016 a frame update before init is refused");
(void)setup_link();
CHECK(sb_link_frame_update(NULL, 0U) == BSP_ERR_PARAM,
"TC-SBLINK-016b a NULL framebuffer is refused");
}
static void tc_frame_build_content(void)
{
uint32_t i;
uint32_t pad_bad = 0U;
sb_link_stats_t st;
(void)setup_link();
fill_bmp(0x11U);
CHECK(sb_link_frame_update(s_bmp, 0xA5A55A5AU) == BSP_OK,
"TC-SBLINK-017 the first frame is built");
CHECK(memcmp(s_frame, s_bmp, SB_FRAME_B_BMP_LEN) == 0,
"TC-SBLINK-017b ... the bitmap region is a verbatim copy of the live framebuffer");
/* 0xA5A55A5A 小端 = 5A 5A A5 A5 */
CHECK((s_frame[SB_FRAME_B_INFO_OFF + 0U] == 0x5AU) &&
(s_frame[SB_FRAME_B_INFO_OFF + 1U] == 0x5AU) &&
(s_frame[SB_FRAME_B_INFO_OFF + 2U] == 0xA5U) &&
(s_frame[SB_FRAME_B_INFO_OFF + 3U] == 0xA5U),
"TC-SBLINK-017c ... the LED mask lands in info[0..3] little-endian");
CHECK((s_frame[SB_FRAME_B_INFO_OFF + 4U] == 0U) &&
(s_frame[SB_FRAME_B_INFO_OFF + 5U] == 0U),
"TC-SBLINK-017d ... info[4..5] is not the LOGO magic");
CHECK((s_frame[SB_FRAME_B_INFO_OFF + 6U] == 0U) &&
(s_frame[SB_FRAME_B_INFO_OFF + 7U] == 0U),
"TC-SBLINK-017e ... info[6..7] reserved is zero");
for (i = SB_FRAME_B_PAD_OFF; i < SB_FRAME_B_CRC_OFF; i++) {
if (s_frame[i] != 0U) {
pad_bad++;
}
}
CHECK(pad_bad == 0U, "TC-SBLINK-017f ... all 998 padding bytes are zero");
CHECK(sb_frame_b_verify(s_frame, (uint16_t)SB_LINK_FRAME_LEN) == BSP_OK,
"TC-SBLINK-017g ... and the CRC covers [0, 4205]");
sb_link_get_stats(&st);
CHECK(st.frame_build == 1U, "TC-SBLINK-017h ... counted exactly once");
CHECK(st.frame_bad == 0U, "TC-SBLINK-017i ... self-check clean");
}
static void tc_frame_unchanged(void)
{
sb_link_stats_t st;
(void)setup_link();
fill_bmp(0x11U);
(void)sb_link_frame_update(s_bmp, 0U);
/* 同样的内容再来一次:不重组。uiTask 每 10 ms 调一次,这条决定了常态成本只有 memcmp */
CHECK(sb_link_frame_update(s_bmp, 0U) == BSP_OK,
"TC-SBLINK-018 an unchanged framebuffer returns OK");
sb_link_get_stats(&st);
CHECK(st.frame_build == 1U, "TC-SBLINK-018b ... without rebuilding the frame");
/* 点阵改一个字节:必须重组 */
s_bmp[1234] ^= 0xFFU;
CHECK(sb_link_frame_update(s_bmp, 0U) == BSP_OK,
"TC-SBLINK-018c a changed byte is picked up");
sb_link_get_stats(&st);
CHECK(st.frame_build == 2U, "TC-SBLINK-018d ... and triggers a rebuild");
/* 点阵没变、只有掩码变:也要重组(info[0..3] 在 CRC 覆盖区内) */
CHECK(sb_link_frame_update(s_bmp, 0x00000001U) == BSP_OK,
"TC-SBLINK-018e a changed LED mask is picked up");
sb_link_get_stats(&st);
CHECK(st.frame_build == 3U, "TC-SBLINK-018f ... and triggers a rebuild too");
CHECK(s_frame[SB_FRAME_B_INFO_OFF] == 0x01U,
"TC-SBLINK-018g ... with the new mask in info[0]");
}
static void tc_tx_kick(void)
{
sb_link_stats_t st;
(void)setup_link();
fill_bmp(0x22U);
/* 还没组帧:召唤来了也不发(发一帧全 0 的图等于让屏板白屏,而且它不报错) */
feed(k_rt, SB_FRAME_A_LEN);
sb_link_get_stats(&st);
CHECK(st.tx_kick == 0U, "TC-SBLINK-019 no frame is sent before the first build");
CHECK(st.tx_busy_drop == 1U, "TC-SBLINK-019b ... the summon is counted as dropped instead");
(void)sb_link_frame_update(s_bmp, 0U);
feed(k_rt, SB_FRAME_A_LEN);
sb_link_get_stats(&st);
CHECK(st.tx_kick == 1U, "TC-SBLINK-019c a valid summon kicks the transfer");
CHECK(sb_tx_calls == 1U, "TC-SBLINK-019d ... through exactly one HAL call");
CHECK(sb_tx_len == SB_LINK_FRAME_LEN, "TC-SBLINK-019e ... of 4208 bytes");
CHECK(sb_tx_buf == s_frame,
"TC-SBLINK-019f ... from the static frame buffer (the DMA-reachable one)");
/* 屏板会一直召唤:上一帧没发完就再来一次 → 绝不叠发 */
feed(k_rt, SB_FRAME_A_LEN);
sb_link_get_stats(&st);
CHECK(sb_tx_calls == 1U,
"TC-SBLINK-019g a second summon during TX does not stack another transfer");
CHECK(st.tx_busy_drop == 2U, "TC-SBLINK-019h ... it is counted as dropped");
/* TxCplt 之后又能发了 */
tx_complete();
sb_link_get_stats(&st);
CHECK(st.tx_done == 1U, "TC-SBLINK-019i TxCplt is counted");
feed(k_rt, SB_FRAME_A_LEN);
CHECK(sb_tx_calls == 2U, "TC-SBLINK-019j ... and the next summon goes out again");
}
static void tc_tx_busy_freezes_frame(void)
{
uint8_t before[SB_FRAME_B_BMP_LEN];
sb_link_stats_t st;
(void)setup_link();
fill_bmp(0x33U);
(void)sb_link_frame_update(s_bmp, 0U);
feed(k_rt, SB_FRAME_A_LEN); /* 起发 → busy */
(void)memcpy(before, s_frame, sizeof(before));
fill_bmp(0x77U); /* 画面变了,但正在发 */
CHECK(sb_link_frame_update(s_bmp, 0U) == BSP_ERR_BUSY,
"TC-SBLINK-020 a frame update during TX is refused");
CHECK(memcmp(s_frame, before, sizeof(before)) == 0,
"TC-SBLINK-020b ... the buffer DMA is reading is left byte-for-byte untouched");
tx_complete();
CHECK(sb_link_frame_update(s_bmp, 0U) == BSP_OK,
"TC-SBLINK-020c ... and the pending change is picked up right after TxCplt");
CHECK(memcmp(s_frame, s_bmp, sizeof(before)) == 0,
"TC-SBLINK-020d ... so no stale frame survives the busy window");
sb_link_get_stats(&st);
CHECK(st.frame_build == 2U, "TC-SBLINK-020e ... exactly one rebuild, no double build");
}
static void tc_tx_kick_failure(void)
{
sb_link_stats_t st;
(void)setup_link();
fill_bmp(0x44U);
(void)sb_link_frame_update(s_bmp, 0U);
sb_tx_force = SB_FAKE_TX_BUSY;
feed(k_rt, SB_FRAME_A_LEN);
sb_link_get_stats(&st);
CHECK(st.tx_kick_fail == 1U, "TC-SBLINK-021 a failed HAL kick is counted");
CHECK(st.tx_kick == 0U, "TC-SBLINK-021b ... and is not counted as a transfer");
/* 失败必须把 busy 清掉,否则此后每次召唤都被判"上一帧还在发",链路再也不发 */
sb_tx_force = SB_FAKE_TX_OK;
feed(k_rt, SB_FRAME_A_LEN);
sb_link_get_stats(&st);
CHECK(st.tx_kick == 1U, "TC-SBLINK-021c ... and the next summon retries successfully");
}
static void tc_tx_abort_on_error(void)
{
sb_link_stats_t st;
(void)setup_link();
fill_bmp(0x55U);
(void)sb_link_frame_update(s_bmp, 0U);
feed(k_rt, SB_FRAME_A_LEN); /* 起发,gState = BUSY_TX */
/* 真机:错误中断打断 TX 时 HAL 把 gState 打回 READY,但**不会**再调 TxCplt →
* busy 会永久挂着。这正是"拔掉屏板后无残留 busy"要防的那条。 */
sb_ore_latched = 1U;
s_uart.RxState = HAL_UART_STATE_READY;
s_uart.gState = HAL_UART_STATE_READY;
s_uart.ErrorCode = HAL_UART_ERROR_ORE;
sb_link_on_error(&s_uart);
sb_link_get_stats(&st);
CHECK(st.tx_abort == 1U, "TC-SBLINK-022 a stuck TX busy is cleared by the error callback");
s_uart.gState = HAL_UART_STATE_READY;
feed(k_rt, SB_FRAME_A_LEN);
CHECK(sb_tx_calls == 2U, "TC-SBLINK-022b ... so the link keeps transmitting afterwards");
/* 反例:TX 真的还在发(gState 仍是 BUSY_TX)时不得判死 */
sb_ore_latched = 0U;
s_uart.RxState = HAL_UART_STATE_BUSY_RX; /* 非阻塞型错误:接收照跑 */
s_uart.ErrorCode = HAL_UART_ERROR_NE;
sb_link_on_error(&s_uart); /* gState 仍 BUSY_TX */
sb_link_get_stats(&st);
CHECK(st.tx_abort == 1U, "TC-SBLINK-022c ... but a still-running TX is not declared dead");
CHECK(sb_tx_calls == 2U, "TC-SBLINK-022d ... and no extra transfer was started");
}
static void tc_key_queue(void)
{
uint8_t i;
sb_link_stats_t st;
(void)setup_link();
CHECK(sb_link_key_take() == 0U, "TC-SBLINK-023 an empty queue yields 0");
feed(k_key, SB_FRAME_A_LEN); /* 屏板实测向量:key = 9 */
CHECK(sb_link_key_take() == 9U, "TC-SBLINK-023b the key byte reaches the consumer");
CHECK(sb_link_key_take() == 0U, "TC-SBLINK-023c ... exactly once");
/* 周期召唤的 key 恒 0:不占队列(屏板自己的语义就是"没有按键事件") */
feed(k_rt, SB_FRAME_A_LEN);
CHECK(sb_link_key_take() == 0U, "TC-SBLINK-023d a periodic summon never queues a key");
/* 队列满:丢**新**的并计数(旧键已经在路上,新键还会有下一次召唤)。
* 环形队列留一格判满 → 容量 = SB_LINK_KEY_QUEUE_LEN - 1 = 3,推 6 次丢 3 次。 */
for (i = 0U; i < (uint8_t)(SB_LINK_KEY_QUEUE_LEN + 2U); i++) {
feed(k_key, SB_FRAME_A_LEN);
}
sb_link_get_stats(&st);
CHECK(st.key_drop == 3U, "TC-SBLINK-023e overflow drops the newest and counts it");
for (i = 0U; i < (uint8_t)(SB_LINK_KEY_QUEUE_LEN - 1U); i++) {
CHECK(sb_link_key_take() == 9U, "TC-SBLINK-023f ... FIFO order is preserved");
}
CHECK(sb_link_key_take() == 0U, "TC-SBLINK-023g ... and the queue is empty again");
}
static void tc_offline(void)
{
sb_link_stats_t st;
(void)setup_link();
feed(k_rt, SB_FRAME_A_LEN);
sb_link_poll(0U);
sb_link_get_stats(&st);
CHECK(st.offline == 0U, "TC-SBLINK-024 a fresh link is online");
/* 9.999 s 无召唤:还不算离线(屏板自复位约 1 s 空窗,按 500 ms 判会反复误报) */
sb_link_poll(SB_LINK_OFFLINE_MS - 1U);
sb_link_get_stats(&st);
CHECK(st.offline == 0U, "TC-SBLINK-024b just under 10 s of silence is still online");
/* 满 10 s:置离线,并且只打一行 */
sb_log_reset();
sb_link_poll(SB_LINK_OFFLINE_MS);
sb_link_get_stats(&st);
CHECK(st.offline == 1U, "TC-SBLINK-024c 10 s of silence marks the board offline");
CHECK(sb_log_has(0U, "OFFLINE") == 1, "TC-SBLINK-024d ... with one dedicated line");
/* 再久也不重复刷屏 */
sb_log_reset();
sb_link_poll(SB_LINK_OFFLINE_MS + 1000U);
CHECK(sb_log_n == 0U, "TC-SBLINK-024e the offline line is not repeated every poll");
/* 屏板回来:一帧就够,立刻清标志并报恢复 */
sb_log_reset();
feed(k_rt, SB_FRAME_A_LEN);
sb_link_poll(SB_LINK_OFFLINE_MS + 1010U);
sb_link_get_stats(&st);
CHECK(st.offline == 0U, "TC-SBLINK-024f a single valid summon brings it back online");
CHECK(sb_log_has(0U, "back online") == 1, "TC-SBLINK-024g ... and says so");
}
static void tc_frame_timing(void)
{
sb_link_stats_t st;
(void)setup_link();
fill_bmp(0x66U);
/* 每次读 CYCCNT 前进 16800 周期 = 100 µs @168 MHz;组帧读两次 → 差值正好 16800。
* 这条钉的是"周期 → µs"的除数(SystemCoreClock/1e6),换算错了会在台架上
* 把 0.17 ms 报成 17 ms 或 1.7 µs,两种都会被当成"组帧太慢/没测到"。 */
sb_cyccnt_step = 16800U;
(void)sb_link_frame_update(s_bmp, 0U);
sb_link_get_stats(&st);
CHECK(st.frame_us == 100U, "TC-SBLINK-025 the cycle count is converted with SystemCoreClock");
CHECK(st.frame_us_max == 100U, "TC-SBLINK-025b ... and the peak is tracked");
s_bmp[0] ^= 0xFFU;
sb_cyccnt_step = 50400U; /* 300 µs */
(void)sb_link_frame_update(s_bmp, 0U);
sb_link_get_stats(&st);
CHECK(st.frame_us == 300U, "TC-SBLINK-025c ... the last build duration");
CHECK(st.frame_us_max == 300U, "TC-SBLINK-025d ... and the peak follows the slower build");
}
static void tc_tx_report(void)
{
(void)setup_link();
fill_bmp(0x88U);
(void)sb_link_frame_update(s_bmp, 0U);
sb_log_reset();
sb_link_poll(SB_LINK_REPORT_MS);
CHECK(sb_log_n == 1U, "TC-SBLINK-026 a changed frame counter produces a report");
CHECK(sb_log_has(0U, "fb=1") == 1, "TC-SBLINK-026b ... carrying the build count");
CHECK(sb_log_has(0U, "tx=0") == 1, "TC-SBLINK-026c ... and the transfer counters");
/* 这一拍会先出一行 OFFLINE(10 s 无有效召唤,2-5),但不该再出 TX 行。
* 断言行数而不是"没看见 tx=",是为了把"离线判定排在周期上报之前"这个顺序也钉住。 */
sb_log_reset();
sb_link_poll(2U * SB_LINK_REPORT_MS);
CHECK(sb_log_n == 1U, "TC-SBLINK-026d only the offline line is emitted, no TX report");
CHECK(sb_log_has(0U, "OFFLINE") == 1, "TC-SBLINK-026e ... and it is the offline one");
}
/* ===================== 按键召唤延迟应答(2026-10-30) ===================== */
/* 模拟"TIM3 1 kHz 分支跑了 n 拍"(真机每拍必调 sb_link_ms_tick,见 Src/main.c) */
static void run_ms_tick(uint32_t n)
{
uint32_t i;
for (i = 0U; i < n; i++) {
sb_link_ms_tick();
}
}
static void tc_key_defer_fresh(void)
{
uint8_t f[SB_FRAME_A_LEN];
sb_link_stats_t st;
(void)setup_link();
fill_bmp(0xA1U);
(void)sb_link_frame_update(s_bmp, 0U); /* 帧备好、线空闲 */
make_frame(f, 0x01U, 0x03U, SB_CMD_KEY, 3U); /* 屏板键 3 = DOWN */
sb_tx_calls = 0U;
feed(f, SB_FRAME_A_LEN);
/* 延迟应答的核心断言:按键召唤**不立刻起发**——立刻发的是按键**之前**的画面,
* 这一答等于白发(用户看到变化还要再等一整轮召唤)。 */
sb_link_get_stats(&st);
CHECK(sb_tx_calls == 0U, "TC-SBLINK-030 key summon does not kick immediately");
CHECK((st.tx_kick == 0U) && (st.tx_busy_drop == 0U),
"TC-SBLINK-030b ... nothing sent and nothing dropped");
CHECK(sb_link_key_take() == 3U, "TC-SBLINK-030c ... the key still enters the queue");
/* 任务侧的等效动作:画面变新 → 组帧 OK → 交凭据 → 下一拍放行 */
s_bmp[7] ^= 0xFFU;
(void)sb_link_frame_update(s_bmp, 0U);
sb_link_key_rendered();
sb_link_ms_tick();
sb_link_get_stats(&st);
CHECK(sb_tx_calls == 1U, "TC-SBLINK-031a release sends exactly one frame");
CHECK(st.defer_fresh == 1U, "TC-SBLINK-031b ... counted as a fresh release");
CHECK(st.tx_kick == 1U, "TC-SBLINK-031c ... and counted as a normal kick");
/* 放行即销扣件:之后的拍不得再发("一召一答"不许变连发) */
tx_complete();
sb_link_ms_tick();
CHECK(sb_tx_calls == 1U, "TC-SBLINK-031d ... and it does not fire again once released");
}
static void tc_key_defer_timeout(void)
{
uint8_t f[SB_FRAME_A_LEN];
sb_link_stats_t st;
(void)setup_link();
fill_bmp(0xB2U);
(void)sb_link_frame_update(s_bmp, 0U);
make_frame(f, 0x01U, 0x03U, SB_CMD_KEY, 2U);
sb_tx_calls = 0U;
feed(f, SB_FRAME_A_LEN);
/* uiTask 被饿(对时突发那类事故):一直不交凭据。到上限必须发旧帧保底——
* 欠发会让屏板白等一整个 205 ms 重问窗口,而"答一帧旧画面"至少守住协议。 */
run_ms_tick(SB_KEY_RESP_MAX_MS - 1U);
CHECK(sb_tx_calls == 0U, "TC-SBLINK-032a still held one ms before the deadline");
sb_link_ms_tick();
sb_link_get_stats(&st);
CHECK(sb_tx_calls == 1U, "TC-SBLINK-032b the deadline sends the old frame");
CHECK(st.defer_old == 1U, "TC-SBLINK-032c ... counted as an old-frame release");
tx_complete();
run_ms_tick(300U);
CHECK(sb_tx_calls == 1U, "TC-SBLINK-032d ... and the deferred window is closed");
}
static void tc_key_defer_rules(void)
{
uint8_t f[SB_FRAME_A_LEN];
sb_link_stats_t st;
(void)setup_link();
fill_bmp(0xC3U);
(void)sb_link_frame_update(s_bmp, 0U);
/* ① 周期召唤不受影响:照旧立刻答(改动只收紧按键那一支) */
make_frame(f, 0x01U, 0x03U, SB_CMD_RT, 0U);
sb_tx_calls = 0U;
feed(f, SB_FRAME_A_LEN);
CHECK(sb_tx_calls == 1U, "TC-SBLINK-033a periodic summon still kicks at once");
/* ② 线忙时的按键召唤:同旧丢单,且**不留扣件**——扣一个没人应答过的召唤,
* 等于下一拍凭空多发一帧(那轮召唤屏板早改口重问了)。 */
tx_complete();
feed(f, SB_FRAME_A_LEN); /* 再占上线 */
make_frame(f, 0x01U, 0x03U, SB_CMD_KEY, 4U);
sb_tx_calls = 0U;
feed(f, SB_FRAME_A_LEN);
sb_link_get_stats(&st);
CHECK(sb_tx_calls == 0U, "TC-SBLINK-033b key summon while TX busy sends nothing");
CHECK((st.defer_fresh == 0U) && (st.defer_old == 0U),
"TC-SBLINK-033c ... and a busy drop leaves nothing deferred");
tx_complete();
run_ms_tick(3U);
CHECK(sb_tx_calls == 0U, "TC-SBLINK-033d ... and it never invents a response later");
/* ③ 新按键召唤清掉上一拍的凭据:不得拿按键之前的重绘冒充"新帧已备好"秒放行 */
(void)setup_link();
fill_bmp(0xD4U);
(void)sb_link_frame_update(s_bmp, 0U);
sb_link_key_rendered(); /* 按键**之前**交的陈旧凭据 */
make_frame(f, 0x01U, 0x03U, SB_CMD_KEY, 8U);
sb_tx_calls = 0U;
feed(f, SB_FRAME_A_LEN);
sb_link_ms_tick();
CHECK(sb_tx_calls == 0U, "TC-SBLINK-033e stale evidence does not release a new hold");
/* ④ 首帧还没组出来(刚上电):按键召唤同旧丢单、不扣件(没帧可扣) */
(void)setup_link();
make_frame(f, 0x01U, 0x03U, SB_CMD_KEY, 1U);
sb_tx_calls = 0U;
feed(f, SB_FRAME_A_LEN);
sb_link_get_stats(&st);
CHECK((sb_tx_calls == 0U) && (st.tx_busy_drop == 1U),
"TC-SBLINK-033f key summon before the first frame still drops");
tx_complete();
run_ms_tick(3U);
CHECK(sb_tx_calls == 0U, "TC-SBLINK-033g ... and never sends a frame that never existed");
}
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("-- batch 2: frame update guards --\n");
tc_frame_update_guards();
printf("-- batch 2: frame B content --\n");
tc_frame_build_content();
printf("-- batch 2: change detection --\n");
tc_frame_unchanged();
printf("-- batch 2: TX kick from the RX callback --\n");
tc_tx_kick();
printf("-- batch 2: frame frozen while TX runs --\n");
tc_tx_busy_freezes_frame();
printf("-- batch 2: TX kick failure --\n");
tc_tx_kick_failure();
printf("-- batch 2: TX aborted by an error interrupt --\n");
tc_tx_abort_on_error();
printf("-- batch 2: key queue --\n");
tc_key_queue();
printf("-- batch 2: offline detection --\n");
tc_offline();
printf("-- batch 2: frame build timing --\n");
tc_frame_timing();
printf("-- batch 2: TX report line --\n");
tc_tx_report();
printf("-- batch 3: key summon deferred answer --\n");
tc_key_defer_fresh();
printf("-- batch 3: deferred answer deadline --\n");
tc_key_defer_timeout();
printf("-- batch 3: deferred answer rules --\n");
tc_key_defer_rules();
printf("\n=== ScreenLink Link Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}