☰
嵌入式DMA驱动开发实战:从原理到串口、SPI、TIM配置与调试
2026/10/27 9:12:49 网站建设 项目流程

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

搞嵌入式驱动开发,绕不开的一个话题就是DMA。你可以不懂RTOS的任务调度细节,也可以暂时不碰文件系统,但只要你的项目涉及到串口收发、SPI高速通信、ADC连续采样、TIM精确波形输出,DMA就是那个你迟早要正面硬刚的东西。我见过太多人在这上面栽跟头——串口收数据丢包、SPI读回来全是0、ADC采样率上不去、测速代码跑出来结果离谱,最后排查半天发现全是DMA配置的问题。

这一期我们就专门把DMA这个东西掰开揉碎讲清楚。从它到底解决了什么问题,到不同外设下DMA的配置思路,再到实际调试中那些让人抓狂的坑,我都会结合自己的项目经验一一展开。不管你是刚接触STM32的新手,还是已经用过DMA但总觉得没吃透的老手,这篇内容应该都能让你有所收获。

DMA全称Direct Memory Access,直译过来就是直接存储器访问。它的核心价值只有一句话:让数据在外设和内存之间搬运时,不需要CPU插手。没有DMA的时候,串口每收到一个字节,CPU就得进一次中断把数据读走;有了DMA,你只需要告诉它“从串口数据寄存器搬到这个数组里,搬512个字节”,然后CPU就可以去干别的事了,搬完了DMA会通知你一声。这个差别在低速场景下可能不明显,但一旦数据量上来,比如SPI接高速ADC、串口跑高波特率、TIM要输出精确的PWM序列,没有DMA基本就是灾难。

2. DMA的核心原理与关键概念拆解

2.1 DMA控制器到底在干什么

很多人对DMA的理解停留在“不用CPU搬数据”这个层面,但具体它是怎么搬的、搬的时候CPU在干嘛、搬完了怎么通知,这些细节才是决定你能不能用好它的关键。

DMA控制器本质上是一个独立的数据搬运工。它内部有源地址寄存器、目标地址寄存器、传输长度寄存器,还有一组配置寄存器用来设定传输模式。你把这些参数配好,使能DMA通道,它就开始一个字节一个字节(或者一个字一个字)地把数据从源搬到目标。每搬一次,源地址和目标地址会根据你设定的增量模式自动调整,传输长度计数器递减,减到零就产生传输完成标志。

这里有个关键点:DMA搬运数据的总线周期和CPU执行指令的总线周期是分时复用的。也就是说,DMA在搬数据的时候,CPU并不是完全不能访问总线,而是两者交替使用。这就是为什么DMA传输本身也会占用一定的总线带宽,在高负载场景下需要权衡。

2.2 通道、请求、优先级:三个必须搞懂的概念

DMA通道是DMA控制器和外设之间的连接纽带。每个外设的DMA请求会映射到特定的通道上,这个映射关系在芯片参考手册的DMA请求映射表里写得清清楚楚。比如STM32F103C8T6上,USART1的TX映射到DMA1通道4,RX映射到DMA1通道5,SPI1的TX是通道3,RX是通道2。你配置的时候如果通道选错了,DMA根本不会响应外设的请求,数据自然也就搬不动。

DMA请求是外设向DMA控制器发出的信号,意思是“我这边有数据了,快来搬”。比如串口收到一个字节,RXNE标志置位,同时会触发DMA请求。DMA控制器收到请求后,根据通道配置决定是否响应。这里要注意,有些外设的DMA请求是需要手动使能的,比如串口的DMA接收使能位DMAR,你不置这个位,串口收到数据也不会触发DMA请求。

优先级则是在多个DMA通道同时有请求时,DMA控制器决定先响应谁的依据。优先级分软件优先级和硬件优先级两层。软件优先级你可以通过寄存器设置,分最高、高、中、低四档。硬件优先级是固定的,通道号越小优先级越高。实际项目中,如果你有多个高速外设同时用DMA,优先级配置不当会导致低优先级通道的数据被延迟搬运,严重时甚至丢数据。

