☰
嵌入式DMA驱动开发实战:从原理到STM32配置与避坑指南
2026/10/7 20:51:48 网站建设 项目流程

1. 为什么DMA是嵌入式驱动开发的必修课

搞嵌入式驱动开发,绕不开的一个话题就是DMA。你可以不知道某些冷门外设的寄存器怎么配,但如果连DMA都玩不转,那基本等于还没入门。我做了十多年底层驱动,从早期的8位机到现在主流的Cortex-M系列,DMA几乎是每个量产项目里都会用到的核心机制。这一期就专门把DMA这件事讲透,从概念到实操,从配置到踩坑,尽量把我知道的都倒出来。

先说清楚DMA到底是什么。DMA全称Direct Memory Access,直接内存访问。它的核心价值就一句话:让数据在外设和内存之间搬运的时候,不需要CPU插手。没有DMA的时候,串口每收到一个字节就触发一次中断,CPU要进中断、读寄存器、写缓冲区,一个字节折腾一次。波特率115200的时候,一秒钟来11520个字节,CPU就要被打断一万多次。这个开销在小项目里可能还能忍,一旦系统里同时跑着串口、SPI、ADC、SD卡,CPU基本就废了,全在搬数据。

DMA的出现就是为了解决这个矛盾。你告诉DMA控制器:源地址在哪、目标地址在哪、搬多少个、搬完要不要告诉我。然后你该干嘛干嘛去,DMA自己在后台把活干了,搬完了再给你一个中断。CPU的利用率一下子就上来了。这也是为什么几乎所有带高速外设的MCU都会配DMA控制器,STM32系列更是从F0到H7全线标配,而且往往不止一个DMA控制器。

这一期的内容适合谁看?如果你正在学STM32的驱动开发,或者工作中需要用到串口DMA、SPI DMA、ADC DMA这些常见组合,又或者你之前用过DMA但总是遇到数据错位、传输不完整、测速上不去这些问题,那这篇内容应该能帮到你。我会从设计思路讲到具体配置,再讲到实际调试中遇到的各种坑,尽量做到看完就能上手。

2. DMA驱动的整体设计思路与方案选型

2.1 什么时候该用DMA,什么时候不该用

很多人一听说DMA能减轻CPU负担,就恨不得所有外设都挂上DMA。但实际上DMA不是万能的,用错了地方反而会增加复杂度。我的经验判断标准是这样的:如果一次数据传输量超过8个字节,或者传输频率高于每秒1000次,那就值得考虑DMA。反过来,如果只是偶尔发几个字节的命令,用中断或者轮询反而更简单直接。

举个例子,配置一个传感器寄存器,通常就是写一两个字节,这种场景用DMA纯属杀鸡用牛刀,配置DMA的代码比直接写寄存器还长。但如果是串口接收不定长数据、ADC连续采样几百个点、SPI读写SD卡的一个扇区,这些场景不用DMA就是跟自己过不去。

还有一个容易被忽略的点:DMA虽然不占CPU,但它占总线。DMA搬运数据的时候会跟CPU争抢总线带宽,如果DMA传输过于频繁,CPU访问内存的速度反而会下降。所以在高实时性要求的场景里,DMA的优先级配置也要仔细考虑,不能无脑给最高。

2.2 三种常见传输模式的选择逻辑

DMA的传输模式基本分三种:单次传输、循环传输、以及带FIFO的突发传输。这三种模式对应不同的应用场景,选错了要么功能不对,要么性能上不去。

单次传输模式最简单,你配置好长度,DMA搬完就停,触发一次传输完成中断。适合那些一次性的、长度固定的传输,比如SPI写一个数据块到Flash。循环传输模式则是搬完一圈自动回到起点继续搬,不需要CPU干预,适合ADC连续采样、串口持续接收这类场景。带FIFO的突发传输是高级玩法,DMA内部有个小缓冲区,可以攒够一定数量的数据再一次性往总线上发,这样能提高总线利用率,适合高带宽场景比如SPI驱动LCD刷屏。

选哪种模式,核心看你的数据流是"一次性"还是"持续性",是"低速率"还是"高带宽"。我一般会先画一个数据流图,标清楚数据从哪来到哪去、频率多少、单次量多大,然后对着图来选模式,基本不会错。

