6.1 Uart课程复现与排障记录(含ringbuffer)

Uart课程复现与排障记录

这份笔记是干什么的

这不是知识整理笔记,也不是最终框架笔记。

这份笔记专门记录三类东西:

  • 我如何把老师的 UART 示例一步一步复现出来

  • 我在真实工程里卡在了什么地方

  • 我最后是怎么排查、怎么修正、怎么形成最小闭环的

当前这份记录聚焦三条主线:

  • 超时解析法

  • DMA + 空闲中断

  • ringbuffer 移植

目标不是把代码整份抄进来,而是留下真正有用的:

  • 当前工程结构

  • 最小关键代码片段

  • 真实现象

  • 真实卡点

  • 真实排查顺序


对应课件

  • 嵌入式开发09:UART

  • 嵌入式开发10:STM32CUBEMX_UART配置


使用方式

以后再看这份笔记,不要从头到尾重读。

按这个顺序看:

  • 先看“本次最小目标”

  • 再看“当前工程结构”

  • 再看“最小关键片段”

  • 最后只看“当前卡点 / 已确认问题 / 下一步准备怎么查”

也就是说,这份笔记以后主要承担的是:

  • 复现实操导航

  • 排障记录

  • 经验回放


当前总目标

A. 超时解析法

  • 跑通最小超时解析例子

  • 能确认首次 HAL_UART_Receive_IT(...) 已启动

  • 能看到超时后整帧被处理

  • 能说明它适合什么场景、不适合什么场景

B. DMA + 空闲中断

  • 跑通 HAL_UARTEx_ReceiveToIdle_DMA(...) 最小例子

  • 能确认 HAL_UARTEx_RxEventCallback(...) 进入

  • 能理解 Size 表示本次有效数据长度

  • 能说明它和超时解析法的主要区别

C. ringbuffer 移植

  • 找到当前 UART 数据流入口和出口

  • 明确 ringbuffer 要替换的是哪段线性缓冲逻辑

  • 至少完成一次 ringbuffer 版接收链路尝试

  • 能说清为什么这里要不要上 ringbuffer


通用前置检查

板级与原理图

先确认:

  • 开发板是否已经板载串口相关链路

  • UART 实际对应哪个接口

  • TX / RX / GND 实际怎么接

当前结论:

  • 这块板子本身具备串口复现条件,不需要额外重搭一套 USB 转 TTL。

  • 但接线问题仍然会直接导致“代码看起来没错、串口助手没反应”的假象。

  • 所以以后排查 UART,仍然默认先查:接线 -> 板级链路 -> 软件配置

CubeMX 基础配置

先确认:

  • USART1 的 TX/RX 已真正打开

  • 波特率是 115200

  • UART 中断已开启

  • 如果进入 DMA 方案,还要确认 DMA 请求、方向、模式是否正确

当前工程组织

当前 uart_test 里和 UART 复现直接相关的文件主要是:

  • Core/Src/usart.c

  • APP/usart_app.c

  • APP/usart_app.h

  • APP/mydefine.h

  • APP/scheduler.c

  • APP/ebtn_app.c

它们当前的职责是:

  • usart.c

    • 串口初始化

    • DMA 初始化与句柄绑定

    • 首次启动接收

  • usart_app.c

    • my_printf

    • UART 接收回调

    • 数据处理任务 uart_task

  • mydefine.h

    • 当前课程阶段的集中外部声明与包含
  • scheduler.c

    • 周期调度 uart_task
  • ebtn_app.c

    • 按键触发最小发送验证

A. 超时解析法复现记录

本次最小目标

  • 跑通最小超时解析链路

  • 确认首次 HAL_UART_Receive_IT(...) 已启动

  • 确认 HAL_UART_RxCpltCallback(...) 参与逐字节接收

  • 确认 uart_task() 能在超时条件成立时输出整帧数据

CubeMX 配置

image-20260506175626728

image-20260506175901145

