news 2026/9/11 14:29:34

DMA原理与实战:从嵌入式到SoC的硬件协处理器详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DMA原理与实战:从嵌入式到SoC的硬件协处理器详解

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控制器要做三件事:

  1. 清空传输计数器:将DMA_CNDTRx寄存器归零;
  2. 置位状态标志:如DMA_ISR.TEIFx(传输错误)、DMA_ISR.HTIFx(半传输)、DMA_ISR.TCIFx(传输完成);
  3. 触发中断或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双缓冲

  1. 配置DMA为循环模式,接收缓冲区设为256字节;
  2. 使能USART的IDLE中断(当RX线空闲1字符时间,即检测到帧结束);
  3. IDLE中断服务程序中:
    • 读取USART_RDR清空RXNE标志(防止重复进中断);
    • 计算DMA当前索引:current_pos = buffer_size - DMA_CNDTRx
    • buffer_startcurrent_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))或直接操作数组下标。

实操步骤:

  1. ADC初始化:ADC_ChannelConfig(ADCx, CH0, ADC_SAMPLETIME_15CYCLES)... 配置四通道;
  2. DMA初始化:DMA_InitTypeDef DMA_InitStruct; DMA_InitStruct.MemoryDataSize = DMA_MEMORY_DATA_SIZE_HALF_WORD; // 16位
  3. 启动: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控制器复位分三步:

  1. DMA_BUS_MODE.SWR=1触发软复位;
  2. 等待32个AHB时钟周期(非毫秒!需用for(volatile int i=0; i<100; i++);粗略延时);
  3. 清除DMA_BUS_MODE.SWR=0,并检查DMA_STATUS.RST=0确认复位完成。

但更深层的问题在于:复位前,必须确保所有DMA通道已停止(DMA_CHx_CTRL.EN=0),且所有描述符队列已清空。否则,复位信号会与正在进行的DMA传输冲突,导致控制器进入不可恢复的锁死态。

经验技巧:在Linux驱动中,rockchip_dwc_eth_qos.cdwmac_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_CCRxUSART_CR3寄存器值逐级检查使能位,确认RCC配置
数据错乱/重复1. DMA数据宽度与外设寄存器宽度不匹配
2. 内存缓冲区未初始化(含随机值)
3. CPU与DMA同时访问同一内存区(缓存一致性)
打印DMA_CPARxDMA_CMARxDMA_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_ISERDMA_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/interruptsperf

在Linux平台(如RK3588),cat /proc/interrupts | grep dma可查看DMA中断触发次数,若数值停滞,说明DMA未工作;perf stat -e dma* -a sleep 1可统计DMA相关事件,如dma-readsdma-writes

7. 结语:DMA不是终点,而是系统协同的起点

写完这篇,我翻出十年前在STM32F103上手写第一个DMA驱动的笔记,上面画着歪歪扭扭的地址箭头和“一定要清标志位!!!”的红字批注。技术在变,从8位MCU到64位SoC,DMA的寄存器越来越复杂,但内核逻辑从未改变:它始终是那个沉默的协作者,用硬件的确定性,换取CPU的无限可能性。

最近在调试一个分布式DMA系统,把RK3588的多个DMA引擎通过PCIe桥接,实现跨芯片数据搬运。这时我才真正体会到分布式dma离散式dma scatgather这些热词背后的重量——它们不再是孤立的技术点,而是构建大规模异构计算系统的毛细血管。DMA的终极形态,或许就是消失于无形,让数据在芯片、内存、外设之间如呼吸般自然流动,而开发者只需专注算法与业务。

如果你正被某个dma failed错误折磨,不妨放下手册,拿起逻辑分析仪,去听听硬件真实的脉搏。那细微的电平跳动里,藏着比任何文档都更诚实的答案。

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

AutoDL回收站清理:彻底释放GPU云服务器磁盘空间

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

作者头像 李华
网站建设 2026/9/11 14:22:53

SAP Fiori Shell扩展实战:UI Plug-In开发与角色激活完整指南

第一次接触 SAP Fiori 的 Shell 扩展时&#xff0c;我其实走了不少弯路。当时业务方的需求很简单&#xff1a;希望在 Fiori Launchpad 顶栏右侧加一个自定义按钮&#xff0c;点开能跳转到企业内部的帮助系统。这种需求听起来不复杂&#xff0c;可当你真正打开 SAP 标准代码&…

作者头像 李华
网站建设 2026/9/11 14:22:49

当代社会价值排序:资本、智力、技能与劳动的博弈

1. 价值排序背后的社会现实解读 "资本&#xff1e;智力&#xff08;ideas&#xff09;&#xff1e;技能&#xff1e;人力劳动"这个排序在当代社会引发了广泛讨论。作为一名观察经济现象十余年的从业者&#xff0c;我亲眼见证了这个价值链条如何影响不同群体的生存状态…

作者头像 李华
网站建设 2026/9/11 14:18:59

如何用 Docker 运行 Android 模拟器:3 分钟跑通 Docker-Android

如何用 Docker 运行 Android 模拟器&#xff1a;3 分钟跑通 Docker-Android 【免费下载链接】docker-android Android in docker solution with noVNC supported, video recording and mcp server 项目地址: https://gitcode.com/GitHub_Trending/do/docker-android CI …

作者头像 李华
网站建设 2026/9/11 14:17:16

亿级并发下网络型负载均衡NLB的架构选型与部署实践

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

作者头像 李华