新闻详情

新闻详情

首页 / 资讯中心 / 详情

GD32F303串口DMA收发:从原理到工程实践的完整指南

发布时间:2026/9/3 23:55:43来源:尧图网络
GD32F303串口DMA收发:从原理到工程实践的完整指南
简介面向GD32F303固件库开发者与嵌入式入门人群这套资料围绕USART通过DMA收发这一典型场景解决串口中断频繁占用CPU的问题提升数据收发效率适用于需要低功耗、高吞吐通信的MCU项目。压缩包内含1251个文件压缩后仅4.18MB其中以466个h头文件和431个c源文件为主体覆盖外设驱动、系统配置与用户应用同时提供uvproj/ewp等工程文件、hex/bin可执行文件及文档说明便于直接编译烧录与二次开发。已有3329人学习浏览具备一定热度。资料不仅包含完整的固件库工程源码和USART配置示例还附带LCD、LED等外设相关文件可帮助读者快速理解串口DMA收发机制并结合工程模板迁移到自己的项目。整体目录结构清晰适合结合CSDN教程与B站视频边学边练。1. 用DMA接管串口到底解决了什么问题1.1 没有DMA时串口收发有多“费人”做过嵌入式串口通信的朋友应该都有体会如果设备需要频繁和上位机、传感器、Wi-Fi模块之类的打交道只用最原始的“中断收发”方式写起来并不难难的是不知道它什么时候会出问题。用传统方式收数据时程序的逻辑通常是串口收到一个字节进一次中断把数据拷贝到缓冲区然后继续等下一个字节。这套逻辑在数据量小、波特率低的时候完全够用但一旦波特率上了115200甚至更高数据包又是几十上百个字节连续进来你会发现CPU大部分时间都被打断去处理串口中断了。更麻烦的是如果主循环里恰好在做耗时操作比如Flash擦写、浮点运算中断响应稍微慢半拍后面的字节就可能被覆盖导致整个数据包错位、丢数据。这时候就要轮到DMADirect Memory Access直接存储器访问出场了。简单说DMA就是一个专门的“搬运工”它能在不需要CPU介入的情况下把外设数据寄存器里的数据直接搬到内存缓冲区或者把内存里的数据直接搬到外设发送寄存器里。CPU只需要在“搬运工”干完活之后收到一个完成通知就行。1.2 为什么这个方案在GD32上值得单独写一篇GD32F303是国产Cortex-M4内核的MCU主频最高能到120MHz硬件配置上对标的是STM32F103系列。固件库的开发方式和STM32的标准外设库很像很多函数名、结构体定义甚至可以直接平移过去。但如果你真的把STM32的代码直接搬过来十有八九会在DMA这块碰到问题。原因很简单GD32的DMA控制器虽然整体结构类似但通道映射、对应关系甚至某些寄存器的位定义都和ST不完全一致。举个例子某些ST型号上USART1_TX对应的DMA通道在GD32F303上可能就不是同一个数字。这种细节不亲自查手册或者跑一遍根本不知道坑在哪里。所以我这篇的重点就放在基于GD32F303固件库把USART通过DMA收发这件事完整跑通讲清楚每一步为什么要这么配以及在实测中会遇到的典型问题。适合正在从STM32往GD32迁移的工程师也适合刚接触GD32固件库、想做串口高效通信的开发者参考。2. 硬件环境与基础配置2.1 我用到的硬件平台我测试用的板子是GD32F303VET6核心板主控芯片内置512KB Flash和64KB SRAM资源对串口DMA这种应用来说绰绰有余。调试工具用的是DAP-LinkIDE选择的是Keil MDK5.37版本固件库用的是GigaDevice官方提供的GD32F30x_Firmware_Library。如果你手里是GD32F303的其他型号比如CBT6、RCT6代码逻辑基本不用改只需要根据芯片型号调整启动文件、宏定义这些基础配置即可。固件库版本建议优先用官方最新的因为老版本里确实有一些DMA相关的bug修过。在开始之前先把需要的文件准备好。工程目录下的关键文件包括gd32f30x_usart.c/gd32f30x_usart.h串口驱动gd32f30x_dma.c/gd32f30x_dma.hDMA驱动gd32f30x_gpio.c引脚复用配置gd32f30x_rcu.c时钟使能这些文件在官方固件库包里都是现成的不用自己写但要确保添加进工程了。2.2 引脚、时钟和中断的基础准备我这次用的是USART1对应引脚是PA9TX和PA10RX。为什么选USART1因为它的DMA接收通道和发送通道刚好分布在两个DMA控制器上方便我分开说清楚映射关系实际项目里也很多用USART1做调试串口的。引脚复用要打开AFIO时钟并且把PA9、PA10配置为复用推挽输出和浮空输入rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_AF); gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9); gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_10);注意GD32的GPIO初始化和ST标准库有个小区别gpio_init函数的参数里把模式、速度、引脚号分开传不像ST那样用一个结构体打包。我见过不少从ST转过来的同事在这卡了一下倒不是多难就是容易顺手写错。USART1和DMA0、DMA1的时钟都要打开rcu_periph_clock_enable(RCU_USART1); rcu_periph_clock_enable(RCU_DMA0); rcu_periph_clock_enable(RCU_DMA1);关于中断配置发送方向如果用DMA的话主循环通常不需要进发送中断接收方向建议开启USART的空闲中断IDLE或者直接轮询DMA接收完成标志。后面在讲接收方案时我会细说这两种方式的取舍。3. 串口参数与DMA通道映射3.1 USART1参数配置串口参数可以先用标准库函数一次性配置到位。我习惯把波特率设为115200、数据位8位、无校验、1位停止位usart_deinit(USART1); usart_baudrate_set(USART1, 115200U); usart_word_length_set(USART1, USART_WL_8BIT); usart_stop_bit_set(USART1, USART_STB_1BIT); usart_parity_config(USART1, USART_PM_NONE); usart_hardware_flow_rts_config(USART1, USART_RTS_DISABLE); usart_hardware_flow_cts_config(USART1, USART_CTS_DISABLE); usart_receive_config(USART1, USART_RECEIVE_ENABLE); usart_transmit_config(USART1, USART_TRANSMIT_ENABLE); usart_enable(USART1);这里大部分配置都是常规操作我要特别提醒一个看起来不起眼但影响很大的点usart_receive_config和usart_transmit_config一定要记得配置否则收发数据会被硬件屏蔽掉。有些例程里写得太精简可能只调用了usart_enable如果遇到“串口完全没反应”的问题先检查这两行有没有配置。3.2 GD32F303的DMA通道映射关系GD32F303有两个DMA控制器DMA0和DMA1每个控制器下有多个通道这个系列具体是DMA0有6个通道、DMA1有6个通道。我的实测数据表明USART1_TX映射到DMA1的通道4USART1_RX映射到DMA0的通道3。这一点要和具体型号的手册里面的DMA request table对照确认不同子系列之间确实存在差异。下面是GD32F303系列中USART相关DMA映射的参考表外设请求DMA控制器通道USART0_TXDMA0通道4USART0_RXDMA0通道3USART1_TXDMA1通道4USART1_RXDMA0通道3USART2_TXDMA0通道2USART2_RXDMA0通道3把映射关系搞清楚是整个DMA配置里最关键的一步。我一开始就是用STM32F103的经验默认USART1_TX在DMA1的通道4、USART1_RX在DMA1的通道5结果初始化后RX死活不动。后来查了GD32F303的数据手册才发现接收通道居然跑到了DMA0上面。所以强烈建议哪怕你已经在ST平台上有很熟的经验用到GD32时也务必打开官方手册的DMA request table核对一遍。4. DMA通道初始化配置解析4.1 接收通道DMA0_Channel3配置DMA初始化在GD32固件库里是通过dma_init配合dma_parameter_struct结构体来实现的。参数看起来不少但拆开看就几件事方向、地址增量、数据宽度、缓冲区大小、优先级。先看接收方向dma_parameter_struct dma_rx_para; dma_deinit(DMA0, DMA_CH3); dma_struct_para_init(dma_rx_para); dma_rx_para.direction DMA_PERIPHERAL_TO_MEMORY; dma_rx_para.peripheral_addr (uint32_t)(USART1_DATA); dma_rx_para.memory_addr (uint32_t)rx_buffer; dma_rx_para.peripheral_inc DMA_PERIPH_INCREASE_DISABLE; dma_rx_para.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_rx_para.peripheral_width DMA_PERIPHERAL_WIDTH_8BIT; dma_rx_para.memory_width DMA_MEMORY_WIDTH_8BIT; dma_rx_para.number RX_BUFFER_SIZE; dma_rx_para.priority DMA_PRIORITY_HIGH; dma_init(DMA0, DMA_CH3, dma_rx_para); dma_circulation_enable(DMA0, DMA_CH3); dma_channel_enable(DMA0, DMA_CH3);逐个解释一下这几个关键参数direction方向设为从外设到内存即DMA把USART1的接收数据寄存器内容搬到我们指定的缓冲区。peripheral_addr外设地址要填USART1的数据寄存器地址。注意GD32固件库里这个寄存器不叫USART1-DR那种写法而是提供了一个宏USART1_DATA用起来更直观。peripheral_inc外设地址不自增因为每次都是从同一个数据寄存器取数据。memory_inc内存地址自增这样连续收到的字节会依次放到缓冲区的不同位置。peripheral_width和memory_width都是8位因为串口一个字节就是8位。number一次DMA传输的总字节数也就是要收多少个字节后产生完成事件。dma_circulation_enable开启循环模式。这是一个非常重要的配置开启后DMA接收完一轮数据会自动把缓冲区指针拉回起点重新开始接收不需要CPU重新配置特别适合连续不断的串口数据流。4.2 发送通道DMA1_Channel4配置发送方向的配置逻辑正好反着来方向是内存到外设其他大部分参数相同dma_parameter_struct dma_tx_para; dma_deinit(DMA1, DMA_CH4); dma_struct_para_init(dma_tx_para); dma_tx_para.direction DMA_MEMORY_TO_PERIPHERAL; dma_tx_para.peripheral_addr (uint32_t)(USART1_DATA); dma_tx_para.memory_addr (uint32_t)tx_buffer; dma_tx_para.peripheral_inc DMA_PERIPH_INCREASE_DISABLE; dma_tx_para.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_tx_para.peripheral_width DMA_PERIPHERAL_WIDTH_8BIT; dma_tx_para.memory_width DMA_MEMORY_WIDTH_8BIT; dma_tx_para.number 0; dma_tx_para.priority DMA_PRIORITY_HIGH; dma_init(DMA1, DMA_CH4, dma_tx_para);这里注意一个细节发送通道的number我先设置为0因为在没有数据要发的时候不需要DMA搬运任何内容。实际要发送数据时再调用一次dma_transfer_number_config设置本次要发送的字节数然后dma_channel_enable启动传输。如果想让发送也能在完成后自动拉回起点可以同样调用dma_circulation_enable(DMA1, DMA_CH4)。但发送方向一般不建议开循环因为发送是“一次性动作”每次要发的内容长度都不一样循环模式反而容易把上一次的数据重复发出去。5. 代码实现收发逻辑与主循环5.1 发送一个数据包的正确姿势DMA发送的最小执行单元是“发起一次搬运任务”。实际使用中最常见的是这个场景主程序里准备好了要发的一包数据然后调用发送函数让DMA把它们全部发出去发完通知一声。一个基础版本的发送函数可以这样写uint8_t tx_buffer[256]; void usart1_dma_send(uint8_t *data, uint16_t len) { uint16_t i; for (i 0; i len; i) { tx_buffer[i] data[i]; } dma_channel_disable(DMA1, DMA_CH4); dma_transfer_number_config(DMA1, DMA_CH4, len); dma_memory_address_config(DMA1, DMA_CH4, DMA_MEMORY_INCREASE_ENABLE, (uint32_t)tx_buffer); dma_channel_enable(DMA1, DMA_CH4); }这段代码的意思是先把要发送的数据拷到tx_buffer这个缓冲区要保证在DMA传输期间不会被其他地方改写然后关闭DMA通道——重新配置传输字节数——重新设置内存地址——最后使能通道启动搬运。提示每次重新配置DMA通道前一定要先执行dma_channel_disable。这不是多此一举而是手册里明确要求的一个操作。如果你在通道仍处于使能状态时去改number等参数行为是不可预知的实测中经常出现发送长度错误或者直接卡死的情况。如果你想在DMA发送完成时得到通知可以打开发送完成中断dma_interrupt_enable(DMA1, DMA_CH4, DMA_INT_FTF); nvic_irq_enable(DMA1_Channel4_IRQn, 0, 0);在中断服务函数里可以设置一个标志变量主循环检测到这个标志后就知道上一包数据已经发出去了。不过要提醒一句如果发送的速度很快、数据量很大中断频率会非常高建议先想清楚你在完成中断里要做的事是不是真的那么紧急否则直接用标志位轮询反而更省心。5.2 接收方向的两种典型方案接收方向是串口DMA方案里比较讲究的部分。我把它拆成两种常见场景来讲。场景一定长数据包如果你的通信协议是固定长度的比如每包数据固定16字节那用DMA就非常舒服。接收通道初始化时把number设为16DMA每收满16个字节就会置上传输完成标志。主循环或者中断里只要检测这个标志就知道一包完整的数据已经到位直接把rx_buffer里的内容拿去解析就行。这种方式的优点是完全不占CPU接收过程中CPU该干嘛干嘛数据满了才通知一次。缺点也很明显协议必须是定长的一旦出现粘包、半包处理起来反而麻烦。场景二不定长数据包空闲中断如果数据包长度不固定纯DMA定长接收就不够用了。常用的做法是DMA USART空闲中断IDLE Line Interrupt。所谓空闲中断就是检测到串口总线上一个字节都没有传输的时间超过了一个字节周期长度硬件会认为这一帧数据已经结束触发IDLE中断。在这个中断里我们可以计算本次一共收到了多少字节// 假设这是USART1的中断处理函数 void USART1_IRQHandler(void) { if (usart_interrupt_flag_get(USART1, USART_INT_FLAG_IDLE) ! RESET) { usart_interrupt_flag_clear(USART1, USART_INT_FLAG_IDLE); rx_len RX_BUFFER_SIZE - dma_transfer_number_get(DMA0, DMA_CH3); rx_complete_flag 1; } }这个代码的核心是利用了循环模式下DMA计数器的值来判断本次接收长度dma_transfer_number_get返回的是“还剩多少字节没搬完”用缓冲区总大小减去这个剩余值就是已经收到的字节数。注意这里有个常见的坑就是usart_interrupt_flag_clear是否真正能清掉IDLE标志。GD32F303固件库里清IDLE标志的时序和相关寄存器位和ST略有区别。如果出现“第一次触发之后第二次就进不来中断”的现象多半是标志没清干净或者清标志的顺序不对。建议实际操作时用示波器或逻辑分析仪观察一下RX引脚波形确认硬件确实产生了完整的一帧停止位。5.3 主循环的完整处理框架把发送和接收串起来主循环的框架大概是这样的uint8_t rx_buffer[128] {0}; volatile uint8_t rx_complete_flag 0; volatile uint16_t rx_len 0; int main(void) { // 系统时钟、GPIO、USART、DMA初始化代码省略 // ... while (1) { if (rx_complete_flag) { rx_complete_flag 0; // 把收到的数据原样回发验证通路是否正常 usart1_dma_send(rx_buffer, rx_len); // 实际项目中这里可以做协议解析、帧校验、业务处理等 } // 其他任务可以继续在这里跑 } }这个回环测试是调通DMA收发最快的方式电脑发一串数据给GD32GD32收到后原样发回电脑上能看到完全一致的内容就证明收发链路已经打通。6. 实测中遇到的典型问题与排查方法6.1 高频率踩坑点的速查表现象可能原因排查方法串口完全无输出发送DMA通道未使能或映射错误检查DMA通道编号与USART1映射表是否一致接收缓冲区全是什么都没变接收DMA未开启循环模式或number参数配置为0单步调试查看DMA寄存器中的CHxCNT值收上来的数据整体错位内存地址增量未开启后到的数据覆盖了前面的检查memory_inc是否配置为DMA_MEMORY_INCREASE_ENABLE数据丢后半段DMA搬运还没完成缓冲区就被上一层逻辑读取走了判断DMA通道完成标志后再读取数据发送完一次后第二次发不出去重复配置时未先dma_channel_disable在dma_transfer_number_config之前先关闭通道空闲中断只触发一次IDLE标志清除时序不对参考官方勘误手册按规定顺序读取状态寄存器和数据寄存器这些基本都是DMA开发里最高频的几个坑。我当年调试第一个GD32串口DMA工程时至少在上面四个问题里来回打转尤其是“第二次发不出去”这个困扰了我整整一个下午最后就是因为在使能状态下直接改了number导致通道状态异常。6.2 两个容易被忽略的关键细节第一个细节是缓冲区生命周期管理。DMA传输是异步的CPU把“发送任务”交给DMA之后DMA还在慢慢搬运如果此时主循环里把tx_buffer的内容给覆盖了那发出去的数据就会变成乱七八糟的值。同理接收方向DMA还在往rx_buffer里写数据如果主循环提前把数据取走去解析可能会读到半包。所以收发缓冲区一定要做好“生产者-消费者”的同步管理最简单的方式就是使用完成标志配合memcpy把数据及时拷走。第二个细节是DMA请求的优先级和仲裁。GD32的DMA0和DMA1每个通道都有优先级设置如果一个DMA控制器上挂了多个外设请求比如USART0和USART1都在用DMA0优先级设置不当可能导致某个外设长时间得不到DMA服务数据被丢弃。建议把实时性要求高的通道优先级设高一些比如本项目中的接收通道设为最高。6.3 一个实用的验证方法在调通基本功能后我强烈建议做一个压力测试让电脑以115200波特率持续发送100KB到500KB的随机数据GD32收到后用DMA回发同样长度的数据电脑端对比收发是否完全一致。只有在这个测试中保持零错误才能认为这套DMA链路在真实环境中是可靠的。如果错误率高不要急着改DMA配置先用逻辑分析仪看波形。常见的原因是引脚接触不良、地线干扰、波特率误差偏大这些物理层问题。DMA寄存器配置反而不太可能是主因因为它本身的配置就是“黑盒”的逻辑对就全对逻辑错就全错很少出现概率性错误。7. 个人经验小结这个项目做到后面我个人最大的体会是DMA说白了就是一个“外设数据快递员”你只要把源地址、目的地址、搬运数量、搬运方向这四个核心参数配置清楚它就按照约定把数据来回搬好。真正花时间的部分不是把一次收发调通而是把收发过程中各种边界情况想明白比如缓冲区什么时候可以写、什么时候不能写、DMA什么时候算完成、标志位什么时候该清。最后再分享一个小技巧GD32的DMA通道编号和USART的搭配关系建议直接做成一个表格贴在代码注释里或者写成一个宏定义。哪怕是同一个系列的不同型号这个映射关系也可能不一样光靠记忆真的不可靠。把映射表写清楚下次换型号或者换串口的时候排查起来能省一大半时间。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

