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 配置


这一段当前真正要确认的只有三件事:
-
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 段已成功复现。
-
当前现象符合“先逐字节收,后超时整帧输出”的预期。

当前卡点
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 配置
基于超时解析,增加配置:

当前要确认的重点:
-
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(...)绑定huart1和hdma_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这条链已经打通
当前阶段的关键结论
-
UART和DMA是绑定关系,不是替代关系。 -
发送相关接口首先面对的仍然是
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_ringbuffer和ringbuffer_pool定义在usart_app.c -
rt_ringbuffer_init(...)直接放在main.c初始化阶段
但这次也出现了一个很重要的移植结论:
-
参考工程能编过,不等于原样搬到当前工程也一定能编过
-
库源码之外,
Keil / ARMCC当前环境本身也是移植变量
预期改动
-
库文件放进
Components/ringbuffer -
usart_app.c增加uart_ringbuffer和ringbuffer_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_ringbuffer和ringbuffer_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.c:uart_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