STM32 串口 DMA 接收:环形缓冲区读写指针为什么会“打架”?从 DMA 计数器到空闲中断的工程做法
用 STM32 的串口收不定长数据,最常见的组合是 DMA + 空闲中断:DMA 在后台搬数据,空闲中断告诉 CPU 一帧收完了。听起来很稳,但很多人调着调着就发现:数据偶尔丢几个字节,或者同一帧被处理了两次。问题大多不在 DMA 本身,而在环形缓冲区读写指针的配合上。
一、DMA 搬数据时,CPU 到底该看什么?DMA 接收时有一个硬件计数器 NDTR,表示还剩多少字节没搬。每收到一个字节,NDTR 减一。很多人写代码时喜欢用“DMA 接收缓冲区长度减去 NDTR”来算当前写到了哪里,这个思路本身没错,但有两个坑:第一,NDTR 是硬件实时更新的,读它和读缓冲区之间有时间差。如果在空闲中断里先读 NDTR 再处理数据,中间又来了新字节,算出来的长度就可能比实际少一个字节。正确做法是进入中断后先关 DMA 或者先读一次 NDTR 并立刻锁存,处理完再重新配置。第二,环形缓冲区“回绕”的时候,DMA 指针会从尾部跳回头部。如果 CPU 还在按“上次位置到当前位置”线性地取数据,回绕点那一段就会被当成旧数据重复处理,或者直接跳过。处理办法是每次只处理“上次处理位置”到“当前 DMA 写位置”之间的增量,遇到回绕就分两段取。
二、读写指针打架的三种典型场景。场景一:处理速度跟不上接收速度。比如波特率 115200,DMA 缓冲区只有 256 字节,CPU 每 10ms 才处理一次。256 字节在 115200 波特率下大约 22ms 就能装满,处理间隔一旦超过这个时间,DMA 就会回绕覆盖还没处理的数据。解决办法要么加大缓冲区,要么缩短处理间隔,要么在 DMA 半满中断里提前处理一半。场景二:空闲中断和 DMA 半满中断同时用,处理了两次。有人为了保险,半满中断和空闲中断里都写了处理逻辑,结果同一段数据被处理两次。正确的做法是只用一个“写位置”指针:不管哪个中断触发,都只处理“上次位置到当前 DMA 写位置”之间的新数据,并更新“上次位置”。这样即使两个中断先后触发,第二次也发现没有新数据,直接返回。场景三:处理函数里改了 DMA 配置。有人在处理数据的中途调用了 HAL_UART_Receive_DMA 重新启动接收,结果 DMA 指针被复位,正在接收的半帧数据直接丢了。DMA 接收一旦启动就不应该在运行中重新配置;正确做法是只在初始化时启动一次,之后靠 NDTR 和空闲中断配合,永远不去动 DMA 的配置。
三、一个能直接用的指针管理模型。定义两个变量:rx_read_pos(CPU 上次处理到的位置)和 DMA 缓冲区长度 RX_BUF_SIZE。每次需要处理时:uint16_t dma_write_pos = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); if (dma_write_pos != rx_read_pos) { if (dma_write_pos > rx_read_pos) { process_data(&rx_buf[rx_read_pos], dma_write_pos - rx_read_pos); } else { process_data(&rx_buf[rx_read_pos], RX_BUF_SIZE - rx_read_pos); process_data(&rx_buf[0], dma_write_pos); } rx_read_pos = dma_write_pos; } 这个模型的关键点只有两个:第一,永远只处理增量,不重复、不遗漏;第二,回绕时分两段,绝不假设数据是连续的。空闲中断里调一次这个函数就够了,半满中断一般不需要。
四、调试时最容易忽略的三个检查点。检查点一:DMA 模式是不是 Circular(循环模式)。串口 DMA 接收必须用循环模式,否则收满一次就停了。很多人用 CubeMX 生成代码时默认是 Normal 模式,收满一轮就再也不进数据,表现就是“开始能收,收着收着就没了”。检查点二:空闲中断有没有真正使能。空闲中断需要手动使能 USART_CR1 的 IDLEIE 位,HAL 库的 UART_Receive_DMA 不会自动帮你开。CubeMX 里勾上 USART 全局中断也不等于开了空闲中断,一定要在代码里确认 __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE) 被执行过。检查点三:DMA 缓冲区有没有放在 DMA 能访问的内存。某些 STM32 型号上,DTCM 或者带 Cache 的内存区域 DMA 访问会有问题,表现就是数据偶尔错乱。最稳妥的做法是把 DMA 接收缓冲区放在普通 SRAM 里,或者用 __attribute__((section(...))) 指定到 DMA 友好的区域。
五、小结。串口 DMA 接收丢数据的根源,十有八九不在 DMA 硬件,而在 CPU 和 DMA 之间的指针配合。记住三个原则:只处理增量、回绕分两段、运行中不改 DMA 配置。把这三个原则落实到位,DMA + 空闲中断这套组合在 115200 甚至更高的波特率下都可以长期稳定跑。
- 点赞
- 收藏
- 关注作者
评论(0)