Files
STM32F4-Base/Drivers/BSP/ScreenLink/sb_proto.c
T
wandering1 dab0d8ba17 feat(屏板): 帧 B 组帧 + 中断起发 TX DMA + 按键队列 + 离线判定(批 2)
批 2 五项全部落地(docs/任务规划/屏板链路.md §3):
- 2-1 sb_link_frame_update():uiTask 渲染后按需组帧,不留 3200 B 影子
  副本,直接拿活显存与帧内点阵段 memcmp;组完自校验,过了才置 ready。
  静态 4208 B 帧缓冲实测落 RW_IRAM1(0x20006d98),不在 CCM。
- 2-2 空闲中断校完 6 B 召唤帧直接点 TX DMA(sb_tx_kick),不排队不攒批;
  busy 期间再来的召唤丢帧并计数,绝不叠发(屏板收帧一次 IDLE 一包)。
- 2-3 按键走环形队列(中断入、任务出),uiTask 按 §4.3 表映射 UI_KEY_*
  后调 ui_port_queue_key;9/10(RESET/FACTORY)按决策四先丢弃并留痕。
- 2-4 HAL_UART_TxCpltCallback 在 rs485.c 加第三行转发(三个全局回调
  落点收一处,防日后重复定义);busy 冻结帧缓冲,错误中断按 gState
  收拾被打断的 TX,防残留 busy 把链路永久锁死。
- 2-5 连续 >=10 s 无有效召唤帧才判离线(不按 500 ms——屏板约 5000 s
  自复位有 1 s 空窗),恢复即清标志;CRC 错帧静默丢弃并计数。

实测:台架 40 s(PC 顶替屏板,--key-fixed 2 持续改画面)rx=143 ok=143
keys=143 crc=0 / tx=143 done=143 drop=0 abor=0 fb=144 bad=0;脚本一停
10 s 准点出 OFFLINE。组帧耗时 509 µs 典型 / 709 µs 峰值——原 0.3 ms
判据系估算,已按实测在规划文档改口为 1 ms 并给出压降路径(未做)。

附带:rs485_on_rx 补 CODE-RULE-09 文档块(本轮改 rs485.c 触发整文件
重扫被拦);CRC 循环计数改 32 位(消掉 ARMCC 每轮 UXTH);bench 脚本
加 --reply/--key-fixed/--dump,并写明 CH340 收不了 700 kbaud 连续流、
--reply 收不全 4208 B 不是固件问题。

主机单测 146/146 PASSED(+65);Keil 0 错误 0 警告;ci_local 全绿
(SKIP=nested-comment,worktree 限制,主树脚本另查 0 新发)。
Code=238,936 B / RW_IRAM1 65.5 % / RW_CCM 87.9 %。
2026-10-29 20:59:36 +08:00

278 lines
12 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 §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;
}