STM32 串口 DMA + 空闲中断收不定长数据:丢字节与误触发的工程排查

举报
yd_223794387 发表于 2026/10/25 14:04:10 2026/10/25
【摘要】 在 STM32 项目里,串口收不定长数据是最常见的需求:GPS 输出 NMEA、蓝牙模块回包、传感器不定长帧。轮询接收浪费 CPU,中断逐字节搬运在高波特率下容易丢数,DMA + 空闲中断(IDLE)是工程上最稳妥的组合。但这套组合有几个容易踩的坑,本文结合实测经验逐一说明。一、基本原理:为什么是 DMA + IDLEDMA 负责把串口接收寄存器的数据连续搬运到内存缓冲区,CPU 不需要逐字...

在 STM32 项目里,串口收不定长数据是最常见的需求:GPS 输出 NMEA、蓝牙模块回包、传感器不定长帧。轮询接收浪费 CPU,中断逐字节搬运在高波特率下容易丢数,DMA + 空闲中断(IDLE)是工程上最稳妥的组合。但这套组合有几个容易踩的坑,本文结合实测经验逐一说明。

一、基本原理:为什么是 DMA + IDLE

DMA 负责把串口接收寄存器的数据连续搬运到内存缓冲区,CPU 不需要逐字节干预。IDLE 中断的含义是:总线上一帧数据结束、出现一个字节时间的空闲,说明一包数据收完了。收到 IDLE 后,CPU 读取 DMA 当前剩余传输量,就能算出本包实际收到了多少字节,再处理这段数据,最后复位 DMA 指针准备下一包。

关键公式很简单:本包长度 = 缓冲区总长度 - DMA 剩余传输量(NDTR)。这个减法必须在关闭 DMA 或暂停接收之后读,否则 NDTR 还在变化,算出来的长度会抖动。

二、第一个坑:IDLE 标志不清,重复进中断

IDLE 标志的清除顺序有讲究:先读 USART_SR(状态寄存器),再读 USART_DR(数据寄存器),标志才算真正清除。只读 SR 不读 DR,或者顺序反了,都会导致中断反复触发,CPU 在中断里空转。HAL 库的 USART_IRQHandler 里已经处理了这一步,但如果自己写了中断入口又调用 HAL 处理,或者在回调里又手动清标志,就可能把时序搞乱。建议只保留一条清除路径,不要两处同时清。

三、第二个坑:DMA 指针复位方式选错

收到一包后,常见的复位做法有两种:一是停止 DMA、改 NDTR、再重新使能;二是直接改 NDTR 而不停止 DMA。第二种在部分型号上并不可靠,改 NDTR 的瞬间 DMA 可能正在搬运,改写被忽略或只改了一半。稳妥做法是:先关闭 DMA 通道,设置 NDTR 为缓冲区长度,把内存指针复位到缓冲区首地址,再重新使能 DMA。关闭时间只有几个时钟周期,对 115200 波特率以下的接收没有影响。

四、第三个坑:缓冲区溢出没有兜底

如果发送方连续发包、中间没有空闲,或者一包数据超过缓冲区长度,DMA 会从头覆盖旧数据,新包把旧包尾巴覆盖掉,表现就是帧头对不上、校验偶尔失败。工程上的兜底有三层:第一,缓冲区按最大包长的 2 倍留余量;第二,收到 IDLE 后立即把数据拷贝到处理队列,不要在原缓冲区上直接解析;第三,DMA 用循环模式时,在 IDLE 处理里判断长度是否等于缓冲区全长,等于全长基本意味着溢出,直接丢弃并记一次溢出计数,便于联调时定位。

五、第四个坑:半双工与 RS485 方向切换

RS485 半双工场景下,发送完成后要切换回接收。常见错误是发送完成中断一到立刻切方向,此时移位寄存器里最后一个字节可能还没真正发完,对端收到的是截断帧。正确做法是等 TC(发送完成)标志,而不是 TXE(发送数据寄存器空)标志。TC 表示移位寄存器也空了,这时再切回接收并重新使能 DMA,才不会丢尾字节。

六、调试清单

联调时如果收数异常,按这个顺序查:第一,看 IDLE 中断是否只在包尾触发一次;第二,打印 NDTR 算出的包长是否与发送长度一致;第三,检查缓冲区是否出现覆盖;第四,用示波器或逻辑分析仪确认波特率误差,STM32 内外时钟误差超过 2% 时高波特率下误码率会明显上升。

示例代码骨架(HAL,接收部分):

#define RX_BUF_LEN 256
uint8_t rx_buf[RX_BUF_LEN];
volatile uint16_t rx_len = 0;

void USART2_IRQHandler(void)
{
    if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE)) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart2);
        HAL_UART_DMAStop(&huart2);
        rx_len = RX_BUF_LEN - __HAL_DMA_GET_COUNTER(huart2.hdmarx);
        /* 把 rx_buf[0..rx_len) 拷贝到处理队列 */
        HAL_UART_Receive_DMA(&huart2, rx_buf, RX_BUF_LEN);
    }
    HAL_UART_IRQHandler(&huart2);
}

注意:上面拷贝到处理队列的一步不要省略,直接在中断里解析长帧会拖长中断时间,影响其他外设。

结语:DMA + IDLE 本身不复杂,复杂的是标志清除顺序、指针复位时序和溢出兜底这三处。把这三处按本文做法固定下来,不定长接收基本就稳了。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。