会话级动态表单渲染与实时校验联动 2026/9/4 0:31:48

会话级动态表单渲染与实时校验联动

会话级动态表单渲染与实时校验联动 在复杂 Agent 交互中,纯文本对话往往无法胜任高精度、多字段的业务参数录入。比如订机票、填写运维工单或配置数据看板时,如果大模型一段一段地追问用户“您的出发时间是哪天?”、“您需要几张票&#xff1…

阅读更多 →
伊顿数据中心固态变压器:AI算力时代供配电架构的范式跃迁 2026/9/4 0:31:48

伊顿数据中心固态变压器:AI算力时代供配电架构的范式跃迁

数据中心正经历一场前所未有的电力挑战。GPU集群的功耗密度以每两年翻番的速度攀升,单机架功率从传统的5-10kW飙升至40kW甚至100kW以上,传统供配电架构的多级变换损耗、庞大的占地空间以及毫秒级响应能力的不足,已成为制约AI算力部署的物理瓶…

阅读更多 →
用Wan3.0制作30秒广告:从分镜脚本到成片的完整工作流 2026/9/4 0:31:48

用Wan3.0制作30秒广告:从分镜脚本到成片的完整工作流

商业广告不可能靠一条随机的短视频拼凑出来。30 秒不算长,但留给每个镜头的有效信息反而非常紧:文案、画面、字幕、配音、音乐、转场都要为同一个卖点服务。用 Wan3.0 做广告内容时,最常被低估的问题不是单个镜头生成得“像不像”&#xff0c…