2.3 中断与DMA的配合策略

DMA不是完全不需要CPU,它只是把"搬数据"这件事从CPU手里接过去了,但"搬完了要干什么"还是得CPU来处理。所以DMA和中断的配合策略很关键。

常见的做法是:DMA传输完成触发中断,在中断里做数据处理或者启动下一次传输。但这里有个坑,如果你在DMA完成中断里做太多事情,比如解析一大段协议、写Flash,那中断时间会很长,影响系统实时性。更好的做法是DMA完成中断里只置一个标志位或者发一个信号量,把实际处理放到主循环或者任务里去做。

还有一种高级用法是"双缓冲",DMA搬第一块的时候CPU处理第二块,搬第二块的时候CPU处理第一块,交替进行。这个在音频处理、高速数据采集里很常见,能把CPU和DMA的并行度拉满。STM32的很多DMA控制器都支持双缓冲模式,配置起来也不复杂,后面实操部分我会详细讲。

3. 核心寄存器与配置细节深度解析

3.1 DMA控制器的核心寄存器组

不管哪家的MCU,DMA控制器的寄存器组基本都包含这几类:全局配置寄存器、通道配置寄存器、源地址寄存器、目标地址寄存器、传输长度寄存器。STM32的DMA控制器稍微复杂一点,还分DMA1和DMA2,每个控制器有多个通道,每个通道有独立的配置寄存器和中断标志。

以STM32F103C8T6为例,它的DMA1有7个通道,每个通道可以映射到不同的外设请求。这个通道映射是硬件固定的,不能随便改,比如USART1_TX固定映射到DMA1_Channel4,USART1_RX固定映射到DMA1_Channel5。配置之前一定要查参考手册的DMA请求映射表,选错通道就是白忙活。

通道配置寄存器里几个关键位需要特别注意:传输方向位(DIR)决定是外设到内存还是内存到外设;循环模式位(CIRC)决定是否自动重载;外设和内存地址增量位(MINC/PINC)决定地址是否自动递增;优先级位(PL)决定多个通道同时请求时谁先谁后。这些位配错一个,行为就完全不对。

3.2 地址对齐与数据宽度的匹配问题

这是新手最容易翻车的地方。DMA传输的时候,源地址和目标地址的数据宽度必须匹配,或者至少要有合理的转换关系。比如外设数据寄存器是16位的,你内存缓冲区定义成8位数组,那DMA搬出来的数据就会错位。

STM32的DMA支持字节、半字、字三种数据宽度,外设和内存可以分别配置。如果两边宽度不一致,DMA会按照配置进行打包或拆分。但这个机制有前提,传输长度和地址对齐都要满足条件。我见过太多人因为缓冲区没做4字节对齐,导致DMA传输HardFault的案例。

实操建议:所有用于DMA的缓冲区,一律用__attribute__((aligned(4)))做4字节对齐,长度也尽量取4的倍数。这个习惯能帮你避开90%的对齐问题。

3.3 传输长度与循环模式的参数计算

传输长度寄存器的值直接决定DMA搬多少个数据单元。注意这里说的是"数据单元",不是字节。如果你配置数据宽度是半字(16位),长度写100,那实际搬的是200字节。这个换算关系一定要搞清楚。

循环模式下,传输长度决定了循环缓冲区的大小。比如ADC连续采样,你想做一个256点的滑动窗口,那传输长度就设256,DMA搬满256个点后自动回到起点继续搬。CPU只需要在完成中断里处理这256个点就行。这里有个细节:循环模式下完成中断的触发时机是搬完一整轮,不是每个点都触发,所以中断频率等于采样率除以缓冲区长度,这个关系在算CPU负载的时候要用到。

4. 串口DMA收发完整实操

4.1 串口DMA发送的配置流程

串口DMA发送是最常见的应用场景,配置流程我按实际项目里的顺序来写。首先初始化串口本身,波特率、数据位、停止位这些常规配置不用多说。然后开启串口的DMA发送请求,在STM32里就是置位USART_CR3寄存器的DMT位。

