Files
STM32F4-Base/Drivers/BSP/ScreenLink/sb_link.c
T
wandering1 dab0d8ba17 feat(屏板): 帧 B 组帧 + 中断起发 TX DMA + 按键队列 + 离线判定(批 2)
批 2 五项全部落地(docs/任务规划/屏板链路.md §3):
- 2-1 sb_link_frame_update():uiTask 渲染后按需组帧,不留 3200 B 影子
  副本,直接拿活显存与帧内点阵段 memcmp;组完自校验,过了才置 ready。
  静态 4208 B 帧缓冲实测落 RW_IRAM1(0x20006d98),不在 CCM。
- 2-2 空闲中断校完 6 B 召唤帧直接点 TX DMA(sb_tx_kick),不排队不攒批;
  busy 期间再来的召唤丢帧并计数,绝不叠发(屏板收帧一次 IDLE 一包)。
- 2-3 按键走环形队列(中断入、任务出),uiTask 按 §4.3 表映射 UI_KEY_*
  后调 ui_port_queue_key;9/10(RESET/FACTORY)按决策四先丢弃并留痕。
- 2-4 HAL_UART_TxCpltCallback 在 rs485.c 加第三行转发(三个全局回调
  落点收一处,防日后重复定义);busy 冻结帧缓冲,错误中断按 gState
  收拾被打断的 TX,防残留 busy 把链路永久锁死。
- 2-5 连续 >=10 s 无有效召唤帧才判离线(不按 500 ms——屏板约 5000 s
  自复位有 1 s 空窗),恢复即清标志;CRC 错帧静默丢弃并计数。

实测:台架 40 s(PC 顶替屏板,--key-fixed 2 持续改画面)rx=143 ok=143
keys=143 crc=0 / tx=143 done=143 drop=0 abor=0 fb=144 bad=0;脚本一停
10 s 准点出 OFFLINE。组帧耗时 509 µs 典型 / 709 µs 峰值——原 0.3 ms
判据系估算,已按实测在规划文档改口为 1 ms 并给出压降路径(未做)。

附带:rs485_on_rx 补 CODE-RULE-09 文档块(本轮改 rs485.c 触发整文件
重扫被拦);CRC 循环计数改 32 位(消掉 ARMCC 每轮 UXTH);bench 脚本
加 --reply/--key-fixed/--dump,并写明 CH340 收不了 700 kbaud 连续流、
--reply 收不全 4208 B 不是固件问题。

主机单测 146/146 PASSED(+65);Keil 0 错误 0 警告;ci_local 全绿
(SKIP=nested-comment,worktree 限制,主树脚本另查 0 新发)。
Code=238,936 B / RW_IRAM1 65.5 % / RW_CCM 87.9 %。
2026-10-29 20:59:36 +08:00

