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