接着配置DMA通道,方向设为内存到外设,外设地址填串口数据寄存器地址,内存地址填你的发送缓冲区,长度填要发送的字节数。外设地址不递增,内存地址递增,优先级根据系统情况定。配置完使能通道,然后使能串口的DMA发送请求,数据就开始搬了。

这里有个关键点:DMA发送完成中断触发的时候,只代表数据已经从内存搬到了串口的发送寄存器,不代表数据已经从串口线上发出去了。如果你在发送完成中断里立刻关闭串口或者进入低功耗,最后几个字节可能会丢。正确的做法是等串口的TC(Transmission Complete)标志置位,或者用DMA完成中断加一个适当的延时。

// 串口DMA发送配置示例(STM32标准库风格) void uart_dma_send(uint8_t *buf, uint16_t len) { DMA_Cmd(DMA1_Channel4, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel4, len); DMA1_Channel4->CMAR = (uint32_t)buf; DMA_Cmd(DMA1_Channel4, ENABLE); USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); }

4.2 串口DMA接收的不定长处理

串口接收比发送麻烦,因为接收长度往往是不确定的。用DMA接收固定长度很简单,但实际协议里经常是变长帧。常见的解决方案有三种:一是用空闲中断配合DMA,串口空闲时触发中断,在中断里读取DMA已经搬运的长度;二是用DMA的传输完成中断加超时判断;三是用循环DMA加软件解析。

我最常用的是空闲中断方案,STM32的串口支持IDLE中断,总线空闲一个字节时间就触发。配合DMA的剩余长度寄存器,能精确算出这一帧收了多少字节。这个方案的好处是不需要预先知道帧长,也不占用定时器资源。

// 空闲中断+DMA接收处理 void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) { USART_ReceiveData(USART1); // 清IDLE标志 uint16_t recv_len = BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); // recv_len就是这一帧的实际长度 process_frame(rx_buf, recv_len); // 重新配置DMA准备下一帧 DMA_Cmd(DMA1_Channel5, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel5, BUF_SIZE); DMA_Cmd(DMA1_Channel5, ENABLE); } }

4.3 串口DMA测速方法与实测数据

很多人关心DMA到底能跑多快,我实测过几组数据供参考。STM32F103C8T6,主频72MHz,串口波特率配置到4.5Mbps,DMA发送1KB数据,实测传输时间约2.3ms,换算下来有效速率约3.5Mbps,接近波特率上限。如果用中断方式发送同样的数据,CPU占用率会飙到60%以上,而DMA方式CPU占用不到5%。

测速的方法很简单:在发送前翻转一个GPIO,在DMA完成中断里再翻转一次,用示波器量两个边沿之间的时间。或者用定时器计数,精度也够。注意测速的时候要排除串口本身的波特率限制,DMA再快也快不过串口线的物理速率。

注意:有些朋友测速发现DMA传输时间比理论值长很多,八成是因为DMA优先级设太低,被其他通道抢了总线。把DMA通道优先级调到最高再测一次。

5. SPI DMA与ADC DMA的实战配置

5.1 SPI DMA驱动LCD刷屏的参数计算

SPI DMA最典型的应用就是驱动LCD刷屏。以常见的ILI9341为例,分辨率240x320,刷满一屏需要240x320x2=153600字节。SPI时钟配置到STM32F407能跑的42MHz,理论传输时间约153600x8/42M≈29ms。实际用DMA传输,实测在35ms左右,刷屏效果很流畅。

配置要点:SPI的DMA请求要同时开TX和RX(如果只写不读,RX可以不开),DMA方向内存到外设,数据宽度根据SPI数据帧格式定,通常是字节。传输长度就是刷屏数据的字节数。这里有个优化技巧:把LCD的片选和命令/数据切换也纳入DMA流程,用GPIO配合DMA传输完成中断来切换,能进一步减少CPU干预。

5.2 ADC DMA连续采样的双缓冲实现

ADC连续采样是DMA循环模式的经典应用。配置ADC为连续转换模式,DMA为循环模式,缓冲区大小设为N,DMA就会不停地往缓冲区里填数据,填满一圈触发一次中断。CPU在中断里处理这一圈数据,同时DMA继续填下一圈。

