Files
STM32F4-Base/Drivers/BSP/ScreenLink/sb_proto.c
T
wandering1 1634088bd7 docs(screenlink): 屏板链路档去「说明」并瘦身,控制台档收编台架日志口
屏板链路设计说明.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 通过。
2026-11-15 21:51:24 +08:00

231 lines
10 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:屏板链路协议层实现(帧 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;
}