news 2026/10/29 10:52:14

样章-第05讲-UART+DMA+环形缓冲

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
样章-第05讲-UART+DMA+环形缓冲

第 5 讲 | UART + DMA + 环形缓冲:一套不掉包的中断收帧架构

【本讲要点】串口收包是 IoT 固件的地基。收包不稳,上面所有逻辑都是空中楼阁。这一讲给你一套可直接移植的收帧框架。


一、串口收包的三层境界

第一层:轮询(新手村)

while(1){if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)){uint8_tb=USART_ReceiveData(USART1);buf[idx++]=b;}}

问题:主循环一旦有别的事(比如处理按键),就会漏掉串口字节。

第二层:中断逐字节(进阶,但有隐患)

voidUSART1_IRQHandler(void){if(USART_GetITStatus(USART1,USART_IT_RXNE)){uint8_tb=USART_ReceiveData(USART1);ring_push(&g_ring,b);/* 每来一个字节进一次中断 */}}

问题:波特率一高(比如 921600),每字节省一次中断,CPU 被淹没。在 48MHz 的 M0 上,这会吃掉可观的 CPU 时间,尤其当"每 1.2 秒要收 24 个数据包"时。

第三层:DMA + 空闲中断(推荐,本讲主题)

让 DMA 自动把字节搬进缓冲,CPU 只在"一帧收完"时被打断一次。

核心思路:

  • DMA 循环模式接收,自动往缓冲区填字节,不占 CPU;
  • 利用 UART 的IDLE(空闲)中断:总线连续空闲一个字符时间,说明一帧结束了;
  • 在 IDLE 中断里,用当前 DMA 写指针 - 上次读指针算出这一帧的长度。

二、DMA + IDLE 的完整实现

/* ==================== uart_rx.h ==================== */#ifndefUART_RX_H#defineUART_RX_H#include<stdint.h>#include<stdbool.h>#defineUART_RX_BUF_SIZE256/* 必须是 2 的幂,方便取模 */voiduart_rx_init(void);/* 从接收缓冲取出完整帧,返回帧长度;无数据返回 0 */uint16_tuart_rx_read_frame(uint8_t*out,uint16_tmax_len);uint32_tuart_rx_drop_count(void);/* 溢出/丢帧计数,用于诊断 */#endif
/* ==================== uart_rx.c ==================== */#include"uart_rx.h"#include"gd32e23x.h"/* 以 GD32E23x 为例,其他 MCU 换成对应头文件 */staticuint8_ts_dma_buf[UART_RX_BUF_SIZE];/* DMA 直接写入的循环缓冲 */staticvolatileuint16_ts_last_pos=0;/* 上次处理到的位置 */staticvolatileuint32_ts_drop_cnt=0;/* ---------- 环形缓冲(单生产者=中断,单消费者=任务) ---------- */staticuint8_ts_ring[UART_RX_BUF_SIZE];staticvolatileuint16_ts_head=0;/* 生产者写指针 */staticvolatileuint16_ts_tail=0;/* 消费者读指针 */staticinlineuint16_tring_next(uint16_tp){return(uint16_t)((p+1)&(UART_RX_BUF_SIZE-1));}staticvoidring_push_isr(uint8_tb){uint16_tnext=ring_next(s_head);if(next==s_tail){s_drop_cnt++;/* 环形缓冲满:丢字节并计数,绝不阻塞 */return;}s_ring[s_head]=b;s_head=next;}staticuint16_tring_read(uint8_t*out,uint16_tmax_len){uint16_tn=0;while(s_tail!=s_head&&n<max_len){out[n++]=s_ring[s_tail];s_tail=ring_next(s_tail);}returnn;}/* ---------- 初始化 ---------- */voiduart_rx_init(void){/* 1. 配置 UART 收发引脚、波特率(略,用官方 SDK) *//* 2. 配置 DMA:循环模式,外设=USART_DATA,内存=s_dma_buf */dma_deinit(DMA_CH3);dma_init(DMA_CH3,DMA_PERIPH_TO_MEMORY,DMA_MEMORY_INCREASE_ENABLE,DMA_PERIPH_INCREASE_DISABLE,DMA_CIRCULAR_MODE_ENABLE,DMA_PRIORITY_HIGH,UART_RX_BUF_SIZE);dma_transfer_number_config(DMA_CH3,UART_RX_BUF_SIZE);dma_memory_address_config(DMA_CH3,(uint32_t)s_dma_buf);dma_peripheral_address_config(DMA_CH3,(uint32_t)&USART_DATA(USART1));dma_channel_enable(DMA_CH3);usart_dma_receive_config(USART1,USART_DENR_ENABLE);/* 3. 使能 IDLE 中断 —— 这是"一帧结束"的信号 */usart_interrupt_enable(USART1,USART_INT_IDLE);nvic_irq_enable(USART1_IRQn,6,0);/* 优先级数值 ≥5,可调 FreeRTOS API */}/* ---------- IDLE 中断:一帧收完 ---------- */voidUSART1_IRQHandler(void){if(usart_interrupt_flag_get(USART1,USART_INT_FLAG_IDLE)!=RESET){/* 读 DR 清标志(顺序不能错) */volatileuint32_ttmp=USART_STAT0(USART1);tmp=USART_DATA(USART1);(void)tmp;/* 当前 DMA 写到哪了 */uint16_tcur_pos=(uint16_t)(UART_RX_BUF_SIZE-dma_transfer_number_get(DMA_CH3));/* 把 [s_last_pos, cur_pos) 之间的新字节搬进环形缓冲 */while(s_last_pos!=cur_pos){ring_push_isr(s_dma_buf[s_last_pos]);s_last_pos=(uint16_t)((s_last_pos+1)&(UART_RX_BUF_SIZE-1));}}}/* ---------- 任务侧:读一帧 ---------- */uint16_tuart_rx_read_frame(uint8_t*out,uint16_tmax_len){returnring_read(out,max_len);}uint32_tuart_rx_drop_count(void){returns_drop_cnt;}