这一段当前真正要确认的只有三件事:

  • USART1 资源已经真的打开

  • 基础参数已设置

  • 中断链路具备进入回调的前提

当前工程结构

A 段跑通时,关键链路是:

  • usart.c

    • 在初始化末尾手动启动第一次 HAL_UART_Receive_IT(...)
  • usart_app.c

    • HAL_UART_RxCpltCallback(...) 负责逐字节续挂

    • uart_task() 负责超时后整帧处理

  • scheduler.c

    • 周期调用 uart_task
  • ebtn_app.c

    • my_printf(&huart1, "hello\r\n") 验证最小发送

最小关键片段

usart.c:首次启动接收

 HAL_UART_Receive_IT(&huart1, uart_rx_buffer, 1);

这一句必须存在,否则 HAL_UART_RxCpltCallback(...) 根本起不来。

usart_app.c:逐字节回调续挂

 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
 {
     if (huart->Instance == USART1)
     {
         uart_rx_ticks = uwTick;
         uart_rx_index++;
         HAL_UART_Receive_IT(&huart1, &uart_rx_buffer[uart_rx_index], 1);
     }
 }

核心不是每一行语法,而是这四步:

  • 核对串口身份

  • 更新时间戳

  • 增加计数

  • 再挂下一次接收

usart_app.c:超时后整帧处理

 if (uwTick - uart_rx_ticks > UART_TIMEOUT_MS)
 {
     my_printf(&huart1, "uart data: %s\n", uart_rx_buffer);
     memset(uart_rx_buffer, 0, uart_rx_index);
     uart_rx_index = 0;
     huart1.pRxBuffPtr = uart_rx_buffer;
 }

这里真正关键的是:

  • 超时判断用 uwTick

  • 输出走 my_printf(&huart1, ...)

  • 清缓冲区后把 huart1.pRxBuffPtr 重新指回起点

scheduler.c:任务必须挂进去

 {uart_task, 5, 0},

没有这一行,就不会有超时后的整帧处理。

ebtn_app.c:最小发送验证

 my_printf(&huart1, "hello\r\n");

预期现象

  • 按键触发时,串口助手先看到 hello

  • 发送一串数据后,不会立刻整帧处理

  • 停止输入并超过 UART_TIMEOUT_MS 后,输出整帧内容

实际现象

  • A 段已成功复现。

  • 当前现象符合“先逐字节收,后超时整帧输出”的预期。

image-20260506184053555

当前卡点

A 段当前已经没有阻塞性卡点,主要是留下易错点给后面复用。

我已经检查过的内容

  • 首次 HAL_UART_Receive_IT(...) 是否手动启动

  • 回调是否进入

  • 时间戳是否更新

  • 调度器 / uart_task 是否真的执行

  • 超时阈值是否合理

这次真正记住的易错点

  • 首次接收没手动启动

  • uart_task() 没挂进调度器

  • UART_TIMEOUT_MS 忘记定义

  • my_printf 不是普通 printf

  • 跨文件变量和头文件包含链没接好

成功闭环记录

成功的关键条件:

  • 首次 HAL_UART_Receive_IT(...) 必须手动启动

  • uart_task() 必须真的被调度器周期执行

  • UART_TIMEOUT_MS 必须定义且量级合理

  • my_printf 的调用方式必须传入 &huart1

  • 跨文件变量声明和 include 链必须打通

当前调用理解

  • 超时解析法适合:不定长数据轻量通信先求快速跑通

  • 真正关键不是某个 API,而是这条链:

    • 首次启动接收

    • 回调逐字节续挂

    • 调度器周期检查超时

    • 超时后整帧处理


B. DMA + 空闲中断已跑通闭环版

本次最小目标

  • 把接收入口从 HAL_UART_Receive_IT(...) 切到 HAL_UARTEx_ReceiveToIdle_DMA(...)

  • 确认 DMA 缓冲区、回调函数、主循环处理函数已经按当前工程结构接通

  • 能说明 Size 是本次有效数据长度

  • 跑通 RxEventCallback -> uart_flag -> uart_task -> my_printf 这条最小闭环