如果要做到零等待,就用双缓冲。STM32的DMA支持双缓冲模式,配置两个缓冲区指针,DMA填缓冲区A的时候CPU处理缓冲区B,填满A自动切到B并触发中断,CPU再处理A。这样理论上CPU处理速度只要跟得上采样速度,就不会丢数据。

// ADC DMA双缓冲配置关键代码 DMA_DoubleBufferModeConfig(DMA2_Stream0, (uint32_t)buf_b, DMA_Memory_0); DMA_DoubleBufferModeCmd(DMA2_Stream0, ENABLE); DMA_SetCurrDataCounter(DMA2_Stream0, BUF_SIZE); DMA_Cmd(DMA2_Stream0, ENABLE); ADC_DMACmd(ADC1, ENABLE); ADC_SoftwareStartConv(ADC1);

5.3 TIM DMA Burst在高级应用中的用法

TIM DMA Burst是个比较高级的用法,简单说就是用DMA来批量更新定时器的寄存器。比如你想用定时器输出一串任意波形的PWM,每个周期的占空比都不同,就可以把占空比序列放在内存里,用DMA在每次更新事件时自动把下一个值搬到CCR寄存器。这样CPU完全不用管,定时器自己就能输出复杂波形。

这个用法在电机控制、LED调光、信号发生器里很有价值。配置的时候要注意DMA请求源要选TIM的更新事件,传输长度等于波形点数,循环模式开启。STM32的TIM DMA Burst还支持突发传输,一次搬多个寄存器,适合需要同时更新多个通道的场景。

6. 常见问题排查与避坑经验实录

6.1 DMA传输不完整或数据错位的排查

这是最高频的问题。排查思路我一般按这个顺序走:先确认传输长度寄存器的值对不对,再看地址对齐有没有问题,然后检查数据宽度配置是否匹配,最后看DMA通道有没有被其他外设抢占。

有个很隐蔽的坑:DMA传输过程中如果CPU访问了正在被DMA搬运的内存区域,可能会读到中间状态的数据。解决办法是用内存屏障指令,或者在DMA完成中断里再访问。还有一种情况是编译器优化导致缓冲区被优化掉,DMA往一个"不存在"的地址搬数据,这个要在缓冲区定义前加volatile关键字。

6.2 DMA测速失败与性能不达标的常见原因

测速失败通常有几个原因:一是DMA优先级配置不当,被高优先级通道一直抢占;二是总线矩阵配置问题,某些MCU的DMA和CPU访问不同SRAM区域的速度不一样;三是外设本身的速率限制,比如SPI时钟没配到最高。

我遇到过一次很典型的案例:SPI DMA刷屏速度只有理论值的一半,查了半天发现是SPI的时钟分频配错了,实际时钟只有21MHz而不是42MHz。所以测速之前一定要先确认外设本身的时钟配置。

6.3 DMA与Cache一致性问题

这个问题在带Cache的MCU上(比如STM32H7、F7系列)特别突出。DMA搬运数据不经过Cache,但CPU访问内存会经过Cache,这就导致CPU可能读到Cache里的旧数据,或者DMA读到还没写回内存的新数据。

解决办法有两种:一是把DMA缓冲区放在非Cache区域,比如STM32H7的DTCM RAM;二是在DMA传输前后做Cache清理和无效化操作。我一般推荐第一种,省事而且没有性能损失。如果非要用Cache区域,那就要严格遵循"写前清理、读后无效"的原则。

问题现象可能原因排查方法解决方案
数据错位地址对齐问题检查缓冲区地址4字节对齐
传输不完整长度配置错误读长度寄存器按数据单元换算
速度不达标优先级或时钟问题查时钟树和优先级调整配置
数据丢失Cache不一致检查Cache配置用非Cache区域
HardFault地址越界查内存映射检查缓冲区大小

6.4 实操心得与独家避坑技巧

分享几个我踩过坑之后总结的经验。第一,DMA缓冲区永远不要放在栈上,栈空间小而且可能被覆盖,一定要用全局数组或者静态分配。第二,DMA传输期间不要动态改变缓冲区内容,如果非要改,先停DMA再改再重启。第三,多个DMA通道同时工作时,优先级配置要仔细,高实时性的通道给高优先级,但也不要全部给最高,否则等于没分优先级。

