4. DMA传输模式:内存到内存、内存到外设、外设到内存的传输流程
DMA传输模式,说白了就是数据搬运的三种基本路线。我做了这么多年芯片验证,发现很多工程师对这三种模式的理解停留在表面,真正写起代码来就容易出问题。今天咱们就把它彻底讲透。
4.1 三种传输模式的核心区别
先看一张总览图,把三种模式的路径画清楚:
三种模式的区别,本质上就是源地址和目标地址落在哪个地址空间。我个人习惯用一张表来快速区分:
| 传输模式 | 源地址 | 目标地址 | 典型场景 |
|---|---|---|---|
| 内存→内存 | 内存空间 | 内存空间 | 数据拷贝、缓冲区刷新 |
| 内存→外设 | 内存空间 | 外设地址空间 | 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));
4.3 内存到外设传输(Mem-to-Peri)
这种模式在嵌入式系统中非常常见。比如你要通过UART发送一包数据,传统做法是CPU一个字节一个字节地写寄存器。用DMA的话,你只需要告诉它「去内存拿数据,送到UART的发送FIFO」,剩下的DMA帮你搞定。
为什么会这样?因为外设的地址空间通常不递增。你想想看,UART的发送寄存器就一个地址,每次写同一个地址,数据就发出去了。所以DMA的目标地址要固定,不能递增。
| 参数 | 配置值 | 说明 |
|---|---|---|
| 源地址 | 内存缓冲区地址 | 递增(读取数据) |
| 目标地址 | 外设寄存器地址 | 固定(不递增) |
| 传输宽度 | 与外设数据宽度匹配 | 如UART为8位 |
| 触发方式 | 软件触发或外设请求 | 通常由CPU启动 |
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误触发。从那以后,我养成了一个习惯:在外设请求信号上加一个同步器和去毛刺电路。
好了,三种传输模式就讲到这里。你只要记住一句话:源和目标地址的递增/固定组合,决定了传输模式。剩下的就是根据具体外设的特性去配置参数了。