2.3 传输模式:普通模式与循环模式的取舍

DMA的传输模式主要分两种:普通模式和循环模式。普通模式下,DMA搬完指定数量的数据后就停止,需要你重新配置才能再次启动。循环模式下,DMA搬完一轮后自动重置地址和计数器,重新开始下一轮,不需要CPU干预。

串口接收通常用循环模式配合空闲中断,这样DMA可以一直往缓冲区里搬数据,你只需要在空闲中断里处理缓冲区里的内容就行。ADC连续采样也常用循环模式,让DMA不停地往数组里填最新的采样值。而SPI读取固定长度的数据、TIM输出特定数量的脉冲,这些场景用普通模式更合适,搬完就停,逻辑清晰。

选择哪种模式,核心看你需要的是“持续不断的数据流”还是“一次性的批量传输”。这个判断标准在实际开发中非常实用,能帮你快速确定配置方向。

3. 不同外设下的DMA配置实战

3.1 串口DMA收发:从丢包到稳定的完整配置

串口DMA接收是嵌入式项目中最常见的DMA应用场景,也是问题最多的场景。我见过太多人配置完DMA接收后发现数据丢包、数据错位、缓冲区溢出,最后不得不退回中断方式。

先讲发送。串口DMA发送的配置相对简单:把要发送的数据放进缓冲区,配置DMA源地址为缓冲区地址,目标地址为串口数据寄存器地址,传输长度设为数据长度,使能DMA通道,然后使能串口的DMA发送请求。DMA会自动把缓冲区里的数据逐个搬到串口数据寄存器,搬完后触发传输完成中断。你可以在中断里做一些收尾工作,比如切换RS485的方向引脚、释放缓冲区等。

接收就复杂一些。最稳妥的方案是DMA循环模式加串口空闲中断。具体做法是:定义一个足够大的接收缓冲区,比如512字节,配置DMA为循环模式,源地址为串口数据寄存器,目标地址为缓冲区首地址,传输长度设为512。使能DMA通道和串口的DMA接收请求。这样DMA会一直往缓冲区里搬数据,搬满512字节后自动回到开头继续搬。

关键在于空闲中断的处理。串口空闲中断在总线空闲一个字节时间后触发,你可以在中断里计算DMA当前还剩多少个字节没搬,从而推算出这次收到了多少数据。计算公式是:接收到的数据长度等于缓冲区总长度减去DMA剩余传输数量。拿到这个长度后,你就可以处理缓冲区里的数据了。处理完之后,你需要重置DMA的传输数量寄存器,让它重新从缓冲区开头开始搬,或者用双缓冲机制来避免数据处理和DMA搬运之间的冲突。

这里有个细节很多人会忽略:DMA的传输数量寄存器在循环模式下是自动重载的,但你在空闲中断里读取它的时候,读到的值是当前剩余的传输数量,这个值在DMA搬运过程中是动态变化的。所以读取的时候最好先关闭DMA通道,读完再打开,避免读到中间状态的值。

3.2 SPI DMA:高速ADC数据采集的关键

SPI接高速ADC是DMA的另一个经典应用场景。安富莱的AD7606模块就是一个典型例子,8通道同步采样,最高200kSPS的采样率,没有DMA根本扛不住这个数据量。

SPI DMA的配置要点在于全双工模式下的收发同步。SPI是全双工总线,发送一个字节的同时会接收一个字节。你用DMA发送命令的同时,也需要用DMA接收数据。STM32的SPI支持TX和RX同时使用DMA,你需要配置两个DMA通道,一个负责发送,一个负责接收,并且要确保两者的传输长度一致。