三、为什么要"环形缓冲"这一层

你可能会问:DMA 已经在缓冲了,为什么还要再套一个环形缓冲?

因为两者的角色不同:

DMA 缓冲环形缓冲
谁写DMA 硬件中断服务程序
谁读中断服务程序应用任务
位置固定循环,会被覆盖队列语义,先进先出
问题任务来不及读就被 DMA 覆盖满时明确丢字节并计数

DMA 缓冲只是一个"中转站",它会被硬件不断覆盖。一旦 CPU 因为高优先级中断延迟处理,DMA 已经绕回覆盖了旧数据。

环形缓冲把"会被覆盖的缓冲"变成"有明确边界的队列":任务什么时候来读都行,读到的都是没被覆盖的完整字节流;真读慢了,也能通过s_drop_cnt明确知道"丢了多少"——可观测,才是工程。


四、粘包与拆包

UART 是字节流,不是"消息"。DMA + IDLE 给你的只是"一段一段的字节",一段里可能有 3 个包,也可能只有半个包。

必须用协议层的规则来切分。常见三种:

方案 1:定长帧

每帧固定 N 字节。最简单,但灵活性差。

if(ring_available()>=FRAME_LEN){read_exact(out,FRAME_LEN);}

方案 2:首尾定界符(推荐用于 AT / 文本协议)

如以\r\n结尾:

uint16_textract_line(uint8_t*out,uint16_tmax){staticuint16_tidx=0;uint8_tb;while(uart_rx_read_frame(&b,1)==1){if(b=='\n'){out[idx]='\0';uint16_tlen=idx;idx=0;returnlen;/* 完整一行 */}if(idx<max-1){out[idx++]=b;}else{idx=0;/* 超长,丢弃 */s_drop_cnt++;}}return0;}

方案 3:长度字段(推荐用于二进制协议)

帧头里带长度,最可靠:

