Files
STM32F4-Base/Drivers/BSP/ScreenLink/sb_link.c
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

317 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.
/*
* 模块名称:屏板链路传输层(USART1 中断接收)
* 模块功能:IDLE 空闲中断断帧收 6 B 帧 A、帧 A 校验、错误中断里清 ORE 并重挂接收、
* 累计统计与周期上报。发送侧(帧 B 组帧 + TX DMA 起发)属批 2,不在本文件。
* 适用平台:STM32F4 系列
* 作者:王建锋
* 创建日期:2026-10-29
* 修改记录:
* 2026-10-29 王建锋 创建初始版本(批 1 的 1-2 / 1-3)
*
* 三个容易踩的点(改之前先读,都已经在台架上被别的东西咬过一次):
*
* ① **每个回调末尾都必须重挂接收**。HAL_UARTEx_ReceiveToIdle_IT 是一次性的:
* 一次 IDLE 事件处理完,HAL 就把 RxState 打回 READY 并关掉 RXNEIE/IDLEIE
* (stm32f4xx_hal_uart.c 的 UART_IRQHandler 里 IDLE 那一支)。漏挂一次链路就
* 永久静默,而现象与"屏板没上电"完全一样——没有报错、没有计数。
*
* ② **错误回调里必须自己清 ORE**。HAL 在阻塞型错误里已经把接收停掉并打回 READY,
* 但它不清 ORE(RM0090 §30.6.1:先读 SR 再读 DR)。ORE 一旦锁存,RXNE 不再置位
* → 接收永远进不来。这就是"拔插一次 TX 线之后屏板再也收不到"的真因。
*
* ③ **本文件只按句柄过滤,不做业务动作**。全局回调(HAL_UARTEx_RxEventCallback /
* HAL_UART_ErrorCallback)的唯一实现处在 Drivers/BSP/RS485/rs485.c,那里只做一行
* 转发;过滤在这里做,于是新增通道不必去改 rs485.c 的判据。业务动作(注键、
* 刷新画面)全部留给任务侧——注键不能在中断里做(docs/任务规划/屏板链路.md 决策三)。
*/
#define DBG_TAG "[SBLINK]"
#include "dbg_log.h"
#include <string.h>
#include "sb_link.h"
/* ======================== 内部状态 ======================== */
/* 绑定的 USART1 句柄;NULL = 未初始化(所有入口据此直接返回) */
static UART_HandleTypeDef *s_huart;
/* 接收缓冲:IDLE 中断的落地目标,长度 SB_LINK_RX_BUF_SIZE(16 B) */
static uint8_t s_rx_buf[SB_LINK_RX_BUF_SIZE];
/* 累计统计:中断侧自增、任务侧只读,故 volatile */
static volatile sb_link_stats_t s_stats;
/* 上报用:上次上报时刻与上次上报时的计数(用来判"有没有变化") */
static uint32_t s_report_ms;
static uint32_t s_report_events;
static uint32_t s_report_err_irq;
static uint32_t s_report_rearm_fail;
static uint8_t s_first_frame_logged;
/* ======================== 内部函数 ======================== */
/*
* 函数功能:把 HAL 的返回状态映射成项目统一的 bsp_err_t
* 入口参数:status - HAL_* 返回的状态
* 返回值:对应的 bsp_err_t;HAL_BUSY 归 BSP_ERR_BUSY(可重试),HAL_OK 归 BSP_OK
* 限定条件:仅本文件内部使用
* 函数说明:与 rs485.c 里那个同名映射同构——HAL 的码**不在本模块之外出现**
* (bsp_err.h 的边界纪律),所以映射写成 static 而不是往公共头里塞。
*/
static bsp_err_t sb_err_from_hal(HAL_StatusTypeDef status)
{
switch (status)
{
case HAL_OK:
return BSP_OK;
case HAL_BUSY:
return BSP_ERR_BUSY;
case HAL_TIMEOUT:
return BSP_ERR_TIMEOUT;
default:
return BSP_ERR_IO;
}
}
/* ======================== 公开函数实现 ======================== */
/*
* 函数功能:初始化屏板链路并挂上第一轮接收
* 入口参数:p_huart - 已由 CubeMX 初始化完成的 USART1 句柄(非空)
* 返回值:BSP_OK / BSP_ERR_PARAM / BSP_ERR_STATE / BSP_ERR_BUSY / BSP_ERR_IO(见头文件)
* 限定条件:须在 osKernelStart 之前或任一任务上下文调用
* 函数说明:波特率由 Src/usart.c 的 USER CODE 区设置,本函数只做一次交叉核对——
* 对不上就报 BSP_ERR_STATE 并打一行 ERR,免得"115200 收 700 kbaud"这种
* 静默错配被误判成接线或协议问题(那种错配的表象是 100 % CRC 错帧)。
*/
bsp_err_t sb_link_init(UART_HandleTypeDef *p_huart)
{
if (p_huart == NULL)
{
return BSP_ERR_PARAM;
}
s_huart = p_huart;
(void)memset((void *)&s_stats, 0, sizeof(s_stats));
s_report_ms = 0U;
s_report_events = 0U;
s_report_err_irq = 0U;
s_report_rearm_fail = 0U;
s_first_frame_logged = 0U;
if (p_huart->Init.BaudRate != SB_LINK_BAUD)
{
/* 串口波特率被打回 CubeMX 的默认值 —— 即 Src/usart.c 的 USER CODE 覆盖丢了。
* ⚠ 日志字面量只写 ASCII:ARMCC 按本地代码页解多字节,中文字面量会报
* #870-D invalid multibyte character sequence(2026-10-29 实测)。 */
DBG_ERROR("baud=%lu expect=%lu (USER CODE override in usart.c lost)",
(unsigned long)p_huart->Init.BaudRate, (unsigned long)SB_LINK_BAUD);
return BSP_ERR_STATE;
}
return sb_link_receive_start();
}
/*
* 函数功能:重新挂上一轮 IDLE 中断接收
* 入口参数:无
* 返回值:BSP_OK / BSP_ERR_STATE / BSP_ERR_BUSY / BSP_ERR_IO(见头文件)
* 限定条件:可在中断上下文调用;不可阻塞
* 函数说明:失败只累计 rearm_fail 计数、不重试——在中断里重试等于把一次总线抖动
* 放大成中断风暴。真失败了靠任务侧的统计行暴露(rearm 列非 0 就是链路已死)。
*/
bsp_err_t sb_link_receive_start(void)
{
bsp_err_t ret;
if (s_huart == NULL)
{
return BSP_ERR_STATE;
}
ret = sb_err_from_hal(HAL_UARTEx_ReceiveToIdle_IT(s_huart, s_rx_buf, SB_LINK_RX_BUF_SIZE));
if (ret != BSP_OK)
{
s_stats.rearm_fail++;
}
return ret;
}
/*
* 函数功能:IDLE 事件入口(由 HAL_UARTEx_RxEventCallback 转发进来)
* 入口参数:p_huart - 触发事件的句柄(非本链路的直接忽略)
* size - 本次断出来的字节数
* 返回值:无
* 限定条件:中断上下文;只做 CRC 与计数,不得打日志、不得阻塞
* 函数说明:先判长度再解析——长度不是 6 时连 CRC 都不必算(屏板只发 6 B 帧 A)。
* 解析结论只落统计,业务动作由任务侧取走统计后做(注键不能在中断里做)。
*/
void sb_link_on_rx_event(UART_HandleTypeDef *p_huart, uint16_t size)
{
sb_frame_a_t frame;
if ((s_huart == NULL) || (p_huart != s_huart))
{
return;
}
s_stats.rx_events++;
s_stats.last_size = size;
if (size != SB_FRAME_A_LEN)
{
s_stats.rx_len_err++;
}
else
{
bsp_err_t ret;
(void)memset((void *)&frame, 0, sizeof(frame));
ret = sb_frame_a_parse(s_rx_buf, size, &frame);
if (ret == BSP_OK)
{
s_stats.rx_ok++;
s_stats.last_cmd = frame.cmd;
s_stats.last_key = frame.key;
if (frame.cmd == SB_CMD_KEY)
{
s_stats.key_events++;
}
}
else if (ret == BSP_ERR_CRC)
{
s_stats.rx_crc_err++;
}
else
{
s_stats.rx_fmt_err++;
}
}
(void)sb_link_receive_start();
}
/*
* 函数功能:错误事件入口(由 HAL_UART_ErrorCallback 转发进来)
* 入口参数:p_huart - 触发事件的句柄(非本链路的直接忽略)
* 返回值:无
* 限定条件:中断上下文;不得打日志、不得阻塞
* 函数说明:先读 SR 再读 DR 清 ORE/NE/FE/PE,再按需重挂接收。两个细节都不能省:
* 1. **清除顺序**:RM0090 §30.6.1 规定的序列就是"读 SR 后读 DR",只读 DR 清不掉 ORE。
* 2. **只有 HAL 真把接收停了才重挂**:HAL 把错误分成两类(stm32f4xx_hal_uart.c 的
* UART_IRQHandler)——
* · ORE 是**阻塞型**:HAL 先 UART_EndRxTransfer(RxState 打回 READY、关接收中断)
* 再回调 → 必须重挂,否则永久静默;
* · NE/FE/PE 是**非阻塞型**:HAL 不动接收,RxState 仍是 BUSY_RX → 此时再调
* ReceiveToIdle_IT 只会拿到 HAL_BUSY,白记一次"重挂失败"。
* 2026-10-29 台架实测踩的正是这一条:每帧都伴随 NE/FE,于是 rearm_fail 跟着
* err_irq 一起涨(160/160),看着像链路坏了,其实一次都没真失败。
* 判据用 RxState 而不是 ErrorCode 的位:它直接回答"现在挂着接收没有",
* 不依赖 HAL 怎么分组错误位。
*/
void sb_link_on_error(UART_HandleTypeDef *p_huart)
{
if ((s_huart == NULL) || (p_huart != s_huart))
{
return;
}
s_stats.err_irq++;
s_stats.err_bits |= p_huart->ErrorCode;
/* STM32F4 通过读 SR + DR 清除 ORE/NE/FE/PE(与 rs485.c 的错误回调同一手法) */
(void)p_huart->Instance->SR;
(void)p_huart->Instance->DR;
/* 只有"接收已经停了"才需要重挂(见上面第 2 条) */
if (p_huart->RxState == HAL_UART_STATE_READY)
{
(void)sb_link_receive_start();
}
}
/*
* 函数功能:链路统计上报(任务侧周期调用)
* 入口参数:now_ms - 当前毫秒时基
* 返回值:无
* 限定条件:任务上下文调用(内部会打日志)
* 函数说明:每 SB_LINK_REPORT_MS 检查一次,计数有变化才打一行;第一帧成功收到时
* 立刻单独打一行(不等周期),台架刚上电就能确认"链路通了、长度对、CRC 过"。
*/
void sb_link_poll(uint32_t now_ms)
{
sb_link_stats_t st;
if (s_huart == NULL)
{
return;
}
sb_link_get_stats(&st);
/* 首帧单独报一次:台架刚上电时不必等一个上报周期才知道链路通没通 */
if ((s_first_frame_logged == 0U) && (st.rx_ok > 0U))
{
s_first_frame_logged = 1U;
DBG_INFO("first frame: size=%u cmd=%u key=%u",
(unsigned)st.last_size, (unsigned)st.last_cmd, (unsigned)st.last_key);
}
if ((uint32_t)(now_ms - s_report_ms) < SB_LINK_REPORT_MS)
{
return;
}
s_report_ms = now_ms;
/* 无变化不刷屏:链路安静时一行都不出,出的一行就是有事实发生 */
if ((st.rx_events == s_report_events) &&
(st.err_irq == s_report_err_irq) &&
(st.rearm_fail == s_report_rearm_fail))
{
return;
}
s_report_events = st.rx_events;
s_report_err_irq = st.err_irq;
s_report_rearm_fail = st.rearm_fail;
DBG_INFO("rx=%lu ok=%lu keys=%lu crc=%lu len=%lu fmt=%lu err=%lu bits=0x%02lX rearm=%lu size=%u key=%u",
(unsigned long)st.rx_events, (unsigned long)st.rx_ok,
(unsigned long)st.key_events,
(unsigned long)st.rx_crc_err, (unsigned long)st.rx_len_err,
(unsigned long)st.rx_fmt_err, (unsigned long)st.err_irq,
(unsigned long)st.err_bits, (unsigned long)st.rearm_fail,
(unsigned)st.last_size, (unsigned)st.last_key);
}
/*
* 函数功能:取一份链路统计快照
* 入口参数:p_out - 输出缓冲(非空)
* 返回值:无
* 限定条件:任一上下文可调;不保证同一时刻一致(中断侧仍在自增)
* 函数说明:逐字段拷出。内部结构是 volatile,整体赋值会把限定词丢掉(ARMCC 报错),
* 故显式逐字段写。
*/
void sb_link_get_stats(sb_link_stats_t *p_out)
{
if (p_out == NULL)
{
return;
}
p_out->rx_events = s_stats.rx_events;
p_out->rx_ok = s_stats.rx_ok;
p_out->rx_crc_err = s_stats.rx_crc_err;
p_out->rx_len_err = s_stats.rx_len_err;
p_out->rx_fmt_err = s_stats.rx_fmt_err;
p_out->key_events = s_stats.key_events;
p_out->err_irq = s_stats.err_irq;
p_out->err_bits = s_stats.err_bits;
p_out->rearm_fail = s_stats.rearm_fail;
p_out->last_size = s_stats.last_size;
p_out->last_cmd = s_stats.last_cmd;
p_out->last_key = s_stats.last_key;
}