news 2026/10/27 18:10:06

嵌入式DMA驱动开发实战:从原理到Linux DMAengine与性能优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式DMA驱动开发实战:从原理到Linux DMAengine与性能优化

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

搞嵌入式驱动开发,绕不开DMA这个坎。你可以把CPU想象成一个效率极高但时薪很贵的资深工程师,而DMA就是那个专门帮你搬砖的实习生——数据搬运这种体力活交给他,CPU腾出手来干真正需要脑子的活。我在早期做串口驱动的时候,没有用DMA,波特率一开到115200以上,CPU有将近40%的时间耗在串口中断里,稍微加点其他任务系统就开始丢包。后来切到DMA方案,同样的硬件,CPU占用直接降到个位数,整个系统的实时性上了一个台阶。

DMA全称Direct Memory Access,直接内存访问。它的核心价值就一句话:在外设和内存之间开辟一条不经过CPU的数据通道。不管是串口收发、SPI批量传输、ADC连续采样,还是LCD刷屏、SD卡读写,只要涉及大批量数据搬运,DMA几乎都是标配。我见过太多项目,前期功能验证阶段用中断方式跑得好好的,一到量产环境数据量上来就各种丢包、卡顿,回头再改DMA,代码架构要大动,非常痛苦。所以我的建议是:驱动设计初期就把DMA纳入方案考量,哪怕第一版不用,接口也要预留。

这篇文章面向的是有一定MCU或嵌入式Linux驱动基础的开发者,不管你是刚接触STM32的初学者,还是已经在做Linux内核驱动但想系统梳理DMA的老手,都能从中找到可以直接参考的内容。我会从方案选型、核心原理、实操配置、问题排查几个维度展开,把DMA驱动开发这件事讲透。

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

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

不是所有场景都适合上DMA。我总结了一个简单的判断标准:

场景特征推荐方案理由
单次传输小于16字节,频率低中断/轮询DMA配置开销大于收益
单次传输大于32字节,或连续传输DMACPU占用显著降低
高频率小包(如每1ms一次)DMA+空闲中断减少中断次数
需要精确时序控制DMA硬件自动搬运,时序稳定
低功耗场景DMACPU可休眠,DMA独立工作

我踩过的一个坑:在一个低功耗项目里,为了省事给一个每次只发4字节的传感器数据也配了DMA,结果每次传输的配置时间比直接写寄存器还长,功耗反而增加了。DMA的优势在于批量,不在于单次。

2.2 MCU裸机DMA vs Linux DMAengine

这是两个完全不同的技术栈,选型逻辑也不一样。

MCU裸机DMA(以STM32为例):直接操作DMA控制器寄存器或使用HAL/LL库,配置通道、源地址、目的地址、传输长度,启动传输,等中断。特点是直接、可控、代码量小。适合资源受限、实时性要求高的场景。

Linux DMAengine框架:内核提供的一套通用DMA抽象层,驱动通过dmaengineAPI申请通道、准备描述符、提交传输。特点是抽象程度高、跨平台好,但学习曲线陡,调试手段相对少。适合嵌入式Linux项目,尤其是需要跨SoC移植的场景。

我的经验是:如果你做的是RTOS或裸机项目,直接啃MCU参考手册的DMA章节,把寄存器搞明白,比什么都强。如果你做的是Linux驱动,老老实实学dmaengine框架,别想着绕过它直接操作寄存器,后期维护会要命。

2.3 通道分配与优先级设计

DMA控制器通常有多个通道,每个通道对应特定的外设请求线。以STM32F103C8T6为例,DMA1有7个通道,每个通道的请求映射是固定的,不能随意分配。比如USART1_TX固定映射到DMA1_Channel4,USART1_RX固定映射到DMA1_Channel5。

这里有个容易忽略的点:通道优先级。当多个通道同时请求时,硬件按优先级仲裁。我的建议是:

  • 实时性要求高的(如ADC采样、电机控制)设最高优先级
  • 大批量但可容忍延迟的(如SD卡读写、LCD刷新)设中低优先级
  • 同优先级的通道按通道号轮询