以AD7606为例,读取8个通道的采样值需要发送若干命令字节,然后接收16个字节的数据(8通道×2字节)。配置DMA时,TX通道的源地址是命令缓冲区,目标地址是SPI数据寄存器;RX通道的源地址是SPI数据寄存器,目标地址是接收缓冲区。两个通道的传输长度都设为命令长度。使能DMA后,SPI的TX和RX请求会分别触发两个DMA通道工作,数据自动完成交换。

这里有个坑:SPI的DMA请求使能位在SPI控制寄存器里,你需要同时使能SPI的DMA发送请求和DMA接收请求。另外,SPI的时钟极性和相位配置必须和ADC匹配,否则接收到的数据全是错位的。我调试AD7606的时候就遇到过这个问题,数据能收到但全是乱码,最后发现是SPI模式设错了。

3.3 TIM DMA Burst:精确波形输出的利器

TIM的DMA Burst功能可能很多人没接触过,但它在需要输出精确波形序列的场景下非常有用。比如你要驱动WS2812灯带,每个灯需要24位数据,每位的高低电平时间有严格要求,用普通GPIO翻转根本做不到精确控制。这时候TIM的DMA Burst就能派上用场。

TIM DMA Burst的原理是利用TIM的更新事件或比较事件触发DMA,把预先计算好的比较值批量写入TIM的比较寄存器,从而改变PWM的占空比。配合DMA循环模式,你可以输出任意波形的PWM序列。

配置步骤大致是这样的:先配置TIM为PWM输出模式,使能TIM的DMA请求。然后配置DMA通道,源地址为波形数据数组,目标地址为TIM的比较寄存器,传输长度设为波形数据长度。使能DMA后,每次TIM更新事件触发DMA搬运一个数据到比较寄存器,PWM占空比随之改变。搬完一轮后,如果是循环模式,DMA自动从头开始,输出周期性的波形序列。

这个功能的关键在于波形数据的计算。你需要根据目标波形的时序要求,反推出每个PWM周期应该设置的比较值。比如WS2812的0码是0.4微秒高电平加0.85微秒低电平,1码是0.8微秒高电平加0.45微秒低电平。假设TIM的时钟是72MHz,预分频后计数频率是1MHz,那么0码的高电平比较值就是0.4,1码就是0.8。把这些值按顺序填入数组,DMA就会自动输出对应的波形。

4. DMA测速:一个看似简单却暗藏玄机的实验

4.1 DMA测速的原理与实现思路

DMA测速这个实验在网络上很火,但很多人跑出来的结果要么是零,要么大得离谱。这个实验的本质是测量DMA搬运数据的速率,通常的做法是:配置DMA从内存到内存搬运一大块数据,用定时器记录搬运开始和结束的时间戳,然后计算传输速率。

具体实现上,你可以用一个高精度定时器,比如STM32的DWT周期计数器或者TIM的微秒计数器。在启动DMA之前读取一次计数值,在DMA传输完成中断里再读取一次,两次之差就是传输耗时。传输的数据量除以耗时就是DMA的搬运速率。

这个实验看起来简单,但有几个关键点决定了你能不能测出正确的结果。第一,DMA的源地址和目标地址必须都是内存,不能是外设,因为外设的传输速率受限于外设本身的时钟,测出来的不是DMA的真实带宽。第二,数据量要足够大,比如几KB到几十KB,否则定时器的分辨率不够,误差会很大。第三,DMA的传输宽度要设成最大,比如32位,这样每次搬运的数据量最大,能更充分地利用总线带宽。

4.2 测速失败的常见原因排查

DMA测速失败代码是热搜词里出现频率很高的一个,说明很多人在这上面卡住了。我总结了几种最常见的失败原因。

第一种是DMA配置错误导致传输根本没启动。比如通道选错了、传输方向设反了、使能位没置位。这种问题最直接的表现就是DMA传输完成中断永远不触发,程序卡在等待中断的地方。排查方法很简单,在启动DMA后读一下DMA的传输数量寄存器,如果值没有变化,说明DMA根本没在搬数据。