#pragmapack(1)typedefstruct{uint8_thead;/* 0xAA */uint8_tcmd;uint16_tlen;/* 数据长度 */uint8_tdata[];}proto_frame_t;#pragmapack()/* 解析:先找到 head,再根据 len 判断整帧是否到齐 */

注意:二进制协议一定要处理"半包"。收到 5 字节但整帧要 20 字节时,必须把这 5 字节留在缓冲里等后续,不能丢掉重来——这是新手最常见的 bug。


五、三个必须处理的边界情况

1. 溢出与丢包统计

环形缓冲满时,不能覆盖,也不能阻塞,只能丢弃并计数:

if(next==s_tail){s_drop_cnt++;/* 明确记账,量产后能远程看到 */return;}

为什么不能覆盖?覆盖会破坏正在传输的帧结构,导致解析出"看起来合法但内容错误"的数据——比丢包危险得多。

2. 帧超时丢弃

如果一帧收了一半,后面再也不来了(对方重启了),这半帧会永远卡在缓冲里。必须加超时清理:

/* 在任务里:若 100ms 内没有新字节且缓冲有残帧,清掉 */if(idx>0&&(xTaskGetTickCount()-last_rx_tick>pdMS_TO_TICKS(100))){idx=0;s_drop_cnt++;}

3. 中断优先级

DMA 和 UART 的中断优先级,数值必须 ≥configMAX_SYSCALL_INTERRUPT_PRIORITY(第 3 讲讲过),否则一旦在 IDLE 中断里调用了 FreeRTOS API(比如xQueueSendFromISR),就会崩。


六、性能对比

同一块 48MHz 的 M0+ 板子,同一个 921600 波特率的收包压力:

方案CPU 占用(收包部分)掉包率
轮询高(且会漏)高
逐字节中断15%~25%低
DMA + IDLE< 1%低

在"每 1.2 秒 24 个包"这种场景下,DMA + IDLE 是唯一不会影响其他任务的选择。


七、小结

四句话:

1. 收包的终极方案是 DMA + 空闲中断,CPU 只在帧结束时被打断一次。
2. DMA 缓冲会被覆盖,必须再套一层环形缓冲,把它变成"有边界的队列"。
3. UART 是字节流,"粘包/半包"必须由协议层切分,二进制协议尤其要注意半包。
4. 溢出要丢弃并计数,绝不覆盖——可观测性比"少丢一个字节"重要得多。

下一讲(第 6 讲),我们把这条链路接到 WiFi 模组上:如何写一个不被阻塞拖垮的 AT 指令驱动。这正是"一次printf让模组掉线"的根因所在。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 10:51:14

Zap 日志库完全指南:5 分钟跑通 Go 高性能结构化日志

Zap 日志库完全指南&#xff1a;5 分钟跑通 Go 高性能结构化日志 【免费下载链接】zap Blazing fast, structured, leveled logging in Go. 项目地址: https://gitcode.com/gh_mirrors/za/zap 凌晨三点被告警叫醒&#xff0c;登上机器想翻日志&#xff1a;一行行裸文本混…

作者头像 李华
网站建设 2026/10/29 10:46:32

70页战略逻辑PPT架构图拆解:从业务矩阵到技术架构的汇报框架

简介&#xff1a;这份70页战略逻辑PPT架构图模板面向企业战略规划者、咨询顾问与产品设计人员&#xff0c;帮助解决战略制定过程中逻辑梳理与汇报呈现的问题。内容围绕战略规划与制定展开&#xff0c;涵盖业务布局、品牌打造、用户体验、产品设计等维度&#xff0c;并延伸至二手…

作者头像 李华
网站建设 2026/10/29 10:39:22

普通RGB摄像头测血氧:Matlab离线IPPG流程与标定避坑指南

简介&#xff1a;这份资源围绕摄像头检测血氧的物理与信号原理展开&#xff0c;面向移动健康、远程医疗及生物医学信号处理方向的学习者与开发者&#xff0c;帮助理解如何用普通RGB摄像头近似实现非接触式SpO₂监测。内容从朗伯-比尔定律切入&#xff0c;讲解氧合血红蛋白与还原…

作者头像 李华