Files
STM32F4-Base/Drivers/BSP/ScreenLink/sb_link.h
T
wandering1 7c8669e52a feat(屏板): USART1 700 kbaud + 收中断传输层 sb_link(1-2/1-3)
批 1 的 1-2 / 1-3:把屏板(DTU-LCD)发来的 6 B 帧 A 在 USART1 上稳定收下来。

1-2 波特率:.ioc 不保存任何 UART 的波特率(只有
USART1.IPParameters=VirtualMode),GUI 里改也不落盘,所以只能在
Src/usart.c 的 USER CODE BEGIN USART1_Init 2 区覆盖 huart1.Init.BaudRate
后重调一次 HAL_UART_Init(第二次调用时 gState 已非 RESET,HAL 只重算
BRR、不再跑 MspInit,GPIO/DMA/NVIC 不会被重配)。开机日志实测
BRR=0x0078,与理论值 84 MHz/(16x700 kHz)=7.5 逐位相符,误差 0%。

1-3 收中断:新建 Drivers/BSP/ScreenLink/sb_link.c/h——16 B 缓冲 + IDLE
中断断帧、帧 A 校验、错误中断里按 RM0090 清 ORE/NE/FE/PE、周期统计上报。
HAL_UARTEx_RxEventCallback / HAL_UART_ErrorCallback 是全局单例(唯一实现
在 rs485.c),故只在那里各加一行转发,按句柄过滤放在 sb_link 内。

台架实测(PC + USB 转 TTL 顶替屏板,COM6@700000)撞出两个真问题:
- 错误回调里无条件重挂 -> 出现 err=160 / rearm=160 同步增长,看着像每帧
  都重挂失败。真因是 HAL 把 NE/FE/PE 归为非阻塞型(接收照跑、RxState 仍是
  BUSY_RX),此时 ReceiveToIdle_IT 只会返回 HAL_BUSY。改为仅当
  RxState == HAL_UART_STATE_READY(ORE 那类阻塞型)时才重挂。
- 日志字面量里带中文 -> ARMCC #870-D invalid multibyte character
  sequence。字面量一律纯 ASCII,中文只写注释。

验证:主机单测 81/81 PASSED(test/host/sb_link/,其中按 RM0090 的清除语义
把"必须 SR->DR 顺序才清得掉 ORE"钉成断言);Keil 0 错误 0 警告;台架
rx == 发送帧数、ok == rx、crc=0、len=0、size=6、keys 随按键帧递增;先用
错波特率灌 64 B 垃圾(len=31、bits=0x06)后自动恢复且 rearm=0。
新增 test/host/screenlink_bench.py 作为可复跑的屏板激励(--key / --glitch-baud)。
2026-10-29 20:13:18 +08:00

184 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.
/*
* 模块名称:屏板链路传输层(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)
* 作者:王建锋
* 创建日期:2026-10-29
* 修改记录:
* 2026-10-29 王建锋 创建初始版本(批 1 的 1-2 / 1-3:700 kbaud + 收中断)
*
* 为什么单独一层、不并进 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,新增通道也不用改它的判据。
*/
#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)
/* ============================================================================
* 二、链路统计
* ==========================================================================*/
/*
* 全部字段都是**累计值**,由中断侧自增、任务侧只读(读出来的是一份快照)。
* 分门别类记而不只记"错了多少次",是因为台架上这几类错指向完全不同的原因:
* · 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) */
} 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 才解析。解析结论只累计计数
* 与"最近一帧",**不做任何业务动作**——注键、刷新都由任务侧取走统计后再做
* (批 2 的 2-3:注键不能在中断里做)。末尾无条件重挂接收。
*/
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 检查一次,只有计数有变化才打一行累计值。
* 第一帧成功收到时立刻打一行(不等周期),台架刚上电就能看到链路通了。
* 离线判定(连续 >=10 s 无召唤帧)属批 2 的 2-5,这里只上报不判定。
*/
void sb_link_poll(uint32_t now_ms);
/*
* 函数功能:取一份链路统计快照
* 入口参数:p_out - 输出缓冲(非空)
* 返回值:无(p_out 为空时直接返回,不改任何东西)
* 限定条件:任一上下文可调;中断侧在自增,快照不保证同一时刻一致(只用于观测与计数)。
* 函数说明:逐字段拷出,避免把 volatile 的内部结构整体赋给调用方。
*/
void sb_link_get_stats(sb_link_stats_t *p_out);
#ifdef __cplusplus
}
#endif
#endif /* SB_LINK_H */