feat(屏板): 协议层 sb_proto(帧 A 解析 / 帧 B 组帧 / CRC-16)

批 1 的 1-1:新建 Drivers/BSP/ScreenLink/sb_proto.c/h,纯逻辑、无 HAL 依赖,
可主机侧单测。屏板侧的协议事实源是 DTU-LCD 的 CRC16.c / MODBUS.c / rs485.h。

- CRC-16/MODBUS 用一张 16 位合并表(512 B,与屏板两张 8 位表同量)。可逐项
  验证的关系:s_crc_table[i] == (auchCRCLo[i] << 8) | auchCRCHi[i]——屏板把
  低字节表叫 Hi、高字节表叫 Lo,这正是下面那个字节序陷阱的根源。
- 帧 A 解析:先校 CRC 再看字段。地址/功能码/命令类型不符报 BSP_ERR_PARAM,
  CRC 不符报 BSP_ERR_CRC(调用方据此决定"计数"还是"静默丢")。RT_CMD 带
  非 0 key 不报错、按 0 收——为这一项丢一帧等于丢一次刷新,代价不对等。
- 帧 B 组帧:普通图(掩码小端)+ LOGO(宽高大端 + 魔数 0x89 0x45),整帧
  4208 B 重写(998 B 填充固定 0x00 且参与 CRC),组完即自校验。
  LOGO 容量按字节数判(((w+7)/8)*h <= 3200)而不是像素数:161x159 只有
  25599 像素,却要 3339 B 才装得下。
- 主机单测 59/59 PASSED(test/host/sb_proto/),覆盖率 74/74 行、6/6 函数。
  其中两条向量是屏板自己算出来的线上字节(01 03 01 00 F0 48 与
  01 03 00 09 31 DE),并按屏板 Check_CRC 的口径反查"它会认这一帧"。
- 双端注册:uvprojx 加 File + IncludePath;模拟器侧走 check_registration.py
  的 MCU-only 豁免(纯逻辑,模拟器无消费路径)。
- Keil 构建 0 错误 0 警告;Code 不变——尚无调用者,链接器按未用节丢弃。

