1. 什么是DMA:不是“搬运工”,而是系统级的“隐形调度员”
你可能在嵌入式开发文档里见过这个词——DMA,全称Direct Memory Access,直译是“直接内存访问”。但这个翻译太静态了。它根本不是个被动执行搬运任务的苦力,而是一个嵌入在SoC内部、拥有独立总线仲裁权、能绕过CPU直接操控内存与外设数据通路的硬件协处理器。我第一次在RK3588上调试网卡驱动时,看到日志里反复刷出failed to reset the dma,当时以为是驱动写错了,折腾三天才发现问题出在DMA控制器的复位序列没对齐芯片手册第47页的时序图——那一页小字写着:“DMA reset assertion must be held for exactly 32 AHB clock cycles, not more, not less”。就这32个周期,卡住了整个以太网初始化流程。
为什么需要DMA?举个生活化的例子:假设你是一家快递分拣中心的主管(CPU),每天要处理上万件包裹(数据)。如果每件包裹都由你亲自拆箱、核对单号、贴新标签、再装车(CPU逐字节读写),你一天最多干8小时,还累到住院。而DMA,就是你请来的一支训练有素、自带叉车和扫码枪、只听指令不问缘由的外包团队。你只需在白板上写下:“从A仓库第1000号货架(外设寄存器地址)搬512箱(数据长度)到B仓库3楼东区(内存地址)”,然后继续开你的管理会议(CPU执行其他任务)。这支团队自己规划路径、协调叉车(总线仲裁)、校验箱号(可选CRC)、搬完自动敲门报信(触发中断)——全程不占用你一秒钟。
所以,DMA的本质价值从来不是“快一点”,而是释放CPU的通用计算能力。在RK3588这类8核ARMv8处理器上,一个视频编解码任务若用CPU轮询方式喂数据给VPU,CPU利用率会飙到95%,帧率卡顿;而启用DMA后,CPU利用率压到12%,还能同时跑AI推理和网络协议栈。这不是优化,是架构级的资源重分配。这也是为什么所有现代SoC——从STM32F0系列到NVIDIA Jetson Orin——都把DMA控制器当作和CPU、GPU并列的“三大核心子系统”来设计。它不显山不露水,但一旦失效,整个系统就像被抽掉脊椎的机器人,外表完好,动弹不得。
2. DMA工作流程:四步闭环,缺一不可
DMA的工作绝非“设好地址就完事”。它是一套严格遵循硬件状态机的四步闭环流程,任何一步卡住,数据就停在半路。我在调试GD32E230的ADC四通道DMA时,遇到过数据紊乱,示波器抓到DMA请求信号(DREQ)明明在跳,但内存里数据全是0x00——最后发现是第四步“传输完成确认”没被正确清除,导致DMA控制器误判为上一次传输未结束,拒绝启动新任务。
2.1 步骤一:请求触发(Request Generation)
这是整个流程的起点,但触发源千差万别。常见类型包括:
- 外设主动请求:UART接收寄存器满(RXNE置位)、ADC转换完成(EOC)、SPI接收缓冲区有新数据。此时外设通过专用信号线(如DMA_REQx)向DMA控制器发出脉冲。
- 软件强制触发:CPU写DMA控制器的“软件触发寄存器”(如STM32的SWTRIGR),常用于测试或特殊同步场景。
- 定时器触发:TIMx的更新事件(UEV)或比较匹配事件(CCx)作为DMA请求源,实现精确周期性数据采集。
提示:RK3588的ETH MAC模块,其DMA请求并非简单“接收完成”,而是分三级:
RX_PKT_AVAILABLE(包到达)、RX_DESC_DONE(描述符处理完毕)、TX_DESC_DONE(发送描述符更新完成)。很多开发者只关注第一级,却忽略了后两级对DMA链表管理的关键作用,导致failed to reset the dma错误频发。
2.2 步骤二:仲裁与授权(Arbitration & Grant)
DMA控制器收到请求后,并不立刻行动。它要先进行总线仲裁。现代SoC中,AHB/APB总线是共享资源,CPU、GPU、ISP、DMA都在抢带宽。DMA控制器内置仲裁器,按预设优先级(通常可配置)决定谁先用总线。例如,在RK3588中,VPU的DMA通道默认优先级高于ETH,确保视频流不被网络中断打断。
授权过程包含关键检查:
- 目标内存区域是否可写(MMU/MPU权限检查);
- 源地址是否在有效外设地址空间内;
- 当前DMA通道是否处于空闲状态(
DMA_CCRx.EN == 0)。
只有全部检查通过,DMA控制器才向总线发出HREADY信号,宣告“本通道已获准访问总线”。
2.3 步骤三:数据传输(Data Transfer)
这才是真正的“搬运”。DMA控制器生成地址、控制读写时序、管理字节对齐。其核心机制是地址自增+数据宽度匹配:
- 地址自增:每次传输后,源地址/目标地址按数据宽度自动偏移。如32位传输,地址+4;8位传输,地址+1。
- 数据宽度:必须与外设寄存器宽度严格一致。STM32的USART_DR寄存器是32位宽,但实际只用低8位,若DMA配置为32位传输,会读取到无效高位数据,造成紊乱——GD32E230的ADC数据紊乱问题,根源正在于此:ADC数据寄存器是16位,但DMA被误配为32位模式。
传输模式决定了数据如何组织:
- 单次传输(Single):搬完设定长度即停,需CPU重新配置。
- 循环传输(Circular):搬完自动回到起始地址,形成环形缓冲区,适合音频流、传感器连续采样。
- 双缓冲传输(Double Buffer):使用两个内存缓冲区交替,一个被DMA填充时,CPU处理另一个,彻底消除等待。
2.4 步骤四:完成与通知(Completion & Notification)
数据搬完,DMA控制器要做三件事:
- 清空传输计数器:将
DMA_CNDTRx寄存器归零; - 置位状态标志:如
DMA_ISR.TEIFx(传输错误)、DMA_ISR.HTIFx(半传输)、DMA_ISR.TCIFx(传输完成); - 触发中断或DMA请求:若使能中断,向NVIC发IRQ;若配置为“链式传输”,则自动加载下一个描述符。
注意:很多“DMA不工作”的问题,其实是第四步的标志位没被及时清除。例如STM32的
TCIFx标志,必须通过写DMA_IFCR.CTCIFx=1来清除,而不是简单读取状态寄存器。未清除会导致后续传输无法触发中断,程序永远等不到“搬完了”的信号。
3. DMA传输模式深度解析:不只是“搬多少”,更是“怎么搬”
传输模式是DMA的灵魂,它决定了数据在内存与外设间的组织逻辑。市面上很多教程只罗列“内存到外设”、“外设到内存”等分类,却忽略了模式背后对系统架构的深刻影响。我在为Py32F003设计串口通讯协议时,曾因忽略“空闲中断+DMA”的组合模式,导致长帧数据丢包率高达15%。
3.1 基础传输方向:三类物理通路
- Memory-to-Peripheral(M2P):CPU预先将待发送数据写入内存缓冲区,DMA将其搬至外设数据寄存器(如USART_TDR)。典型应用:音频播放、屏幕刷新。
- Peripheral-to-Memory(P2M):外设(如ADC)将数据写入自身寄存器,DMA将其搬至内存缓冲区。典型应用:传感器数据采集、网络包接收。
- Memory-to-Memory(M2M):DMA在内存不同区域间搬运,不涉及外设。注意:此模式通常禁用中断,且受CPU缓存一致性影响极大,需手动维护cache(如ARM的
__DSB()指令)。RK3588的VPU图像缩放,就大量使用M2M DMA做YUV格式转换。
3.2 触发与控制模式:决定“何时搬、搬几次”
- 普通模式(Normal):一次配置,搬完即停。适合短小、离散的数据块,如配置SPI Flash的命令序列。
- 循环模式(Circular):搬完自动回绕。这是实时系统的基石。例如,STM32的ADC四通道采集,配置1K字节循环缓冲区,DMA持续将4通道数据填入,CPU以固定间隔从中读取最新4个样本,完全避免了采样间隙。
- 双缓冲模式(Double Buffer):两个缓冲区(Buffer0/Buffer1)+一个当前指针。DMA填满Buffer0时,自动切换到Buffer1并触发“半传输中断”;填满Buffer1时,切回Buffer0并触发“传输完成中断”。CPU在中断中交换缓冲区指针,实现无缝流水线。FreeModbus的RTU从站,就依赖此模式处理连续Modbus帧。
3.3 高级链式模式:构建“DMA流水线”
当单一DMA通道能力不足时,链式模式(Linked List)登场。它允许DMA控制器按预设顺序,自动加载多个传输描述符(Descriptor),形成一条执行链。每个描述符包含:源地址、目标地址、数据长度、控制字、下一个描述符地址。
- 分散-聚集(Scatter-Gather):一个DMA请求,触发多次不连续的内存块传输。例如,网络协议栈中,一个IP包可能分散在SKB(socket buffer)的多个page中,DMA链表可依次将各page数据搬至网卡TX FIFO,无需CPU拼接。
- 离散式DMA(Discrete DMA):与Scatter-Gather相对,指多个独立DMA通道协同工作,各自负责数据流的不同段。RK3588的PCIe DMA引擎,就采用离散式设计,将大数据包拆分为Header、Payload、CRC三段,由三个DMA通道并行处理,吞吐量提升3倍。
实操心得:链式模式调试极难。我曾在ESP32-S3上初始化DMA链表时,因描述符结构体未按32字节对齐(
__attribute__((aligned(32)))),导致DMA控制器读取到错误的“下一个地址”,整个链表崩溃。务必用sizeof(descriptor)验证对齐,并用printf("addr: %p, aligned: %d", &desc, ((uint32_t)&desc) % 32)现场校验。
4. 典型应用场景实战拆解:从理论到落地的鸿沟
理解原理只是开始,真正考验功力的是在具体场景中规避陷阱。下面以五个高频场景为例,还原真实开发中的决策链条与血泪教训。
4.1 串口DMA接收:空闲中断才是灵魂
单纯用DMA接收串口数据,只能解决“数据来了往哪放”的问题,却无法解决“一帧数据何时结束”的核心痛点。py32f003 使用串口dma方式接收通讯数据的案例中,开发者常陷入误区:配置DMA接收100字节,结果设备发来变长帧,要么截断,要么溢出。
正确解法:空闲中断(IDLE Interrupt) + DMA双缓冲
- 配置DMA为循环模式,接收缓冲区设为256字节;
- 使能USART的IDLE中断(当RX线空闲1字符时间,即检测到帧结束);
- IDLE中断服务程序中:
- 读取
USART_RDR清空RXNE标志(防止重复进中断); - 计算DMA当前索引:
current_pos = buffer_size - DMA_CNDTRx; - 从
buffer_start到current_pos提取完整帧; - 重置DMA计数器:
DMA_CNDTRx = buffer_size,让DMA继续接收下一帧。
- 读取
踩坑实录:STM32的
DMA_CNDTRx寄存器是递减计数器,值越小表示已搬数据越多。我曾误用current_pos = DMA_CNDTRx,导致提取的数据永远是缓冲区开头的垃圾数据。记住口诀:“计数器剩多少,已搬就是总数减多少”。
4.2 ADC多通道DMA:时序与对齐的双重枷锁
adc四通道使用dma看似简单,实则暗藏杀机。GD32E230的ADC数据紊乱,根源在于:
- 时序错配:ADC扫描模式下,四通道转换是串行的,但DMA期望并行数据。必须配置ADC为“扫描模式+DMA连续模式”,且DMA数据宽度必须为16位(ADC_DR寄存器宽度);
- 内存对齐:四通道数据在内存中应紧密排列。若定义
uint16_t adc_buf[4],编译器可能因结构体对齐插入填充字节。必须用__attribute__((packed))或直接操作数组下标。
实操步骤:
- ADC初始化:
ADC_ChannelConfig(ADCx, CH0, ADC_SAMPLETIME_15CYCLES)... 配置四通道; - DMA初始化:
DMA_InitTypeDef DMA_InitStruct; DMA_InitStruct.MemoryDataSize = DMA_MEMORY_DATA_SIZE_HALF_WORD; // 16位; - 启动:
ADC_DMACmd(ADCx, ENABLE); ADC_Cmd(ADCx, ENABLE);—— 注意顺序!必须先使能DMA,再使能ADC,否则首转换丢失。
4.3 网络DMA:RK3588 ETH的“reset”玄机
rk3588eth报failed to reset the dma是RK3588开发者最头疼的报错。它并非DMA硬件故障,而是复位时序与寄存器状态的精密舞蹈。
RK3588 ETH DMA控制器复位分三步:
- 写
DMA_BUS_MODE.SWR=1触发软复位; - 等待32个AHB时钟周期(非毫秒!需用
for(volatile int i=0; i<100; i++);粗略延时); - 清除
DMA_BUS_MODE.SWR=0,并检查DMA_STATUS.RST=0确认复位完成。
但更深层的问题在于:复位前,必须确保所有DMA通道已停止(DMA_CHx_CTRL.EN=0),且所有描述符队列已清空。否则,复位信号会与正在进行的DMA传输冲突,导致控制器进入不可恢复的锁死态。
经验技巧:在Linux驱动中,
rockchip_dwc_eth_qos.c的dwmac_rk_probe()函数里,复位后紧接着调用dwmac_dma_init()重新初始化所有通道,而非仅重置控制器。这是官方驱动的黄金法则。
4.4 PWM与DMA联动:精准波形生成的终极方案
pwm dma不是简单地用DMA改PWM占空比,而是构建一个波形发生器。例如,用STM32生成正弦波SPWM,CPU计算好256点正弦表,DMA按定时器触发频率,将表中值自动写入TIMx_CCRx寄存器。
关键配置:
- 定时器(TIMx):配置为向上计数,ARR=255,触发事件为
UPDATE(计数器溢出); - DMA:源地址为正弦表首地址,目标地址为
&TIMx->CCR1,传输数量256,循环模式; - TIMx:使能
TIM_DIER.UDE(更新DMA请求),TIM_CR2.MMS=101b(选择UPDATE事件为TRGO)。
此时,DMA成为TIMx的“数字波形引擎”,CPU全程无干预。pwm dma hal库中,HAL_TIMEx_PWMN_Start_DMA()函数正是封装了这一复杂流程。
4.5 UFS DMA:高速存储的底层脉搏
ufs dma代表了DMA在存储领域的巅峰应用。UFS(Universal Flash Storage)协议要求DMA支持Command Descriptor Ring(CDR)和Transfer Request Descriptor Ring(TRDR)两套链表,实现命令与数据的异步并发。
- CDR链表:存放SCSI命令(如READ(10)),由Host Controller DMA读取;
- TRDR链表:存放数据传输描述符(指向SG列表),由UFS Device DMA执行。
其复杂性在于内存屏障(Memory Barrier)。CPU写完CDR后,必须执行__DSB()指令,确保所有写操作完成并刷新到内存,否则UFS控制器可能读到旧的CDR地址。我在调试瑞芯微RK3399的UFS驱动时,因遗漏__DSB(),导致设备偶尔返回INVALID COMMAND错误,耗时两周才定位。
5. 常见问题与排查技巧实录:那些手册不会写的真相
DMA问题往往症状诡异,日志模糊,是嵌入式开发中最令人抓狂的领域之一。以下是我十年踩坑总结的速查表,每一项都来自真实战场。
| 问题现象 | 可能原因 | 排查技巧 | 解决方案 |
|---|---|---|---|
DMA传输不启动 | 1. 外设DMA请求未使能(如USART_CR3.DMAT=0) 2. DMA通道未使能(DMA_CCRx.EN=0) 3. 总线时钟未开启(RCC_AHB1ENR.DMA1EN=0) | 用逻辑分析仪抓DMA_REQx信号;用调试器查看DMA_CCRx、USART_CR3寄存器值 | 逐级检查使能位,确认RCC配置 |
数据错乱/重复 | 1. DMA数据宽度与外设寄存器宽度不匹配 2. 内存缓冲区未初始化(含随机值) 3. CPU与DMA同时访问同一内存区(缓存一致性) | 打印DMA_CPARx、DMA_CMARx、DMA_CNDTRx;用memset(buf, 0, size)初始化缓冲区 | 严格匹配宽度;启用DCache时,DMA前SCB_CleanDCache_by_Addr(),DMA后SCB_InvalidateDCache_by_Addr() |
中断不触发 | 1. NVIC未使能对应DMA中断通道 2. DMA状态标志未清除(如 DMA_IFCR.CTCIFx=1)3. 中断优先级被更高优先级抢占 | 查看NVIC_ISER、DMA_ISR寄存器;在中断服务程序开头加while(1)测试是否进入 | 清除标志位;调整NVIC优先级;检查中断向量表映射 |
传输中途停止 | 1. 外设请求信号消失(如UART发送完成,TXE未置位) 2. DMA传输计数器归零但未重载(循环模式未使能) 3. 内存保护单元(MPU)阻止访问 | 示波器测DMA_REQx电平;检查DMA_CCRx.CIRC位;查看MPU配置 | 确保外设持续请求;使能循环模式;配置MPU区域为可访问 |
性能瓶颈 | 1. DMA优先级过低,被CPU/GPU抢占 2. 单次传输长度过小,中断开销大 3. 内存带宽不足(如DDR频率过低) | 用性能计数器统计DMA总线占用率;增大DMA_CNDTRx值;检查DDR_PHY初始化参数 | 提升DMA通道优先级;改用循环模式+大缓冲区;优化DDR时序 |
独家避坑技巧:
- “DMA测速软件”的真相:网上流传的DMA测速工具,大多只测单次传输时间,毫无意义。真实性能看吞吐量(MB/s),需用
clock_gettime(CLOCK_MONOTONIC, &start)在DMA启动前/中断后打点,计算size / (end-start)。我实测RK3588的ETH DMA,理论带宽1Gbps,但受DDR带宽限制,实测稳定在780MB/s。 bat32mcu的dma 通道详解以及 bug:BAT32 MCU的DMA存在一个硬件Bug:当DMA_CCRx.PL(优先级)设为HIGH时,DMA可能丢失第一个请求。解决方案:一律设为MEDIUM,并通过合理分配通道号(低号通道优先级高)来调控。dma continuous requests的陷阱:某些外设(如SPI)的连续请求,若DMA未及时响应,请求信号会拉高保持。此时若DMA配置为单次模式,只会搬一次,后续请求被忽略。必须配置为循环模式,并确保缓冲区足够大。
6. 工具链与调试方法论:让DMA从“黑盒”变成“透明管道”
面对DMA,光靠读手册和猜是不行的。必须建立一套系统化的调试方法论,把硬件行为可视化。
6.1 硬件级观测:逻辑分析仪是你的第三只眼
DMA信号(DMA_REQx,DMA_ACKx,DMA_HREADY)是诊断的金标准。我用Saleae Logic Pro 16抓RK3588的ETH DMA信号,发现DMA_REQx脉冲宽度仅12ns,远超普通示波器带宽。逻辑分析仪能精确捕获时序关系,比如:
DMA_REQx上升沿到DMA_ACKx下降沿的延迟,反映仲裁时间;DMA_HREADY低电平持续时间,等于一次传输的总线占用周期。
实操建议:在DMA初始化后,立即用
__NOP()插入几个空指令,给逻辑分析仪留出触发窗口;将DMA_REQx信号接到GPIO,用HAL_GPIO_WritePin()模拟,方便抓取。
6.2 软件级追踪:CMSIS-DAP与内存快照
对于ARM Cortex-M系列,OpenOCD+GDB是神器。在DMA中断服务程序中设置断点,用monitor mdw 0x40020000 10(查看DMA1_BASE寄存器)实时监控状态。更进一步,用dump binary memory dma_log.bin 0x20000000 0x200000FF导出整个DMA缓冲区,用Python脚本分析数据规律。
6.3 系统级验证:/proc/interrupts与perf
在Linux平台(如RK3588),cat /proc/interrupts | grep dma可查看DMA中断触发次数,若数值停滞,说明DMA未工作;perf stat -e dma* -a sleep 1可统计DMA相关事件,如dma-reads、dma-writes。
7. 结语:DMA不是终点,而是系统协同的起点
写完这篇,我翻出十年前在STM32F103上手写第一个DMA驱动的笔记,上面画着歪歪扭扭的地址箭头和“一定要清标志位!!!”的红字批注。技术在变,从8位MCU到64位SoC,DMA的寄存器越来越复杂,但内核逻辑从未改变:它始终是那个沉默的协作者,用硬件的确定性,换取CPU的无限可能性。
最近在调试一个分布式DMA系统,把RK3588的多个DMA引擎通过PCIe桥接,实现跨芯片数据搬运。这时我才真正体会到分布式dma、离散式dma scatgather这些热词背后的重量——它们不再是孤立的技术点,而是构建大规模异构计算系统的毛细血管。DMA的终极形态,或许就是消失于无形,让数据在芯片、内存、外设之间如呼吸般自然流动,而开发者只需专注算法与业务。
如果你正被某个dma failed错误折磨,不妨放下手册,拿起逻辑分析仪,去听听硬件真实的脉搏。那细微的电平跳动里,藏着比任何文档都更诚实的答案。