CubeMX 配置

基于超时解析,增加配置:

image-20260506205047062

当前要确认的重点:

  • USART1 仍然是 TX_RX

  • USART1 中断仍然开启

  • USART1_RX 已挂上 DMA 请求

  • DMA 方向是 Peripheral to Memory

  • 内存递增开启

  • 当前工程 DMA 模式是 Normal

  • 当前工程主动关闭了 DMA_IT_HT

当前工程结构

B 段在当前 uart_test 里的链路是:

  • usart.c

    • 启动 HAL_UARTEx_ReceiveToIdle_DMA(...)

    • 关闭半传输中断

    • 通过 __HAL_LINKDMA(...) 绑定 huart1hdma_usart1_rx

  • usart_app.c

    • 保留 DMA 缓冲区和标志位

    • HAL_UARTEx_RxEventCallback(...) 里拷贝有效数据并重启下一轮 DMA 接收

    • uart_task() 里输出收到的数据

  • scheduler.c

    • 仍然调度 uart_task
  • ebtn_app.c

    • 继续用 my_printf(&huart1, "hello\r\n") 验证最小发送

最小关键片段

usart.c:启动 DMA + 空闲接收

 HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_rx_dma_buffer, sizeof(uart_rx_dma_buffer));
 __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);

usart.c:DMA 与 UART 绑定

 __HAL_LINKDMA(uartHandle, hdmarx, hdma_usart1_rx);

这句说明:

  • hdma_usart1_rx 不是孤立存在

  • 它和 huart1 绑在一起

  • 所以后面很多接口表面上传的是 &huart1,HAL 会顺着它找到 DMA

usart_app.c:DMA 回调

 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
 {
     if (huart->Instance == USART1)
     {
         HAL_UART_DMAStop(huart);
         memcpy(uart_dma_buffer, uart_rx_dma_buffer, Size);
         uart_flag = 1;
         memset(uart_rx_dma_buffer, 0, sizeof(uart_rx_dma_buffer));
         HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_rx_dma_buffer, sizeof(uart_rx_dma_buffer));
         __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);
     }
 }

当前只抓住这几点:

  • 参数 huart 仍然是 UART_HandleTypeDef *

  • Size 是本次实际有效数据长度

  • 回调里先交接数据,再重启下一轮 DMA 接收

usart_app.c:DMA 版处理任务

 void uart_task(void)
 {
     if(uart_flag == 0)
         return;
 ​
     uart_flag = 0;
     my_printf(&huart1, "DMA data: %s\n", uart_dma_buffer);
     memset(uart_dma_buffer, 0, sizeof(uart_dma_buffer));
 }

这里最关键的是:

  • 输出走 my_printf(&huart1, ...)

  • 不再依赖当前工程里未确认重定向的 printf(...)

mydefine.h:当前必须声明到位的变量

 extern uint8_t uart_rx_dma_buffer[128];
 extern UART_HandleTypeDef huart1;
 extern DMA_HandleTypeDef hdma_usart1_rx;

ebtn_app.c:最小发送链继续保留

 my_printf(&huart1, "hello\r\n");

实际现象

本轮最终确认到的真实现象是:

  • 工程结构已经切到 DMA + 空闲中断 版本

  • hello 一度打不出来,其中一个原因是接线问题

  • uart_task() 原来用 printf(...) 没反应,改成 my_printf(&huart1, ...) 后才真正走当前可用串口输出链

  • 修正接线和输出口之后,串口助手发送一串数据,已经能看到:

    • DMA data: ...

当前卡点

本轮主要阻塞点已经被清掉,当前没有继续阻塞 B 段闭环的核心卡点。

