Files
STM32F4-Base/Drivers/BSP/ScreenLink/sb_link.h
T
wandering1 7708f12c10 fix(ui/screenlink): 按键应答扣到按键后画面,反馈典型 250-450 ms 降至 70-135 ms
按键召唤答的是按键前的旧帧:uiTask 还没消化键答案就发走了,新画面
只能等下一次周期召唤(280 ms/轮)才带得出——真机反馈 250-450 ms 的
主构成(根因账本 docs/子系统设计/屏板链路设计说明.md §7.1)。

改动(用户批准范围 A+B+C;屏板固件未动,决策三不变):
- sb_link:按键召唤改为只入队不起发;uiTask 同拍重绘并组帧成功后交
  凭据(sb_link_key_rendered),TIM3 1 kHz 的 sb_link_ms_tick 以 ≤1 ms
  放行新帧。140 ms 截止(SB_KEY_RESP_MAX_MS,+59.6 ms 线上 <205 ms
  屏板放弃线)保底发旧帧,"一召一答"契约不破;线忙时同旧丢单、
  不留扣件。起发两中断源(USART1 IDLE/TIM3)共用 sb_tx_kick,
  "判忙→占位"在 PRIMASK 临界区原子完成。
- ui_task:排空注键挪到组帧之前,注键→即派即绘→组帧同拍闭环;
  组帧返回 BSP_OK 才交重绘凭据(BUSY 拍帧还没反映按键)。
- ui_port:注键单槽改 8 深环(连按不再互相覆盖,满丢最旧+计数);
  新增 ui_port_service_keys:同拍 lv_indev_read + lv_refr_now,
  省掉 indev ≤20 ms + 刷新 ≤20 ms 两段等待。
- ui_menu_view:菜单移动选中只失效新旧两行带(黄金参考零像素变化),
  滚动/异常回退整框失效。
- simulator:窗口循环 sleep 扣除本轮已耗时(原先无条件睡满
  lv_timer_handler 返回值,叠加 Windows Sleep 15.6 ms 粒度放大观感)。
- 串口 TX 上报新增 dfok/dfold(凭据放行新帧数/超时保底旧帧数):
  dfold 连涨 = uiTask 被饿 ≥140 ms 的直接现场证据。

验证:test/host/sb_link 164/164(新增 18 条:不立刻起发/凭据放行/
140 ms 截止保底/忙时不留扣件/陈旧凭据不放行等边界);菜单 8 状态 +
内容页 66 张黄金像素 PASS;Keil @build 0 错误 0 警告(两次);
BOM 99 文件无漂移、代码规范、doc-links 本次文件零问题、cppcheck PASS。
文档:屏板规划文档已被主干收编进设计说明,延迟账本落
docs/子系统设计/屏板链路设计说明.md §7.1,并同步 §4 口径、§8 接口、
§9.1 单测台账 146→164、§10 坑账本补条。

RAM 台账(分支不跑 --write,并行分支提交约定):本笔盖戳行
RW_IRAM1 94,336 B (72.0%) / RW_CCM 57,376 B (87.9%),较上一台账行
+4,400 B——为距上次台账构建以来分支累计差(含批 3 状态灯等未盖戳
提交),非全部本笔引入;本次自身增量按对象计为 ui_port.o +12 B
(8 深键环+丢计数+indev 指针)与 sb_link/o 数十字节级。
gate: only mem-budget/mem-numbers pending lead ledger backfill
(合并落主干后 Lead 统一 --write 回填受管区)。
2026-10-30 09:50:31 +08:00

