/* * 模块名称:屏板链路协议层实现(帧 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; /* 32 位计数:用 uint16_t 循环变量时 ARMCC 每轮会多生成一条 UXTH(实测见 * fromelf 反汇编),4206 B 一轮就是 4206 条无用指令。帧 B 每帧都要算满 4206 B, * 这条值得省(组帧预算见 docs/任务规划/屏板链路.md §3 的 2-1)。 */ uint32_t n = (uint32_t)len; while (n > 0U) { crc = (uint16_t)((crc >> 8) ^ s_crc_table[(uint8_t)(crc ^ *p_data)]); p_data++; n--; } 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; }