news 2026/3/12 13:59:37

GD32平台串口DMA初始化配置小白指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GD32平台串口DMA初始化配置小白指南

GD32串口DMA配置实战:从零实现高效无感数据接收

你有没有遇到过这种情况——主控芯片明明性能不差,却因为串口每收到一个字节就触发一次中断,CPU被频繁打断,系统卡顿、响应迟缓?尤其是在处理高速传感器数据或固件升级时,传统轮询加中断的方式几乎成了性能瓶颈。

今天我们就来解决这个痛点:在GD32平台上,用DMA让串口通信彻底“解放CPU”。不是理论堆砌,而是手把手带你走完初始化全流程,理解每一行代码背后的逻辑和坑点。


为什么你需要串口DMA?

先说结论:

串口+DMA = 零CPU干预 + 高吞吐 + 实时性强

我们以常见的应用场景为例:

  • 波特率 115200bps,每秒传输约11.5KB;
  • 若使用中断方式接收,每个字节都会引发中断,意味着每秒进入中断服务函数超过1万次;
  • 每次中断都有上下文保存与恢复开销,CPU利用率飙升,还容易丢帧。

而换成DMA后呢?

  • 只需配置一次,后续数据自动搬进内存缓冲区;
  • CPU可以专心跑控制算法、UI刷新或者干脆休眠省电;
  • 数据到达一定长度(如缓冲满或空闲线检测)才通知CPU处理。

这才是现代嵌入式系统的正确打开方式。


GD32的USART与DMA是怎么配合工作的?

别急着写代码,先搞清楚硬件是怎么协作的。

外设请求机制:USART喊一声,DMA动起来

在GD32中,USART模块内置了两个关键控制位:

USART_CTL0(RDEN) // 接收DMA使能 USART_CTL0(TDEN) // 发送DMA使能

当你设置RDEN = 1后,每当USART接收到一个字节并存入RDR寄存器(接收数据寄存器),它就会向DMA控制器发出一个“我有数据了!”的信号——这就是所谓的DMA请求

DMA控制器监听到该请求后,立即执行预设动作:
&USARTx->RDR地址里的数据读出来,写到你的内存缓冲区里

整个过程不需要CPU参与,就像快递员(DMA)看到包裹到了(RDR非空),直接取走放进仓库(rx_buffer),完全不用你出门接。


DMA三要素:源、目的、数量

任何一次DMA传输都围绕这三个核心参数展开:

要素串口接收场景
源地址&USART1->RDR—— 固定不变
目的地址(uint32_t)rx_buffer—— 内存缓冲首地址
传输数量BUFFER_SIZE—— 一次性搬运多少个字节

此外还需设定:
- 数据宽度:通常为字节(8bit)
- 地址增量模式:源地址禁止自增(RDR是固定地址),目的地址启用自增
- 传输方向:外设 → 内存
- 工作模式:推荐使用循环模式(Circular Mode)

✅ 循环模式有多香?缓冲区满了不会停,而是从头开始继续填,形成一个“永不断流”的数据管道。


实战第一步:时钟不能忘!

所有外设操作的第一步永远是——开时钟!

GD32采用APB总线架构,我们需要依次开启:
- GPIOA 的时钟(用于PA9/PA10引脚)
- USART1 的时钟
- DMA1 的时钟(假设使用DMA1_Channel2)

/* 使能所需外设时钟 */ rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_USART1); rcu_periph_clock_enable(RCU_DMA1);

⚠️ 注意:不同型号GD32芯片可能映射关系略有差异。例如某些系列中USART1_RX对应的是DMA1_Channel5而非Channel2,请查阅《GD32Fxxx_User_Manual》确认DMA通道绑定表。


第二步:GPIO复用配置

PA9作为TX,PA10作为RX,必须配置为复用推挽输出 / 浮空输入模式:

/* 配置PA9(TX)为复用推挽输出 */ gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9); /* 配置PA10(RX)为浮空输入 */ gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_10);

这里GPIO_MODE_AF_PP表示 Alternate Function Push-Pull(复用推挽),确保TX能主动驱动信号;RX则设为浮空输入即可,由外部设备拉高拉低。


第三步:初始化USART基本参数

接下来配置串口基本通信格式:波特率115200,8位数据,无校验,1停止位。

usart_deinit(USART1); // 先复位避免遗留状态 usart_baudrate_set(USART1, 115200); usart_word_length_set(USART1, USART_WL_8BIT); usart_stop_bit_set(USART1, USART_STB_1BIT); usart_parity_config(USART1, USART_PM_NONE); // 使能发送和接收功能 usart_transmit_config(USART1, USART_TRANSMIT_ENABLE); usart_receive_config(USART1, USART_RECEIVE_ENABLE); // 关闭全局中断,但保留DMA相关使能 usart_interrupt_disable(USART1, USART_INT_RBNE);

