1634088bd7
屏板链路设计说明.md -> 屏板链路设计.md(去档名「说明」,与 ADC 节拍 / 运行与自检记录两篇口径一致);控制台单写者日志设计说明.md 同改。 两档并存期间旧名引用 0 残留。 - 屏板链路 §3.2 台架日志口(RS232#1 / CN1 端子 / COM3)移入控制台档 §3.4:日志口的接线属 UART5 控制台通道,不属屏板链路;屏板档只留 「别去 CN1 找 USART1 端子」这句接线防错提示。 - 屏板链路 §7.1 由「根因账本 + 修复前后对比」改写为现状描述:延迟 应答机制表 + 耗时分解表 + 两个计数口径 + 已知边界,40 行压到 24 行。 顺带修正 §8 的错引:原指 `网络自愈架构设计.md` §8(实为「延后项」, 不含 uiTask 内容)改为 §4.1 `net_reconcile_tick`。 - §9.1 增补长稳行(串口日志 2026-11-13 18:31 -> 2026-11-15 21:43, 单次上电 51.2 h:rx=658262 ok=658262、crc/len/fmt/err 全 0、 drop=7 仅启动期、kerr/abor/bad=0、全程 off=0);§9.2 销项三条 (仪器项、2 h 长稳、len=7 观察项),新增「按键通路」一条——51.2 h 日志 keys=0 kq=0 dfok=0 dfold=0,延迟应答在真机上从未执行过。 - 删除 sb_frame_b_build_logo() 及 SB_LOGO_MAGIC_* / SB_SCREEN_W/H 与对应单测(TC-SBPROTO-022/023 全组、024c/024d),组帧入口只剩 sb_frame_b_build_image()。sb_proto 单测 59 -> 45 条,45/45 通过。 - cfg_file.c 出厂模板三条中文字面量改 \uXXXX 转义:消除全量重编时的 4 条 #870-D invalid multibyte character sequence,运行期字节不变, 源码回到纯 ASCII(符合「字符串字面量必须纯 ASCII」)。 验证:keil-build 0 Error 0 Warning;sb_proto 45/45、sb_link 164/164; pre-commit 48 钩子全 Passed;check_doc_links --sections-strict 通过。
231 lines
10 KiB
C
231 lines
10 KiB
C
/*
|
||
* 模块名称:屏板链路协议层实现(帧 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 <string.h>
|
||
|
||
#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 §7)。 */
|
||
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),调用方不必预先清零。
|
||
* 函数说明:掩码的位语义不归本模块管(见 App/algo/sb_led_map.h)。
|
||
*/
|
||
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(重算 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;
|
||
}
|