feat(屏板): USART1 700 kbaud + 收中断传输层 sb_link(1-2/1-3)
批 1 的 1-2 / 1-3:把屏板(DTU-LCD)发来的 6 B 帧 A 在 USART1 上稳定收下来。 1-2 波特率:.ioc 不保存任何 UART 的波特率(只有 USART1.IPParameters=VirtualMode),GUI 里改也不落盘,所以只能在 Src/usart.c 的 USER CODE BEGIN USART1_Init 2 区覆盖 huart1.Init.BaudRate 后重调一次 HAL_UART_Init(第二次调用时 gState 已非 RESET,HAL 只重算 BRR、不再跑 MspInit,GPIO/DMA/NVIC 不会被重配)。开机日志实测 BRR=0x0078,与理论值 84 MHz/(16x700 kHz)=7.5 逐位相符,误差 0%。 1-3 收中断:新建 Drivers/BSP/ScreenLink/sb_link.c/h——16 B 缓冲 + IDLE 中断断帧、帧 A 校验、错误中断里按 RM0090 清 ORE/NE/FE/PE、周期统计上报。 HAL_UARTEx_RxEventCallback / HAL_UART_ErrorCallback 是全局单例(唯一实现 在 rs485.c),故只在那里各加一行转发,按句柄过滤放在 sb_link 内。 台架实测(PC + USB 转 TTL 顶替屏板,COM6@700000)撞出两个真问题: - 错误回调里无条件重挂 -> 出现 err=160 / rearm=160 同步增长,看着像每帧 都重挂失败。真因是 HAL 把 NE/FE/PE 归为非阻塞型(接收照跑、RxState 仍是 BUSY_RX),此时 ReceiveToIdle_IT 只会返回 HAL_BUSY。改为仅当 RxState == HAL_UART_STATE_READY(ORE 那类阻塞型)时才重挂。 - 日志字面量里带中文 -> ARMCC #870-D invalid multibyte character sequence。字面量一律纯 ASCII,中文只写注释。 验证:主机单测 81/81 PASSED(test/host/sb_link/,其中按 RM0090 的清除语义 把"必须 SR->DR 顺序才清得掉 ORE"钉成断言);Keil 0 错误 0 警告;台架 rx == 发送帧数、ok == rx、crc=0、len=0、size=6、keys 随按键帧递增;先用 错波特率灌 64 B 垃圾(len=31、bits=0x06)后自动恢复且 rearm=0。 新增 test/host/screenlink_bench.py 作为可复跑的屏板激励(--key / --glitch-baud)。
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""屏板链路传输层(sb_link)的主机侧单元测试运行器(纯标准库)。
|
||||
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/sb_link/sb_link_test.c(它以 #include 纳入**生产源文件**
|
||||
Drivers/BSP/ScreenLink/sb_link.c 与 sb_proto.c),在 PC 上离线验证 USART1 接收这一层:
|
||||
挂接收 / IDLE 事件分派 / 坏帧分类 / **错误中断里清 ORE 并重挂** / 重挂失败的码映射 /
|
||||
周期统计上报(含时基回绕)。零硬件、零烧录、毫秒级。
|
||||
|
||||
工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness
|
||||
`test/host/common/unit_test_harness.py`(本文件只做声明)。
|
||||
|
||||
为什么必须主机侧测(而不是只用台架):
|
||||
这一层最要命的两条在台架上都"看不见":
|
||||
① **漏一次重挂 = 链路永久静默**,现象与"屏板没上电 / 线接反"完全同形,没有报错;
|
||||
② **ORE 没清干净 = 一次噪声永久打死接收**,台架上要拔插 TX 线才复现,且复现后
|
||||
只能靠断电重来。主机侧用桩把 RM0090 的清除语义(先读 SR 再读 DR)实现出来,
|
||||
于是"顺序写反"会直接让下一次挂接收失败、用例立刻红。
|
||||
|
||||
桩目录(`stub/`,必须排在 include 路径最前,覆盖真实头):
|
||||
main.h 假 USART 寄存器(读序列可记账、ORE 可注入)+ 假 ReceiveToIdle_IT(返回值可脚本)
|
||||
dbg_log.h 日志换成行缓冲,于是能断言"打了几行、说了什么"
|
||||
|
||||
用法(仓库根目录):
|
||||
python test/host/sb_link/run_unit_test.py
|
||||
或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/sb_link/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
|
||||
|
||||
设计依据:docs/任务规划/屏板链路.md §3 批 1(1-3 的判据)、§1.3(接收只要 16 B);
|
||||
docs/产品需求/产品需求设计文档.md §5.16.2(帧 A 结构)。
|
||||
创建日期:2026-10-29
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
|
||||
# harness 位于 test/host/common/
|
||||
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
|
||||
|
||||
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
|
||||
|
||||
# 桩目录必须排在最前:它覆盖 main.h(真实那份会拉进 core_cm4.h)与 dbg_log.h(会拉进 App 层)
|
||||
BUILD = TestBuild(
|
||||
name="ScreenLink Link Tests",
|
||||
src=os.path.join(HERE, "sb_link_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(HERE, "stub"),
|
||||
os.path.join(ROOT, "Drivers", "BSP", "ScreenLink"),
|
||||
os.path.join(ROOT, "Drivers", "BSP"),
|
||||
HERE,
|
||||
],
|
||||
summary_marker="=== ScreenLink Link Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
@@ -0,0 +1,482 @@
|
||||
/*
|
||||
* 模块名称:屏板链路传输层(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 <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.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;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - dbg_log.h(sb_link 模块专用)
|
||||
* 模块功能:把 DBG_INFO / DBG_ERROR 换成"写进一个可读的行缓冲",于是用例能断言
|
||||
* 「打了几行、第一行说了什么」,而不是只能靠人看台架串口。
|
||||
* ⚠ 仅供 test/host/sb_link/ 使用,不参与 MCU 构建。
|
||||
* 真实 dbg_log.h 会拉进 console_log.h / sys_clock.h(App 层)并带时间戳,
|
||||
* 本模块的用例只关心"有没有上报",故整份替换掉,不搬那条链。
|
||||
* 创建日期:2026-10-29
|
||||
*/
|
||||
#ifndef DBG_LOG_H
|
||||
#define DBG_LOG_H
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define SB_LOG_MAX (32U)
|
||||
#define SB_LOG_LINE_MAX (192U)
|
||||
|
||||
static char sb_log[SB_LOG_MAX][SB_LOG_LINE_MAX];
|
||||
static uint32_t sb_log_n;
|
||||
|
||||
static void sb_log_add(const char *p_lvl, const char *p_fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
size_t used;
|
||||
|
||||
if (sb_log_n >= SB_LOG_MAX) {
|
||||
return;
|
||||
}
|
||||
(void)snprintf(sb_log[sb_log_n], SB_LOG_LINE_MAX, "%s ", p_lvl);
|
||||
used = strlen(sb_log[sb_log_n]);
|
||||
va_start(ap, p_fmt);
|
||||
(void)vsnprintf(sb_log[sb_log_n] + used, (size_t)(SB_LOG_LINE_MAX - used), p_fmt, ap);
|
||||
va_end(ap);
|
||||
sb_log_n++;
|
||||
}
|
||||
|
||||
static void sb_log_reset(void)
|
||||
{
|
||||
sb_log_n = 0U;
|
||||
sb_log[0][0] = '\0';
|
||||
}
|
||||
|
||||
/* 第 idx 行是否包含 needle(idx 越界返回 0) */
|
||||
static int sb_log_has(uint32_t idx, const char *p_needle)
|
||||
{
|
||||
if ((idx >= sb_log_n) || (idx >= SB_LOG_MAX)) {
|
||||
return 0;
|
||||
}
|
||||
return (strstr(sb_log[idx], p_needle) != NULL) ? 1 : 0;
|
||||
}
|
||||
|
||||
#define DBG_INFO(...) sb_log_add("INFO", __VA_ARGS__)
|
||||
#define DBG_ERROR(...) sb_log_add("ERR", __VA_ARGS__)
|
||||
|
||||
#endif /* DBG_LOG_H */
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - main.h(sb_link 模块专用)
|
||||
* 模块功能:给 Drivers/BSP/ScreenLink/sb_link.c 提供"可脚本控制"的 HAL 面:
|
||||
* ① 假 USART 寄存器:SR / DR 的**读取顺序与次数**可记账,ORE 锁存可注入;
|
||||
* ② 假 ReceiveToIdle_IT:记录挂接收的缓冲/长度,返回值可脚本指定(OK/BUSY/TIMEOUT/ERROR)。
|
||||
* ⚠ 仅供 test/host/sb_link/ 使用,不参与 MCU 构建(MCU 走真实 main.h -> stm32f4xx_hal.h)。
|
||||
*
|
||||
* 为什么必须桩:真实链 main.h -> stm32f4xx_hal.h -> stm32f4xx.h -> core_cm4.h 里是
|
||||
* __IO 寄存器绝对地址与 ARMCC 内联汇编,宿主 MSVC 既编不动也访问不了。
|
||||
*
|
||||
* ★ 本桩唯一的技巧点:`->SR` / `->DR` 的读没法用普通结构体成员钩住(成员访问不能替换)。
|
||||
* 于是把 USART_TypeDef 的两个成员做成**函数指针**,再用宏把成员名改写成「成员名()」:
|
||||
* 生产代码 p_huart->Instance->SR
|
||||
* 预处理后 p_huart->Instance->sr_cell()
|
||||
* 即一次真实的函数调用 —— 测试因此能记账"读了什么、什么顺序",并让 ORE 的清除
|
||||
* 严格按 RM0090 §30.6.1 的序列生效(**先读 SR 再读 DR 才清得掉**,顺序反了清不掉)。
|
||||
* 改成员名之前先确认生产代码只用 `->SR` / `->DR` 这一种写法(sb_link.c 里就两处)。
|
||||
*
|
||||
* 创建日期:2026-10-29
|
||||
*/
|
||||
#ifndef MAIN_H
|
||||
#define MAIN_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ===== 与真实头同值的枚举(stm32f4xx_hal_def.h / stm32f4xx_hal_uart.h) ===== */
|
||||
typedef enum {
|
||||
HAL_OK = 0x00U,
|
||||
HAL_ERROR = 0x01U,
|
||||
HAL_BUSY = 0x02U,
|
||||
HAL_TIMEOUT = 0x03U
|
||||
} HAL_StatusTypeDef;
|
||||
|
||||
/* RxState 的两个取值:错误回调里"要不要重挂"就靠它判(见 sb_link.c 的 on_error 说明) */
|
||||
#define HAL_UART_STATE_READY (0x20U)
|
||||
#define HAL_UART_STATE_BUSY_RX (0x22U)
|
||||
|
||||
/* HAL 的错误位(ErrorCode):ORE 是阻塞型,NE/FE/PE 是非阻塞型 */
|
||||
#define HAL_UART_ERROR_PE (0x01U)
|
||||
#define HAL_UART_ERROR_NE (0x02U)
|
||||
#define HAL_UART_ERROR_FE (0x04U)
|
||||
#define HAL_UART_ERROR_ORE (0x08U)
|
||||
|
||||
/* ===== ① 假 USART 寄存器(读法见文件头的★) ===== */
|
||||
typedef struct {
|
||||
uint32_t (*sr_cell)(void);
|
||||
uint32_t (*dr_cell)(void);
|
||||
} USART_TypeDef;
|
||||
|
||||
#define SR sr_cell()
|
||||
#define DR dr_cell()
|
||||
|
||||
#define SB_FAKE_SR_ORE (0x0008U) /* USART_SR_ORE,bit3 */
|
||||
#define SB_FAKE_SR_CLEAN (0x00C0U) /* TXE|TC 置位,无错误位 */
|
||||
#define SB_FAKE_DR_VALUE (0x00A5U)
|
||||
|
||||
#define SB_FAKE_TRACE_MAX (8U)
|
||||
|
||||
static uint32_t sb_reg_trace[SB_FAKE_TRACE_MAX]; /* 'S' = 读了 SR,'D' = 读了 DR */
|
||||
static uint32_t sb_reg_n;
|
||||
static uint32_t sb_ore_latched; /* 1 = ORE 处于锁存态 */
|
||||
static uint32_t sb_sr_seen; /* 1 = 刚读过 SR(DR 才清得掉 ORE) */
|
||||
|
||||
static uint32_t sb_sr_cell(void)
|
||||
{
|
||||
if (sb_reg_n < SB_FAKE_TRACE_MAX) {
|
||||
sb_reg_trace[sb_reg_n] = (uint32_t)'S';
|
||||
}
|
||||
sb_reg_n++;
|
||||
sb_sr_seen = 1U;
|
||||
return (sb_ore_latched != 0U) ? SB_FAKE_SR_ORE : SB_FAKE_SR_CLEAN;
|
||||
}
|
||||
|
||||
static uint32_t sb_dr_cell(void)
|
||||
{
|
||||
if (sb_reg_n < SB_FAKE_TRACE_MAX) {
|
||||
sb_reg_trace[sb_reg_n] = (uint32_t)'D';
|
||||
}
|
||||
sb_reg_n++;
|
||||
/* RM0090 §30.6.1:读 SR 之后再读 DR 才清 ORE;顺序反了清不掉(真机也这样) */
|
||||
if (sb_sr_seen != 0U) {
|
||||
sb_ore_latched = 0U;
|
||||
}
|
||||
sb_sr_seen = 0U;
|
||||
return SB_FAKE_DR_VALUE;
|
||||
}
|
||||
|
||||
static USART_TypeDef g_sb_uart = { sb_sr_cell, sb_dr_cell };
|
||||
|
||||
/* ===== ② 假 UART 句柄 ===== */
|
||||
typedef struct {
|
||||
uint32_t BaudRate;
|
||||
uint32_t WordLength;
|
||||
uint32_t StopBits;
|
||||
uint32_t Parity;
|
||||
uint32_t Mode;
|
||||
uint32_t HwFlowCtl;
|
||||
uint32_t OverSampling;
|
||||
} UART_InitTypeDef;
|
||||
|
||||
typedef struct {
|
||||
USART_TypeDef *Instance;
|
||||
UART_InitTypeDef Init;
|
||||
volatile uint32_t gState;
|
||||
volatile uint32_t RxState;
|
||||
volatile uint32_t ErrorCode;
|
||||
} UART_HandleTypeDef;
|
||||
|
||||
/* ===== ③ 假 ReceiveToIdle_IT ===== */
|
||||
#define SB_FAKE_ARM_OK (0U)
|
||||
#define SB_FAKE_ARM_BUSY (1U)
|
||||
#define SB_FAKE_ARM_TIMEOUT (2U)
|
||||
#define SB_FAKE_ARM_ERROR (3U)
|
||||
|
||||
static uint32_t sb_arm_calls;
|
||||
static uint32_t sb_arm_force; /* SB_FAKE_ARM_* */
|
||||
static uint8_t *sb_arm_buf;
|
||||
static uint16_t sb_arm_size;
|
||||
|
||||
static HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *p_huart,
|
||||
uint8_t *p_buf,
|
||||
uint16_t size)
|
||||
{
|
||||
sb_arm_calls++;
|
||||
sb_arm_buf = p_buf;
|
||||
sb_arm_size = size;
|
||||
|
||||
if (sb_arm_force == SB_FAKE_ARM_BUSY) {
|
||||
return HAL_BUSY;
|
||||
}
|
||||
if (sb_arm_force == SB_FAKE_ARM_TIMEOUT) {
|
||||
return HAL_TIMEOUT;
|
||||
}
|
||||
if (sb_arm_force == SB_FAKE_ARM_ERROR) {
|
||||
return HAL_ERROR;
|
||||
}
|
||||
/* ORE 锁存时真机是收不了的(RXNE 不再置位)——桩照这个语义走,
|
||||
* 于是"ORE 没清干净"必然表现为挂接收失败,用例能抓住 */
|
||||
if (sb_ore_latched != 0U) {
|
||||
return HAL_ERROR;
|
||||
}
|
||||
/* 挂上了:RxState 进 BUSY_RX。错误回调据此判断"接收是不是还在跑"
|
||||
* (真机也是这样:非阻塞型错误不动 RxState,阻塞型错误会把它打回 READY) */
|
||||
p_huart->RxState = HAL_UART_STATE_BUSY_RX;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* ===== ④ 测试用的复位/查询把手 ===== */
|
||||
static void sb_fake_reset(void)
|
||||
{
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0U; i < SB_FAKE_TRACE_MAX; i++) {
|
||||
sb_reg_trace[i] = 0U;
|
||||
}
|
||||
sb_reg_n = 0U;
|
||||
sb_ore_latched = 0U;
|
||||
sb_sr_seen = 0U;
|
||||
sb_arm_calls = 0U;
|
||||
sb_arm_force = SB_FAKE_ARM_OK;
|
||||
sb_arm_buf = NULL;
|
||||
sb_arm_size = 0U;
|
||||
}
|
||||
|
||||
/* 寄存器读序列是否为期望的字符串(如 "SD"):长度不等或内容不符都返回 0 */
|
||||
static int sb_fake_trace_is(const char *p_expect)
|
||||
{
|
||||
uint32_t i;
|
||||
uint32_t len = (uint32_t)0U;
|
||||
|
||||
while (p_expect[len] != '\0') {
|
||||
len++;
|
||||
}
|
||||
if (sb_reg_n != len) {
|
||||
return 0;
|
||||
}
|
||||
for (i = 0U; i < len; i++) {
|
||||
if (sb_reg_trace[i] != (uint32_t)(unsigned char)p_expect[i]) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endif /* MAIN_H */
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""屏板链路台架激励:用 PC + USB 转 TTL **顶替屏板**,按屏板的召唤节拍发 6 B 帧 A。
|
||||
|
||||
做什么:
|
||||
在一个 TTL 串口上按屏板的节拍(默认 280 ms)发帧 A,模拟屏板的周期召唤;
|
||||
可穿插按键帧(屏板有键时最早 205 ms 就会再来一帧,本脚本只在周期点替换内容,
|
||||
不额外插帧——批 1 只验"收得到、长度对、CRC 过",不验应答时序)。
|
||||
主机侧的统计由固件自己打(`[SBLINK] rx=... ok=... crc=... len=...`),
|
||||
本脚本只负责"有东西可收",不读被测板——**收侧观测面是串口日志,不是本脚本**。
|
||||
|
||||
为什么不用串口助手 GUI:
|
||||
1-3 的判据含"连续 10 min 无丢帧",手点是做不到的;而且帧内容必须逐字节对
|
||||
(CRC 低字节在前,写错一位就是 100 % CRC 错帧,与"波特率错"同形)。
|
||||
|
||||
接法(docs/任务规划/屏板链路.md §2 决策一):
|
||||
适配器 TXD -> U5.B10(网络 ST_RX0,PA10) 适配器 RXD -> U5.A10(网络 ST_TX0,PA9)
|
||||
适配器 GND -> 板地
|
||||
⚠ 必须交叉接(TX 对 RX);USART1 的 TTL 只从 U5 出去,CN1 上没有 USART1 端子。
|
||||
串口参数 700000 8N1,与固件 `SB_LINK_BAUD` 一致。
|
||||
|
||||
与日志口的关系:
|
||||
日志走 RS-232 侧(默认 COM3,由 serial-assistant daemon 占用),本脚本占用的是
|
||||
**另一路 TTL 口**(台架上通常是 COM6)——两者互不相干,可同时跑。
|
||||
|
||||
用法(仓库根目录):
|
||||
& '.venv\\Scripts\\python.exe' test/host/screenlink_bench.py --port COM6 --seconds 30
|
||||
& '.venv\\Scripts\\python.exe' test/host/screenlink_bench.py --port COM6 --seconds 600 # 10 min 判据
|
||||
--key N 每 N 个周期把召唤帧换成按键帧(键值在 1/2/3/4/7/8/9 里轮换;0 = 不发按键帧)
|
||||
|
||||
退出码:0 = 按时发完;1 = 串口打不开 / 发送失败;2 = 用法错误。
|
||||
|
||||
设计依据:docs/任务规划/屏板链路.md §3 批 1 的 1-3 判据、§5 台架联调步骤第 2 步。
|
||||
创建日期:2026-10-29
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import serial
|
||||
except ImportError: # pragma: no cover - 环境缺失时给人话
|
||||
print("[ERR] 缺少 pyserial:请用项目 .venv 的 python 运行本脚本")
|
||||
sys.exit(2)
|
||||
|
||||
# 屏板固件里的 KEY_TYPE:7 个物理键可达 1/2/3/4/7/8/9(无 5/6,9 = RESET)
|
||||
KEY_VALUES = (1, 2, 3, 4, 7, 8, 9)
|
||||
|
||||
CMD_KEY = 0x00
|
||||
CMD_RT = 0x01
|
||||
ADDR = 0x01
|
||||
FUNC = 0x03
|
||||
|
||||
|
||||
def crc16_modbus(data):
|
||||
"""标准 CRC-16/MODBUS(与 sb_proto.c 同算法、同口径:写进帧里低字节在前)。"""
|
||||
crc = 0xFFFF
|
||||
for byte in data:
|
||||
crc ^= byte
|
||||
for _ in range(8):
|
||||
if crc & 1:
|
||||
crc = (crc >> 1) ^ 0xA001
|
||||
else:
|
||||
crc >>= 1
|
||||
return crc & 0xFFFF
|
||||
|
||||
|
||||
def build_frame(cmd, key=0):
|
||||
"""组一帧帧 A:01 03 <cmd> <key> <CRC_L> <CRC_H>,CRC 只覆盖前 4 字节。"""
|
||||
body = bytes((ADDR, FUNC, cmd, key))
|
||||
crc = crc16_modbus(body)
|
||||
return body + bytes((crc & 0xFF, (crc >> 8) & 0xFF))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="屏板链路台架激励(PC 顶替屏板)")
|
||||
ap.add_argument("--port", required=True, help="USB 转 TTL 适配器的串口,如 COM6")
|
||||
ap.add_argument("--baud", type=int, default=700000, help="波特率(默认 700000,与固件一致)")
|
||||
ap.add_argument("--seconds", type=float, default=30.0, help="发多久(秒),默认 30")
|
||||
ap.add_argument("--period-ms", type=float, default=280.0,
|
||||
help="召唤周期(ms),默认 280(屏板是 RT_count>55 -> 第 56 拍 x 5 ms)")
|
||||
ap.add_argument("--key", type=int, default=0,
|
||||
help="每 N 个周期把帧换成按键帧(0 = 只发周期召唤)")
|
||||
ap.add_argument("--glitch-baud", type=int, default=0,
|
||||
help="先按这个(错的)波特率灌一串垃圾再正常发,用来验 ORE/NE/FE 之后能否自愈"
|
||||
"(0 = 不灌;建议 115200)")
|
||||
ap.add_argument("--glitch-bytes", type=int, default=32,
|
||||
help="灌多少字节垃圾(默认 32)")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.period_ms <= 0 or args.seconds <= 0:
|
||||
print("[ERR] --period-ms / --seconds 必须为正")
|
||||
return 2
|
||||
|
||||
rt_frame = build_frame(CMD_RT)
|
||||
print("RT frame : %s" % rt_frame.hex(" ").upper())
|
||||
|
||||
# ---- 噪声注入(可选):用错波特率灌垃圾,制造 NE/FE/ORE ----
|
||||
# 真实现场对应的是"拔插一次 TX 线";错波特率产生的错误更密集,是更强的考验。
|
||||
# 关键判据不是"有没有报错",而是**之后还能不能正常收**——ORE 一旦清不掉,
|
||||
# 接收就永久进不来,现象与"屏板没上电"完全一样(见 sb_link.c 文件头第 ② 条)。
|
||||
if args.glitch_baud > 0 and args.glitch_bytes > 0:
|
||||
try:
|
||||
gser = serial.Serial(args.port, args.glitch_baud, bytesize=8, parity="N",
|
||||
stopbits=1, timeout=0)
|
||||
junk = bytes((0xA5, 0x5A, 0x00, 0xFF, 0x3C, 0xC3)) * (args.glitch_bytes // 6 + 1)
|
||||
gser.write(junk[:args.glitch_bytes])
|
||||
gser.flush()
|
||||
gser.close()
|
||||
print("[GLITCH] %d B @%d(错波特率灌噪声)" % (args.glitch_bytes, args.glitch_baud))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print("[WARN] 噪声注入失败(继续正常发):%s" % exc)
|
||||
|
||||
try:
|
||||
ser = serial.Serial(args.port, args.baud, bytesize=8, parity="N", stopbits=1, timeout=0)
|
||||
except Exception as exc: # noqa: BLE001 - 打开失败原因要原样给人看
|
||||
print("[ERR] 打开 %s @%d 失败:%s" % (args.port, args.baud, exc))
|
||||
return 1
|
||||
|
||||
sent = 0
|
||||
keys = 0
|
||||
t0 = time.monotonic()
|
||||
next_at = t0
|
||||
try:
|
||||
while True:
|
||||
now = time.monotonic()
|
||||
if now - t0 >= args.seconds:
|
||||
break
|
||||
if now < next_at:
|
||||
time.sleep(min(next_at - now, 0.02))
|
||||
continue
|
||||
|
||||
if args.key > 0 and (sent % args.key) == (args.key - 1):
|
||||
key = KEY_VALUES[keys % len(KEY_VALUES)]
|
||||
frame = build_frame(CMD_KEY, key)
|
||||
keys += 1
|
||||
tag = "KEY %d" % key
|
||||
else:
|
||||
frame = rt_frame
|
||||
tag = "RT"
|
||||
|
||||
ser.write(frame)
|
||||
ser.flush()
|
||||
sent += 1
|
||||
if sent <= 3 or tag != "RT":
|
||||
print(" #%d %s -> %s" % (sent, tag, frame.hex(" ").upper()))
|
||||
|
||||
next_at += args.period_ms / 1000.0
|
||||
# 落后太多就别追了(PC 侧卡顿不该变成突发流量)
|
||||
if next_at < time.monotonic() - 1.0:
|
||||
next_at = time.monotonic()
|
||||
except KeyboardInterrupt:
|
||||
print("\n[INFO] 用户中断")
|
||||
finally:
|
||||
ser.close()
|
||||
|
||||
elapsed = time.monotonic() - t0
|
||||
print("[DONE] 发送 %d 帧(其中按键帧 %d)用时 %.1f s,平均周期 %.1f ms"
|
||||
% (sent, keys, elapsed, (elapsed * 1000.0 / sent) if sent else 0.0))
|
||||
print("[NEXT] 回看日志口:grep '[SBLINK]' —— 期望 rx == %d、ok == rx、crc=0、len=0" % sent)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user