还有一个技巧:调试DMA的时候,善用DMA的传输完成、半传输、传输错误这三个中断。半传输中断在双缓冲和流式处理里特别有用,能在数据搬一半的时候就通知CPU开始处理,进一步降低延迟。

7. 不同STM32系列的DMA差异与选型建议

7.1 F1/F4系列的DMA架构特点

STM32F1和F4系列用的是传统的DMA控制器,F1有DMA1(7通道),F4有DMA1和DMA2(各8通道)。通道和外设请求的映射是固定的,配置的时候要查表。F4的DMA支持双缓冲和FIFO,功能比F1强不少。F1的DMA比较简单,适合入门学习,但功能有限,比如不支持双缓冲。

F4系列的DMA在性能上足够应付大多数应用,SPI DMA刷屏、ADC DMA采样、串口DMA收发都没问题。如果项目对DMA性能要求不是极端高,F4是性价比很高的选择。

7.2 H7系列的MDMA与Cache处理

STM32H7系列引入了MDMA(Master DMA),架构和F1/F4完全不同。MDMA有独立的总线主接口,能访问更大的地址空间,支持更复杂的传输描述符。H7还有DMAMUX,可以把任意DMA请求路由到任意通道,灵活性大大提高。

但H7的Cache问题也最让人头疼。我的建议是:DMA缓冲区优先放DTCM RAM,这块内存CPU访问最快而且不经过Cache,天然没有一致性问题。如果DTCM不够用,再考虑用AXI SRAM加Cache维护操作。

7.3 选型时的关键考量因素

选MCU的时候,DMA相关的考量点主要有这几个:DMA通道数量够不够用、支不支持双缓冲、有没有FIFO、和外设的映射关系是否满足需求、带不带Cache。通道数量要留余量,别刚好够用,后期加功能就捉襟见肘了。双缓冲和FIFO在高性能场景里是刚需,选型时最好确认一下。

我个人经验是,如果项目里DMA用途比较多,直接上F4或H7,别在F1上折腾。F1的DMA通道少、功能弱,后期扩展很痛苦。当然如果只是简单的串口DMA收发,F1也够用,成本还低。

8. 从寄存器到HAL库的配置对比

8.1 标准库与HAL库的DMA配置差异

标准库配置DMA是直接操作寄存器结构体,代码直观但可移植性差。HAL库把DMA配置封装成了句柄和初始化结构体,跨系列移植方便,但代码层次多,出问题不好查。

以串口DMA发送为例,标准库就是填几个寄存器然后使能,HAL库则是HAL_UART_Transmit_DMA()一个函数搞定,但内部做了很多事情,包括状态机管理、中断处理、回调调用。新手用HAL库上手快,但建议至少把标准库的配置流程走一遍,理解底层在干什么,不然出了问题完全不知道从哪查。

8.2 CubeMX配置DMA的实操要点

用CubeMX配置DMA能省很多事,但有几个地方要注意。一是在DMA Settings里添加请求的时候,要选对外设和方向;二是优先级和模式要手动确认,CubeMX的默认值不一定适合你的场景;三是中断使能要勾选,不然回调函数不会触发。

CubeMX生成的代码里,DMA初始化在MX_DMA_Init()里,外设的DMA关联在各自的初始化函数里。如果要改配置,最好在CubeMX里改完重新生成,不要直接改生成代码,不然下次生成会被覆盖。

8.3 手写DMA驱动的核心步骤

如果项目要求不用库,纯手写DMA驱动,核心步骤是这样的:先使能DMA控制器时钟,然后配置通道的CCR寄存器(方向、模式、宽度、优先级),接着填CPAR和CMAR寄存器(外设和内存地址),再填CNDTR寄存器(传输长度),最后使能通道。中断的话还要配置NVIC和使能相应的中断位。

手写驱动的优势是代码量小、执行效率高、完全可控,缺点是移植性差、容易出错。我一般建议在资源极度受限或者对性能要求极高的场景才手写,否则用库更稳妥。

9. DMA调试工具与验证方法

9.1 用逻辑分析仪验证DMA时序