这次真正留下来的排障记忆点是:

  • 串口助手没反应,不等于 DMA 一定没收到,先分清是接线问题还是输出口问题

  • my_printf(...) 不能误传 &hdma_usart1_rx

  • uart_task() 在当前工程里不能继续裸用 printf(...)

  • 必须先把最小发送链 hello 验证通,再看 DMA 回调链

我已经检查过的内容

  • USART1 的 DMA 接收已经在初始化中启动

  • 半传输中断 DMA_IT_HT 已主动关闭

  • HAL_UARTEx_RxEventCallback(...) 已存在于当前工程

  • uart_task() 仍然被调度器执行

  • uart_rx_dma_buffer / huart1 / hdma_usart1_rx 已做外部声明

  • my_printf(...) 的第一个参数类型已经核对清楚

  • 当前最小发送链 hello 已恢复,之前还有接线问题

  • uart_task() 的输出口已经从 printf(...) 修正为 my_printf(&huart1, ...)

  • RxEventCallback -> uart_flag -> uart_task 这条链已经打通

当前阶段的关键结论

  • UARTDMA 是绑定关系,不是替代关系。

  • 发送相关接口首先面对的仍然是 UART 句柄

  • DMA 更像 UART 背后的“搬运工”,不是应用层直接拿来替代 huart1 的“主控对象”。

  • 当前工程里,稳定可用的输出口是 my_printf(&huart1, ...)

  • DMA + 空闲中断 相比超时解析法,关键区别是:

    • 超时解析法靠软件时间差判断一帧结束

    • DMA + 空闲中断靠 DMA 搬运数据、UART 空闲事件给出一帧边界

下一步

  • 当前 B 段最小闭环已经完成。

  • 下一步进入 C. ringbuffer 移植

  • 如果以后这条 DMA 链再次失效,默认回查顺序仍然是:

    • 先看最小发送链 hello

    • 再看接线

    • 再看 my_printf(...) 的发送对象

    • 再看 HAL_UARTEx_RxEventCallback(...) -> uart_flag -> uart_task()


C. ringbuffer 移植记录

本次最小目标

  • 找到当前 UART 数据流入口和出口

  • 明确 ringbuffer 要替换的是哪段线性缓冲逻辑

  • 至少完成一次 ringbuffer 版接收链路尝试

  • 能说清为什么这里要不要上 ringbuffer

我要先改哪条数据流

  • 回调入口:HAL_UARTEx_RxEventCallback(...)

  • 处理出口:uart_task()

这次先明确替换关系:

  • ringbuffer 不是替掉 DMA

  • ringbuffer 也不是替掉 uart_rx_dma_buffer

  • 真正被替掉的是这条线性中转链:

    • DMA 回调 -> memcpy 到 uart_dma_buffer -> uart_flag -> uart_task 输出
  • 替换后的目标链是:

    • DMA 回调 -> rt_ringbuffer_put(...) -> uart_task 里 rt_ringbuffer_get(...) -> 输出

参考工程借鉴点

这次参考了:

  • D:\Embedded_Softwave_trellis\西门子嵌入式\scr\GD32\GD32_Xifeng_ADDA_波特率(460800)

真正借鉴的是三件事:

  • ringbuffer 作为独立库放在 Components/ringbuffer

  • uart_ringbufferringbuffer_pool 定义在 usart_app.c

  • rt_ringbuffer_init(...) 直接放在 main.c 初始化阶段

但这次也出现了一个很重要的移植结论:

  • 参考工程能编过,不等于原样搬到当前工程也一定能编过

  • 库源码之外,Keil / ARMCC 当前环境本身也是移植变量

预期改动

  • 库文件放进 Components/ringbuffer

  • usart_app.c 增加 uart_ringbufferringbuffer_pool

  • main.c 增加 rt_ringbuffer_init(...)

  • DMA 回调不再把数据复制到线性待处理缓冲,而是直接写入 ringbuffer

  • uart_task() 不再直接依赖 DMA 中转数组,而是从 ringbuffer 里读出数据再打印

  • 最后验证 uart_flag 在 ringbuffer 版里是不是还必要