文档侧的 CRC 字节序订正见下一个提交。
This commit is contained in:
2026-10-29 19:17:28 +08:00
parent 71b6a5fa14
commit b25d70e962
7 changed files with 1081 additions and 21 deletions
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""屏板链路协议层(sb_proto)的主机侧纯逻辑单元测试运行器(纯标准库)。
做什么:
用本机 MSVC 编译 test/host/sb_proto/sb_proto_test.c(它以 #include 纳入**生产源文件**
Drivers/BSP/ScreenLink/sb_proto.c),在 PC 上离线验证帧 A 解析 / 帧 B 组帧 /
CRC-16/MODBUS:教科书校验值、屏板实测的召唤帧字节、坏帧分支、4208 B 帧的
点阵-掩码-填充-CRC 落位、LOGO 宽高容量守卫、以及"按屏板 Check_CRC 的口径复核
本模块组的帧会不会被屏板接受"。零硬件、零烧录、毫秒级。
工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness
`test/host/common/unit_test_harness.py`(本文件只做声明)。
为什么要主机侧单测(而不是只用台架):
屏板不认帧时,它的超时判定只看"收没收到一整包"、不看 CRC,现场只表现为**画面不刷新**——
这条现象与波特率错、时序错、线接反完全同形,台架上无法归因。把 CRC 与字节序钉在主机侧,
台架上剩下的就只可能是物理层问题。另一处只能在这里测:CRC 的**字节序**口径。
屏板固件 Calculate_CRC() 返回的是高低互换后的「线上字」,它自己的注释把低字节叫"高字节";
照字面实现会让每一帧都差一次字节交换,而现象又是"画面不刷新"——这里钉了两条屏板实测向量。
本层为**纯逻辑**:无寄存器、无 RTOS、无文件/外设依赖,故**不需要任何桩目录**。
用法(仓库根目录):
python test/host/sb_proto/run_unit_test.py
或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/sb_proto/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
设计依据:docs/产品需求/产品需求设计文档.md §5.16.2(帧结构);
docs/任务规划/屏板链路.md §3 批 1(1-1 的判据);
协议事实源 = 屏板固件工程 D:\\Code\\DTU Code\\DTU-LCD 的 CRC16.c / MODBUS.c。
创建日期: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
# 无桩目录:被测代码只依赖 stdint.h / string.h。
# 两个 include 目录各管一个头:ScreenLink 找 sb_proto.h,Drivers/BSP 找 bsp_err.h。
BUILD = TestBuild(
name="ScreenLink Proto Tests",
src=os.path.join(HERE, "sb_proto_test.c"),
include_dirs=[
os.path.join(ROOT, "Drivers", "BSP", "ScreenLink"),
os.path.join(ROOT, "Drivers", "BSP"),
HERE,
],
summary_marker="=== ScreenLink Proto Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+490
View File
@@ -0,0 +1,490 @@
/*
* 模块名称:屏板链路协议层(sb_proto)主机侧单元测试
* 模块功能:在 PC 上对 Drivers/BSP/ScreenLink/sb_proto.c 做离线验证。
*
* 被测对象:真实源文件(以 #include 原样纳入,保证测的是生产代码而非副本):
* ../../../Drivers/BSP/ScreenLink/sb_proto.c
* 本层为**纯逻辑**(只依赖 stdint.h 与 string.h),故**无需任何桩**。
*
* 为什么要主机侧单测(而不是只用台架):
* 1. 协议层只有两种结局:屏板认 / 屏板不认。屏板不认时它的超时判定只看「收没收到一整包」、
* 不看 CRC,现场只表现为**画面不刷新**——在台架上这条现象与波特率错、时序错、线接反
* 完全同形,无法归因。主机侧把 CRC 与字节序钉死,才谈得上"发出去的就是对的"。
* 2. 帧 A 的坏帧分支(长度不对 / 地址不对 / 功能码不对 / CRC 错)在台架上要造噪声才可能出现,
* 且一出现就被复位冲掉;主机侧是几行断言。
* 3. 帧 B 4208 B 的边界(填充段参与 CRC、LOGO 宽高装不下点阵)不占 MCU 的 RAM,毫秒级。
*
* 覆盖点(对应 docs/产品需求/产品需求设计文档.md §5.16.2 帧结构与任务规划 1-1 判据):
* TC-SBPROTO-001 CRC-16/MODBUS 教科书校验值:"123456789" -> 0x4B37
* TC-SBPROTO-002 CRC 与 Modbus RTU 教科书帧一致:01 03 00 00 00 0A -> 0xCDC5(线上 C5 CD)
* TC-SBPROTO-003 查表法 == 逐位法(300 组伪随机长度/内容,钉住表被改错)
* TC-SBPROTO-004 查表法边界:len 个字节与逐字节累积等价(前 1 字节 / 全 0 / 全 FF)
* TC-SBPROTO-010 帧 A 实测向量:屏板发的周期召唤 01 03 01 00 F0 48 -> RT_CMD / key 0
* TC-SBPROTO-011 帧 A 实测向量:按键召唤 01 03 00 09 31 DE -> KEY_CMD / key 9
* TC-SBPROTO-012 帧 A 的 key:RT 命令下非 0 也按 0 收(不为此丢一帧)
* TC-SBPROTO-013 帧 A 坏帧:CRC 错 / 长度 5 与 7 / 地址 0x02 / 功能码 0x04 / 命令类型 0x02
* TC-SBPROTO-014 帧 A 参数守卫:空指针两路;失败时不动 *p_out
* TC-SBPROTO-020 帧 B 普通图:点阵逐字节、掩码小端、info[4..7] 为 0、填充段全 0、CRC 自校验
* TC-SBPROTO-021 帧 B 普通图:整帧重写(预填 0xAA 的缓冲被清干净)、重复组帧结果一致
* TC-SBPROTO-022 帧 B LOGO:宽高大端、魔数 0x89 0x45、info[6..7] 为 0、CRC 自校验
* TC-SBPROTO-023 帧 B LOGO 宽高守卫:161x159 与 160x161 被拒(按字节数判容量)、宽或高为 0 被拒
* TC-SBPROTO-024 帧 B 参数守卫:空指针三处
* TC-SBPROTO-030 帧 B 自校验:改点阵 / 改填充段 / 改 CRC 自身 / 长度不对
* TC-SBPROTO-040 屏板侧视角:用屏板 Check_CRC 的口径(高低互换后比对)复核本模块组的两种帧
*
* 通过:全部 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"
/* ===================== 工具 ===================== */
/* 伪随机(自持实现,不依赖 rand() 的实现差异,换编译器也复现同一组向量) */
static uint32_t s_rng = 0x20261029U;
static uint32_t rng_next(void)
{
s_rng = (s_rng * 1103515245U) + 12345U;
return (s_rng >> 8) & 0x00FFFFFFU;
}
/* 逐位法 CRC-16/MODBUS:与被测查表法互为独立实现,用来抓表项被改错 */
static uint16_t crc16_bitwise(const uint8_t *p, uint16_t len)
{
uint16_t crc = 0xFFFFU;
uint16_t i;
uint8_t b;
for (i = 0U; i < len; i++)
{
crc ^= (uint16_t)p[i];
for (b = 0U; b < 8U; b++)
{
if ((crc & 1U) != 0U)
{
crc = (uint16_t)((crc >> 1) ^ 0xA001U);
}
else
{
crc = (uint16_t)(crc >> 1);
}
}
}
return crc;
}
/*
* 屏板 Check_CRC() 的等价物(照屏板固件 Middlewares/Modbus/CRC16.c 的口径):
* 屏板的 Calculate_CRC() 返回的是高低字节互换后的「线上字」,所以它把帧尾两字节按
* 「前一个是高」拼起来再比。本函数只用 sb_crc16 的标准值复刻这一口径,
* 用来回答"屏板会不会认这一帧"。
*/
static int screen_board_accepts(const uint8_t *buf, uint16_t len)
{
uint16_t wire;
uint16_t crc;
if (len < 2U)
{
return 0;
}
wire = (uint16_t)(((uint16_t)buf[len - 2U] << 8) | (uint16_t)buf[len - 1U]);
crc = sb_crc16(buf, (uint16_t)(len - 2U));
/* 标准值高低互换 == 屏板 Calculate_CRC() 的返回值 */
return ((uint16_t)((crc << 8) | (crc >> 8)) == wire) ? 1 : 0;
}
/* 按给定 4 字节组一帧帧 A(用生产代码自己的 CRC,再按线上顺序摆) */
static void make_frame_a(uint8_t *p_out, uint8_t addr, uint8_t func, uint8_t cmd, uint8_t key)
{
uint16_t crc;
p_out[0] = addr;
p_out[1] = func;
p_out[2] = cmd;
p_out[3] = key;
crc = sb_crc16(p_out, (uint16_t)SB_FRAME_A_CRC_OFF);
p_out[4] = (uint8_t)(crc & 0xFFU);
p_out[5] = (uint8_t)((crc >> 8) & 0xFFU);
}
static uint8_t s_frame[SB_FRAME_B_LEN]; /* 4208 B,帧 B 输出缓冲 */
static uint8_t s_bmp[SB_FRAME_B_BMP_LEN]; /* 3200 B,点阵源 */
static void fill_bmp_pattern(void)
{
uint16_t i;
for (i = 0U; i < SB_FRAME_B_BMP_LEN; i++)
{
s_bmp[i] = (uint8_t)((i * 37U) & 0xFFU);
}
}
/* ===================== 用例 ===================== */
static void tc_crc_vectors(void)
{
static const uint8_t k_check[] = { '1', '2', '3', '4', '5', '6', '7', '8', '9' };
static const uint8_t k_modbus[] = { 0x01, 0x03, 0x00, 0x00, 0x00, 0x0A };
uint16_t crc;
crc = sb_crc16(k_check, (uint16_t)sizeof(k_check));
CHECK(crc == 0x4B37U, "TC-SBPROTO-001 CRC-16/MODBUS check value of '123456789' is 0x4B37");
crc = sb_crc16(k_modbus, (uint16_t)sizeof(k_modbus));
CHECK(crc == 0xCDC5U,
"TC-SBPROTO-002 the textbook Modbus RTU frame yields 0xCDC5, so the wire bytes are C5 CD (low first)");
}
static void tc_crc_table(void)
{
uint8_t buf[64];
uint16_t n;
uint16_t i;
int mismatch = 0;
for (n = 0U; n < 300U; n++)
{
uint16_t len = (uint16_t)(rng_next() % 65U);
for (i = 0U; i < len; i++)
{
buf[i] = (uint8_t)(rng_next() & 0xFFU);
}
if (sb_crc16(buf, len) != crc16_bitwise(buf, len))
{
mismatch++;
}
}
CHECK(mismatch == 0,
"TC-SBPROTO-003 the table walk agrees with the bitwise reference on 300 pseudo-random buffers");
buf[0] = 0x00U;
CHECK(sb_crc16(buf, 1U) == crc16_bitwise(buf, 1U),
"TC-SBPROTO-004 a single 0x00 byte: table and bitwise agree (initial value is really 0xFFFF)");
for (i = 0U; i < 32U; i++)
{
buf[i] = 0xFFU;
}
CHECK(sb_crc16(buf, 32U) == crc16_bitwise(buf, 32U),
"TC-SBPROTO-004b 32 bytes of 0xFF: table and bitwise agree");
}
static void tc_frame_a_vectors(void)
{
/* 屏板 RS485_RefreshScreenCMD() 在 700000 8N1 上真实发出去的字节:
* 周期召唤(type=RT_CMD=1, key=0)与按键召唤(type=KEY_CMD=0, key=9)。
* 这两条是拿屏板自己的算法算出来的线上字节,**不是**本模块算的。 */
static const uint8_t k_rt[] = { 0x01, 0x03, 0x01, 0x00, 0xF0, 0x48 };
static const uint8_t k_key[] = { 0x01, 0x03, 0x00, 0x09, 0x31, 0xDE };
sb_frame_a_t out;
memset(&out, 0, sizeof(out));
CHECK(sb_frame_a_parse(k_rt, (uint16_t)sizeof(k_rt), &out) == BSP_OK,
"TC-SBPROTO-010 the screen board periodic summon 01 03 01 00 F0 48 parses");
CHECK((out.cmd == SB_CMD_RT) && (out.key == 0U),
"TC-SBPROTO-010b it reports RT_CMD with key 0");
memset(&out, 0, sizeof(out));
CHECK(sb_frame_a_parse(k_key, (uint16_t)sizeof(k_key), &out) == BSP_OK,
"TC-SBPROTO-011 the screen board key summon 01 03 00 09 31 DE parses");
CHECK((out.cmd == SB_CMD_KEY) && (out.key == 9U),
"TC-SBPROTO-011b it reports KEY_CMD with key 9");
/* 屏板会认本模块自己组出来的这两帧吗(同一口径反查) */
CHECK(screen_board_accepts(k_rt, (uint16_t)sizeof(k_rt)) == 1,
"TC-SBPROTO-011c the board side accepts the periodic summon frame");
CHECK(screen_board_accepts(k_key, (uint16_t)sizeof(k_key)) == 1,
"TC-SBPROTO-011d the board side accepts the key summon frame");
}
static void tc_frame_a_lenient_key(void)
{
uint8_t buf[SB_FRAME_A_LEN];
sb_frame_a_t out;
/* 屏板周期召唤本应带 key = 0;真带了非 0 也收下(丢一帧就等于丢一次刷新) */
make_frame_a(buf, SB_ADDR, SB_FUNC_READ_HOLDING, SB_CMD_RT, 0x2AU);
memset(&out, 0, sizeof(out));
CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_OK,
"TC-SBPROTO-012 an RT frame carrying a non-zero key is still accepted");
CHECK(out.key == 0U, "TC-SBPROTO-012b its key is normalised to 0 (the key field only means something for KEY_CMD)");
/* KEY_CMD 的 key 原样透出,包括 0 与 0xFF */
make_frame_a(buf, SB_ADDR, SB_FUNC_READ_HOLDING, SB_CMD_KEY, 0x00U);
CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_OK && out.key == 0x00U,
"TC-SBPROTO-012c a KEY_CMD with key 0 is accepted and keeps 0");
make_frame_a(buf, SB_ADDR, SB_FUNC_READ_HOLDING, SB_CMD_KEY, 0xFFU);
CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_OK && out.key == 0xFFU,
"TC-SBPROTO-012d a KEY_CMD with key 0xFF is accepted and keeps 0xFF");
}
static void tc_frame_a_reject(void)
{
uint8_t buf[SB_FRAME_A_LEN];
sb_frame_a_t out;
sb_frame_a_t sentinel;
/* CRC 错:改 CRC 低字节 / 改数据字节 */
make_frame_a(buf, SB_ADDR, SB_FUNC_READ_HOLDING, SB_CMD_RT, 0U);
buf[4] ^= 0x01U;
CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_ERR_CRC,
"TC-SBPROTO-013 a flipped CRC byte is reported as CRC error");
make_frame_a(buf, SB_ADDR, SB_FUNC_READ_HOLDING, SB_CMD_RT, 0U);
buf[2] ^= 0x02U;
CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_ERR_CRC,
"TC-SBPROTO-013b a flipped payload byte is reported as CRC error");
/* 长度不对 */
make_frame_a(buf, SB_ADDR, SB_FUNC_READ_HOLDING, SB_CMD_RT, 0U);
CHECK(sb_frame_a_parse(buf, (uint16_t)(SB_FRAME_A_LEN - 1U), &out) == BSP_ERR_PARAM,
"TC-SBPROTO-013c a 5-byte frame is rejected");
CHECK(sb_frame_a_parse(buf, (uint16_t)(SB_FRAME_A_LEN + 1U), &out) == BSP_ERR_PARAM,
"TC-SBPROTO-013d a 7-byte frame is rejected (the idle interrupt may hand over a longer burst)");
/* 地址 / 功能码 / 命令类型不符(CRC 都是对的,纯粹是内容不对) */
make_frame_a(buf, 0x02U, SB_FUNC_READ_HOLDING, SB_CMD_RT, 0U);
CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_ERR_PARAM,
"TC-SBPROTO-013e slave address 0x02 is rejected even with a good CRC");
make_frame_a(buf, SB_ADDR, 0x04U, SB_CMD_RT, 0U);
CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_ERR_PARAM,
"TC-SBPROTO-013f function code 0x04 is rejected even with a good CRC");
make_frame_a(buf, SB_ADDR, SB_FUNC_READ_HOLDING, 0x02U, 0U);
CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_ERR_PARAM,
"TC-SBPROTO-013g command type 0x02 is rejected even with a good CRC");
/* 失败时不动 *p_out */
sentinel.cmd = 0x5AU;
sentinel.key = 0xA5U;
out = sentinel;
make_frame_a(buf, 0x02U, SB_FUNC_READ_HOLDING, SB_CMD_RT, 0U);
(void)sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out);
CHECK((out.cmd == 0x5AU) && (out.key == 0xA5U),
"TC-SBPROTO-014 a failed parse leaves the caller result struct untouched");
}
static void tc_frame_a_guard(void)
{
uint8_t buf[SB_FRAME_A_LEN];
sb_frame_a_t out;
make_frame_a(buf, SB_ADDR, SB_FUNC_READ_HOLDING, SB_CMD_RT, 0U);
CHECK(sb_frame_a_parse(0, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_ERR_PARAM,
"TC-SBPROTO-014b a null buffer is rejected");
CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, 0) == BSP_ERR_PARAM,
"TC-SBPROTO-014c a null result pointer is rejected");
}
static void tc_frame_b_image(void)
{
uint16_t i;
uint16_t crc;
int bmp_ok = 1;
int pad_ok = 1;
fill_bmp_pattern();
CHECK(sb_frame_b_build_image(s_frame, s_bmp, 0xDEADBEEFU) == BSP_OK,
"TC-SBPROTO-020 building an image frame succeeds");
for (i = 0U; i < SB_FRAME_B_BMP_LEN; i++)
{
if (s_frame[i] != s_bmp[i])
{
bmp_ok = 0;
}
}
CHECK(bmp_ok == 1, "TC-SBPROTO-020b the 3200-byte bitmap lands at offset 0 unchanged");
CHECK((s_frame[SB_FRAME_B_INFO_OFF + 0U] == 0xEFU) &&
(s_frame[SB_FRAME_B_INFO_OFF + 1U] == 0xBEU) &&
(s_frame[SB_FRAME_B_INFO_OFF + 2U] == 0xADU) &&
(s_frame[SB_FRAME_B_INFO_OFF + 3U] == 0xDEU),
"TC-SBPROTO-020c the LED mask goes into info[0..3] little-endian (info[0] = bit0..7)");
CHECK((s_frame[SB_FRAME_B_INFO_OFF + 4U] == 0U) && (s_frame[SB_FRAME_B_INFO_OFF + 5U] == 0U) &&
(s_frame[SB_FRAME_B_INFO_OFF + 6U] == 0U) && (s_frame[SB_FRAME_B_INFO_OFF + 7U] == 0U),
"TC-SBPROTO-020d info[4..7] stay 0 for a normal picture (the LOGO magic must not leak in)");
for (i = SB_FRAME_B_PAD_OFF; i < SB_FRAME_B_CRC_OFF; i++)
{
if (s_frame[i] != 0U)
{
pad_ok = 0;
}
}
CHECK(pad_ok == 1, "TC-SBPROTO-020e the 998-byte padding block is all zero");
crc = sb_crc16(s_frame, (uint16_t)SB_FRAME_B_CRC_OFF);
CHECK((s_frame[SB_FRAME_B_CRC_OFF] == (uint8_t)(crc & 0xFFU)) &&
(s_frame[SB_FRAME_B_CRC_OFF + 1U] == (uint8_t)((crc >> 8) & 0xFFU)),
"TC-SBPROTO-020f the CRC sits at 4206/4207 low byte first");
CHECK(sb_frame_b_verify(s_frame, (uint16_t)SB_FRAME_B_LEN) == BSP_OK,
"TC-SBPROTO-020g the built frame verifies");
CHECK(screen_board_accepts(s_frame, (uint16_t)SB_FRAME_B_LEN) == 1,
"TC-SBPROTO-040 the board side Check_CRC accepts the image frame");
}
static void tc_frame_b_rewrite(void)
{
static uint8_t first[SB_FRAME_B_LEN];
uint16_t i;
int pad_ok = 1;
/* 预填脏数据:整帧重写必须把填充段与 info 一起清干净 */
memset(s_frame, 0xAA, sizeof(s_frame));
fill_bmp_pattern();
CHECK(sb_frame_b_build_image(s_frame, s_bmp, 0x00000001U) == BSP_OK,
"TC-SBPROTO-021 building over a dirty buffer succeeds");
for (i = SB_FRAME_B_PAD_OFF; i < SB_FRAME_B_CRC_OFF; i++)
{
if (s_frame[i] != 0U)
{
pad_ok = 0;
}
}
CHECK(pad_ok == 1, "TC-SBPROTO-021b a 0xAA-pre-filled buffer is fully rewritten (padding zeroed)");
memcpy(first, s_frame, sizeof(first));
CHECK(sb_frame_b_build_image(s_frame, s_bmp, 0x00000001U) == BSP_OK,
"TC-SBPROTO-021c building the same content twice succeeds");
CHECK(memcmp(first, s_frame, sizeof(first)) == 0,
"TC-SBPROTO-021d the same inputs give a byte-identical frame (idempotent)");
}
static void tc_frame_b_logo(void)
{
fill_bmp_pattern();
CHECK(sb_frame_b_build_logo(s_frame, s_bmp, 160U, 160U) == BSP_OK,
"TC-SBPROTO-022 building a 160x160 LOGO frame succeeds");
CHECK((s_frame[SB_FRAME_B_INFO_OFF + 0U] == 0x00U) && (s_frame[SB_FRAME_B_INFO_OFF + 1U] == 0xA0U) &&
(s_frame[SB_FRAME_B_INFO_OFF + 2U] == 0x00U) && (s_frame[SB_FRAME_B_INFO_OFF + 3U] == 0xA0U),
"TC-SBPROTO-022b width and height go into info[0..3] big-endian");
CHECK((s_frame[SB_FRAME_B_INFO_OFF + 4U] == 0x89U) && (s_frame[SB_FRAME_B_INFO_OFF + 5U] == 0x45U),
"TC-SBPROTO-022c the LOGO magic 0x89 0x45 sits in info[4..5]");
CHECK((s_frame[SB_FRAME_B_INFO_OFF + 6U] == 0U) && (s_frame[SB_FRAME_B_INFO_OFF + 7U] == 0U),
"TC-SBPROTO-022d info[6..7] stay 0 (reserved)");
CHECK(sb_frame_b_verify(s_frame, (uint16_t)SB_FRAME_B_LEN) == BSP_OK,
"TC-SBPROTO-022e the LOGO frame verifies");
CHECK(screen_board_accepts(s_frame, (uint16_t)SB_FRAME_B_LEN) == 1,
"TC-SBPROTO-040b the board side Check_CRC accepts the LOGO frame");
/* 小图也要能组:1x1 只需 1 字节点阵 */
CHECK(sb_frame_b_build_logo(s_frame, s_bmp, 1U, 1U) == BSP_OK,
"TC-SBPROTO-022f a 1x1 LOGO is accepted (the capacity check is about bytes, not pixels)");
}
static void tc_frame_b_logo_guard(void)
{
fill_bmp_pattern();
/* 161x159 = 25599 像素(看起来"装得下"),实际要 21x159 = 3339 B > 3200 B */
CHECK(sb_frame_b_build_logo(s_frame, s_bmp, 161U, 159U) == BSP_ERR_PARAM,
"TC-SBPROTO-023 161x159 is rejected: 3339 bytes of bitmap do not fit in 3200");
CHECK(sb_frame_b_build_logo(s_frame, s_bmp, 160U, 161U) == BSP_ERR_PARAM,
"TC-SBPROTO-023b 160x161 is rejected (3220 bytes)");
CHECK(sb_frame_b_build_logo(s_frame, s_bmp, 168U, 152U) == BSP_OK,
"TC-SBPROTO-023c 168x152 is accepted (exactly 3192 bytes, width not a multiple of 8 is fine)");
CHECK(sb_frame_b_build_logo(s_frame, s_bmp, 0U, 160U) == BSP_ERR_PARAM,
"TC-SBPROTO-023d a zero width is rejected");
CHECK(sb_frame_b_build_logo(s_frame, s_bmp, 160U, 0U) == BSP_ERR_PARAM,
"TC-SBPROTO-023e a zero height is rejected");
}
static void tc_frame_b_guard(void)
{
fill_bmp_pattern();
CHECK(sb_frame_b_build_image(0, s_bmp, 0U) == BSP_ERR_PARAM,
"TC-SBPROTO-024 a null frame buffer is rejected (image)");
CHECK(sb_frame_b_build_image(s_frame, 0, 0U) == BSP_ERR_PARAM,
"TC-SBPROTO-024b a null bitmap is rejected (image)");
CHECK(sb_frame_b_build_logo(0, s_bmp, 160U, 160U) == BSP_ERR_PARAM,
"TC-SBPROTO-024c a null frame buffer is rejected (LOGO)");
CHECK(sb_frame_b_build_logo(s_frame, 0, 160U, 160U) == BSP_ERR_PARAM,
"TC-SBPROTO-024d a null bitmap is rejected (LOGO)");
CHECK(sb_frame_b_verify(0, (uint16_t)SB_FRAME_B_LEN) == BSP_ERR_PARAM,
"TC-SBPROTO-024e a null frame is rejected by verify");
}
static void tc_frame_b_verify(void)
{
fill_bmp_pattern();
(void)sb_frame_b_build_image(s_frame, s_bmp, 0x00000000U);
s_frame[100] ^= 0x01U;
CHECK(sb_frame_b_verify(s_frame, (uint16_t)SB_FRAME_B_LEN) == BSP_ERR_CRC,
"TC-SBPROTO-030 a flipped bitmap byte is caught");
s_frame[100] ^= 0x01U;
CHECK(sb_frame_b_verify(s_frame, (uint16_t)SB_FRAME_B_LEN) == BSP_OK,
"TC-SBPROTO-030b flipping it back restores the verdict");
/* 填充段屏板不读,但它参与 CRC:改了必须报错,否则"填充随便填"会悄悄破坏帧 */
s_frame[SB_FRAME_B_PAD_OFF + 500U] = 0x01U;
CHECK(sb_frame_b_verify(s_frame, (uint16_t)SB_FRAME_B_LEN) == BSP_ERR_CRC,
"TC-SBPROTO-030c a modified padding byte is caught (the padding really is inside the CRC range)");
s_frame[SB_FRAME_B_PAD_OFF + 500U] = 0x00U;
s_frame[SB_FRAME_B_CRC_OFF] ^= 0xFFU;
CHECK(sb_frame_b_verify(s_frame, (uint16_t)SB_FRAME_B_LEN) == BSP_ERR_CRC,
"TC-SBPROTO-030d a corrupted CRC byte is caught");
(void)sb_frame_b_build_image(s_frame, s_bmp, 0U);
CHECK(sb_frame_b_verify(s_frame, (uint16_t)(SB_FRAME_B_LEN - 1U)) == BSP_ERR_PARAM,
"TC-SBPROTO-030e a length other than 4208 is rejected");
}
int main(void)
{
printf("=== ScreenLink Proto Tests ===\n");
printf("-- CRC-16/MODBUS check values --\n");
tc_crc_vectors();
printf("-- CRC table vs bitwise reference --\n");
tc_crc_table();
printf("-- frame A: the board's real summon bytes --\n");
tc_frame_a_vectors();
printf("-- frame A: key handling --\n");
tc_frame_a_lenient_key();
printf("-- frame A: rejection paths --\n");
tc_frame_a_reject();
printf("-- frame A: argument guards --\n");
tc_frame_a_guard();
printf("-- frame B: image (bitmap + LED mask) --\n");
tc_frame_b_image();
printf("-- frame B: full-frame rewrite / idempotence --\n");
tc_frame_b_rewrite();
printf("-- frame B: LOGO (width/height + magic) --\n");
tc_frame_b_logo();
printf("-- frame B: LOGO capacity guard --\n");
tc_frame_b_logo_guard();
printf("-- frame B: argument guards --\n");
tc_frame_b_guard();
printf("-- frame B: self-verification --\n");
tc_frame_b_verify();
printf("\n=== ScreenLink Proto Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}