注意:优先级只影响仲裁顺序,不影响单次传输的原子性。高优先级通道不会打断正在进行的低优先级传输,只是在下一次仲裁时优先。

3. DMA核心原理与关键细节拆解

3.1 数据传输的三要素:源、目的、长度

任何一次DMA传输,本质上就是三个参数的组合:从哪里搬、搬到哪里、搬多少。听起来简单,但每个参数都有坑。

源地址和目的地址:可以是内存地址,也可以是外设数据寄存器地址。关键点在于地址是否递增。比如串口发送,源地址是内存缓冲区,每次传输后地址要递增(指向下一个字节);目的地址是串口数据寄存器,固定不变。这个递增/固定的配置如果搞反了,要么数据全发的是第一个字节,要么直接跑飞。

传输长度:以数据单元为单位,不是字节。如果数据宽度配置为16位,传输长度10,实际传输20字节。这个在ADC采样场景特别容易搞错,ADC是12位精度,通常用16位宽度存储,算缓冲区大小时要按16位算。

3.2 传输模式:单次 vs 循环 vs 双缓冲

单次模式(Normal):传输完成后停止,需要重新配置才能再次传输。适合一次性大批量传输,比如SPI刷一整屏数据。

循环模式(Circular):传输完成后自动重载,从头开始。适合连续周期性传输,比如ADC连续采样、串口接收。我做的环境监控项目里,ADC用循环模式DMA,CPU完全不用管,定时去缓冲区取最新值就行。

双缓冲模式(Double Buffer):有两个缓冲区交替使用,一个在传输时另一个可以被CPU处理。适合高速连续数据流,比如音频采集。STM32的DMA支持双缓冲,但配置稍复杂,需要设置两个内存地址和两个长度。

3.3 中断类型与触发时机

DMA的中断类型主要有三种:

  • 传输完成中断(TC):整个传输完成后触发。最常用。
  • 半传输中断(HT):传输到一半时触发。双缓冲和流式处理时有用。
  • 传输错误中断(TE):传输过程中出错触发。必须处理,否则出错后DMA会静默停止。

实操心得:TE中断一定要开,并且要在中断里打印错误状态寄存器。我遇到过DMA传输偶尔丢数据的问题,查了两天才发现是传输错误中断没开,DMA出错后自动停止了,但主循环还在等数据,整个系统就卡死了。

3.4 数据宽度与对齐问题

DMA支持8位、16位、32位数据宽度。源和目的的数据宽度可以不同,但必须保证对齐。比如源是8位数组,目的是16位外设寄存器,DMA会自动做打包,但要求源地址按8位对齐,目的地址按16位对齐。

对齐问题在结构体传输时特别容易出问题。比如你定义了一个结构体,里面有char、int、short混合,直接拿结构体地址做DMA源,很可能因为对齐问题导致数据错乱。我的做法是:DMA传输的缓冲区一律用__attribute__((aligned(4)))强制4字节对齐,数据宽度统一用32位或8位,避免混合宽度。

4. 实操过程与核心环节实现

4.1 STM32串口DMA收发完整配置

以STM32F103C8T6的USART1为例,目标是实现不定长数据的DMA接收和发送。

第一步:时钟和GPIO配置

// 使能时钟 RCC->APB2ENR |= RCC_APB2ENR_USART1EN | RCC_APB2ENR_IOPAEN; RCC->AHBENR |= RCC_AHBENR_DMA1EN; // PA9 TX 复用推挽输出 GPIOA->CRH &= ~(0xF << 4); GPIOA->CRH |= (0xB << 4); // PA10 RX 浮空输入 GPIOA->CRH &= ~(0xF << 8); GPIOA->CRH |= (0x4 << 8);

第二步:USART配置

USART1->BRR = 72000000 / 115200; // 72MHz主频 USART1->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE; USART1->CR1 |= USART_CR1_IDLEIE; // 开启空闲中断 USART1->CR3 |= USART_CR3_DMAR | USART_CR3_DMAT; // 使能DMA收发

第三步:DMA接收配置

