Files
wandering1 8744c833e5 docs(屏板链路): 规划文档收编为设计说明事实源,全仓引用与端口口径同步
- 重写 docs/子系统设计/屏板链路设计说明.md:链路角色、串口与端口配置、
  物理接线、协议实现口径、键值与状态灯映射、资源与耗时、对外接口、
  已验证/未闭环、坑账本——只写现状事实,演进过程查 git 历史
- 删除 docs/任务规划/屏板链路.md(内容已全部沉淀进上述设计说明,
  登记簿「完成即销项」;docs/任务规划/README.md 相应两行一并移除)
- 代码与主机侧测试的文档指针改指设计说明对应章节:
  sb_link.h/.c、sb_proto.h/.c、sb_led_map.h、console_log.c、ui_task.c、
  test/host 的 sb_link/sb_proto/sb_led_map 三套与两个台架脚本
- docs/README.md:设计文档索引行与设计说明描述更新,任务规划目录 7→6 文件
- docs/产品需求/产品需求设计文档.md:串口通道现状口径同步——第 11/12/13 条
  已交付(4208 B 应答/状态灯掩码/键上行/双出口并发)、LOGO 下发出局不实现、
  帧长实测固化、调试日志口 UART5/RS232#1(§4.2/§5.16/§6.6/§9.12/§9.13)
- docs/子系统设计/控制台单写者日志设计说明.md v1.8:全文端口口径由
  USART1 同步为 UART5/RS232#1(CON_UART* 单点宏),ORE 机理标注跨端口
  同因,时间线补端口迁移行;头部修订指针改为实际章节名

说明:本任务经用户显式授权在任务树内编辑 docs/任务规划/ 与产品需求基线
(铁律「单写者」豁免一次);主树合并后统一跑全量 check_doc_links --all。
2026-10-30 09:22:44 +08:00

278 lines
12 KiB
C
Raw Permalink 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「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;
}