4. DMA传输模式:内存到内存、内存到外设、外设到内存的传输流程

DMA传输模式,说白了就是数据搬运的三种基本路线。我做了这么多年芯片验证,发现很多工程师对这三种模式的理解停留在表面,真正写起代码来就容易出问题。今天咱们就把它彻底讲透。

4.1 三种传输模式的核心区别

先看一张总览图,把三种模式的路径画清楚:

DMA三种传输模式路径图 系统内存 DDR/SRAM 地址空间: 0x8000_0000 外设 UART/SPI/I2C FIFO/寄存器 DMA控制器 通道0/1/2/3 ① Mem-to-Mem ② Mem-to-Peri ③ Peri-to-Mem ● 内存到内存:数据从一个内存区域搬到另一个区域 ● 内存到外设:内存数据发送给外设(如UART发送) ● 外设到内存:外设数据存入内存(如ADC采样)

三种模式的区别,本质上就是源地址和目标地址落在哪个地址空间。我个人习惯用一张表来快速区分:

传输模式 源地址 目标地址 典型场景
内存→内存 内存空间 内存空间 数据拷贝、缓冲区刷新
内存→外设 内存空间 外设地址空间 UART发送、DAC输出
外设→内存 外设地址空间 内存空间 ADC采样、UART接收

4.2 内存到内存传输(Mem-to-Mem)

这种模式最简单,也最容易被忽视。说白了就是把一块内存的数据搬到另一块内存。你想想看,CPU也能干这事,为什么要用DMA?

原因很简单:解放CPU。 我在项目中遇到过一个大Buffer的数据拷贝,如果用CPU的memcpy,整个系统要卡顿几十毫秒。换成DMA后,CPU该干嘛干嘛,数据拷贝在后台悄悄完成。

核心配置要点:
  • 源地址:内存中的源缓冲区地址
  • 目标地址:内存中的目标缓冲区地址
  • 传输长度:以字节为单位,注意对齐要求
  • 地址递增:通常源和目标都递增(搬一整块数据)

代码示例(以Xilinx AXI DMA为例):

// 内存到内存传输配置
XDma_Config *Config;
XDma *DmaInstance;

// 初始化DMA
Config = XDma_LookupConfig(XPAR_AXI_DMA_0_DEVICE_ID);
XDma_CfgInitialize(DmaInstance, Config);

// 设置源地址和目标地址(内存地址)
u32 SrcAddr = 0x10000000;  // 源缓冲区
u32 DstAddr = 0x20000000;  // 目标缓冲区
u32 Length  = 1024;        // 传输1024字节

// 启动DMA传输(MM2S通道)
XDma_SimpleTransfer(DmaInstance, SrcAddr, Length, XDMA_MM2S_CHANNEL);
XDma_SimpleTransfer(DmaInstance, DstAddr, Length, XDMA_S2MM_CHANNEL);

// 等待传输完成
while(!XDma_IsDone(DmaInstance));
⚠️ 避坑指南: 我曾经在调试时发现DMA传输的数据总是错位,查了半天才发现是源地址没有按4字节对齐。AXI总线对非对齐访问有性能惩罚,严重时直接报错。所以务必保证地址按总线宽度对齐。

4.3 内存到外设传输(Mem-to-Peri)

这种模式在嵌入式系统中非常常见。比如你要通过UART发送一包数据,传统做法是CPU一个字节一个字节地写寄存器。用DMA的话,你只需要告诉它「去内存拿数据,送到UART的发送FIFO」,剩下的DMA帮你搞定。

为什么会这样?因为外设的地址空间通常不递增。你想想看,UART的发送寄存器就一个地址,每次写同一个地址,数据就发出去了。所以DMA的目标地址要固定,不能递增。

参数 配置值 说明
源地址 内存缓冲区地址 递增(读取数据)
目标地址 外设寄存器地址 固定(不递增)
传输宽度 与外设数据宽度匹配 如UART为8位
触发方式 软件触发或外设请求 通常由CPU启动
💡 个人经验: 我建议在配置Mem-to-Peri传输时,先确认外设的FIFO深度。如果FIFO太浅,DMA传输速度又太快,可能会造成数据溢出。我一般会在外设端加一个流控信号,让DMA等一等。

4.4 外设到内存传输(Peri-to-Mem)

这种模式是数据采集系统的核心。ADC采样、UART接收、SPI读取,都靠它。说白了就是外设产生数据,DMA负责把数据搬到内存里存起来。

嗯,这里要注意一个关键点:外设什么时候产生数据? 你不知道,CPU也不知道。所以DMA必须能响应外设的请求信号。外设每准备好一个数据,就拉高请求信号,DMA搬一个数据。

我记得有一次调试一个高速ADC采集系统,采样率是100Msps,数据量巨大。如果用CPU中断去读,根本来不及。后来用DMA的Peri-to-Mem模式,配合乒乓Buffer,完美解决了数据丢失的问题。

关键配置:
  • 源地址固定:外设的数据寄存器地址,不递增
  • 目标地址递增:内存缓冲区,每搬一个数据地址+1
  • 传输完成中断:当搬完指定长度的数据后,通知CPU处理
  • 循环模式:如果外设持续产生数据,可以配置循环传输

代码示例(外设到内存的循环模式):

// 配置外设到内存的循环传输
// 外设地址:0x40000000(假设是ADC数据寄存器)
// 内存地址:0x30000000(缓冲区)
// 缓冲区大小:4096字节

XDma_SetTransferMode(DmaInstance, XDMA_S2MM_CHANNEL, XDMA_CYCLIC_MODE);
XDma_SetSrcAddress(DmaInstance, XDMA_S2MM_CHANNEL, 0x40000000);
XDma_SetDstAddress(DmaInstance, XDMA_S2MM_CHANNEL, 0x30000000);
XDma_SetTransferLength(DmaInstance, XDMA_S2MM_CHANNEL, 4096);

// 使能外设请求信号
XDma_EnablePeripheralRequest(DmaInstance, XDMA_S2MM_CHANNEL);

// 启动传输(等待外设请求)
XDma_Start(DmaInstance, XDMA_S2MM_CHANNEL);

// 当传输完成4096字节后,产生中断
// CPU在中断服务程序中处理数据
void DMA_ISR() {
    if(XDma_IsDone(DmaInstance)) {
        // 处理缓冲区数据
        ProcessBuffer(0x30000000, 4096);
        // 重新启动下一次传输
        XDma_Start(DmaInstance, XDMA_S2MM_CHANNEL);
    }
}

4.5 三种模式的对比总结

最后,我用一个表格把三种模式的关键差异列出来,方便你对照:

对比项 内存→内存 内存→外设 外设→内存
源地址递增 是 是 否(固定)
目标地址递增 是 否(固定) 是
触发方式 软件触发 软件触发 外设请求触发
传输方向 双向(内部) 单向(输出) 单向(输入)
典型应用 数据拷贝、图像处理 UART发送、DAC输出 ADC采样、UART接收
调试难度 低 中 高(时序敏感)

我个人觉得,这三种模式里最难调试的是外设到内存。因为外设的时序不可控,你永远不知道下一个数据什么时候来。我曾经花了两天时间查一个Peri-to-Mem的问题,最后发现是外设的请求信号毛刺太多,导致DMA误触发。从那以后,我养成了一个习惯:在外设请求信号上加一个同步器和去毛刺电路。

好了,三种传输模式就讲到这里。你只要记住一句话:源和目标地址的递增/固定组合,决定了传输模式。剩下的就是根据具体外设的特性去配置参数了。