// 先关闭通道再配置 DMA1_Channel5->CCR &= ~DMA_CCR_EN; DMA1_Channel5->CPAR = (uint32_t)&USART1->DR; DMA1_Channel5->CMAR = (uint32_t)rx_buffer; DMA1_Channel5->CNDTR = RX_BUFFER_SIZE; DMA1_Channel5->CCR = DMA_CCR_MINC | DMA_CCR_TCIE | DMA_CCR_TEIE; DMA1_Channel5->CCR |= DMA_CCR_EN;

第四步:空闲中断处理

void USART1_IRQHandler(void) { if (USART1->SR & USART_SR_IDLE) { USART1->SR; USART1->DR; // 清标志 uint16_t len = RX_BUFFER_SIZE - DMA1_Channel5->CNDTR; // 处理接收到的len字节数据 process_rx_data(rx_buffer, len); // 重新启动DMA接收 DMA1_Channel5->CCR &= ~DMA_CCR_EN; DMA1_Channel5->CNDTR = RX_BUFFER_SIZE; DMA1_Channel5->CCR |= DMA_CCR_EN; } }

这套方案的核心思路是:DMA负责搬数据,空闲中断负责判断一帧结束。相比每收到一个字节就中断一次,CPU占用降低了一个数量级。

4.2 SPI DMA刷屏实战

SPI刷LCD屏是DMA的经典应用。以ST7789驱动的240x320屏幕为例,一帧数据153600字节,如果用SPI中断发送,CPU基本干不了别的。

// SPI1 TX DMA配置 DMA1_Channel3->CPAR = (uint32_t)&SPI1->DR; DMA1_Channel3->CMAR = (uint32_t)lcd_buffer; DMA1_Channel3->CNDTR = LCD_BUFFER_SIZE; DMA1_Channel3->CCR = DMA_CCR_MINC | DMA_CCR_DIR | DMA_CCR_TCIE; DMA1_Channel3->CCR |= DMA_CCR_EN; // 启动SPI DMA发送 SPI1->CR2 |= SPI_CR2_TXDMAEN;

这里有个关键点:SPI的DMA请求是TXE(发送寄存器空)触发的,DMA每搬一个字节到DR,硬件自动发送。传输完成后TC中断触发,此时最后一个字节可能还在移位寄存器里,需要等BSY标志清零才能拉CS。

踩坑记录:我一开始在TC中断里直接拉高CS,结果最后一个字节总是丢失。后来改成在TC中断里等SPI1->SR & SPI_SR_BSY清零再拉CS,问题解决。这个细节在参考手册里写得很隐蔽,但实际项目中非常关键。

4.3 ADC连续采样与DMA循环模式

环境监控项目里,我需要同时采集8路ADC通道,每100ms取一次平均值。

// ADC配置为扫描模式,DMA循环 ADC1->CR1 |= ADC_CR1_SCAN; ADC1->CR2 |= ADC_CR2_DMA | ADC_CR2_CONT; ADC1->SQR1 = (8 - 1) << 20; // 8个转换 // 设置通道顺序 ADC1->SQR3 = ADC_CHANNEL_0 | (ADC_CHANNEL_1 << 5) | ...; // DMA循环模式 DMA1_Channel1->CPAR = (uint32_t)&ADC1->DR; DMA1_Channel1->CMAR = (uint32_t)adc_buffer; DMA1_Channel1->CNDTR = 8; DMA1_Channel1->CCR = DMA_CCR_MINC | DMA_CCR_CIRC | DMA_CCR_PSIZE_16 | DMA_CCR_MSIZE_16; DMA1_Channel1->CCR |= DMA_CCR_EN; ADC1->CR2 |= ADC_CR2_ADON | ADC_CR2_SWSTART;

循环模式下,DMA自动把8个通道的数据循环写入adc_buffer,CPU随时可以读取最新值。这里数据宽度配置为16位,因为ADC是12位精度,用16位存储刚好。

4.4 Linux DMAengine驱动框架实操

Linux下的DMA驱动走的是另一套逻辑。以SPI DMA为例,核心流程是:

// 1. 申请DMA通道 chan = dma_request_chan(dev, "tx"); // 2. 配置传输参数 slave_config.direction = DMA_MEM_TO_DEV; slave_config.dst_addr = spi_reg_base + SPI_TX_REG; slave_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE; slave_config.dst_maxburst = 16; dmaengine_slave_config(chan, &slave_config); // 3. 准备描述符 desc = dmaengine_prep_slave_sg(chan, sgl, sg_len, DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT); // 4. 设置回调 desc->callback = spi_dma_tx_complete; desc->callback_param = spi; // 5. 提交传输 dmaengine_submit(desc); dma_async_issue_pending(chan);

Linux DMAengine的关键在于**描述符(descriptor)**的概念。一次传输可以拆成多个描述符,硬件按链表顺序执行。dmaengine_prep_slave_sg用于外设到内存的散列表传输,dmaengine_prep_dma_cyclic用于循环传输。

经验之谈:Linux DMA调试最有效的手段是打开CONFIG_DMA_API_DEBUG,内核会检查地址对齐、长度合法性等问题。另外/sys/kernel/debug/dmaengine/下面有每个通道的状态信息,排查通道占用问题非常有用。

5. 常见问题与排查技巧实录

5.1 DMA传输丢数据

这是最常见的问题,原因通常有几种:

现象可能原因排查方法
偶尔丢最后一个字节TC中断时数据还在移位寄存器等BSY标志清零再处理
连续传输时丢中间数据传输错误中断未处理开启TE中断,打印错误状态
高速传输时丢包DMA优先级太低被抢占提高通道优先级
接收不定长数据丢帧空闲中断未及时处理降低中断延迟,或增大缓冲区

5.2 DMA传输速度上不去

DMA测速是很多人关心的问题。实测STM32F103在72MHz主频下,SPI DMA传输速度可以跑到SPI时钟的满速(18MHz),但有几个限制因素:

  • 总线仲裁:DMA和CPU共享总线,CPU频繁访问内存会拖慢DMA
  • 外设时钟:SPI时钟不能超过外设允许的最大值
  • 内存带宽:如果源数据在Flash里,Flash访问速度可能成为瓶颈

我的测速方法是:用定时器计时,DMA传输固定长度数据,算实际速率。实测SPI1 DMA在18MHz时钟下,传输1MB数据耗时约0.47秒,速率约2.1MB/s,基本达到理论值。

5.3 DMA与Cache一致性问题

这个问题在带Cache的MCU(如STM32H7、i.MX RT系列)上特别突出。DMA直接访问内存,不经过Cache,如果CPU刚写过数据还在Cache里没回写到内存,DMA搬走的就是旧数据。

解决方案有三种:

  1. 使用非Cache区域:把DMA缓冲区放在MPU配置为非Cache的内存区
  2. 手动Cache维护:发送前SCB_CleanDCache_by_Addr,接收后SCB_InvalidateDCache_by_Addr
  3. 使用DMA一致性内存:Linux下用dma_alloc_coherent申请

血泪教训:我在一个STM32H7项目里,DMA发送的数据总是随机出错,查了三天才发现是Cache问题。发送前加了Clean操作后,问题彻底解决。带Cache的MCU做DMA,Cache维护不是可选项,是必选项。

5.4 常见问题速查表

问题排查方向快速解决
DMA完全不工作时钟、通道使能、请求映射检查RCC和DMA_CCR_EN
传输长度不对CNDTR配置、数据宽度确认单位是数据单元不是字节
数据错位地址递增配置、对齐检查MINC和PINC,强制对齐
中断不触发中断使能、NVIC配置检查TCIE和NVIC_EnableIRQ
循环模式不循环CIRC位未设置确认DMA_CCR_CIRC
Linux下通道申请失败通道被占用或未注册查dmaengine debugfs

6. 进阶技巧与性能优化

6.1 双缓冲实现零等待数据流

双缓冲的核心思想是:DMA在写缓冲区A的时候,CPU处理缓冲区B;DMA写满A切换到B时,CPU处理A。这样CPU和DMA完全并行,没有等待。

STM32的DMA支持双缓冲模式,配置两个内存地址和两个长度,硬件自动切换。Linux下可以用dmaengine_prep_dma_cyclic配合两个period来实现类似效果。

6.2 DMA与RTOS的协同

在RTOS环境下,DMA传输完成中断里不要做耗时操作,应该释放信号量或发送消息队列,让任务去处理数据。我常用的模式是:

void DMA_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; // 清中断标志 vTaskNotifyGiveFromISR(dma_task_handle, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }

任务里等通知,收到后处理数据。这样中断延迟极短,数据处理在任务上下文,可以安全调用RTOS API。

6.3 DMA测速工具与方法

自己写一个简单的DMA测速工具其实不难。核心思路:用定时器记录DMA传输前后的时间戳,传输固定长度数据,计算速率。

uint32_t t1 = TIM2->CNT; // 启动DMA传输 while (!dma_done); uint32_t t2 = TIM2->CNT; float speed = DATA_SIZE / ((t2 - t1) / (float)TIMER_CLK);

注意定时器要配置为最高分辨率,避免计时误差。另外测速时要关闭其他中断,排除干扰。

6.4 低功耗场景下的DMA使用

低功耗场景下,DMA的价值更加突出。CPU可以进入Sleep或Stop模式,DMA独立工作,传输完成后通过中断唤醒CPU。这样平均功耗可以降低一个数量级。

配置要点:DMA中断要能唤醒CPU,NVIC和EXTI要正确配置唤醒源,外设时钟在低功耗模式下要保持开启。

7. 从驱动开发到系统架构的思考

做了这么多年嵌入式驱动,我越来越觉得DMA不仅仅是一个外设,它是一种系统设计思维。当你习惯用DMA的视角看问题,你会自然而然地思考:哪些事情必须CPU做,哪些可以交给硬件,数据流怎么设计才能让各个模块并行工作。

嵌入式架构师和普通驱动工程师的区别,很大程度上就体现在这里。普通工程师关注“这个功能怎么实现”,架构师关注“这个数据流怎么设计最合理”。DMA就是数据流设计的核心工具之一。

如果你正在准备嵌入式面试,DMA相关的问题几乎必考。常见的问题包括:DMA的工作原理、传输模式的区别、Cache一致性问题、DMA与中断的配合使用。建议不要死记硬背,而是自己动手写一遍配置代码,把每个寄存器的含义搞清楚,面试时自然对答如流。

最后分享一个我个人的习惯:每次做新项目,我都会先画一张数据流图,标出所有需要DMA的路径,然后逐个配置验证。这个习惯帮我避免了很多后期返工。DMA配置不复杂,但细节多,前期多花半小时理清楚,后期能省好几天调试时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 18:07:54

LLC谐振电源设计实战:从英飞凌手册到250W样机调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/27 18:07:09

QPSK跳频通信系统Matlab仿真与误码率闭环建模

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/27 18:06:55

MSP430F5529实现25Hz方波发生、峰值测量与显示闭环

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/27 18:05:05

无限视界世界生成与实时控制技术解析

1. 项目概述&#xff1a;无限视界的世界生成与实时控制去年在NeurIPS评审会上第一次看到这个研究方向时&#xff0c;我正喝着第三杯咖啡提神。当"无限视界世界生成"这个短语跳入眼帘&#xff0c;咖啡杯差点脱手——这不正是解决开放世界游戏开发痛点的终极方案吗&…

作者头像 李华
网站建设 2026/10/27 18:04:58

YOLOv26改进:重参数化与双阶段卷积提升目标检测精度

1. 项目背景与核心价值 在目标检测领域&#xff0c;YOLO系列算法始终保持着前沿地位。最新发布的YOLOv26在保持实时性的同时&#xff0c;通过引入重参数化瓶颈模块和双阶段卷积结构&#xff0c;显著提升了检测精度。这种改进并非简单的模块堆砌&#xff0c;而是基于对特征复用和…

作者头像 李华
网站建设 2026/10/27 18:04:34

西电微机原理课设:MFC键盘电子乐器从零到发声实战

简介&#xff1a;这份资源是西安电子科技大学微机原理课程设计的完整项目包&#xff0c;面向正在完成课设或自学微机接口与音频编程的本科生。项目以C MFC框架实现键盘电子乐器演奏程序&#xff0c;解决微机键盘映射电子琴音阶、节拍控制与声卡输出等典型课设问题。压缩包共95个…

作者头像 李华