第二种是定时器配置问题导致时间测量不准。比如定时器的时钟源没使能、预分频值设得太大导致分辨率不够、计数方向设反了。这种问题的表现是测出来的速率明显偏离预期,要么大得离谱要么小得可怜。排查方法是先用定时器测量一个已知延时,比如软件延时1毫秒,看看测出来的值对不对。

第三种是内存访问冲突导致DMA传输被延迟。如果你在DMA搬运的同时CPU也在频繁访问同一块内存区域,总线仲裁会导致DMA的搬运周期被拉长,测出来的速率就会偏低。解决办法是在测速期间让CPU执行一些不访问内存的指令,比如空循环,或者干脆让CPU进入低功耗模式。

第四种是缓存问题。如果你的芯片有Cache,DMA搬运的数据可能还在Cache里没写回内存,或者CPU读到的数据是Cache里的旧数据。这种问题在带Cache的MCU上很常见,解决办法是在DMA传输前后做Cache清理或无效化操作。

4.3 实测数据与经验值参考

我在STM32F103C8T6上做过一组DMA内存到内存的测速实验,主频72MHz,DMA传输宽度32位,数据量32KB。实测下来,DMA的搬运速率大约在30MB/s到35MB/s之间。这个数据可以作为参考,如果你测出来的结果在这个范围内,说明配置基本没问题。如果明显低于这个值,比如只有几MB/s,那就要检查是不是传输宽度设成了8位,或者总线仲裁配置有问题。

在STM32F4系列上,由于总线矩阵更先进,DMA的搬运速率可以到60MB/s以上。H7系列更高,能到100MB/s以上。这些数据可以帮助你判断自己的测速结果是否合理。

5. DMA调试中的常见问题与排查技巧

5.1 数据传输不完整或错位

这是DMA调试中最常见的问题。数据传了一半停了,或者数据顺序乱了,或者接收到的数据整体偏移了几个字节。这类问题的根源通常有几个:传输长度设错了、地址增量模式配错了、缓冲区溢出、DMA和外设的位宽不匹配。

排查的时候,我习惯先用最简单的场景验证DMA本身是否工作正常。比如配置一个内存到内存的DMA传输,传输固定长度的数据,看看能不能完整搬过去。如果这个都过不了,那说明DMA的基础配置有问题。如果内存到内存没问题,再逐步加上外设,一个一个排查。

地址增量模式是个容易出错的地方。外设到内存的传输,外设地址通常是不增的,因为外设的数据寄存器地址固定;内存地址通常是增的,因为你要把数据依次存到数组里。如果这两个设反了,数据就会全部堆在一个地址上,或者从错误的地址读取数据。

5.2 DMA传输完成中断不触发

中断不触发的问题,排查思路是从源头往末端查。先确认DMA通道是否使能了,再确认外设的DMA请求是否使能了,然后确认DMA的中断使能位是否置位了,最后确认NVIC的中断使能和优先级配置是否正确。

有一个很容易忽略的点:DMA的传输完成标志在中断服务函数里必须手动清除,否则中断会一直触发或者再也不触发。清除标志的时机也很关键,要在处理完数据之后清除,不能在处理之前清除,否则可能丢失下一次传输完成的事件。

另外,有些芯片的DMA传输完成中断和传输错误中断是分开的,如果你只使能了传输完成中断,传输出错的时候不会有任何提示,程序就会莫名其妙地卡住。建议在调试阶段把传输错误中断也打开,方便定位问题。

5.3 串口DMA接收数据丢包

串口DMA接收丢包是热搜词里反复出现的问题。丢包的原因通常有三个:缓冲区太小、处理速度跟不上、DMA配置不当。

缓冲区太小是最常见的原因。如果你用循环模式,缓冲区设了128字节,但对方一次发了256字节,那前128字节会被后128字节覆盖,导致丢包。解决办法是根据实际数据量把缓冲区设得足够大,或者用双缓冲机制。

