Files
STM32F4-Base/Drivers/BSP/ScreenLink/sb_link.h
T
wandering1 0b84c43d2e feat(屏板): 状态灯位映射 sb_led_map + 104 RUN 观测口(批 3);附 4-1 并发压测脚本
批 3(LOGO 按用户裁定出局,不实现、sb_proto 的 LOGO 组帧挂待删标注):
- 新增 App/algo/sb_led_map.c/h:业务状态 → 屏板 LED 位掩码的唯一映射表
  (纯逻辑,主机单测可跑)。ID1 运行心跳 500ms 半周期闪(与主板 LED 同拍)、
  ID2 保护动作、ID3 104 链路 RUN、ID4 采样电平故障(不选 sticky——灯要能灭)、
  ID5 保护启动 picked;bit0 与 ID 6~19 恒 0。这张表参考源码里没有,属自定,
  出处 docs/产品需求/产品需求设计文档.md §5.16.3 ①。
- iec104_serv 新增 iec104_serv_link_run()(透传 link 状态机,不另存镜像量)。
- ui_task.c 帧组装块每拍现采四路状态源(均为任意上下文可读观测口)折算后
  传 sb_link_frame_update——替换批 2 的恒 0 占位;掩码变了帧就重组,
  复用 busy/dirty 机制,无新通路。
- 主机单测 test/host/sb_led_map 17/17(位语义/恒零位/闪烁相位/tick 回绕/
  NULL 契约);注册豁免走 SIM_EXEMPT(模拟器无屏板串口)。

批 4 的 4-1 双通道并发回归(真屏板在线实测):
- 新增 test/host/remolcd_pull_test.py:RemoDispBus 拉帧压测客户端
  (INIT→LCDMEM 10 Hz、逐帧 XOR+长度校验、往返延迟统计、--dump PBM)。
- 实测:网络侧两轮 2200 拉帧零错零超时(min 35/avg 63/max 131 ms);
  屏板侧 60 s 隔离窗口 rx +233 全收、drop 零新增、bad=0、off=0、无
  OFFLINE——两侧互不拖。观察项 len=7(断帧尺寸≠6,累计 7 次)出现窗口
  无法归因压测、隔离窗口零新增、有效帧零损失,已挂设计说明 §5.7 待查。

附带:sb_link.h frame_update 注释回填实测耗时(509/688 µs 取代旧估算)。
Keil 0 错误 0 警告;ci_local 全绿(SKIP=nested-comment,主树脚本另查 0 新发);
Code=239,064 B / RW_IRAM1 65.5 % / RW_CCM 87.9 %;覆盖率 90.1 % / 44 套件。
2026-10-30 00:39:57 +08:00

