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 %。
This commit is contained in:
2026-10-29 20:59:36 +08:00
parent e5b4182a9f
commit dab0d8ba17
15 changed files with 1263 additions and 132 deletions
+99 -14
View File
@@ -1,24 +1,29 @@
/*
* 模块名称:屏板链路传输层(USART1 中断接收)
* 模块功能:把屏板(DTU-LCD)发来的 6 B 帧 A 从 USART1 上稳定收下来:
* IDLE 空闲中断断帧、帧 A 校验、错误中断里清 ORE 并重挂接收、统计上报。
* **本层不发送、不组帧**(帧 B 的组帧与 TX DMA 起发属批 2,见
* docs/任务规划/屏板链路.md §3 的 2-1 / 2-2)。
* 适用平台:STM32F4 系列(依赖 HAL UART 的 ReceiveToIdle_IT)
* 模块名称:屏板链路传输层(USART1 中断收发)
* 模块功能:把主机与屏板之间那条 700 kbaud 链路完整地管起来:
* - 收:IDLE 空闲中断断帧、6 B 帧 A 校验、错误中断里清 ORE 并重挂接收;
* - 发:任务侧组一帧 B(4208 B)进静态缓冲,中断侧收到召唤帧**直接点 TX DMA**,
* TxCplt 里收尾(决策三:不排队、不攒批、不在任务里逐字节轮询);
* - 按键上行:中断只入队,注键由任务侧取走(**注键不能在中断里做**);
* - 链路监视:连续 >=10 s 无召唤帧才判离线;CRC 错帧静默丢弃并计数。
* 协议字节级编解码(CRC / 帧 A 解析 / 帧 B 组帧)在 sb_proto,本层只做搬运与时序。
* 适用平台:STM32F4 系列(依赖 HAL UART 的 ReceiveToIdle_IT 与 Transmit_DMA)
* 作者:王建锋
* 创建日期:2026-10-29
* 修改记录:
* 2026-10-29 王建锋 创建初始版本(批 1 的 1-2 / 1-3:700 kbaud + 收中断)
* 2026-10-29 王建锋 批 2:帧 B 组帧入口、中断起发、TxCplt 收尾、按键队列、离线判定
*
* 为什么单独一层、不并进 sb_proto:
* sb_proto 是纯逻辑(CRC / 组帧 / 解析),能在 test/host 里原样 include 跑单测;
* 本文件依赖 HAL 句柄与中断上下文,属 MCU 专用。两者分开,前者才留得住"零桩单测"。
*
* 全局回调的落点(改之前先读):
* HAL 的 HAL_UARTEx_RxEventCallback / HAL_UART_ErrorCallback 是**全局单例**,
* 本工程唯一实现处是 Drivers/BSP/RS485/rs485.c(USE_HAL_UART_REGISTER_CALLBACKS = 0,
* 没法按句柄注册回调)。那里只做一行转发,**按句柄过滤在本文件里做**——
* 于是 rs485.c 不必知道屏板链路用的是哪个 huart,新增通道也不用改它的判据。
* HAL 的 HAL_UARTEx_RxEventCallback / HAL_UART_ErrorCallback / HAL_UART_TxCpltCallback
* 是**全局单例**,本工程唯一实现处是 Drivers/BSP/RS485/rs485.c
* (USE_HAL_UART_REGISTER_CALLBACKS = 0,没法按句柄注册回调)。那里只做一行转发,
* **按句柄过滤在本文件里做**——于是 rs485.c 不必知道屏板链路用的是哪个 huart,
* 新增通道也不用改它的判据。
*/
#ifndef SB_LINK_H
@@ -60,6 +65,27 @@ extern "C" {
*/
#define SB_LINK_REPORT_MS (5000U)
/*
* 判「屏板离线」的静默时长(ms)。**不能按 500 ms 判**:屏板固件约 5000 s 会自复位一次,
* 复位期间约 1 s 不召唤;再叠上主机的重挂/噪声恢复,短窗口会把"屏板活着但歇了口气"
* 误判成离线,然后在下一次召唤时又"恢复"——现场看到的是离线标志反复跳。
* 10 s 的取法:比 1 s 自复位空窗大一个数量级,又远小于"人发现屏板黑屏"的时间。
* 出处 docs/任务规划/屏板链路.md §3 的 2-5。
*/
#define SB_LINK_OFFLINE_MS (10000U)
/*
* 按键队列深度(帧 → 注键的交接)。**实际可存 LEN-1 = 3 个**(环形队列留一格判满)。
* 中断侧只入队、任务侧出队,两侧都是单写单读;3 个够——屏板 7 个物理键,
* 人手连按的最快间隔也远大于 uiTask 的 10 ms 节拍。满了丢最**新**的一帧并计数
* (丢新的比丢旧的好:旧键已经在路上,新键按一下还会有下一次召唤)。
*/
#define SB_LINK_KEY_QUEUE_LEN (4U)
/* 帧 B 的线上长度(= SB_FRAME_B_LEN)。单独起个名字是为了让调用方按"链路帧"理解,
* 不必知道协议层的命名;两者恒等由下面一行保证。 */
#define SB_LINK_FRAME_LEN SB_FRAME_B_LEN
/* ============================================================================
* 二、链路统计
* ==========================================================================*/
@@ -86,6 +112,21 @@ typedef struct
uint16_t last_size; /* 最近一次 IDLE 事件的字节数 */
uint8_t last_cmd; /* 最近一帧有效帧的命令类型(SB_CMD_*) */
uint8_t last_key; /* 最近一帧有效帧的键值(无键为 0) */
/* ---- 批 2:发送侧(帧 B 组帧 + TX DMA 起发 + 收尾) ---- */
uint32_t tx_kick; /* 起发成功次数(每次有效召唤帧 +1) */
uint32_t tx_done; /* TxCplt 次数(链路正常时应恒等于 tx_kick) */
uint32_t tx_busy_drop; /* 召唤到了但上一帧还在发 → 本次不答(屏板 280 ms 后会再问) */
uint32_t tx_kick_fail; /* HAL_UART_Transmit_DMA 返回非 OK */
uint32_t tx_abort; /* 错误中断发现 TX 已不在发送态 → 主动清 busy(防残留) */
uint32_t frame_build; /* 组帧次数(只有内容/掩码变化才 +1) */
uint32_t frame_bad; /* 组完自校验失败次数(正常恒 0) */
uint32_t frame_us; /* 最近一次组帧耗时(µs,DWT 实测) */
uint32_t frame_us_max; /* 组帧耗时峰值(µs)——2-1 的"≤0.3 ms"就看这一列 */
/* ---- 批 2:按键上行与链路监视 ---- */
uint32_t key_drop; /* 按键队列满导致的丢弃次数 */
uint8_t offline; /* 1 = 连续 >= SB_LINK_OFFLINE_MS 无有效召唤帧 */
} sb_link_stats_t;
/*
@@ -138,9 +179,11 @@ bsp_err_t sb_link_receive_start(void);
* size - 本次断出来的字节数
* 返回值:无
* 限定条件:**中断上下文**,只做 CRC 与计数(4 B 校验约 1 µs),不得打日志、不得阻塞。
* 函数说明:长度不是 6 就直接记 rx_len_err 并丢弃;是 6 才解析。解析结论只累计计数
* 与"最近一帧",**不做任何业务动作**——注键、刷新都由任务侧取走统计后再做
* (批 2 的 2-3:注键不能在中断里做)。末尾无条件重挂接收。
* 函数说明:长度不是 6 就直接记 rx_len_err 并丢弃;是 6 才解析。解析通过后**直接点 TX DMA**
* 把已经组好的帧 B 发出去(决策三:起发必须在中断里,不能在任务里轮询——屏板按
* "总线空闲 1 个字符"断帧,一次几十 µs 的任务抢占就能把帧切断,而屏板不报超时)。
* 业务动作仍然不做:按键只入队,注键由任务侧 sb_link_key_take() 取走后做。
* 末尾无条件重挂接收。
*/
void sb_link_on_rx_event(UART_HandleTypeDef *p_huart, uint16_t size);
@@ -163,10 +206,52 @@ void sb_link_on_error(UART_HandleTypeDef *p_huart);
* 限定条件:任务上下文调用,**不可在中断里调**(内部会打日志)。
* 函数说明:每 SB_LINK_REPORT_MS 检查一次,只有计数有变化才打一行累计值。
* 第一帧成功收到时立刻打一行(不等周期),台架刚上电就能看到链路通了。
* 离线判定(连续 >=10 s 无召唤帧)属批 2 的 2-5,这里只上报不判定。
* 同时维护"屏板离线"标志:连续 >= SB_LINK_OFFLINE_MS 没有有效召唤帧才置 1,
* 恢复时清 0 并打一行。判定只在这里做(任务侧),中断侧只自增计数。
*/
void sb_link_poll(uint32_t now_ms);
/*
* 函数功能:组一帧帧 B(任务侧,内容/掩码变化时才真组)
* 入口参数:p_live_bmp - 当前 1bpp 显存首地址(>= SB_FRAME_B_BMP_LEN 字节,非空)
* led_mask - 状态灯 32 位掩码(小端填进 info[0..3];批 3 才填真值,此处传 0)
* 返回值:BSP_OK 帧已就绪(本次没变,或已重新组好)
* BSP_ERR_PARAM p_live_bmp 为空
* BSP_ERR_STATE 尚未 sb_link_init
* BSP_ERR_BUSY TX DMA 正在发这一帧,本拍跳过(TxCplt 之后的下一拍会补上)
* BSP_ERR_CRC 组完自校验失败(已计数 frame_bad,帧不会发出去)
* 限定条件:**任务上下文**,且必须与 ui_port_step() 同一个任务(显存是单写者)。
* 单帧约 0.17 ms(3200 B 拷贝 + 4206 B CRC),只在变化时付一次。
* 函数说明:1. **不持有显存副本**——直接拿 p_live_bmp 与帧缓冲里的点阵段 memcmp,
* 没变就立刻返回(约 20 µs)。省掉 3200 B 影子缓冲,也省掉每拍重算 CRC。
* 2. **TX 占用期间绝不写帧缓冲**(那会让 DMA 发出半新半旧的"脏帧"):
* 忙就直接返回 BSP_ERR_BUSY,下一拍再来——屏板 280 ms 一轮,丢一帧无害。
* 3. 组帧后立刻 sb_frame_b_verify() 自校验;不通过就不置 ready,帧不会被发出去。
*/
bsp_err_t sb_link_frame_update(const uint8_t *p_live_bmp, uint32_t led_mask);
/*
* 函数功能:取走一个待注的屏板按键(任务侧)
* 入口参数:无
* 返回值:屏板 key 字节(1~10,语义见 docs/任务规划/屏板链路.md §4.3);
* 0 = 队列空(屏板自己的 0 就是"无键",两者同义,不必再分)
* 限定条件:任务上下文;中断侧在入队,本函数出队。单生产者单消费者,不需要关中断。
* 函数说明:只吐**屏板原始键号**,不映射 UI_KEY_*——键号是协议语义,UI 键码是 UI 语义,
* 两者在 uiTask 里汇合(那一层才知道当前是哪个页面在接键)。
* key == 9/10(RESET/FACTORY)照原样吐出,丢不丢由调用方按决策四裁定。
*/
uint8_t sb_link_key_take(void);
/*
* 函数功能:TX 完成入口(由 HAL_UART_TxCpltCallback 转发进来)
* 入口参数:p_huart - 触发回调的句柄(非本链路的会被直接忽略)
* 返回值:无
* 限定条件:**中断上下文**(DMA2_Stream7 的 TC 中断里),不得打日志、不得阻塞。
* 函数说明:只清 busy 标志并计数。**不做组帧**——组帧要 0.17 ms,放进中断会拖累
* 整个系统的中断响应;有变化就让任务侧下一拍补(frame_update 会看到差异)。
*/
void sb_link_on_tx_cplt(UART_HandleTypeDef *p_huart);
/*
* 函数功能:取一份链路统计快照
* 入口参数:p_out - 输出缓冲(非空)