diff --git a/Drivers/BSP/ScreenLink/sb_proto.c b/Drivers/BSP/ScreenLink/sb_proto.c new file mode 100644 index 00000000..53ea50e3 --- /dev/null +++ b/Drivers/BSP/ScreenLink/sb_proto.c @@ -0,0 +1,273 @@ +/* + * 模块名称:屏板链路协议层实现(帧 A 解析 / 帧 B 组帧 / CRC-16) + * 模块功能:见 sb_proto.h 的模块头。本文件只做字节级编解码,不碰寄存器、DMA、任务与时间。 + * 适用平台:STM32F4 系列(ARMCC v5);仅依赖 stdint.h 与 string.h,主机侧可编译。 + * 作者:王建锋 + * 创建日期:2026-10-29 + * 修改记录: + * 2026-10-29 王建锋 创建:按屏板固件 CRC16.c / MODBUS.c 对齐,并订正 CRC 字节序口径 + * + * ============================================================================ + * CRC 的字节序:这里有一个会让人白查一天的口径陷阱,先看这段 + * ============================================================================ + * 屏板固件 Middlewares/Modbus/CRC16.c 的 Calculate_CRC() 返回的**不是**教科书上那个 + * CRC-16/MODBUS 值,而是它高低字节互换后的「线上字」。举实测值: + * + * 数据 "123456789" 屏板返回 0x374B CRC-16/MODBUS 标准值 0x4B37 + * 数据 01 03 00 00 00 0A 屏板返回 0xC5CD 标准值 0xCDC5 + * + * 于是屏板 RS485_RefreshScreenCMD() 里那句「cmdbuf[4] = crc >> 8」(注释写"CRC校验高字节") + * 实际写出去的是**标准 CRC 的低字节**——两处命名都是反的,但**线上字节是对的**: + * 上面第二帧的线上 CRC 就是 C5 CD(Modbus RTU 的低字节在前)。 + * + * 结论(本模块采用的写法): + * · sb_crc16() 返回**标准 CRC-16/MODBUS 值**(可用教科书校验值 0x4B37 直接对); + * · 写帧时一律 **低字节在前**:帧 A 偏移 4 = 低、偏移 5 = 高;帧 B 偏移 4206 = 低、4207 = 高。 + * 若照屏板注释的字面意思写成「高字节在前」,两侧算出的值差一次字节交换,屏板会把每一帧都 + * 判成 CRC 错(它的超时判定只看"收没收到一整包"、不看 CRC,所以现场只表现为画面不刷新), + * 极易误判成波特率或时序问题。主机单测里钉了屏板算法的实测向量,改这两行会立刻红。 + * + * 表:屏板用两张 8 位表(auchCRCHi / auchCRCLo),本模块用一张 16 位表(同样 512 B 常量)。 + * 两者关系是可逐项验证的恒等式:s_crc_table[i] == (auchCRCLo[i] 左移 8) 或 auchCRCHi[i] + * ——即屏板把**低字节表叫 Hi、高字节表叫 Lo**,这也是上面那个反相的根源。 + */ + +#include + +#include "sb_proto.h" + +/* ============================================================================ + * CRC-16/MODBUS 查表(多项式 0xA001 反转式,初值 0xFFFF) + * 表的生成方式:对 i = 0..255,取 crc = i,做 8 次「最低位为 1 则右移后异或 0xA001, + * 否则只右移」,所得 crc 即表项。主机单测逐项复核前 8 项与末项,并复算屏板的实测向量。 + * ==========================================================================*/ +static const uint16_t s_crc_table[256] = +{ + 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, + 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, + 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, + 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, + 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, + 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, + 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, + 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, + 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, + 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, + 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, + 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, + 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, + 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, + 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, + 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, + 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, + 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, + 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, + 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, + 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, + 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, + 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, + 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, + 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, + 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, + 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, + 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, + 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, + 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, + 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, + 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 +}; + +/* ============================================================================ + * 私有工具 + * ==========================================================================*/ + +/* + * 函数功能:把 CRC 写到帧尾(低字节在前,见文件头那段说明) + * 入口参数:p_frame - 帧首地址 + * off - CRC 域起始偏移 + * crc - sb_crc16() 返回的标准 CRC 值 + * 返回值:无 + * 限定条件:p_frame 至少长 off + 2。 + * 函数说明:帧 A 与帧 B 共用,两处字节序必须一致——写反了屏板会把每一帧都判成 CRC 错。 + */ +static void sb_put_crc(uint8_t *p_frame, uint16_t off, uint16_t crc) +{ + p_frame[off] = (uint8_t)(crc & 0xFFU); /* 低字节在前 */ + p_frame[off + 1U] = (uint8_t)((crc >> 8) & 0xFFU); +} + +/* ============================================================================ + * 公开接口实现 + * ==========================================================================*/ + +uint16_t sb_crc16(const uint8_t *p_data, uint16_t len) +{ + uint16_t crc = 0xFFFFU; + + while (len > 0U) + { + crc = (uint16_t)((crc >> 8) ^ s_crc_table[(uint8_t)(crc ^ *p_data)]); + p_data++; + len--; + } + return crc; +} + +/* + * 函数功能:解析一帧帧 A(屏板发来的 6 B 召唤 / 按键帧) + * 入口参数:p_buf - 收到的字节首地址 + * len - 收到的字节数 + * p_out - 解析结果输出(成功时写 cmd / key,失败时不改) + * 返回值:BSP_OK / BSP_ERR_CRC(帧被破坏,计数后丢弃)/ BSP_ERR_PARAM(不是合法召唤帧,直接丢) + * 限定条件:只认固定 6 B;先校 CRC 再看字段。 + * 函数说明:细节与"为什么地址/功能码不符也报 PARAM"见 sb_proto.h 的同名声明。 + */ +bsp_err_t sb_frame_a_parse(const uint8_t *p_buf, uint16_t len, sb_frame_a_t *p_out) +{ + uint16_t crc; + uint8_t cmd; + + if ((p_buf == 0) || (p_out == 0)) + { + return BSP_ERR_PARAM; + } + if (len != SB_FRAME_A_LEN) + { + return BSP_ERR_PARAM; + } + + /* 先校 CRC:字节已经乱了的话,再去解释地址/功能码没有意义 */ + crc = sb_crc16(p_buf, (uint16_t)SB_FRAME_A_CRC_OFF); + if ((p_buf[SB_FRAME_A_CRC_OFF] != (uint8_t)(crc & 0xFFU)) || + (p_buf[SB_FRAME_A_CRC_OFF + 1U] != (uint8_t)((crc >> 8) & 0xFFU))) + { + return BSP_ERR_CRC; + } + + if (p_buf[0] != SB_ADDR) + { + return BSP_ERR_PARAM; + } + if (p_buf[1] != SB_FUNC_READ_HOLDING) + { + return BSP_ERR_PARAM; + } + + cmd = p_buf[2]; + if ((cmd != SB_CMD_KEY) && (cmd != SB_CMD_RT)) + { + return BSP_ERR_PARAM; + } + + p_out->cmd = cmd; + /* 周期召唤的 key 字段按协议恒为 0;真收到非 0 也按 0 处理(不为此丢一帧) */ + p_out->key = (cmd == SB_CMD_KEY) ? p_buf[3] : (uint8_t)0U; + return BSP_OK; +} + +/* + * 函数功能:组一帧帧 B「普通图」(点阵 + 32 位 LED 掩码) + * 入口参数:p_frame - 输出缓冲(>= SB_FRAME_B_LEN) + * p_bmp - 点阵源(>= SB_FRAME_B_BMP_LEN) + * led_mask - 状态灯位掩码,小端填进 info[0..3] + * 返回值:BSP_OK / BSP_ERR_PARAM(指针为空) + * 限定条件:整帧重写(含 998 B 填充与 CRC),调用方不必预先清零。 + * 函数说明:掩码的位语义不归本模块管(见任务规划批 3)。 + */ +bsp_err_t sb_frame_b_build_image(uint8_t *p_frame, const uint8_t *p_bmp, uint32_t led_mask) +{ + if ((p_frame == 0) || (p_bmp == 0)) + { + return BSP_ERR_PARAM; + } + + /* 整帧重写:998 B 填充与 info[4..7] 一并归零,调用方不必预先清零 */ + (void)memset(p_frame, 0, (size_t)SB_FRAME_B_LEN); + (void)memcpy(p_frame, p_bmp, (size_t)SB_FRAME_B_BMP_LEN); + + /* info[0..3]:LED 位掩码,小端(info[0] = bit0..7) */ + p_frame[SB_FRAME_B_INFO_OFF + 0U] = (uint8_t)(led_mask & 0xFFU); + p_frame[SB_FRAME_B_INFO_OFF + 1U] = (uint8_t)((led_mask >> 8) & 0xFFU); + p_frame[SB_FRAME_B_INFO_OFF + 2U] = (uint8_t)((led_mask >> 16) & 0xFFU); + p_frame[SB_FRAME_B_INFO_OFF + 3U] = (uint8_t)((led_mask >> 24) & 0xFFU); + + sb_put_crc(p_frame, (uint16_t)SB_FRAME_B_CRC_OFF, sb_crc16(p_frame, (uint16_t)SB_FRAME_B_CRC_OFF)); + return BSP_OK; +} + +/* + * 函数功能:组一帧帧 B「LOGO」(点阵 + 宽高 + 魔数 0x89 0x45) + * 入口参数:p_frame - 输出缓冲(>= SB_FRAME_B_LEN) + * p_bmp - 点阵源(>= SB_FRAME_B_BMP_LEN) + * width - 图片宽度(像素),大端填进 info[0..1] + * height - 图片高度(像素),大端填进 info[2..3] + * 返回值:BSP_OK / BSP_ERR_PARAM(指针空 / 宽高为 0 / 点阵装不下) + * 限定条件:按字节数判容量(((width + 7) / 8) * height <= 3200),不是按像素数。 + * 函数说明:本帧会覆盖屏板 Flash 扇区 126,发送时机由调用方决定。 + */ +bsp_err_t sb_frame_b_build_logo(uint8_t *p_frame, const uint8_t *p_bmp, uint16_t width, uint16_t height) +{ + uint32_t need_bytes; + + if ((p_frame == 0) || (p_bmp == 0)) + { + return BSP_ERR_PARAM; + } + if ((width == 0U) || (height == 0U)) + { + return BSP_ERR_PARAM; + } + + /* 按字节数判容量而不是像素数:161x159 只有 25599 像素,却要 21x159 = 3339 B 才装得下 */ + need_bytes = ((uint32_t)width + 7U) / 8U; + need_bytes *= (uint32_t)height; + if (need_bytes > (uint32_t)SB_FRAME_B_BMP_LEN) + { + return BSP_ERR_PARAM; + } + + (void)memset(p_frame, 0, (size_t)SB_FRAME_B_LEN); + (void)memcpy(p_frame, p_bmp, (size_t)SB_FRAME_B_BMP_LEN); + + /* info[0..1] 宽、info[2..3] 高,均大端 */ + p_frame[SB_FRAME_B_INFO_OFF + 0U] = (uint8_t)((width >> 8) & 0xFFU); + p_frame[SB_FRAME_B_INFO_OFF + 1U] = (uint8_t)(width & 0xFFU); + p_frame[SB_FRAME_B_INFO_OFF + 2U] = (uint8_t)((height >> 8) & 0xFFU); + p_frame[SB_FRAME_B_INFO_OFF + 3U] = (uint8_t)(height & 0xFFU); + /* info[4..5] LOGO 魔数;info[6..7] 保留(已归零) */ + p_frame[SB_FRAME_B_INFO_OFF + 4U] = SB_LOGO_MAGIC_HI; + p_frame[SB_FRAME_B_INFO_OFF + 5U] = SB_LOGO_MAGIC_LO; + + sb_put_crc(p_frame, (uint16_t)SB_FRAME_B_CRC_OFF, sb_crc16(p_frame, (uint16_t)SB_FRAME_B_CRC_OFF)); + return BSP_OK; +} + +/* + * 函数功能:自校验一帧帧 B(重算 CRC 并与帧尾比对) + * 入口参数:p_frame - 待校验帧首地址 + * len - 帧长度,必须等于 SB_FRAME_B_LEN + * 返回值:BSP_OK / BSP_ERR_CRC(不一致)/ BSP_ERR_PARAM(指针空或长度不对) + * 限定条件:只验 CRC,不检查填充段是否全 0、也不检查掩码位语义。 + * 函数说明:供组帧路径自查(2-4 的「组帧自校验 100%」接这里)。 + */ +bsp_err_t sb_frame_b_verify(const uint8_t *p_frame, uint16_t len) +{ + uint16_t crc; + + if (p_frame == 0) + { + return BSP_ERR_PARAM; + } + if (len != SB_FRAME_B_LEN) + { + return BSP_ERR_PARAM; + } + + crc = sb_crc16(p_frame, (uint16_t)SB_FRAME_B_CRC_OFF); + if ((p_frame[SB_FRAME_B_CRC_OFF] != (uint8_t)(crc & 0xFFU)) || + (p_frame[SB_FRAME_B_CRC_OFF + 1U] != (uint8_t)((crc >> 8) & 0xFFU))) + { + return BSP_ERR_CRC; + } + return BSP_OK; +} diff --git a/Drivers/BSP/ScreenLink/sb_proto.h b/Drivers/BSP/ScreenLink/sb_proto.h new file mode 100644 index 00000000..0682f73e --- /dev/null +++ b/Drivers/BSP/ScreenLink/sb_proto.h @@ -0,0 +1,164 @@ +/* + * 模块名称:屏板链路协议层(帧 A 解析 / 帧 B 组帧 / CRC-16) + * 模块功能:把「主机与屏板之间那两种帧」的编解码收成一处纯逻辑: + * - CRC-16/MODBUS(表与屏板固件逐字节一致,见 Drivers/BSP/ScreenLink/sb_proto.c) + * - 帧 A(屏板 → 主机,固定 6 B)解析:校验后吐出「命令类型 + 按键值」 + * - 帧 B(主机 → 屏板,固定 4208 B)组帧:普通图(LED 掩码)/ LOGO(宽高 + 魔数) + * - 帧 B 自校验(组完立刻能验,供组帧路径自查) + * 本模块**不碰寄存器、不碰 DMA、不碰任务**:串口怎么配、谁来发、什么时候发都不归它管 + * (那是批 1 的 1-2 / 1-3 与批 2 的事)。 + * 适用平台:STM32F4 系列(ARMCC v5);不依赖 HAL,主机侧可直接编译(故可主机单测)。 + * 作者:王建锋 + * 创建日期:2026-10-29 + * 修改记录: + * 2026-10-29 王建锋 创建:按屏板固件 D:\Code\DTU Code\DTU-LCD 的 CRC16.c / MODBUS.c 逐字节对齐 + * + * 协议事实源:屏板固件工程(不在本仓库) + * Middlewares/Modbus/CRC16.c —— 两张 256 字节查表 + Calculate_CRC() / Check_CRC() + * Middlewares/Modbus/MODBUS.c —— RS485_RefreshScreenCMD()(帧 A 组帧,6 B) + * RS485_Process()(帧 B 解析:3200 点阵 + 8 字节 info + CRC 在帧尾) + * Drivers/BSP/RS485/rs485.h —— UART_RX_LEN = 4208(帧 B 的线上长度,注释写 3208 是错的) + * 需求侧口径见 docs/产品需求/产品需求设计文档.md §5.16.2(帧结构)与 §5.16.3(按键枚举)。 + * + * ⚠ 两个长度别混:屏板 `BMP_LEN` 的 3208 是**写 Flash 的记录长度**(点阵 3200 + 宽 4 + 高 4), + * 不是帧长;帧长以 `Check_CRC(..., UART_RX_LEN)` 的 **4208** 为准。本模块只认 4208。 + */ + +#ifndef SB_PROTO_H +#define SB_PROTO_H + +#include + +#include "bsp_err.h" /* bsp_err_t / BSP_OK / BSP_ERR_*:本模块只报统一错误码 */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* ============================================================================ + * 一、帧长与字段偏移(单一出处,别在调用方另抄一份) + * ==========================================================================*/ + +/* 帧 A:屏板 → 主机,固定 6 B。CRC 只覆盖前 4 B,高字节在前。 */ +#define SB_FRAME_A_LEN (6U) +#define SB_FRAME_A_CRC_OFF (4U) + +/* 帧 B:主机 → 屏板,固定 4208 B。CRC 覆盖 [0, 4205],高字节在前。 */ +#define SB_FRAME_B_LEN (4208U) +#define SB_FRAME_B_BMP_LEN (3200U) /* 点阵:160 行 x 20 B/行,1 bpp,MSB 在前 */ +#define SB_FRAME_B_INFO_LEN (8U) /* info[0..7] */ +#define SB_FRAME_B_INFO_OFF (3200U) +#define SB_FRAME_B_PAD_OFF (3208U) /* 保留填充起点 */ +#define SB_FRAME_B_PAD_LEN (998U) /* [3208, 4205]:屏板不读,但参与 CRC,须固定填 0 */ +#define SB_FRAME_B_CRC_OFF (4206U) + +/* 帧 A 的固定字段值 */ +#define SB_ADDR (0x01U) /* 从站地址:**主机的地址**,不是屏板的 */ +#define SB_FUNC_READ_HOLDING (0x03U) /* 功能码:读保持寄存器 */ +#define SB_CMD_KEY (0x00U) /* 命令类型:按键事件(立即召唤) */ +#define SB_CMD_RT (0x01U) /* 命令类型:周期刷新 */ + +/* 帧 B 的 LOGO 魔数(info[4] / info[5]) */ +#define SB_LOGO_MAGIC_HI (0x89U) +#define SB_LOGO_MAGIC_LO (0x45U) + +/* 屏幕几何:LOGO 的宽高要在这一片点阵装得下 */ +#define SB_SCREEN_W (160U) +#define SB_SCREEN_H (160U) + +/* ============================================================================ + * 二、帧 A 的解析结果 + * ==========================================================================*/ + +/* 解析成功后的有效字段。addr / func 恒为 SB_ADDR / SB_FUNC_READ_HOLDING, + * 不再回吐(校验不过直接报错,能走到这里就一定是这两个值)。 */ +typedef struct +{ + uint8_t cmd; /* SB_CMD_KEY / SB_CMD_RT */ + uint8_t key; /* cmd == SB_CMD_KEY 时有效;cmd == SB_CMD_RT 时恒为 0(见 sb_frame_a_parse 说明) */ +} sb_frame_a_t; + +/* ============================================================================ + * 三、公开接口 + * ==========================================================================*/ + +/* + * 函数功能:计算 CRC-16/MODBUS(多项式 0xA001 反转式、初值 0xFFFF、查表法) + * 入口参数:p_data - 待校验数据首地址 + * len - 参与校验的字节数(不含 CRC 本身) + * 返回值:16 位**标准 CRC-16/MODBUS 值**(教科书校验值:数据 "123456789" → 0x4B37) + * 限定条件:p_data 必须非空且 len >= 1;本函数是纯算术,不做空指针检查(与相量系纯算法函数同口径)。 + * 写进帧里一律**低字节在前**——屏板固件那个同名函数返回的是高低互换后的「线上字」, + * 两处命名都是反的,但线上字节是对的。陷阱与实测向量见 sb_proto.c 文件头,改前务必看。 + * 函数说明:查表法而非逐位法。帧 B 一次要算 4206 B:逐位法约 13 万拍(毫秒量级), + * 查表法约 2.5 万拍(0.15 ms 量级,168 MHz 估算),后者才留得住 2-1 的 0.3 ms + * 组帧预算——而那份预算还要分给 3200 B 的点阵拷贝,2-1 实测时两者要一起算。 + */ +uint16_t sb_crc16(const uint8_t *p_data, uint16_t len); + +/* + * 函数功能:解析一帧帧 A(屏板发来的 6 B 召唤 / 按键帧) + * 入口参数:p_buf - 收到的字节首地址 + * len - 收到的字节数(空闲中断断出来的实际长度) + * p_out - 解析结果输出(成功时写 cmd / key,失败时不改) + * 返回值:BSP_OK 解析成功,*p_out 有效 + * BSP_ERR_CRC CRC 不符:帧被破坏,应计数后静默丢弃(不要回任何东西) + * BSP_ERR_PARAM 不是一个合法的召唤帧:指针空 / len != 6 / 地址不是 0x01 / + * 功能码不是 0x03 / 命令类型不是 0 或 1 —— 丢弃即可,不必计数 + * 限定条件:只认固定 6 B 的帧 A;屏板的帧 B 应答方向相反,不经过本函数。 + * 函数说明: + * 1. **先校 CRC 再看字段**:CRC 不过说明字节已经乱了,再去解释地址/功能码没有意义。 + * 2. 地址与功能码不符也报 BSP_ERR_PARAM 而不是新造错误码——bsp_err.h 的码是跨模块的 + * 「能不能重试」语义,不是协议细节;对调用方而言这几种都等于「这不是给本机的合法召唤帧」。 + * 要区分细节就比对原始字节(屏板这两个字段是写死的,出现不符说明已经错位)。 + * 3. cmd == SB_CMD_RT 时若 key 非 0 **不报错**,按 0 忽略:屏板的周期召唤本应带 0, + * 但为这一项丢一帧就等于丢一次刷新(280 ms 的画面不动),代价不对等。 + */ +bsp_err_t sb_frame_a_parse(const uint8_t *p_buf, uint16_t len, sb_frame_a_t *p_out); + +/* + * 函数功能:组一帧帧 B「普通图」(点阵 + 32 位 LED 掩码) + * 入口参数:p_frame - 输出缓冲,长度必须 >= SB_FRAME_B_LEN + * p_bmp - 点阵源,长度必须 >= SB_FRAME_B_BMP_LEN(160x160 @1bpp) + * led_mask - 状态灯位掩码,小端填进 info[0..3](info[0] = bit0..7) + * 返回值:BSP_OK 组帧完成,整帧(含 CRC)已就绪 + * BSP_ERR_PARAM 指针为空 + * 限定条件:p_frame 会被**整帧重写**(含 998 B 填充与 CRC),调用方不必预先清零; + * 组好的帧在下次调用前有效,本模块不持有任何缓冲。 + * 函数说明:填充段固定填 0x00(屏板不读,但参与 CRC)。掩码的位语义不归本模块管 + * (bit0 恒 0 / bit20~31 恒 0 这类业务约束属状态灯映射表,见任务规划批 3)。 + */ +bsp_err_t sb_frame_b_build_image(uint8_t *p_frame, const uint8_t *p_bmp, uint32_t led_mask); + +/* + * 函数功能:组一帧帧 B「LOGO」(点阵 + 宽高 + 魔数 0x89 0x45) + * 入口参数:p_frame - 输出缓冲,长度必须 >= SB_FRAME_B_LEN + * p_bmp - 点阵源,长度必须 >= SB_FRAME_B_BMP_LEN + * width - 图片宽度(像素),大端填进 info[0..1] + * height - 图片高度(像素),大端填进 info[2..3] + * 返回值:BSP_OK 组帧完成 + * BSP_ERR_PARAM 指针为空 / 宽高为 0 / 超出 160x160 点阵容量 + * 限定条件:宽高必须满足 ((width + 7) / 8) * height <= 3200 —— 屏板拿这两个数直接显示, + * 装不下的宽高会让它读越 3200 B 的点阵缓冲,故本函数按字节数而非像素数判容量。 + * 函数说明:LOGO 帧会把屏板 Flash 0x0803F000(扇区 126)整块覆盖,且屏板不校验是否同一张图; + * 本模块只负责把帧组对,**什么时候发、能不能重复发由调用方决定**。 + */ +bsp_err_t sb_frame_b_build_logo(uint8_t *p_frame, const uint8_t *p_bmp, uint16_t width, uint16_t height); + +/* + * 函数功能:自校验一帧帧 B(重算 CRC 并与帧尾比对) + * 入口参数:p_frame - 待校验帧首地址 + * len - 帧长度,必须等于 SB_FRAME_B_LEN + * 返回值:BSP_OK CRC 与帧尾一致 + * BSP_ERR_CRC 不一致(帧被改写 / 组帧时漏算) + * BSP_ERR_PARAM 指针空或 len 不是 4208 + * 限定条件:只验 CRC,不检查填充段是否全 0、也不检查掩码位语义。 + * 函数说明:给组帧路径自查用(2-4 的「组帧自校验 100%」直接接这里)。 + */ +bsp_err_t sb_frame_b_verify(const uint8_t *p_frame, uint16_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* SB_PROTO_H */ diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index e591b41c..3c0a6c72 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -337,7 +337,7 @@ --diag_suppress=1,68,111,188,191,546,1293 USE_HAL_DRIVER,STM32F407xx,NDEBUG - ../Inc;../Drivers/STM32F4xx_HAL_Driver/Inc;../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy;../Drivers/CMSIS/Device/ST/STM32F4xx/Include;../Drivers/CMSIS/Include;../Drivers/BSP;../Drivers/BSP/CH395F;../Drivers/BSP/GD5F2GQ5UE;../Drivers/BSP/TPAFE5160;../Drivers/BSP/SD2506;../Drivers/BSP/RS485;../Drivers/BSP/NET;../Drivers/BSP/NET/lftpd;../Drivers/BSP/NETCFG;../App;../App/app;../App/algo;../App/cfg;../App/fs;../App/log;../App/sync;../App/task;../App/protocol/iec104;../App/util;../Middlewares/Third_Party/FreeRTOS/Source/include;../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2;../Middlewares/Third_Party/FreeRTOS/Source/portable/RVDS/ARM_CM4F;../Lib/dhara;../Lib/FatFs;../test/board;../Lib/lvgl;../Drivers/BSP/UI;../Lib + ../Inc;../Drivers/STM32F4xx_HAL_Driver/Inc;../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy;../Drivers/CMSIS/Device/ST/STM32F4xx/Include;../Drivers/CMSIS/Include;../Drivers/BSP;../Drivers/BSP/CH395F;../Drivers/BSP/GD5F2GQ5UE;../Drivers/BSP/TPAFE5160;../Drivers/BSP/SD2506;../Drivers/BSP/RS485;../Drivers/BSP/ScreenLink;../Drivers/BSP/NET;../Drivers/BSP/NET/lftpd;../Drivers/BSP/NETCFG;../App;../App/app;../App/algo;../App/cfg;../App/fs;../App/log;../App/sync;../App/task;../App/protocol/iec104;../App/util;../Middlewares/Third_Party/FreeRTOS/Source/include;../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2;../Middlewares/Third_Party/FreeRTOS/Source/portable/RVDS/ARM_CM4F;../Lib/dhara;../Lib/FatFs;../test/board;../Lib/lvgl;../Drivers/BSP/UI;../Lib @@ -1623,6 +1623,11 @@ 1 ..\Drivers\BSP\RS485\rs485.c + + sb_proto.c + 1 + ..\Drivers\BSP\ScreenLink\sb_proto.c + net_select.c 1 diff --git a/scripts/check_registration.py b/scripts/check_registration.py index 5f501dfc..6460f15e 100644 --- a/scripts/check_registration.py +++ b/scripts/check_registration.py @@ -135,6 +135,7 @@ SIM_EXEMPT = [ ("Drivers/BSP/NET/net_pending.c", "MCU 挂起 recv 模型(依赖 ch395f_* SPI 读出与 FreeRTOS 通知),模拟器另有 net_socket_sim.c 按外部语义模拟"), ("Drivers/BSP/RS485/rs485.c", "MCU RS-485 驱动"), ("Drivers/BSP/SD2506/sd2506.c", "MCU RTC 驱动"), + ("Drivers/BSP/ScreenLink/sb_proto.c", "屏板链路协议层纯逻辑(帧 A 解析 / 帧 B 组帧 / CRC-16),消费者 = MCU 侧 USART1 收发起发(批 1/2),模拟器无消费路径;主机侧纯逻辑单测覆盖于 test/host/sb_proto"), ("Drivers/BSP/TPAFE5160/tpafe5160.c", "MCU 计量芯片驱动"), ("Drivers/BSP/UI/ui_port.c", "MCU UI 端口层(显示/触摸硬件),模拟器另有 sim/sim_port.c"), ] diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index bc586c0a..799f1b45 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -344,6 +344,14 @@ "lines_covered": 108, "lines_valid": 112 }, + "Drivers/BSP/ScreenLink/sb_proto.c": { + "func_rate": 1.0, + "funcs_covered": 6, + "funcs_valid": 6, + "line_rate": 1.0, + "lines_covered": 74, + "lines_valid": 74 + }, "Drivers/BSP/UI/ui_data.c": { "func_rate": 0.9767, "funcs_covered": 42, @@ -607,6 +615,13 @@ "lines_total": 961, "ok": true }, + "sb_proto": { + "files": { + "Drivers/BSP/ScreenLink/sb_proto.c": 74 + }, + "lines_total": 74, + "ok": true + }, "sha256": { "files": { "App/util/sha256.c": 114 @@ -741,22 +756,23 @@ "Drivers/BSP/NET/net_socket.c", "Drivers/BSP/NETCFG/ftp_cfg.c", "Drivers/BSP/NETCFG/net_cfg.c", + "Drivers/BSP/ScreenLink/sb_proto.c", "Drivers/BSP/UI/ui_data.c", "Drivers/BSP/UI/ui_net_sink.c", "Drivers/BSP/UI/ui_w_soe.c" ], "totals": { "files_generated_excluded": 5, - "files_tested": 46, - "files_untested": 151, - "func_rate": 0.9317, - "funcs_covered": 532, - "funcs_valid": 571, - "line_rate": 0.8983, - "lines_covered": 7058, - "lines_valid": 7857, - "modules_ok": 41, - "modules_total": 41 + "files_tested": 47, + "files_untested": 164, + "func_rate": 0.9324, + "funcs_covered": 538, + "funcs_valid": 577, + "line_rate": 0.8993, + "lines_covered": 7132, + "lines_valid": 7931, + "modules_ok": 42, + "modules_total": 42 }, "untested_sources": [ { @@ -768,7 +784,7 @@ "path": "Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c" }, { - "code_lines": 407, + "code_lines": 420, "path": "App/app/ui_data_feed.c" }, { @@ -796,7 +812,7 @@ "path": "Drivers/BSP/SD2506/sd2506.c" }, { - "code_lines": 249, + "code_lines": 263, "path": "App/task/adc_task.c" }, { @@ -815,6 +831,10 @@ "code_lines": 206, "path": "Drivers/BSP/CH395F/ch395f.h" }, + { + "code_lines": 206, + "path": "Drivers/BSP/UI/ui_data.h" + }, { "code_lines": 205, "path": "Drivers/BSP/UI/ui_page_net_cfg.c" @@ -827,14 +847,14 @@ "code_lines": 200, "path": "Drivers/BSP/UI/ui_w_ytview.c" }, - { - "code_lines": 196, - "path": "Drivers/BSP/UI/ui_data.h" - }, { "code_lines": 157, "path": "Drivers/BSP/TPAFE5160/tpafe5160.c" }, + { + "code_lines": 152, + "path": "Drivers/BSP/UI/ui_page_runreport.c" + }, { "code_lines": 150, "path": "Drivers/BSP/UI/ui_page_password.c" @@ -844,12 +864,12 @@ "path": "Drivers/BSP/UI/ui_page_device_info.c" }, { - "code_lines": 134, - "path": "App/task/protect_task.c" + "code_lines": 135, + "path": "App/app/app_main.c" }, { - "code_lines": 132, - "path": "App/app/app_main.c" + "code_lines": 134, + "path": "App/task/protect_task.c" }, { "code_lines": 129, @@ -999,6 +1019,10 @@ "code_lines": 40, "path": "Drivers/BSP/UI/ui_page_deadband.c" }, + { + "code_lines": 36, + "path": "Drivers/BSP/ScreenLink/sb_proto.h" + }, { "code_lines": 36, "path": "Drivers/BSP/UI/ui_w_records.h" @@ -1115,6 +1139,18 @@ "code_lines": 22, "path": "Drivers/BSP/NETCFG/ftp_cfg.h" }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_accident.c" + }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_alarm.c" + }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_oper.c" + }, { "code_lines": 22, "path": "Drivers/BSP/UI/ui_page_runlog.c" @@ -1127,6 +1163,14 @@ "code_lines": 22, "path": "Drivers/BSP/UI/ui_page_soe.c" }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_start.c" + }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_yk.c" + }, { "code_lines": 20, "path": "Drivers/BSP/UI/ui_net_sink.h" @@ -1339,6 +1383,14 @@ "code_lines": 5, "path": "Drivers/BSP/UI/ui_page_ac.h" }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_accident.h" + }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_alarm.h" + }, { "code_lines": 5, "path": "Drivers/BSP/UI/ui_page_dc.h" @@ -1347,6 +1399,22 @@ "code_lines": 5, "path": "Drivers/BSP/UI/ui_page_device_info.h" }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_oper.h" + }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_runreport.h" + }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_start.h" + }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_yk.h" + }, { "code_lines": 4, "path": "Drivers/BSP/UI/ui_page_runlog.h" diff --git a/test/host/sb_proto/run_unit_test.py b/test/host/sb_proto/run_unit_test.py new file mode 100644 index 00000000..47809879 --- /dev/null +++ b/test/host/sb_proto/run_unit_test.py @@ -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)) diff --git a/test/host/sb_proto/sb_proto_test.c b/test/host/sb_proto/sb_proto_test.c new file mode 100644 index 00000000..7f226db3 --- /dev/null +++ b/test/host/sb_proto/sb_proto_test.c @@ -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 +#include +#include + +/* ===================== 断言 ===================== */ +static int s_total = 0, s_passed = 0, s_failed = 0; + +#define CHECK(cond, name) do { \ + s_total++; \ + if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \ + else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \ +} while (0) + +/* ===================== 被测代码 ===================== */ +/* 原样纳入生产源文件(纯逻辑,无需桩) */ +#include "sb_proto.c" + +/* ===================== 工具 ===================== */ + +/* 伪随机(自持实现,不依赖 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; +}