326 lines
20 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 里收尾(不排队、不攒批、不在任务里逐字节轮询);
* 例外(2026-10-30 按键延迟整治):**按键召唤延迟应答**——key≠0 的
* 召唤到达时当前帧必然还是"按键前"的内容(键还在中断队列里没被画),
* 立刻发出去等于让屏板白等一轮;改为扣住这一答,等新帧组好(任务侧
* 同拍完成)再由 1 kHz 节拍钩子在中断里放行,超时则发旧帧保底
* - 按键上行:中断只入队,注键由任务侧取走(**注键不能在中断里做**);
* - 链路监视:连续 >=10 s 无召唤帧才判离线;CRC 错帧静默丢弃并计数。
* 协议字节级编解码(CRC / 帧 A 解析 / 帧 B 组帧)在 sb_proto,本层只做搬运与时序。
* 适用平台:STM32F4 系列(依赖 HAL UART 的 ReceiveToIdle_IT 与 Transmit_DMA)
* 作者:王建锋
* 创建日期:2026-10-29
* 修改记录:
* 2026-10-29 王建锋 创建初始版本(700 kbaud + 收中断)
* 2026-10-29 王建锋 增加:帧 B 组帧入口、中断起发、TxCplt 收尾、按键队列、离线判定
* 2026-10-30 王建锋 按键延迟整治:按键召唤**延迟应答**——收到
* key≠0 的帧不再立刻把"按键前的旧帧"发走,而是扣住等
* 任务侧把按键画进新帧(上限 SB_KEY_RESP_MAX_MS),
* 一召唤一答与"起发必须在 IDLE 中断"两条约束都不变;
* sb_link_ms_tick() 挂在 1 kHz 采样节拍上驱动扣件超时
*
* 为什么单独一层、不并进 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 §2)。
*/
#define SB_LINK_RX_BUF_SIZE (16U)
/*
* 统计上报周期(ms)。只在计数有变化时才打一行,链路空闲不刷屏;
* 台架验收靠的就是这一行里的累计值(判据见 docs/子系统设计/屏板链路设计说明.md §9)。
*/
#define SB_LINK_REPORT_MS (5000U)
/*
* 判「屏板离线」的静默时长(ms)。**不能按 500 ms 判**:屏板固件约 5000 s 会自复位一次,
* 复位期间约 1 s 不召唤;再叠上主机的重挂/噪声恢复,短窗口会把"屏板活着但歇了口气"
* 误判成离线,然后在下一次召唤时又"恢复"——现场看到的是离线标志反复跳。
* 10 s 的取法:比 1 s 自复位空窗大一个数量级,又远小于"人发现屏板黑屏"的时间。
* 出处 docs/子系统设计/屏板链路设计说明.md §2 链路监视。
*/
#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
/*
* 按键召唤延迟应答的上限(ms,2026-10-30 按键延迟整治的核心参数)。
*
* 为什么要有这个窗:按键召唤到达的那一刻,键还躺在中断队列里没被画——立刻应答
* 发出去的是**按键前**的旧帧,屏板白等一轮,新画面要等下一次召唤(平均 ~140 ms、
* 最坏 280 ms)才带得出来。现在把按键召唤扣住:任务侧同拍完成"出键→即派即绘→
* 组帧"后交回凭据(sb_link_key_rendered),1 kHz 节拍钩子 ≤1 ms 内放行新帧。
* 实测路径预算:召唤落点→下一拍 ≤10 + 渲染地板 ≤62 + 组帧 ≤1 ≈ **73 ms**,
* 留了将近一倍余量给"召唤恰好落在数据拍/重绘中段"这类不利相位。
*
* 为什么是 140 而不是更大:扣住的窗从召唤时刻起算,到点必须发(旧帧)保"一召唤
* 一答";140 + 线上 59.6 ≈ 199.6 < **205**(屏板放弃计时——它带键重问的最早时刻),
* 超时答案也赶在下一次召唤之前发完,不与任何重问对撞。再大就要撞上 205 线。
* 出处:docs/子系统设计/屏板链路设计说明.md §7.1(205/280 两线)与本函数说明。
*/
#define SB_KEY_RESP_MAX_MS (140U)
/* ============================================================================
* 二、链路统计
* ==========================================================================*/
/*
* 全部字段都是**累计值**,由中断侧自增、任务侧只读(读出来的是一份快照)。
* 分门别类记而不只记"错了多少次",是因为台架上这几类错指向完全不同的原因:
* · 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 的帧数(按键召唤,按键队列的消费源) */
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) */
/* ---- 发送侧(帧 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),预算 ≤1 ms(docs/子系统设计/屏板链路设计说明.md §7) */
/* ---- 按键上行与链路监视 ---- */
uint32_t key_drop; /* 按键队列满导致的丢弃次数 */
/* ---- 2026-10-30 按键延迟整治:延迟应答的两类放行 ---- */
uint32_t defer_fresh; /* 等到新画面后放行(理想路径;正常按键应几乎全落这里) */
uint32_t defer_old; /* 扣到 SB_KEY_RESP_MAX_MS 仍未见新画面 → 发旧帧保底
* (uiTask 被饿住的直接证据;持续非 0 要先查任务卡点) */
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 的任务
* 抢占就能把帧切断,而屏板不报超时)。**按键召唤(cmd=KEY 且 key≠0)
* 例外:只入队、扣住不应答**(延迟应答,见 SB_KEY_RESP_MAX_MS 的账),
* 由 sb_link_ms_tick() 在新帧就绪或超时时放行——这一答必须走同一个
* 中断起发通道,绝不因为"等画面"而退化成任务态发送。
* 业务动作仍然不做:按键只入队,注键由任务侧 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 §7。
* 函数说明: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 §5);
* 0 = 队列空(屏板自己的 0 就是"无键",两者同义,不必再分)
* 限定条件:任务上下文;中断侧在入队,本函数出队。单生产者单消费者,不需要关中断。
* 函数说明:只吐**屏板原始键号**,不映射 UI_KEY_*——键号是协议语义,UI 键码是 UI 语义,
* 两者在 uiTask 里汇合(那一层才知道当前是哪个页面在接键)。
* key == 9/10(RESET/FACTORY)照原样吐出,丢不丢由调用方定(当前口径见
* docs/子系统设计/屏板链路设计说明.md §5)。
*/
uint8_t sb_link_key_take(void);
/*
* 函数功能:交"按键已画进画面、帧与活显存一致"的放行凭据(延迟应答用)
* 入口参数:无
* 返回值:无
* 限定条件:任务上下文(uiTask);本拍 sb_link_frame_update() 返回 BSP_OK 之后才准调
* 函数说明:按键召唤不再立刻应答(见 sb_link_on_rx_event),TIM3 的 sb_link_ms_tick()
* 看到本凭据才把"带新画面的那一答"放上线。BUSY 拍次不许交(画面还没进帧)。
*/
void sb_link_key_rendered(void);
/*
* 函数功能:按键召唤延迟应答的毫秒时钟(1 kHz 调用)
* 入口参数:无
* 返回值:无
* 限定条件:**中断上下文**(Src/main.c TIM3 分支),不得调内核 API / 打日志
* 函数说明:有扣件时——凭据已到就起发新帧(dfok+1),扣满 SB_KEY_RESP_MAX_MS
* 则发当前帧保底(dfold+1)。初始化前调用安全(直接返回)。
*/
void sb_link_ms_tick(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 */