/* * 模块名称:屏板链路传输层(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 #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; }