636 lines
25 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(4208 B)进静态缓冲、中断侧收到召唤帧直接点 TX DMA、
* TxCplt 里收尾;按键入队/出队;离线判定与统计上报。
* 适用平台:STM32F4 系列
* 作者:王建锋
* 创建日期:2026-10-29
* 修改记录:
* 2026-10-29 王建锋 创建初始版本(批 1 的 1-2 / 1-3)
* 2026-10-29 王建锋 批 2:组帧入口 + 中断起发 + TxCplt 收尾 + 按键队列 + 离线判定
*
* 四个容易踩的点(改之前先读,前三条都已在台架上被别的东西咬过一次):
*
* ① **每个回调末尾都必须重挂接收**。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 / HAL_UART_TxCpltCallback)的唯一实现处在
* Drivers/BSP/RS485/rs485.c,那里只做一行转发;过滤在这里做,于是新增通道不必去
* 改 rs485.c 的判据。业务动作(注键、刷新画面)全部留给任务侧——注键不能在中断里做。
*
* ④ **帧缓冲在 TX 期间只读**。4208 B 一次交给 DMA,DMA 读的是内存里的同一份数据;
* 任务侧若在中途改写点阵,发出去的就是"前半帧旧、后半帧新"的脏帧——CRC 照样对
* (组帧时会重算),屏板也照样收,只是画面撕裂,极难归因。故 s_tx_busy 期间
* sb_link_frame_update() 直接返回 BSP_ERR_BUSY,一个字节都不碰。
*/
#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];
/*
* 帧 B 的静态缓冲:4208 B,**落 RW_IRAM1(SRAM1)、不落 CCM**。
* 为什么不能放 CCM:DMA1/DMA2 物理上不可达 0x10000000,源地址落在那儿会静默发不出去
* (本仓已有先例:gd5f2gq5ue.c 的 DMA 可达窗口判据)。普通 static 在默认 scatter 下
* 就落在 RW_IRAM1,不要给它加 section 属性。`.map` 里可核对:本符号在 RW_IRAM1 段。
* 为什么不能放栈上:任务栈随 FreeRTOS 堆驻留 CCM(见 gd5f2gq5ue.h 的说明),同样不可达。
*/
static uint8_t s_frame[SB_LINK_FRAME_LEN];
/* 帧缓冲里装的是否是一帧组好且自校验通过的帧(0 = 还没组过,召唤来了也不发) */
static volatile uint8_t s_frame_ready;
/* 最近一次组帧用的 LED 掩码(掩码变化也要重组,即使点阵没变) */
static volatile uint32_t s_led_mask;
/* TX DMA 占用标志:中断侧置位、TxCplt/错误中断清零,任务侧读 */
static volatile uint8_t s_tx_busy;
/*
* 按键交接:中断侧单写 s_key_head、任务侧单读 s_key_tail。
* 两个下标都只由各自的写者推进,所以不需要临界区(单生产者单消费者环形队列)。
*/
static volatile uint8_t s_key_q[SB_LINK_KEY_QUEUE_LEN];
static volatile uint8_t s_key_head;
static volatile uint8_t s_key_tail;
/* 累计统计:中断侧自增、任务侧只读,故 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 uint32_t s_report_tx_kick;
static uint32_t s_report_frame_build;
static uint8_t s_first_frame_logged;
/* 离线判定:上次看到"有效召唤帧"的时刻与当时的计数基线。
* "是否已判离线"不另存变量——s_stats.offline 本身就是那个状态位(任务侧唯一写者)。 */
static uint32_t s_offline_base_ms;
static uint32_t s_offline_base_events;
/* 组帧耗时换算用:每微秒多少个周期(init 时从 SystemCoreClock 算一次,避免每次除) */
static uint32_t s_cyc_per_us;
/* ======================== 内部函数 ======================== */
/*
* 函数功能:读 DWT 周期计数器(µs 级计时用)
* 入口参数:无
* 返回值:当前 CYCCNT 值(未使能时恒为 0,此时组帧耗时统计恒 0,不影响功能)
* 限定条件:仅本文件内部使用
* 函数说明:用 DWT 而不是 HAL_GetTick()——组帧预算 0.3 ms,毫秒时基根本量不出来。
* 使能放在 sb_link_init() 里做(幂等),不依赖 console_log 是否先跑过。
*/
static uint32_t sb_cyc_now(void)
{
return DWT->CYCCNT;
}
/*
* 函数功能:把 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;
}
}
/*
* 函数功能:把一个屏板键号压进交接队列
* 入口参数:key - 屏板原始键号(非 0;调用方已过滤)
* 返回值:无
* 限定条件:**中断上下文**;队列满时丢本次并计数,不等待、不覆盖旧值
* 函数说明:丢新的而不是丢旧的——旧键已经在路上,而新键按下去还会有下一次召唤。
*/
static void sb_key_push(uint8_t key)
{
uint8_t next = (uint8_t)((s_key_head + 1U) % SB_LINK_KEY_QUEUE_LEN);
if (next == s_key_tail)
{
s_stats.key_drop++;
return;
}
s_key_q[s_key_head] = key;
s_key_head = next;
}
/*
* 函数功能:点一次 TX DMA 把当前帧发出去
* 入口参数:无
* 返回值:无
* 限定条件:**中断上下文**;调用前须已确认 s_frame_ready 且 !s_tx_busy
* 函数说明:起发失败只计数不清帧——下一轮召唤还会再点一次。busy 由 TxCplt 清,
* 不由本函数清(清了就等于允许叠发,屏板会把两帧粘成一帧撕裂)。
*/
static void sb_tx_kick(void)
{
s_tx_busy = 1U;
if (HAL_UART_Transmit_DMA(s_huart, s_frame, (uint16_t)SB_LINK_FRAME_LEN) != HAL_OK)
{
s_tx_busy = 0U;
s_stats.tx_kick_fail++;
return;
}
s_stats.tx_kick++;
}
/* ======================== 公开函数实现 ======================== */
/*
* 函数功能:初始化屏板链路并挂上第一轮接收
* 入口参数: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_report_tx_kick = 0U;
s_report_frame_build = 0U;
s_first_frame_logged = 0U;
s_frame_ready = 0U;
s_led_mask = 0U;
s_tx_busy = 0U;
s_key_head = 0U;
s_key_tail = 0U;
s_offline_base_ms = 0U;
s_offline_base_events = 0U;
/* 组帧耗时统计要用 DWT 的周期计数器。置使能位是幂等的,重复调用无副作用;
* **不重置 CYCCNT**——task_prof 也在用它,清掉会打乱别人正在进行的计时。 */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
s_cyc_per_us = SystemCoreClock / 1000000U;
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++;
/* 只入队,不注键:注键会走 ui_port 的输入队列,属业务动作(决策三)。
* key == 0 是"无键",屏板自己的语义就是没有事件,不必占队列。 */
if (frame.key != 0U)
{
sb_key_push(frame.key);
}
}
/* 决策三:起发就在这儿,不在任务里轮询。一召唤 → 一答 4208 B,绝不叠发。
* 上一帧还没发完(屏板自己超时重问过)就丢这一轮:多发一帧会让屏板把
* 两帧粘成一帧撕裂,而且它接收溢出后会永久收不到东西(见 §4.4 的警示)。 */
if (s_frame_ready == 0U)
{
s_stats.tx_busy_drop++; /* 帧还没组出来(刚上电)——与"上一帧未发完"同类记 */
}
else if (s_tx_busy != 0U)
{
s_stats.tx_busy_drop++;
}
else
{
sb_tx_kick();
}
}
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 怎么分组错误位。
* 3. **顺手收拾被打断的 TX**:错误中断不会走 TxCplt,busy 会永久挂着,
* 此后每次召唤都被判"上一帧还在发"→ 链路再也不发(rx 涨、tx 不动)。
* 判据用 gState,见下面代码里的说明。
*/
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;
/*
* TX 被错误打断时 HAL 会把 gState 打回 READY,但**不会**再调 TxCplt →
* s_tx_busy 会永久挂着,此后每一次召唤都被判"上一帧还在发",链路再也不发东西
* (表象:rx 一直涨、tx 不动)。判据用 gState 而不是自己另外记账:它直接回答
* "TX 现在还在不在发",不依赖 HAL 在错误分支里具体怎么分组。
* 这一条就是 2-4 判据"拔掉屏板后无残留 busy"的落点。
*/
if ((s_tx_busy != 0U) && (p_huart->gState != HAL_UART_STATE_BUSY_TX))
{
s_tx_busy = 0U;
s_stats.tx_abort++;
}
/* 只有"接收已经停了"才需要重挂(见上面第 2 条) */
if (p_huart->RxState == HAL_UART_STATE_READY)
{
(void)sb_link_receive_start();
}
}
/*
* 函数功能:链路统计上报与屏板离线判定(任务侧周期调用)
* 入口参数:now_ms - 当前毫秒时基
* 返回值:无
* 限定条件:任务上下文调用(内部会打日志)
* 函数说明:1. **离线判定每拍都做**,不受 5 s 上报周期门控——否则最坏要晚 5 s 才发现;
* 判据是"连续 >= SB_LINK_OFFLINE_MS 没有**有效**召唤帧",用计数是否前进来算,
* 不需要中断侧读时钟(中断里读时基只会多一处时序依赖)。
* 不能用 500 ms:屏板约 5000 s 自复位一次,复位期间约 1 s 不召唤。
* 2. 上报每 SB_LINK_REPORT_MS 一次,**只有计数有变化才打**;RX 与 TX 各一行,
* 分开是因为控制台单行上限 192 字符,塞一行会被截断(截断的行比不打印更误导)。
* 3. 第一帧成功收到时立刻单独打一行,台架刚上电就能确认"链路通了、长度对、CRC 过"。
* 4. ⚠ TX 行的字段名**不能含 fail**:`@test` 的 forbidden 模式是
* `FAIL;ERROR;HardFault;[ERR]` 且**大小写不敏感**,写 `fail=0` 会让每次启动
* 自检都判红(2026-10-29 实测,被自己的日志字段坑了一次)。故用 kerr / abor。
*/
void sb_link_poll(uint32_t now_ms)
{
sb_link_stats_t st;
uint8_t rx_changed;
uint8_t tx_changed;
if (s_huart == NULL)
{
return;
}
sb_link_get_stats(&st);
/* ---- 屏板离线 / 恢复(每拍判,不等上报周期) ---- */
if (st.rx_ok != s_offline_base_events)
{
if (s_stats.offline != 0U)
{
s_stats.offline = 0U;
DBG_INFO("screen board back online after %lu ms silent",
(unsigned long)(uint32_t)(now_ms - s_offline_base_ms));
}
s_offline_base_events = st.rx_ok;
s_offline_base_ms = now_ms;
}
else if ((s_stats.offline == 0U) &&
((uint32_t)(now_ms - s_offline_base_ms) >= SB_LINK_OFFLINE_MS))
{
s_stats.offline = 1U;
DBG_INFO("screen board OFFLINE: no valid summon for >= %u ms",
(unsigned)SB_LINK_OFFLINE_MS);
}
/* 首帧单独报一次:台架刚上电时不必等一个上报周期才知道链路通没通 */
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;
/* 无变化不刷屏:链路安静时一行都不出,出的一行就是有事实发生 */
rx_changed = (uint8_t)((st.rx_events != s_report_events) ||
(st.err_irq != s_report_err_irq) ||
(st.rearm_fail != s_report_rearm_fail));
tx_changed = (uint8_t)((st.tx_kick != s_report_tx_kick) ||
(st.frame_build != s_report_frame_build));
if ((rx_changed == 0U) && (tx_changed == 0U))
{
return;
}
if (rx_changed != 0U)
{
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);
}
if (tx_changed != 0U)
{
s_report_tx_kick = st.tx_kick;
s_report_frame_build = st.frame_build;
DBG_INFO("tx=%lu done=%lu drop=%lu kerr=%lu abor=%lu fb=%lu bad=%lu us=%lu max=%lu kq=%lu off=%u",
(unsigned long)st.tx_kick, (unsigned long)st.tx_done,
(unsigned long)st.tx_busy_drop, (unsigned long)st.tx_kick_fail,
(unsigned long)st.tx_abort, (unsigned long)st.frame_build,
(unsigned long)st.frame_bad, (unsigned long)st.frame_us,
(unsigned long)st.frame_us_max, (unsigned long)st.key_drop,
(unsigned)st.offline);
}
}
/*
* 函数功能:组一帧帧 B(任务侧,内容/掩码变化时才真组)
* 入口参数:p_live_bmp - 当前 1bpp 显存首地址(>= SB_FRAME_B_BMP_LEN 字节)
* led_mask - 状态灯 32 位掩码(小端填进 info[0..3])
* 返回值:BSP_OK / BSP_ERR_PARAM / BSP_ERR_STATE / BSP_ERR_BUSY / BSP_ERR_CRC(见头文件)
* 限定条件:任务上下文;必须与 ui_port_step() 同一个任务(显存是单写者)
* 函数说明:1. 变化判据 = 点阵 memcmp + 掩码比较。**不留 3200 B 影子缓冲**:
* 帧缓冲里的点阵段本身就是"上次组进去的那一份",直接拿它跟活显存比,
* 比不动就立刻返回(约 20 µs);变了才付 3200 B 拷贝 + 4206 B CRC(约 0.17 ms)。
* 代价是每拍一次 memcmp,换来省下 3200 B SRAM 和每拍 0.15 ms 的 CRC。
* 2. TX 占用期间直接返回 BUSY,一个字节都不写(脏帧的成因见文件头第 ④ 条)。
* 3. 组完立刻自校验;不过就清 ready,这一帧不会被发出去(宁可发旧帧也不发坏帧)。
*/
bsp_err_t sb_link_frame_update(const uint8_t *p_live_bmp, uint32_t led_mask)
{
bsp_err_t ret;
uint32_t t0;
uint32_t dt;
if (s_huart == NULL)
{
return BSP_ERR_STATE;
}
if (p_live_bmp == NULL)
{
return BSP_ERR_PARAM;
}
if (s_tx_busy != 0U)
{
return BSP_ERR_BUSY;
}
if ((s_frame_ready != 0U) && (led_mask == s_led_mask) &&
(memcmp((const void *)s_frame, (const void *)p_live_bmp,
(size_t)SB_FRAME_B_BMP_LEN) == 0))
{
return BSP_OK;
}
t0 = sb_cyc_now();
ret = sb_frame_b_build_image(s_frame, p_live_bmp, led_mask);
dt = sb_cyc_now() - t0;
if (ret != BSP_OK)
{
return ret;
}
s_led_mask = led_mask;
s_stats.frame_us = (s_cyc_per_us != 0U) ? (dt / s_cyc_per_us) : 0U;
if (s_stats.frame_us > s_stats.frame_us_max)
{
s_stats.frame_us_max = s_stats.frame_us;
}
s_stats.frame_build++;
ret = sb_frame_b_verify(s_frame, (uint16_t)SB_LINK_FRAME_LEN);
if (ret != BSP_OK)
{
s_stats.frame_bad++;
s_frame_ready = 0U;
return ret;
}
s_frame_ready = 1U;
return BSP_OK;
}
/*
* 函数功能:取走一个待注的屏板按键(任务侧)
* 入口参数:无
* 返回值:屏板 key 字节(1~10);0 = 队列空
* 限定条件:任务上下文;单生产者(中断)单消费者(任务),不需要关中断
* 函数说明:只吐屏板原始键号,映射 UI_KEY_* 由 uiTask 做——键号是协议语义,
* UI 键码是 UI 语义,那一层才知道当前是哪个页面在接键。
*/
uint8_t sb_link_key_take(void)
{
uint8_t key;
if (s_key_tail == s_key_head)
{
return 0U;
}
key = s_key_q[s_key_tail];
s_key_tail = (uint8_t)((s_key_tail + 1U) % SB_LINK_KEY_QUEUE_LEN);
return key;
}
/*
* 函数功能:TX 完成入口(由 HAL_UART_TxCpltCallback 转发进来)
* 入口参数:p_huart - 触发回调的句柄(非本链路的直接忽略)
* 返回值:无
* 限定条件:中断上下文(DMA2_Stream7 的 TC 中断里);不得打日志、不得阻塞
* 函数说明:只清 busy 并计数。**不在这里组帧**——组帧要 0.17 ms,放进中断会拖累
* 全系统中断响应;内容有变化时任务侧下一拍 sb_link_frame_update() 会看到差异。
*/
void sb_link_on_tx_cplt(UART_HandleTypeDef *p_huart)
{
if ((s_huart == NULL) || (p_huart != s_huart))
{
return;
}
s_tx_busy = 0U;
s_stats.tx_done++;
}
/*
* 函数功能:取一份链路统计快照
* 入口参数: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;
p_out->tx_kick = s_stats.tx_kick;
p_out->tx_done = s_stats.tx_done;
p_out->tx_busy_drop = s_stats.tx_busy_drop;
p_out->tx_kick_fail = s_stats.tx_kick_fail;
p_out->tx_abort = s_stats.tx_abort;
p_out->frame_build = s_stats.frame_build;
p_out->frame_bad = s_stats.frame_bad;
p_out->frame_us = s_stats.frame_us;
p_out->frame_us_max = s_stats.frame_us_max;
p_out->key_drop = s_stats.key_drop;
p_out->offline = s_stats.offline;
}