逻辑分析仪是调试DMA的利器。把SPI的时钟、数据、片选接到分析仪上,能直观看到DMA传输的时序。如果发现数据之间有异常间隔,说明DMA被抢占了或者配置有问题。串口DMA的话,接TX线看波形,能确认数据是否连续发送。

我常用的验证方法是:DMA发送一串已知数据,用分析仪抓下来,跟原始数据对比。如果完全一致,说明DMA配置没问题;如果有错位或丢失,再逐步排查。

9.2 用GPIO翻转法测量DMA耗时

没有逻辑分析仪的时候,GPIO翻转法也能凑合。在DMA启动前拉高一个GPIO,在完成中断里拉低,用示波器或者另一个定时器测量高电平持续时间,就是DMA传输耗时。这个方法精度取决于GPIO翻转和中断响应的速度,一般能到微秒级,够用了。

9.3 内存断点与DMA传输监控

用调试器的时候,可以在DMA缓冲区上设内存断点,观察DMA写入的过程。不过要注意,DMA写入不触发CPU的内存断点,所以这个方法只能看CPU的访问,不能直接监控DMA。想看DMA的实时进度,可以读CNDTR寄存器,看剩余传输长度,反推已经搬了多少。

10. 性能优化与进阶技巧

10.1 提高DMA吞吐量的几个手段

提高DMA吞吐量,核心是减少总线竞争和提高突发效率。具体手段包括:把DMA缓冲区放在访问速度最快的内存区域、提高DMA通道优先级、启用FIFO和突发传输、减少同时工作的DMA通道数量。在STM32H7上,还可以用MDMA的链表模式,一次配置多个传输描述符,减少CPU干预。

实测下来,同样的SPI刷屏,启用FIFO和突发传输后,速度能提升15%到20%。这个提升在刷屏这种大数据量场景里很可观。

10.2 DMA与RTOS的配合注意事项

在RTOS环境下用DMA,要注意中断优先级和任务优先级的配合。DMA完成中断的优先级不能低于调用DMA服务的任务优先级,否则可能出现优先级反转。另外,DMA缓冲区的管理要考虑任务间的互斥,避免多个任务同时操作同一个DMA通道。

我一般会给每个DMA通道配一个信号量,任务发起DMA传输后等信号量,DMA完成中断里释放信号量。这样任务和DMA的同步就很清晰,也不会有竞争问题。

10.3 低功耗场景下的DMA使用策略

低功耗场景下,DMA能在CPU休眠的时候继续搬运数据,这是它的一大优势。比如ADC定时采样,CPU可以进Stop模式,DMA配合定时器触发ADC转换并搬运数据,搬够一定数量再唤醒CPU处理。这样平均功耗能降一个数量级。

配置的时候要注意,进入低功耗前要确认DMA和触发源都配置好了,唤醒中断要正确使能。另外,有些低功耗模式下DMA时钟会关闭,要查手册确认。

11. 我个人的DMA使用体会

写了这么多,最后分享几点个人体会。DMA这个东西,入门不难,但要用好需要经验积累。我刚开始用的时候也踩过不少坑,数据错位、传输不完整、测速上不去,这些问题都遇到过。后来慢慢总结出一套自己的配置流程和检查清单,基本能避开大部分问题。

我的建议是,新手先把串口DMA收发吃透,这是最基础也最常用的。然后进阶到ADC DMA和SPI DMA,这两个覆盖了大部分数据采集和显示场景。最后再研究双缓冲、TIM DMA Burst这些高级用法。每学一个场景,就实际写代码跑一遍,用逻辑分析仪或者GPIO翻转法验证一下,比看十遍文档都管用。

还有一点,DMA的配置没有绝对的标准答案,不同的MCU、不同的应用场景,最优配置可能完全不同。关键是要理解每个配置位背后的含义,知道改了会有什么影响,这样才能根据实际情况灵活调整。我见过太多人直接抄网上的配置代码,结果换个芯片或者换个场景就完全跑不起来,就是因为没理解原理。

DMA这块内容其实还有很多可以展开的,比如DMA和DMA2D的区别、MDMA的链表描述符、DMA在以太网和USB中的应用等等。这些等以后有机会再单独开一期讲。如果你在DMA使用中遇到什么奇怪的问题,欢迎一起交流,很多时候别人的一个提示就能省你好几天的排查时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询