处理速度跟不上是指你在空闲中断里处理数据的时间太长,导致DMA已经把缓冲区写满了你还没处理完。这种情况在高速通信时很常见。解决办法是缩短中断处理时间,把数据处理放到主循环里做,中断里只做数据拷贝和标志置位。

DMA配置不当包括传输宽度设错、地址增量模式设错、优先级太低被其他DMA通道抢占等。这些都需要根据具体情况逐一排查。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
DMA传输不启动通道选错、使能位未置位读传输数量寄存器看是否变化对照参考手册检查通道映射和使能位
数据错位地址增量模式配错检查源和目标地址增量设置外设地址不增,内存地址增
传输完成中断不触发中断使能位未置位、标志未清除检查DMA中断使能寄存器和NVIC配置使能中断,在ISR中清除标志
串口接收丢包缓冲区太小、处理太慢增大缓冲区测试、测量中断处理时间增大缓冲区、优化中断处理逻辑
测速结果异常传输宽度设错、定时器配置错误检查DMA传输宽度和定时器时钟配置设为最大传输宽度、校准定时器
SPI接收全0SPI模式配置错误检查CPOL和CPHA设置根据从设备手册配置SPI模式

6. 进阶话题:双缓冲与DMA性能优化

6.1 双缓冲机制的原理与实现

双缓冲是解决DMA搬运和CPU处理之间冲突的经典方案。它的核心思想是准备两块缓冲区,DMA在写其中一块的时候,CPU处理另一块,两者交替进行,互不干扰。

在STM32上,DMA的双缓冲模式可以通过寄存器直接配置。你设置两个内存地址寄存器,DMA会在两个缓冲区之间自动切换。每次传输完成时,DMA会触发中断,你可以在中断里知道当前DMA正在写哪块缓冲区,然后去处理另一块。

双缓冲的优势在于消除了数据处理和DMA搬运之间的等待时间。在单缓冲模式下,你必须等DMA搬完一整轮才能处理数据,处理期间DMA是停着的。双缓冲模式下,DMA永远在搬,CPU永远在处理,效率翻倍。

实现双缓冲的时候要注意一点:中断里切换缓冲区的操作要快,不能做耗时的事情。通常的做法是在中断里设置一个标志,告诉主循环哪块缓冲区有数据了,主循环看到标志后去处理。这样中断处理时间极短,不会影响DMA的连续搬运。

6.2 DMA性能优化的几个实用技巧

第一个技巧是合理设置DMA优先级。如果你有多个DMA通道同时工作,把带宽需求最大的通道设为最高优先级,避免它的数据被延迟搬运。但也要注意,优先级太高的通道会抢占其他通道的总线时间,可能导致低优先级通道的数据丢失。

第二个技巧是使用FIFO模式。STM32的DMA控制器内置了FIFO,你可以配置FIFO的阈值和突发传输长度。突发传输的意思是DMA一次占用总线连续搬运多个数据,而不是搬一个释放一次总线。突发传输能显著提高总线利用率,尤其是在高负载场景下。

第三个技巧是对齐访问地址。DMA的传输效率受地址对齐影响很大。如果源地址和目标地址都是4字节对齐的,DMA可以用32位宽度传输,效率最高。如果地址不对齐,DMA可能被迫用8位宽度传输,效率直接降到四分之一。

第四个技巧是减少DMA传输次数。比如你要搬运1000个字节,与其配置1000次8位传输,不如配置250次32位传输。传输次数越少,DMA控制器的开销越小,整体效率越高。

7. 从项目实战中总结的DMA使用心得

7.1 配置DMA时的检查清单

每次配置DMA的时候,我都会按照一个固定的检查清单过一遍,能避免绝大多数低级错误。这个清单包括:确认外设的DMA请求映射到哪个通道、确认传输方向、确认源地址和目标地址、确认地址增量模式、确认传输宽度、确认传输模式、确认优先级、确认中断使能、确认外设的DMA请求使能位。

