18. GD32VW553 DMA控制器原理详解:从轮询、中断到高效数据传输

发布时间:2026/7/11 5:43:39

18. GD32VW553 DMA控制器原理详解:从轮询、中断到高效数据传输 18. GD32VW553 DMA控制器原理详解从轮询、中断到高效数据传输大家好我是老李一个在嵌入式领域摸爬滚打了十几年的工程师。今天咱们来聊聊GD32VW553里的一个“效率神器”——DMA控制器。很多刚开始接触单片机的朋友一听到DMA就觉得头大寄存器、通道、仲裁一堆概念。别怕今天我就用最直白的话带你从最基础的轮询、中断讲起一步步拆解DMA是怎么工作的最后再手把手教你用DMA来高效接收串口数据。保证你听完之后不仅能懂原理还能立刻上手用起来。1. 数据传输的“三板斧”轮询、中断与DMA在嵌入式系统里CPU也就是单片机的大脑经常需要在外设比如串口、ADC和存储器比如内存之间搬运数据。怎么搬最省力、最高效通常有三种方法咱们挨个看看。1.1 轮询法CPU当“监工”想象一下你CPU要等一个快递外设数据。轮询法就是你隔几秒就跑下楼看看快递柜有没有你的包裹。在程序里就是在主循环里不停地检查外设的状态标志位。while(1) { // 主循环干其他事... if (USART_GetFlagStatus(USART0, USART_FLAG_RXNE) ! RESET) { // 发现串口收到数据了 data USART_ReceiveData(USART0); // CPU亲自去把数据读进来 // 处理数据... } // ...继续干其他事 }特点实现简单但CPU太累了。如果数据来得不频繁或者数据量很小还能凑合。一旦数据量大或者来得勤CPU就光顾着“看快递”了其他正经活比如控制电机、刷新屏幕全耽误了。在实际项目里我一般只在调试或者极简单的场景下用轮询。1.2 中断法外设“打电话”叫CPU还是等快递这次你装了个APP中断系统。快递到了柜子会自动给你手机发个通知触发中断你才下楼去取。// 中断服务函数 void USART0_IRQHandler(void) { if (USART_GetFlagStatus(USART0, USART_FLAG_RXNE) ! RESET) { data USART_ReceiveData(USART0); // CPU响应通知亲自取数据 // 处理数据... } }特点CPU不用再傻等了效率比轮询高。但是取快递搬运数据这个体力活还是CPU自己干的。如果快递特别多高频中断或者每个快递都巨重大数据量CPU就会频繁地停下手中的活跑去取快递取完再回来接着干这个“放下-拿起”的过程上下文切换本身也很耗时。对于数据量不大的常规通信中断法是个不错的选择我用得也很多。1.3 DMA法雇了个“专职搬运工”这次你直接雇了个跑腿小哥DMA控制器。快递到了柜子通知跑腿小哥小哥自己去取然后按照你给的地址内存地址把快递放好。放好了再给你发个消息说“搞定了”。整个过程你完全不用插手。特点CPU彻底解放了数据搬运的脏活累活全交给DMA这个硬件模块去干CPU可以专心执行核心计算任务。这对于像音频流、图像数据、高速采集这类大数据量传输的场景效率提升是巨大的。GD32VW553内置的DMA控制器就是咱们的“金牌跑腿小哥”。简单总结一下轮询是CPU自己不停地问中断是外设叫CPUCPU自己去干DMA是外设叫DMADMA自己去干干完告诉CPU一声。2. 深入认识GD32VW553的DMA控制器知道了DMA的好咱们来看看GD32VW553这位“跑腿小哥”到底有啥本事。2.1 核心架构与特性根据GD32VW553的用户手册它的DMA控制器主要有以下特点1个DMA控制器所有通道都归它管。8个独立通道可以理解为有8个跑腿小哥每个可以负责一项不同的搬运任务比如通道1给串口用通道2给ADC用。两个AHB总线接口一个专门连接存储器内存一个专门连接外设如串口、SPI。这样它就能同时在存储器和外设之间“修路”数据搬运更顺畅。8个4字深度的FIFO每个通道都有一个小仓库FIFO深度是4个字32位系统下1字4字节。数据先从源地址读到这个小仓库暂存一下再搬到目的地址。这个小仓库能平滑数据流防止因为总线忙而导致数据丢失。支持多种数据宽度可以按8位、16位、32位来搬运数据非常灵活。2.2 DMA的三种工作模式DMA小哥能接三种类型的活模式源端目的端典型应用场景外设到存储器外设如USART数据寄存器存储器如数组rx_buffer串口DMA接收串口收到数据DMA自动存到内存数组里。存储器到外设存储器如数组tx_buffer外设如USART数据寄存器串口DMA发送DMA自动从内存数组取数据发给串口发送出去。存储器到存储器存储器如数组src存储器如数组dst内存数据批量拷贝比如图像缓冲区的复制。这里有个关键点在“存储器到存储器”模式下DMA控制器会使用它的外设主机接口去读源存储器用存储器主机接口去写目的存储器。虽然名字叫“外设接口”但它本质上也是一个高速访问存储器的通道。2.3 通道与外设的映射8个通道Channel 0-7不是随便用的每个通道固定负责几个特定的外设。你需要根据数据手册里的“DMA请求映射表”来分配。比如你想用串口0USART0的接收功能做DMA可能就需要查表找到它对应的是DMA的哪个通道例如Channel 4。配置的时候千万不能配错这个坑我踩过调了半天没数据最后发现通道号写错了。2.4 优先级仲裁谁先来如果好几个外设同时喊DMA小哥干活多个通道同时有请求听谁的呢这就需要一个“调度员”——仲裁器。 GD32VW553的DMA有两个仲裁器分别管外设端和存储器端。决定先干哪个活的规则有两层软件优先级你在程序里给每个通道设置的优先级分为低、中、高、超高四级。硬件优先级如果两个通道的软件优先级一样那么通道编号小的那个比如Channel 2比Channel 5优先级更高。2.5 地址生成怎么找下一个包裹DMA小哥搬东西要知道从哪里搬源地址搬到哪里去目的地址。搬完一个下一个地址在哪这就由“地址生成算法”决定。固定模式地址不变。比如源地址是串口的数据寄存器地址0x40013804每次DMA都从这个固定地址读数据。增量模式每搬完一个数据地址自动增加。增加多少呢取决于你设置的数据宽度。如果你设置的是32位4字节传输那地址就自动4。配置心得外设端比如串口的数据寄存器地址是固定的所以必须设为固定模式。存储器端比如你定义的数组uint8_t buffer[100]地址是连续的所以必须设为增量模式。 这个配置反了数据就全乱套了是一个常见的错误点。2.6 循环模式与单次模式单次模式DMA小哥搬完你指定的数量比如100个字节就停工了然后发个中断告诉你“活干完了”。如果你想让他再搬一次你得在中断里手动重新设置搬运数量再启动他。循环模式DMA小哥搬完指定的数量后硬件会自动把搬运数量重置为初始值然后接着搬循环不断。特别适合需要持续传输的场景比如ADC连续采样。2.7 DMA也会“打报告”中断事件DMA小哥虽然能干但遇到事情还是会向CPU汇报。每个通道都有独立的中断可以报告5种情况传输完成一整批数据全部搬完了。半传输完成数据搬了一半比如你要搬100个搬完50个时报告一次。这个在处理“双缓冲区”时特别有用。传输错误出错了比如访问了非法地址DMA会停止工作。FIFO错误/异常那个小仓库FIFO操作出问题了。单次模式异常在单次传输模式下配置不当可能触发。我们可以根据需要使能这些中断。比如我们通常最关心“传输完成中断”知道数据搬完了好去处理。3. 实战串口DMA接收数据理论说了这么多不来点实战就是纸上谈兵。咱们就以最常用的串口DMA接收为例看看具体怎么用。3.1 为什么串口接收要用DMA假设你的设备要通过串口接收一帧很长的数据比如512字节的配置文件。用中断每收到1个字节串口就触发一次中断CPU就要跳转一次。收512字节CPU就被打断了512次大部分时间都在做保存现场、取数据、恢复现场这些杂活。用DMA你只需要告诉DMA两件事1从串口数据寄存器这个地址搬2搬到内存的buffer[512]这个数组里。然后DMA就会在后台默默工作每收到一个字节就自动存到数组里。512个字节收完DMA才产生一次中断通知CPU。CPU在这期间完全自由3.2 配置思路与步骤结合GD32VW553的库函数配置串口DMA接收的大致流程如下第一步初始化DMA通道dma_parameter_struct dma_init_struct; // 1. 选择对应的DMA和通道根据数据手册映射表假设USART0_RX用DMA0_CH4 dma_deinit(DMA0, DMA_CH4); // 2. 配置外设地址固定为串口数据寄存器地址 dma_init_struct.periph_addr (uint32_t)USART_DATA(USART0); // 3. 配置内存地址你的接收数组 dma_init_struct.memory_addr (uint32_t)rx_buffer; // 4. 配置传输方向外设到存储器 dma_init_struct.direction DMA_PERIPH_TO_MEMORY; // 5. 配置数据宽度外设和存储器都设为8位和串口数据宽度一致 dma_init_struct.periph_width DMA_PERIPH_WIDTH_8BIT; dma_init_struct.memory_width DMA_MEMORY_WIDTH_8BIT; // 6. 设置优先级 dma_init_struct.priority DMA_PRIORITY_HIGH; // 7. 设置外设地址不增量内存地址增量 dma_init_struct.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_init_struct.memory_inc DMA_MEMORY_INCREASE_ENABLE; // 8. 设置循环模式如果需要持续接收 dma_init_struct.circular_mode DMA_CIRCULAR_MODE_ENABLE; // 9. 设置要传输的数据数量 dma_init_struct.number BUFFER_SIZE; // 10. 应用配置 dma_init(DMA0, DMA_CH4, dma_init_struct);第二步配置串口使其产生DMA请求// 使能USART0的DMA接收请求 usart_dma_receive_config(USART0, USART_DENR_ENABLE);第三步使能DMA通道开始工作dma_channel_enable(DMA0, DMA_CH4);第四步可选使能DMA传输完成中断// 使能DMA通道4的传输完成中断 dma_interrupt_enable(DMA0, DMA_CH4, DMA_INT_FTF); // 配置NVIC嵌套向量中断控制器 nvic_irq_enable(DMA0_Channel4_IRQn, 0, 0);第五步编写中断服务函数// 当DMA搬完指定数量的数据后会进入这个中断 void DMA0_Channel4_IRQHandler(void) { if(dma_interrupt_flag_get(DMA0, DMA_CH4, DMA_INT_FLAG_FTF)) { // 清除中断标志 dma_interrupt_flag_clear(DMA0, DMA_CH4, DMA_INT_FLAG_FTF); // 数据处理此时rx_buffer里已经装满了新数据 process_received_data(rx_buffer, BUFFER_SIZE); // 如果是单次模式可能需要在这里重新设置传输数量并启动 // dma_channel_disable(DMA0, DMA_CH4); // dma_transfer_number_config(DMA0, DMA_CH4, BUFFER_SIZE); // dma_channel_enable(DMA0, DMA_CH4); } }3.3 一个更高效的技巧串口空闲中断DMA上面的方法在数据长度固定时很好用。但如果数据长度不定怎么办总不能一直开着DMA循环接收吧这里分享一个我项目中常用的“组合拳”串口空闲中断IDLE DMA。思路使能串口的空闲中断当串口总线上一段时间没有新数据就产生中断。同时开启DMA接收方向指向一个足够大的循环缓冲区。过程数据开始传输DMA默默地将数据搬到缓冲区。一帧数据传完了串口总线空闲触发空闲中断。在空闲中断里我们通过查询DMA当前还剩余多少数据没搬有一个寄存器存这个值就能计算出这一帧到底收到了多少字节的数据。然后处理这一帧数据并重置DMA准备接收下一帧。优势无论这一帧数据有多长只在帧结束时产生一次空闲中断。CPU介入的频率极低效率非常高。// 串口空闲中断服务函数示例 void USART0_IRQHandler(void) { // 判断是否是空闲中断 if(usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE) ! RESET) { // 清除空闲中断标志方法比较特殊需要先读USART_STAT再读USART_DATA usart_data_receive(USART0); // 计算本次接收到的数据长度 // 假设DMA配置的传输总数量是RX_BUFFER_SIZE remaining_count dma_transfer_number_get(DMA0, DMA_CH4); received_len RX_BUFFER_SIZE - remaining_count; // 处理 received_len 长度的数据 process_data(rx_buffer, received_len); // 重置DMA准备下次接收先失能再设置数量再使能 dma_channel_disable(DMA0, DMA_CH4); dma_transfer_number_config(DMA0, DMA_CH4, RX_BUFFER_SIZE); dma_channel_enable(DMA0, DMA_CH4); } }好了关于GD32VW553的DMA从原理到实战咱们就聊到这里。DMA用熟了绝对是提升系统性能的利器。刚开始配置时寄存器比较多容易懵多对照数据手册和例程动手调一调很快就能掌握。遇到问题多想想DMA这位“跑腿小哥”的工作流程思路就会清晰很多。

相关新闻