注意最后这句usart_interrupt_disable(...RBNE):我们不希望每收到一个字节就进中断,所以手动关掉“接收缓冲非空中断”。

但我们后面会开启IDLE中断来判断一帧结束,这点稍后再说。


第四步:配置DMA通道(重点来了!)

现在进入最关键的一步:DMA初始化。

我们选择DMA1_Channel2对应 USART1_RX 请求源。

#define RX_BUFFER_SIZE 256 uint8_t rx_buffer[RX_BUFFER_SIZE] __attribute__((aligned(4))); // 对齐避免总线错误

📌 建议将缓冲区声明为全局静态变量,并加上__attribute__((aligned(4))),防止因未对齐访问导致HardFault。

下面是DMA配置代码:

dma_parameter_struct dma_init_struct; /* 复位DMA通道 */ dma_deinit(DMA1, DMA_CH2); /* 配置DMA参数 */ dma_init_struct.periph_addr = (uint32_t)&USART1_RDATA; // 源:USART RDR寄存器 dma_init_struct.memory_addr = (uint32_t)rx_buffer; // 目的:内存缓冲 dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY; // 方向:外设→内存 dma_init_struct.number = RX_BUFFER_SIZE; // 总字节数 dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; // 外设地址不变 dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; // 内存地址递增 dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; // 字节宽度 dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT; dma_init_struct.priority = DMA_PRIORITY_MEDIUM; // 中等优先级 dma_init_struct.mode = DMA_CIRCULAR_MODE; // 循环模式! /* 应用配置 */ dma_init(DMA1, DMA_CH2, &dma_init_struct); /* 开启DMA通道 */ dma_channel_enable(DMA1, DMA_CH2);

关键点解析:

  • DMA_CIRCULAR_MODE是实现持续接收的核心,填满后自动回绕;
  • memory_inc = ENABLE确保数据依次填入缓冲区;
  • periph_inc = DISABLE因为RDR地址固定;
  • 优先级设为中等,避免抢占关键任务,又不至于被其他DMA压住。

第五步:打通USART与DMA的“任督二脉”

光配好DMA还不够,必须告诉USART:“以后有数据记得叫DMA来搬!”

这一句至关重要:

/* 使能USART的DMA接收请求 */ usart_dma_receive_config(USART1, USART_DENR_ENABLE);

等效于设置寄存器中的RDEN位。一旦开启,只要RDR中有新数据,DMA就会自动启动一次传输。

至此,硬件链路已全部打通


如何知道收到了哪些数据?IDLE中断来帮忙!

虽然DMA一直在默默搬运数据,但我们怎么知道“一包完整的命令已经收完”?

答案是:利用空闲线检测(IDLE Line Detection)机制

当串行总线上连续一段时间没有新数据到来时(即帧间间隔),USART会触发一个 IDLE 标志位。我们可以开启对应的中断,在其中获取当前DMA已接收的位置,从而确定有效数据长度。

开启IDLE中断

/* 清除可能存在的旧标志 */ usart_flag_clear(USART1, USART_FLAG_IDLE); /* 使能IDLE中断 */ usart_interrupt_enable(USART1, USART_INT_IDLE); nvic_irq_enable(USART1_IRQn, 1, 1); // 设置NVIC优先级

在中断服务函数中处理数据