阅读更多 →
Dify工作流YAML模板库:160+案例解析与实战应用指南 2026/9/4 0:31:48

Dify工作流YAML模板库:160+案例解析与实战应用指南

简介:本资源为面向Dify低代码AI应用开发者的160余个可直接导入的工作流模板集合,适用于内容创作、教育出版、知识管理及自动化数据处理等多场景,尤其适合不熟悉编程但希望快速搭建智能工作流的运营、教育者与中小企业技术实践者。压缩包含109…

阅读更多 →
UE5游戏开发实战:构建RTS资源采集闭环系统 2026/9/4 0:28:48

UE5游戏开发实战:构建RTS资源采集闭环系统

很多刚开始研究 UE5 RTS 的同学,通常会把第一个 Demo 放在“单位移动”和“兵种战斗”上,结果做到中期才发现:真正把整个玩法串起来的,其实是资源采集这一条非常基础又非常容易被小看的链路。单位走到矿脉、采集、带回基地、更新 …

阅读更多 →
Claude API工程化:从Prompt Eval到稳定运行的关键链路 2026/9/4 0:28:48

Claude API工程化:从Prompt Eval到稳定运行的关键链路

Claude Certified Architect 这套前置课程里,最容易被低估的其实不是 API 怎么调,而是 Part 5 这条从 Prompt Eval 到 Running 的完整链路。很多人拿到 Claude API 之后,第一件事就是复制一个示例请求跑通,然后以为结束了。但真正…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