实际改动

库接入阶段

  • 库文件放到了:Components/ringbuffer

  • include path 已加入工程

  • ringbuffer.c 已加入 Keil 编译组

第一个真实错误:不是路径问题,是 inline 链接问题

建库后第一次编译,不是报“头文件找不到”,而是:

  • Undefined symbol rt_ringbuffer_status

这次的判断过程要记住:

  • ringbuffer.h 已经能找到

  • ringbuffer.c 也已经参与编译

  • 所以这不是路径问题,也不是源文件没加进工程

  • 真正出问题的是 inline 在当前 Keil / ARMCC 环境下的链接语义

这次回查后的更准确结论是:rt_ringbuffer_status(...) 这个内部辅助函数必须带 static

可行的改法至少有两种:

  • ringbuffer.h 里把
 #define rt_inline   inline

改成

 #define rt_inline   static inline
  • 或者更贴近参考工程地,直接把函数写成
 static inline enum rt_ringbuffer_state rt_ringbuffer_status(struct rt_ringbuffer *rb)

当前结论:

  • 这不是 ringbuffer 逻辑错误

  • 这是当前工程工具链兼容性问题

  • 关键不是一定改宏,而是这个内部函数最终必须带 static

  • 在你当前环境里,单独 inline 不稳,static inline 最保险

骨架接入阶段

当前工程最终落点是:

  • Components/ringbuffer

    • 保存通用 ringbuffer 库
  • APP/usart_app.c

    • 定义 uart_ringbufferringbuffer_pool

    • DMA 回调里写 ringbuffer

    • uart_task() 里读 ringbuffer

  • APP/mydefine.h

    • extern 声明,供 main.c 使用
  • Core/Src/main.c

    • 在初始化阶段执行 rt_ringbuffer_init(...)

最小关键片段

usart_app.c:定义 ringbuffer 对象和池子

 struct rt_ringbuffer uart_ringbuffer;
 uint8_t ringbuffer_pool[128];

main.c:初始化 ringbuffer

 rt_ringbuffer_init(&uart_ringbuffer, ringbuffer_pool, sizeof(ringbuffer_pool));

usart_app.c:DMA 回调改为写入 ringbuffer

 rt_ringbuffer_put(&uart_ringbuffer, uart_rx_dma_buffer, Size);
 memset(uart_rx_dma_buffer, 0, sizeof(uart_rx_dma_buffer));
 HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_rx_dma_buffer, sizeof(uart_rx_dma_buffer));
 __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);

这里的关键理解是:

  • DMA 仍然负责搬运到 uart_rx_dma_buffer

  • ringbuffer 负责把这一批有效数据接管下来

  • 所以后面处理任务就不再直接盯着 DMA 临时数组

usart_app.cuart_task() 改为从 ringbuffer 读取

 void uart_task(void)
 {
     uint16_t length;
 ​
     length = rt_ringbuffer_data_len(&uart_ringbuffer);
     if (length == 0)
         return;
 ​
     rt_ringbuffer_get(&uart_ringbuffer, uart_dma_buffer, length);
     my_printf(&huart1, "DMA data: %s\n", uart_dma_buffer);
     memset(uart_dma_buffer, 0, sizeof(uart_dma_buffer));
 }

这里最终说明:

  • ringbuffer 已经自己表达“有没有数据”

  • uart_task() 可以直接根据 rt_ringbuffer_data_len(...) 决定是否处理

uart_flag 的最终判断

这次专门做了去 uart_flag 验证,结论是:

  • 学习和迁移阶段,先保留 uart_flag 有利于缩小排障范围

  • 但当前最终版里,uart_flag 已经不是必须层

  • 只要 uart_task() 直接根据 rt_ringbuffer_data_len(...) 判空也能稳定工作,就说明 uart_flag 是冗余层

实际现象

