7708f12c10
按键召唤答的是按键前的旧帧: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 回填受管区)。
1018 lines
42 KiB
C
1018 lines
42 KiB
C
/*
|
||
* 模块名称:屏板链路传输层(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;
|
||
}
|