void USART1_IRQHandler(void) { if (usart_flag_get(USART1, USART_FLAG_IDLE)) { // 必须先读USR再读RDR才能清除IDLE标志 (void) usart_data_receive(USART1); // Dummy read (void) usart_flag_get(USART1, USART_FLAG_IDLE); // Clear flag // 获取DMA当前剩余可接收字节数 uint32_t num_received = RX_BUFFER_SIZE - dma_transfer_number_get(DMA1, DMA_CH2); // 提交有效数据给协议解析层 handle_received_frame(rx_buffer, num_received); // 可选:重置CNDTR以便下一轮计数(仅非循环模式需要) // 但在循环模式下无需重置,DMA自动续传 } }

🔥 这才是真正的“变长帧接收”利器!无论对方发10字节还是200字节,都能精准捕获完整报文。


常见问题与避坑指南

❌ 坑点1:缓冲区定义在栈上

void uart_dma_init() { uint8_t rx_buffer[256]; // 错!局部变量位于栈,生命周期短且可能被覆盖 ... }

✅ 正确做法:定义为静态全局变量或使用动态分配(malloc),确保地址稳定。


❌ 坑点2:忘记开启DMA通道

即使配置完成,若漏掉dma_channel_enable(),DMA也不会工作。


❌ 坑点3:DMA未清标志导致首次传输失败

建议每次重新启用前调用dma_deinit()或手动清除传输完成标志。


❌ 坑点4:波特率不准导致误码

尤其在内部RC振荡器下,时钟偏差可能导致通信异常。建议使用外部晶振(HSE)提升精度。


高阶玩法:双缓冲 + 半传输中断

如果你追求极致稳定性,还可以启用半传输中断(HTIE)

原理是:当DMA搬完一半数据(比如128/256)时触发中断,你可以提前处理前半部分数据,而后半部分继续接收。这样既能降低延迟,又能防溢出。

dma_interrupt_enable(DMA1, DMA_CH2, DMA_INT_HTF);

结合双缓冲策略,甚至可以做到“无缝切换”,适用于音频流、图像传输等大数据场景。


小结:这套方案适合谁?

这套串口DMA+IDLE的组合拳特别适合以下场景:

应用类型是否适用
Modbus RTU通信✅ 强烈推荐
自定义二进制协议✅ 完美支持变长帧
GPS模块数据采集✅ 高效稳定
Bootloader固件更新✅ 支持大块数据接收
多传感器数据聚合✅ 降低CPU负担

结语:掌握它,你就迈过了初级开发的门槛

串口DMA看似只是个小功能,但它背后体现的是对外设协同、内存管理、中断机制、实时性设计的综合理解。

当你第一次看到LED不再闪烁(说明CPU不再忙于处理中断),而数据依然准确无误地流入缓冲区时,那种“系统真正活起来”的感觉,会让你爱上嵌入式开发。

如果你在调试过程中遇到问题,欢迎留言交流。我可以帮你分析波形、查看寄存器状态,甚至一起看逻辑分析仪截图。

下一步你想学什么?
- 多路串口DMA同时工作?
- DMA发送实现高速日志输出?
- 结合FreeRTOS做消息队列传递?

评论区告诉我,下一期我们继续深挖GD32实战技巧。

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

ZRT-II 机器人减速器性能测试系统

一、产品概述ZRT-II 机器人减速器性能测试系统是四川志方科技有限公司自主研发的精密测试设备,专为机器人用精密减速器 (谐波、RV、行星等) 的综合性能测试设计。该系统采用模块化架构,全面覆盖减速器各项核心性能指标测试,是机器人减速器研发…

作者头像 李华
网站建设 2026/3/11 6:04:48

多芯片支持下jflash下载步骤详解

多芯片系统下 JFlash 固件烧录实战指南:从连接到自动化的全流程解析 在嵌入式开发的进阶之路上,单片机早已不是主角。现代工业控制、车载电子和智能网关设备中, 一个硬件板卡上集成多个MCU 已成常态——主控跑应用逻辑,协处理器…

作者头像 李华
网站建设 2026/3/11 6:04:34

GPT-SoVITS早停机制设置建议:防止资源浪费

GPT-SoVITS早停机制设置建议:防止资源浪费 在语音合成技术快速演进的今天,个性化音色克隆已不再是实验室里的高门槛实验,而是逐渐走入直播、有声书、虚拟偶像等实际应用场景。尤其是像 GPT-SoVITS 这类开源项目,凭借“一分钟数据即…

作者头像 李华
网站建设 2026/3/11 9:19:38

Proteus使用教程:实战案例解析单片机仿真应用

用Proteus玩转单片机仿真:从零搭建一个温度监控系统你有没有过这样的经历?写好了一段51单片机的代码,信心满满地烧录进芯片,结果LED不亮、LCD乱码、串口没输出……排查半天才发现是晶振接错了,或者延时函数算错了机器周…

作者头像 李华
网站建设 2026/3/11 19:14:44

废品回收小程序开发上线运营推广全维度玩法分析

在“双碳”政策推进与居民环保意识提升的背景下,传统废品回收的“低效、分散、报价模糊”痛点愈发突出,废品回收小程序凭借“在线下单上门回收透明报价”的核心优势,成为连接居民、企业与回收商的关键载体。从开发搭建到上线运营,…

作者头像 李华
网站建设 2026/3/11 6:35:52

python汽车丢失车辆高速收费管理系统 车联网位置信息管理软件的设计与实现_pycharm django vue flask

目录已开发项目效果实现截图开发技术路线相关技术介绍核心代码参考示例结论源码lw获取/同行可拿货,招校园代理 :文章底部获取博主联系方式!已开发项目效果实现截图 同行可拿货,招校园代理 python汽车丢失车辆高速收费管理系统 车联网位置信息管理软件…

作者头像 李华