当前已经确认到的真实现象:

  • ringbuffer 版本能正常编译、下载、运行

  • 串口助手多次发送短数据,能够重复打印:

    • DMA data: 1

    • DMA data: 12

    • DMA data: 123

  • 按键触发的 hello 仍然正常,说明发送链没有被这次移植破坏

  • 说明当前最小闭环已经打通:

    • DMA + 空闲中断 + ringbuffer + uart_task + my_printf

当前能确认的是:

  • 不是只能收一次

  • 不是改完 ringbuffer 后发送链就坏掉

  • ringbuffer 已经真实接管了“回调后数据入库”这一步

当前卡点

当前 C 段最小移植已经完成,没有阻塞性卡点。

当前还保留的后续验证点主要是更高负载场景:

  • 如果后面连续更快发、多帧背靠背发、缓冲区逼近上限,是否还稳定

我已经检查过的内容

  • ringbuffer 库文件已加入工程

  • ringbuffer.c 已加入 Keil 编译组

  • include path 已配置到 Components/ringbuffer

  • ringbuffer 结构体和池子已定义

  • rt_ringbuffer_init(...) 已执行

  • 接收入口已切到 ringbuffer 写入方式

  • 处理出口已切到 ringbuffer 读取方式

  • rt_ringbuffer_data_len(...) / rt_ringbuffer_put(...) / rt_ringbuffer_get(...) 已完成最小验证

  • uart_flag 已验证可删除,当前版本已不再依赖它

这次真正记住的易错点

  • ringbuffer 移植的第一个坑,可能不是逻辑问题,而是工具链对 inline 的处理差异

  • 参考工程能编过,不代表当前工程原样搬过来也能编过

  • ringbuffer 替掉的不是 DMA,而是“DMA 回调后的线性中转方式”

  • 学习和迁移阶段可以先保留 uart_flag 缩小排障范围,但最终版不一定需要它

  • 只有当 uart_task() 直接根据 rt_ringbuffer_data_len(...) 也能稳定跑通时,才能确认 uart_flag 真的是冗余层

当前阶段的关键结论

  • 当前这版 ringbuffer 已完成最小闭环。

  • 对于当前工程,uart_flag 不是必须层。

  • ringbuffer 真正解决的是:

    • 接收和处理速度不完全一致时的数据暂存

    • 比单个线性数组更自然的生产者 / 消费者解耦

  • 但如果只是简单、低负载、先求跑通,B 段的 DMA + 空闲中断版本已经够用。

下一步

  • inline -> static 这一条已经回查完成,当前工程需要保留 static

  • 如果继续深挖 ringbuffer,再补这几类验证:

    • 连续快速多次发送

    • 更长数据包

    • 多帧背靠背发送

    • 接近缓冲区上限时的行为


本轮阶段总结

什么时候我会先选超时解析法

  • 不定长数据

  • 数据量轻

  • 先求快速跑通

  • 先建立最小接收闭环

什么时候我会考虑 DMA + 空闲中断

  • 数据是一波一波来的

  • 单次数据量更大

  • 不希望每个字节都触发 CPU 处理

  • 想减轻 CPU 压力

什么时候我才会继续上 ringbuffer

  • 数据是持续流式到来

  • 接收和处理速度可能不一致

  • 线性数组开始不好管理

  • 需要更强的生产者/消费者解耦

如果下次现场再做 UART,我默认起手顺序是

  • 先确认板级链路和串口资源

  • 再打通最小发送

  • 再挂首次接收启动

  • 再验证回调链和调度器链

  • 最后才考虑是否要升级到 DMA 或 ringbuffer


下次继续前要做什么

  • A. 超时解析法已完成最小闭环

  • 继续验证 B 的 RxEventCallback -> uart_flag -> uart_task 链是否真的打通

  • C. ringbuffer 移植已完成最小闭环记录

  • 补更高负载场景下的 ringbuffer 验证记录

  • 已确认当前工程里 rt_ringbuffer_status(...) 需要保留 static