271 lines
16 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.
/*
* 模块名称:屏板链路传输层(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 / HAL_UART_TxCpltCallback
* 是**全局单例**,本工程唯一实现处是 Drivers/BSP/RS485/rs485.c
* (USE_HAL_UART_REGISTER_CALLBACKS = 0,没法按句柄注册回调)。那里只做一行转发,
* **按句柄过滤在本文件里做**——于是 rs485.c 不必知道屏板链路用的是哪个 huart,
* 新增通道也不用改它的判据。
*/
#ifndef SB_LINK_H
#define SB_LINK_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include "main.h" /* UART_HandleTypeDef / USART_TypeDef / HAL_StatusTypeDef */
#include "bsp_err.h" /* bsp_err_t / BSP_OK / BSP_ERR_*:对外只报统一错误码 */
#include "sb_proto.h" /* SB_FRAME_A_LEN / sb_frame_a_t / sb_frame_a_parse */
/* ============================================================================
* 一、链路参数
* ==========================================================================*/
/*
* 串口参数:700000 8N1、16 倍过采样。
* 实际写入发生在 Src/usart.c 的 `USER CODE BEGIN USART1_Init 2`(CubeMX 的 .ioc
* **不保存任何 UART 的波特率**,GUI 里改也不落盘,所以只能在 USER CODE 区覆盖)。
* 理论值:APB2 84 MHz / (16 x 700 kHz) = 7.5 → BRR = 0x0078,误差 0 %。
*/
#define SB_LINK_BAUD (700000U)
/*
* 接收缓冲:16 B 足够。
* 屏板只发 6 B 的帧 A(周期召唤 / 按键召唤),16 B 留出一次噪声冲击的余量。
* ⚠ 不要照抄屏板侧的 4208 B——那是帧 B 的长度,方向相反(主机发、屏板收),
* 放进接收侧白占 SRAM(判据见 docs/任务规划/屏板链路.md §1.3)。
*/
#define SB_LINK_RX_BUF_SIZE (16U)
/*
* 统计上报周期(ms)。只在计数有变化时才打一行,链路空闲不刷屏;
* 台架验收靠的就是这一行里的累计值(判据见 docs/任务规划/屏板链路.md §3 的 1-3)。
*/
#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
/* ============================================================================
* 二、链路统计
* ==========================================================================*/
/*
* 全部字段都是**累计值**,由中断侧自增、任务侧只读(读出来的是一份快照)。
* 分门别类记而不只记"错了多少次",是因为台架上这几类错指向完全不同的原因:
* · rx_crc_err —— 波特率/接线问题(帧收到了但字节错位),最常见的真故障;
* · rx_len_err —— 断帧判据或噪声冲击(收到的不是 6 B),也可能是一次噪声把两帧粘在一起;
* · rx_fmt_err —— 地址/功能码/命令字不符(帧完整但内容不是给本机的);
* · err_irq —— ORE/NE/FE/PE 触发过错误中断,即线上真的有噪声或溢出。
*/
typedef struct
{
uint32_t rx_events; /* IDLE 事件总数(每来一包 +1,含错帧) */
uint32_t rx_ok; /* 帧 A 校验通过(含 cmd = 按键帧) */
uint32_t rx_crc_err; /* CRC 不符 */
uint32_t rx_len_err; /* 长度不是 SB_FRAME_A_LEN */
uint32_t rx_fmt_err; /* 地址/功能码/命令字不符 */
uint32_t key_events; /* 其中 cmd == SB_CMD_KEY 的帧数(按键召唤,2-3 的消费源) */
uint32_t err_irq; /* 错误中断次数(ORE/NE/FE/PE) */
uint32_t err_bits; /* 错误中断里见过哪些位(HAL_UART_ERROR_* 的并集,见下) */
uint32_t rearm_fail; /* 重挂接收失败次数(正常应恒 0,非 0 说明链路已静默) */
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;
/*
* err_bits 的位定义 = HAL 的 HAL_UART_ERROR_* 原值(PE 0x01 / NE 0x02 / FE 0x04 / ORE 0x08)。
* 直接照抄 HAL 的值而不是自己另编一套:这一列的唯一用途是**判断物理层哪里不对**,
* 与 HAL 手册对照着看最省事。经验判读:
* · 只有 ORE(0x08) → 处理不过来(本链路 6 B/280 ms 不该出现,出现了就是重挂漏了);
* · NE(0x02)为主 → 线上有毛刺(线太长、无屏蔽、共地不良);
* · FE(0x04)为主 → 波特率对不上(对端不是 700000,或分频误差过大);
* · 全是 NE/FE 但 rx_ok 照样涨 → 数据没坏、只是采样点被判噪声,属"能用的告警",别当故障。
*/
/* ============================================================================
* 三、公开接口
* ==========================================================================*/
/*
* 函数功能:初始化屏板链路并挂上第一轮接收
* 入口参数:p_huart - 已由 CubeMX 初始化完成的 USART1 句柄(非空)
* 返回值:BSP_OK 已挂上接收,后续每包都会进 sb_link_on_rx_event
* BSP_ERR_PARAM 句柄为空
* BSP_ERR_STATE 句柄的波特率不是 SB_LINK_BAUD(Src/usart.c 的 USER CODE 覆盖丢了)
* BSP_ERR_BUSY 首次挂接收失败(HAL 侧忙,通常是上一轮没收完)
* BSP_ERR_IO 首次挂接收失败(HAL 侧状态错)
* 限定条件:须在 osKernelStart 之前或任一任务上下文调用;本函数不清任何外设。
* 函数说明:波特率**不由本函数设置**——那一步在 Src/usart.c 的 USER CODE 区
* (CubeMX 重生成只保护那里)。这里只做一次交叉核对:波特率对不上就报
* BSP_ERR_STATE 并打一行 ERR,免得"115200 收 700 kbaud"这种静默错配
* 被误判成接线或协议问题。
*/
bsp_err_t sb_link_init(UART_HandleTypeDef *p_huart);
/*
* 函数功能:重新挂上一轮 IDLE 中断接收
* 入口参数:无
* 返回值:BSP_OK 挂上了
* BSP_ERR_STATE 尚未 sb_link_init
* BSP_ERR_BUSY HAL 侧忙(上一轮接收未结束)
* BSP_ERR_IO HAL 侧状态错
* 限定条件:可在中断上下文调用(rx / error 两个回调都靠它续命);不可阻塞。
* 函数说明:HAL 的 ReceiveToIdle_IT 是**一次性**的:一次 IDLE 事件处理完就把
* RxState 打回 READY 并关掉 RXNEIE/IDLEIE。所以每个回调末尾都必须重挂,
* 漏一次链路就永久静默(表象与"屏板没上电"一模一样)。
*/
bsp_err_t sb_link_receive_start(void);
/*
* 函数功能:IDLE 事件入口(由 HAL_UARTEx_RxEventCallback 转发进来)
* 入口参数:p_huart - 触发事件的句柄(非本链路的会被直接忽略)
* size - 本次断出来的字节数
* 返回值:无
* 限定条件:**中断上下文**,只做 CRC 与计数(4 B 校验约 1 µs),不得打日志、不得阻塞。
* 函数说明:长度不是 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);
/*
* 函数功能:错误事件入口(由 HAL_UART_ErrorCallback 转发进来)
* 入口参数:p_huart - 触发事件的句柄(非本链路的会被直接忽略)
* 返回值:无
* 限定条件:**中断上下文**;不得打日志、不得阻塞。
* 函数说明:先读 SR 再读 DR 清掉 ORE/NE/FE/PE(RM0090 §30.6.1 的清除序列),再重挂接收。
* ⚠ 这一步不是可选的:HAL 在阻塞型错误里已经把 RxState 打回 READY 并关了
* 接收中断,**而它自己不清 ORE**。ORE 一旦锁存,接收就再也进不来 →
* 一次噪声就把链路永久打死,而现象与"屏板没上电"完全一样。
*/
void sb_link_on_error(UART_HandleTypeDef *p_huart);
/*
* 函数功能:链路统计上报(任务侧周期调用)
* 入口参数:now_ms - 当前毫秒时基(调用方给,便于主机单测;MCU 侧传 HAL_GetTick())
* 返回值:无
* 限定条件:任务上下文调用,**不可在中断里调**(内部会打日志)。
* 函数说明:每 SB_LINK_REPORT_MS 检查一次,只有计数有变化才打一行累计值。
* 第一帧成功收到时立刻打一行(不等周期),台架刚上电就能看到链路通了。
* 同时维护"屏板离线"标志:连续 >= 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];位的语义见
* App/algo/sb_led_map.h——位变了帧就重组,灯随下一轮召唤出去)
* 返回值: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.5 ms(典型)/ 0.8 ms(峰值),没变只付 memcmp(≈20 µs);
* 判据口径见 docs/任务规划/屏板链路.md §3 的 2-1。
* 函数说明: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 - 输出缓冲(非空)
* 返回值:无(p_out 为空时直接返回,不改任何东西)
* 限定条件:任一上下文可调;中断侧在自增,快照不保证同一时刻一致(只用于观测与计数)。
* 函数说明:逐字段拷出,避免把 volatile 的内部结构整体赋给调用方。
*/
void sb_link_get_stats(sb_link_stats_t *p_out);
#ifdef __cplusplus
}
#endif
#endif /* SB_LINK_H */