这个清单看起来简单,但实际开发中漏掉任何一项都可能导致DMA不工作。我建议你把这个清单打印出来贴在显示器旁边,每次配置DMA的时候对照检查,养成习惯后能省下大量调试时间。

7.2 调试DMA的实用手段

调试DMA的时候,光靠看代码很难定位问题。我常用的手段有几个:用调试器查看DMA寄存器的实时值、在DMA中断里翻转GPIO用示波器观察、用串口打印DMA传输前后的缓冲区内容对比、用逻辑分析仪抓外设总线的波形。

其中查看DMA寄存器是最直接的方法。你可以看到传输数量寄存器的值在变化,说明DMA在工作;看到传输完成标志置位了,说明一轮传输结束了;看到传输错误标志置位了,说明配置有问题。这些信息比单步调试代码有用得多。

用GPIO翻转配合示波器观察是另一个好办法。在DMA传输开始的时候翻转一个GPIO,在传输完成中断里再翻转一次,用示波器测量两个翻转之间的时间,就能直观地看到DMA的传输耗时。这个方法在测速和性能优化的时候特别有用。

7.3 不同芯片平台的DMA差异

不同厂商的MCU在DMA实现上有不少差异,这些差异会直接影响你的配置方式。STM32的DMA控制器分DMA1和DMA2,不同系列的通道映射不同,F4和F1的DMA架构就有明显区别。ESP32的DMA叫GDMA,配置方式和STM32完全不同,需要单独学习。GD32的DMA和STM32高度兼容,但有些细节寄存器有差异。

跨平台移植DMA代码的时候,不能直接复制粘贴,必须对照目标芯片的参考手册重新确认通道映射、寄存器地址和配置位定义。我在这上面踩过坑,把STM32F1的DMA代码直接搬到F4上,结果通道映射全变了,调了半天才发现问题。

7.4 关于DMA驱动安装的说明

热搜词里出现了DMA驱动安装,这里需要澄清一下。在嵌入式开发中,DMA是芯片内部的一个硬件模块,不需要安装额外的驱动程序。你只需要在代码里配置DMA控制器就行。所谓的DMA驱动安装,通常是指在某些开发环境里安装芯片支持包或者配置调试工具,这和DMA本身没有直接关系。如果你在Windows设备管理器里看到未知设备需要安装DMA驱动,那大概率是主板芯片组的DMA控制器驱动,和嵌入式开发无关。

8. 写在最后

DMA这个东西,入门容易精通难。刚开始用的时候,你可能只需要照着例程改改参数就能跑起来。但真正遇到问题的时候,能不能快速定位、能不能从原理层面理解为什么出错,就决定了你的调试效率。

我的建议是,不要满足于“能用就行”。每次配置DMA的时候,多问自己几个为什么:为什么这个外设映射到这个通道?为什么这里要用循环模式而不是普通模式?为什么传输宽度要设成32位?把这些想清楚了,下次遇到新外设、新场景,你就能举一反三,而不是到处找例程抄。

另外,DMA的调试工具和手段也很重要。学会看寄存器、学会用示波器观察时序、学会用逻辑分析仪抓总线波形,这些技能在调试DMA的时候比单步调试代码有用得多。我自己的经验是,DMA的问题百分之八十都能通过看寄存器和抓波形定位到,剩下的百分之二十才是代码逻辑问题。

最后分享一个我常用的DMA调试小技巧:在DMA传输完成中断里,把接收到的数据缓冲区的前几个字节通过串口打印出来。如果打印出来的数据和你预期的一致,说明DMA搬运本身没问题,问题出在后续处理上;如果打印出来的是乱码或者全零,那问题就在DMA配置或者外设配置上。这个简单的动作能帮你快速缩小排查范围,省下大量时间。

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

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

立即咨询