资讯动态

嵌入式驱动开发必修课:DMA原理、配置与实战避坑指南

发布时间:2026/10/27 9:19:39 来源:尧图企业网站定制
1. 为什么 DMA 是嵌入式驱动开发的必修课搞嵌入式驱动开发绕不开一个话题CPU 到底该不该亲自搬数据。你写一个串口接收驱动如果每来一个字节就进一次中断CPU 就被打断一次波特率一高中断频率直接爆炸主循环基本没时间干正事。这时候 DMA 就登场了——Direct Memory Access直接存储器访问说白了就是让一个独立的硬件搬运工替 CPU 把数据从外设搬到内存或者从内存搬到外设搬完了再通知 CPU 一声。我在实际项目里踩过最典型的坑就是早期用 STM32F103C8T6 做串口屏通信115200 波特率下用中断接收屏幕刷新一快就丢包。后来换成串口 DMA 接收CPU 占用率从百分之三十多直接掉到个位数丢包问题也消失了。这个对比让我彻底理解了 DMA 的价值它不是性能优化选项而是很多场景下的必需品。这一期内容适合谁看如果你正在写串口、SPI、ADC 这类外设驱动或者你听说过 DMA 但一直没搞明白它到底怎么配、怎么用、怎么排查问题那这篇就是给你准备的。我会从设计思路讲到寄存器级别的配置再讲到实测中遇到的各种坑尽量把 DMA 这个东西讲透。核心关键词就两个嵌入式驱动开发和DMA围绕它们展开。2. DMA 驱动设计的整体思路与方案选型2.1 先搞清楚 DMA 到底解决了什么问题很多人学 DMA 的时候一上来就看寄存器手册结果看完还是不知道什么时候该用它。我的建议是先建立一个判断标准当数据传输是批量、连续、无需 CPU 逐字节处理的时候就该考虑 DMA。举个生活化的类比。CPU 处理数据就像你自己去超市买东西一件一件往购物车里放。中断方式相当于你每拿一件商品就有人打电话问你拿到了吗你接电话的时间比拿东西还长。DMA 方式相当于你雇了一个跑腿小哥给他一张购物清单他一次性把东西全买回来放你桌上你只需要最后清点一下。显然东西越多跑腿小哥的优势越明显。具体到嵌入式场景下面这几类需求是 DMA 的典型应用高速串口通信波特率超过 57600 之后中断接收的 CPU 开销就不可忽视了115200 以上基本必须上 DMA。SPI 大批量数据传输比如驱动 LCD 屏幕、读写 Flash一次传输几百上千字节用 DMA 能释放大量 CPU 时间。ADC 连续采样需要以固定频率采集大量数据时DMA 配合定时器触发是标准做法。内存到内存的数据搬运比如图像缓冲区的拷贝虽然不如外设 DMA 常见但某些场景下也能省不少事。2.2 选型时要想清楚的几个问题在动手配置之前有几个决策点必须先想明白否则后面改起来很痛苦。第一个问题用哪个 DMA 控制器和通道不同芯片的 DMA 资源分配差别很大。以 STM32F103C8T6 为例它有 DMA1 的 7 个通道和 DMA2 的 5 个通道具体型号有差异每个通道固定绑定特定的外设请求。比如 USART1_RX 只能用 DMA1_Channel5USART1_TX 只能用 DMA1_Channel4。这个映射关系是硬件定死的不能随便改。所以第一步永远是查参考手册的 DMA 请求映射表。第二个问题用普通模式还是循环模式普通模式Normal传完一次就停需要软件重新使能。循环模式Circular传完自动从头开始适合连续采集场景。串口接收通常用循环模式配合空闲中断ADC 连续采样也用循环模式。第三个问题数据宽度怎么设外设和内存的数据宽度可以独立配置支持字节、半字、字三种。这里有个容易踩的坑如果外设是 8 位数据寄存器内存缓冲区是 uint8_t 数组那两边都设成字节就行。但如果内存缓冲区是 uint16_t 数组而外设是 8 位DMA 会按照你配置的宽度去搬结果就是数据错位。宽度配置必须和实际数据类型严格对应。第四个问题优先级怎么排多个 DMA 通道同时工作时优先级决定了谁先占用总线。如果有一个通道对实时性要求特别高就把它设成最高优先级。但要注意优先级只影响仲裁顺序不能保证绝对的时间确定性。2.3 中断还是轮询DMA 完成通知机制的选择DMA 传输完成后怎么通知 CPU这也是个设计决策。常见的有三种方式通知方式适用场景优点缺点传输完成中断单次批量传输响应及时逻辑清晰每次都要进中断半传输中断双缓冲处理可以流水线处理逻辑复杂度上升轮询标志位简单场景实现最简单浪费 CPU 时间我个人的经验是串口接收用空闲中断加 DMASPI 发送用传输完成中断ADC 连续采样用半传输加全传输双中断。这套组合基本能覆盖百分之九十的场景。3. 核心寄存器与配置细节深度解析3.1 DMA 控制器的内部结构要真正理解 DMA 的配置得先知道它内部有哪些寄存器。以常见的 ARM Cortex-M 系列芯片为例每个 DMA 通道有这么几个关键寄存器CCR通道配置寄存器控制使能、方向、模式、优先级、数据宽度、中断开关等。CNDTR数据传输数量寄存器设置要传输多少个数据项每传一个自动减一。CPAR外设地址寄存器外设数据寄存器的地址。CMAR内存地址寄存器内存缓冲区的地址。配置的本质就是把这四个寄存器填对。CCR 决定怎么传CNDTR 决定传多少CPAR 和 CMAR 决定从哪传到哪。3.2 数据宽度与地址增量配置这是最容易出错的地方我单独拿出来讲。CCR 里有四个位控制数据宽度和地址增量PSIZE[1:0]外设数据宽度00 字节、01 半字、10 字。MSIZE[1:0]内存数据宽度编码同上。PINC外设地址增量模式置 1 则每传一次外设地址自动加。MINC内存地址增量模式置 1 则每传一次内存地址自动加。关键点来了外设地址通常不增量内存地址通常增量。因为外设的数据寄存器地址是固定的你每次都是读同一个寄存器而内存缓冲区是连续的每次要写到不同位置。所以典型配置是 PINC 等于 0MINC 等于 1。但有个例外内存到内存传输时两边都要增量。这个场景在图像处理里比较常见。还有一个隐藏的坑当 PSIZE 和 MSIZE 不一致时DMA 会按照较小的宽度来传输。比如外设设成字、内存设成字节那实际每次只传一个字节传输次数会翻四倍。这个行为在参考手册里有说明但很多人不看结果 CNDTR 设错了导致数据不全。3.3 循环模式与双缓冲机制循环模式的价值在于无人值守。串口接收如果不用循环模式每次收完一帧就得重新配置 DMA中间的空窗期可能丢数据。用了循环模式DMA 自己转圈你只需要在合适的时机去读缓冲区就行。双缓冲Double Buffer是更高级的玩法。它有两个内存缓冲区DMA 往 A 写的时候 CPU 读 B写满 A 自动切到 B同时触发中断通知 CPU 处理 A。这样理论上可以做到零等待的连续数据流。STM32 的 DMA 支持双缓冲模式通过 CCR 里的 CIRC 位和 DBM 位配合实现。不过双缓冲的调试难度明显更高我建议先把单缓冲循环模式用熟再考虑双缓冲。3.4 中断标志与清除时机DMA 的中断标志有四个传输完成TC、半传输HT、传输错误TE、全局GIF。实际使用中TC 和 HT 用得最多。这里有个经典陷阱中断标志必须在中断服务函数里手动清除否则会反复进中断。清除方式是往 IFCR 寄存器对应位写 1。不同通道的标志位在不同位置写的时候要小心别清错了别人的标志。我见过有人的代码里中断一直进查了半天发现是标志没清。这种问题在逻辑分析仪上看就是中断引脚一直有脉冲但代码逻辑又没错很容易让人怀疑人生。4. 串口 DMA 接收的完整实操流程4.1 硬件初始化顺序串口 DMA 接收的初始化顺序很重要顺序错了可能收不到数据。我的标准流程是这样的使能时钟先开 GPIO、USART、DMA 三个时钟。少开一个后面全是坑。配置 GPIORX 设成浮空输入或上拉输入TX 设成复用推挽输出。配置 USART波特率、数据位、停止位、校验位先不使能。配置 DMA设置 CPAR 为 USART 数据寄存器地址CMAR 为接收缓冲区地址CNDTR 为缓冲区大小CCR 配置方向为外设到内存、循环模式、字节宽度、内存增量、开启传输完成中断。使能 DMA 通道注意DMA 要先于 USART 使能。使能 USART 的 DMA 接收请求置位 USART_CR3 的 DMAR 位。使能 USART最后开串口。这个顺序的逻辑是先让搬运工就位再让外设开始产生数据。反过来做的话外设已经开始收数据了DMA 还没准备好前几个字节就丢了。4.2 空闲中断配合 DMA 的接收策略单纯用 DMA 循环模式有个问题你不知道一帧数据什么时候结束。解决方案是开启串口的空闲中断IDLE。空闲中断的触发条件是总线在一个字节时间内没有新数据。这正好是一帧结束的标志。具体做法是在空闲中断里读取 DMA 的 CNDTR 寄存器用缓冲区总大小减去当前剩余数量就得到这一帧收到的字节数。然后处理数据最后把 CNDTR 重新设回缓冲区大小准备接收下一帧。void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { // 清除空闲中断标志先读 SR 再读 DR volatile uint32_t tmp; tmp USART1-SR; tmp USART1-DR; (void)tmp; // 计算接收到的字节数 uint16_t recv_len RX_BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); // 处理数据 ProcessFrame(rx_buffer, recv_len); // 重新配置 DMA准备下一帧 DMA_Cmd(DMA1_Channel5, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel5, RX_BUF_SIZE); DMA_Cmd(DMA1_Channel5, ENABLE); } }这段代码里有个细节清除空闲中断标志必须先读 SR 再读 DR这是硬件规定的序列少一步标志清不掉。我第一次写的时候只读了 SR结果中断反复触发查了好久才发现。4.3 缓冲区大小的计算与选择缓冲区多大合适这取决于你的最大帧长和数据处理速度。我的经验公式是缓冲区大小等于最大帧长的两倍以上。假设你的协议最大帧是 256 字节那缓冲区至少设 512 字节。为什么要两倍因为 DMA 是循环写的如果 CPU 处理速度跟不上新数据会覆盖旧数据。两倍缓冲给了 CPU 一个帧周期的处理时间。如果数据量特别大比如做固件升级一帧可能几 KB那就得考虑双缓冲或者分段处理了。4.4 串口 DMA 发送的实现要点发送比接收简单一些但也有讲究。发送用普通模式把数据填到缓冲区设置 CNDTR 为数据长度使能 DMA等传输完成中断里做收尾。关键点发送完成中断里要等 TC 标志置位而不是 DMA 的传输完成标志。因为 DMA 传输完成只代表数据全部写进了 USART 数据寄存器但最后一个字节可能还在移位寄存器里没发出去。如果这时候就切换状态或者关闭串口最后一个字节会丢。正确做法是在 DMA 传输完成中断里等待 USART 的 TC 标志置位然后再做后续处理。5. SPI DMA 与 ADC DMA 的实战配置5.1 SPI DMA 驱动 LCD 屏幕SPI 配 DMA 是驱动大尺寸 LCD 的标准方案。以常见的 ST7789 屏幕为例刷一屏 240x320 的 RGB565 数据是 153600 字节用 SPI 逐字节发送即使 SPI 时钟跑到 40MHzCPU 也得全程陪着。用 DMA 的话CPU 只需要启动传输然后去干别的事。配置要点SPI 的 TX 请求映射到某个 DMA 通道查手册确认。DMA 方向设为内存到外设。外设地址不增量内存地址增量。数据宽度根据 SPI 数据帧格式设置通常是字节或半字。传输完成后触发中断在中断里拉高 CS 或者做其他收尾。有个坑要注意SPI 的 DMA 请求使能位在 SPI_CR2 寄存器里不是 CR1。我见过有人翻遍 CR1 找不到 DMA 使能位最后发现找错寄存器了。5.2 ADC 连续采样配合 DMAADC 加 DMA 加定时器触发这是采集模拟量的黄金组合。定时器以固定频率触发 ADC 转换每次转换完成 DMA 自动把结果搬到内存数组采够一定数量触发中断让 CPU 处理。配置流程配置定时器设置合适的触发频率。比如要 10kHz 采样率定时器就设成 10kHz 更新频率。配置 ADC选择外部触发源为定时器事件使能连续转换模式。配置 DMA方向外设到内存循环模式半字宽度ADC 结果是 12 位用半字存。使能 ADC 的 DMA 请求。启动定时器和 ADC。这里的关键参数是采样率和缓冲区大小的匹配。假设采样率 10kHz缓冲区 1024 个半字那每 102.4 毫秒触发一次传输完成中断。CPU 有 100 毫秒的时间处理这 1024 个数据压力很小。5.3 内存到内存的 DMA 搬运内存到内存传输用得相对少但在某些场景下很有用。比如你要把一块图像数据从 Flash 搬到 RAM 做处理或者做乒乓缓冲的切换。配置上和普通 DMA 的区别是方向设为内存到内存源地址和目的地址都要增量并且要软件触发启动置位 CCR 的 SW 位。注意内存到内存传输会占用总线带宽如果同时有外设 DMA 在工作可能会互相影响。所以这种传输一般放在系统空闲时做或者用低优先级。6. DMA 测速与性能评估方法6.1 为什么要做 DMA 测速DMA 测速不是为了炫技而是为了验证你的配置是否达到了预期性能。比如你配了一个串口 DMA 接收理论上能跑到 1Mbps但实测只有 500kbps那就说明有问题需要排查。测速的核心指标有两个吞吐量单位时间传输的字节数和CPU 占用率传输过程中 CPU 被占用的比例。6.2 用定时器和计数器做吞吐量测试最简单的测速方法开一个定时器在 DMA 传输开始和结束时分别读取计数器的值算出耗时再除以传输字节数。// 传输前 uint32_t start TIM2-CNT; DMA_Cmd(DMA1_Channel4, ENABLE); // 传输完成中断里 uint32_t end TIM2-CNT; uint32_t elapsed end - start; // 单位取决于定时器频率 float throughput (float)transfer_size / elapsed * timer_freq;这个方法的精度取决于定时器频率。要测高速传输定时器频率得够高比如 72MHz 的定时器分辨率就是 13.9 纳秒足够用了。6.3 CPU 占用率的测量思路CPU 占用率怎么测一个实用的方法是在 DMA 传输期间让 CPU 跑一个空循环计数对比没有 DMA 传输时的计数差值就是被 DMA 占用的时间比例。更精确的方法是看总线仲裁的统计但那个需要芯片支持。对于日常开发空循环计数法已经够用了。我实测过 STM32F103 在 72MHz 下串口 115200 波特率用中断接收CPU 占用率约 8%换成 DMA 接收后降到 0.5% 以下。这个差距在低功耗场景下尤其重要因为 CPU 占用率直接关系到能不能进低功耗模式。6.4 测速失败的常见原因测速结果不对通常有这几个原因定时器频率设错了算出来的吞吐量差一个数量级先检查这个。DMA 传输被中断打断高优先级中断会抢占总线导致传输时间变长。数据宽度配置错误前面说过宽度不一致会导致实际传输次数变化。缓冲区太小频繁的传输完成中断本身就有开销缓冲区太小会让中断开销占比过高。7. 常见问题排查与避坑经验实录7.1 DMA 传输不启动的排查思路DMA 配好了但数据不动这是最常见的问题。我的排查顺序是这样的检查时钟DMA 控制器的时钟开了吗这个最容易被忽略。检查通道映射外设请求和 DMA 通道的对应关系对吗查手册确认。检查使能顺序DMA 通道使能了吗外设的 DMA 请求使能了吗两个都要。检查 CNDTR传输数量设的是 0 吗设 0 的话 DMA 认为没有数据要传直接完成。检查地址CPAR 和 CMAR 的地址对吗特别是外设地址要取数据寄存器的地址不是外设基地址。7.2 数据错位与丢失问题数据错位通常和宽度配置有关。如果外设是 8 位、内存是 16 位DMA 会按 8 位传但内存地址按 16 位增量结果就是数据写到了错误的位置。数据丢失则可能是缓冲区溢出。DMA 循环模式下如果 CPU 处理速度跟不上新数据会覆盖还没处理的旧数据。解决办法是加大缓冲区或者提高处理速度。7.3 中断反复触发的处理中断反复触发九成是标志没清。DMA 的标志在 IFCR 寄存器串口空闲中断的标志清除需要读 SR 再读 DR。这两个地方都要检查。还有一种可能是中断优先级配置有问题导致中断嵌套异常。检查 NVIC 的优先级分组和具体优先级设置。7.4 常见问题速查表现象可能原因排查方法DMA 完全不工作时钟未开、通道映射错误查时钟使能寄存器、查手册映射表传输数量不对CNDTR 设置错误、宽度不匹配检查 CNDTR 初值、检查 PSIZE/MSIZE数据错位地址增量配置错误检查 PINC/MINC 设置中断反复触发标志未清除检查 IFCR 清除代码传输速度慢优先级低、总线冲突提高优先级、检查其他 DMA 活动最后一个字节丢失未等 TC 标志在中断里等待 USART TC 置位7.5 几个我踩过的坑坑一DMA 和中断同时用导致数据竞争。有一次我在串口 DMA 接收的空闲中断里直接处理数据同时主循环也在读同一个缓冲区结果数据错乱。后来改成中断里只做标记主循环处理问题解决。共享缓冲区的访问一定要有明确的同步机制。坑二循环模式下 CNDTR 重装时机不对。在空闲中断里重装 CNDTR 之前必须先关闭 DMA 通道否则重装的值可能不生效。这个细节手册里有写但很容易看漏。坑三SPI DMA 发送时 CS 信号提前拉高。DMA 传输完成中断触发时最后一个字节可能还在 SPI 移位寄存器里。如果这时候拉高 CS最后一个字节就丢了。解决办法是等 SPI 的 BSY 标志清零再拉高 CS。坑四ADC DMA 的缓冲区对齐问题。ADC 结果是 12 位存在 16 位变量里。如果缓冲区定义成 uint8_t 数组DMA 按半字写入时会跨字节边界导致数据解析错误。缓冲区类型必须和 DMA 数据宽度匹配。8. 不同芯片平台的 DMA 差异与适配8.1 STM32 系列的 DMA 特点STM32 的 DMA 分为 DMA1 和 DMA2不同系列的核心架构差异不小。F1 系列的 DMA 通道是固定映射的F4 系列引入了 DMA 流的概念每个流可以配置不同的通道灵活性更高。H7 系列则有 DMAMUX可以把任意外设请求路由到任意 DMA 流自由度最大。从 F1 迁移到 F4 时最大的变化是通道变成了流加通道选择。代码结构要调整但概念是相通的。8.2 ESP32 的 DMA 使用要点ESP32 的 DMA 架构和 STM32 差别较大。它的 UART 有内置的 FIFODMA 是通过 UHCI 或者直接寄存器操作实现的。用 ESP-IDF 开发时串口 DMA 接收通常用uart_read_bytes配合驱动层的缓冲区底层会自动使用 DMA。有个常见问题是ESP32 的 UART DMA 接收发送端需要改变发送方式吗答案是不需要。DMA 是接收端的事发送端正常发就行。接收端用 DMA 只是提高了接收效率对发送端没有要求。8.3 跨平台移植的注意事项从一个平台移植到另一个平台DMA 相关的代码基本要重写因为寄存器完全不同。但配置的思路是一样的确定通道映射、设置数据宽度、配置地址增量、选择模式、处理中断。我的建议是把 DMA 配置抽象成几个函数初始化、启动传输、停止传输、获取剩余数量。这样移植时只需要改这几个函数的实现上层逻辑不用动。9. 进阶话题DMA 与实时性的平衡9.1 DMA 对实时性的影响DMA 虽然解放了 CPU但它占用总线带宽。当多个 DMA 通道同时工作时总线仲裁会引入延迟。对于硬实时任务这个延迟可能不可接受。解决办法是合理分配优先级把实时性要求高的 DMA 通道设成高优先级同时避免多个高带宽 DMA 同时工作。9.2 与 RTOS 的配合在 RTOS 环境下用 DMA要注意中断和任务之间的同步。常见做法是用信号量或者消息队列在 DMA 完成中断里释放信号量任务里等待信号量。注意中断服务函数里不能调用可能阻塞的 API。FreeRTOS 里要用xSemaphoreGiveFromISR而不是xSemaphoreGive。9.3 低功耗场景下的 DMA 策略低功耗应用里DMA 的价值更大因为它可以让 CPU 在数据传输期间进入睡眠。配置好 DMA 后CPU 执行 WFI 指令进入睡眠DMA 完成中断唤醒 CPU 处理数据处理完继续睡。这样平均功耗可以降很多。但要注意有些低功耗模式下 DMA 也会停止工作需要查手册确认。比如 STM32 的 STOP 模式下DMA 是否可用取决于具体型号和配置。10. 调试工具与手段10.1 逻辑分析仪抓 DMA 时序逻辑分析仪是调试 DMA 的利器。抓 SPI 的 CLK 和 MOSI可以直观看到 DMA 传输时数据是否连续、有没有间隙。抓串口的 TX 和 RX可以确认数据是否正确。我习惯在 DMA 传输开始和结束时翻转一个 GPIO用逻辑分析仪测这个 GPIO 的高电平时间就是传输耗时。这个方法比软件计时更准确因为它不受中断延迟影响。10.2 用调试器查看 DMA 寄存器在 IDE 里查看 DMA 的寄存器状态可以快速定位问题。重点看 CCR 的 EN 位是否置位、CNDTR 是否在递减、ISR 里有没有错误标志。如果 CNDTR 一直不变说明 DMA 根本没启动。如果 CNDTR 在变但数据不对说明地址或宽度配置有问题。10.3 软件层面的验证方法除了硬件工具软件层面也可以做验证。比如在 DMA 传输前后对缓冲区做校验和对比发送端和接收端的数据是否一致。或者用已知模式填充缓冲区传输后检查模式是否被正确搬运。这些方法虽然不如逻辑分析仪直观但胜在方便不需要额外硬件。11. 一些实战中的个人体会DMA 这个东西看手册觉得简单真正用起来坑不少。我最大的体会是配置顺序和标志清除这两件事占了 DMA 调试问题的一大半。把这两个环节做扎实大部分问题都能避免。另外不要一上来就追求双缓冲、链式传输这些高级特性。先把单通道普通模式跑通再逐步加复杂度。我见过太多人一上来就搞复杂配置结果出了问题根本不知道是哪里的错。还有一点DMA 的调试信息要打足。在关键节点加日志记录 CNDTR 的值、中断触发次数、传输耗时。这些数据在排查问题时非常有用。等系统稳定了再把日志去掉。最后说一个实际项目里的经验。我们做过一个数据采集设备8 路串口同时工作每路都用 DMA 接收。一开始没注意优先级分配结果高速路数的数据偶尔丢包。后来把波特率最高的两路设成最高优先级问题就解决了。多路 DMA 场景下优先级分配不是可选项是必选项。这个系列后续还可以聊聊 DMA 和 Cache 的一致性问题那个在带 Cache 的芯片上是个大坑涉及内存屏障和 Cache 维护操作值得单独开一期。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