3. DMA 传输类型详解:内存到内存、内存到外设、外设到内存、外设到外设传输的差异与配置
DMA 控制器支持的传输类型决定了数据在系统总线上的流动路径。根据源端和目标端的不同,DMA 传输可分为四种基本类型:内存到内存、内存到外设、外设到内存和外设到外设。每种类型在硬件触发方式、地址递增规则、传输粒度以及驱动配置上存在显著差异。
核心要点: 传输类型的本质是“谁控制流控(Flow Controller)”以及“地址是否自动递增”。内存到内存由软件触发,外设相关传输由硬件握手信号触发。
3.1 四种传输类型的架构对比
下图展示了四种 DMA 传输类型在系统总线上的数据流向。箭头方向代表数据移动方向,控制信号线(DREQ/DACK)仅在外设参与时出现。
3.2 传输类型核心差异表
下表从触发方式、地址模式、流控角色、典型场景和驱动配置要点五个维度对比四种传输类型:
| 对比维度 | 内存 → 内存 | 内存 → 外设 | 外设 → 内存 | 外设 → 外设 |
|---|---|---|---|---|
| 触发方式 | 软件写触发位 | 外设 DREQ 硬件触发 | 外设 DREQ 硬件触发 | 外设 A/B 双 DREQ |
| 源地址模式 | 递增(+1/+2/+4) | 递增 | 固定(外设 FIFO) | 固定(外设 A) |
| 目标地址模式 | 递增 | 固定(外设 FIFO) | 递增 | 固定(外设 B) |
| 流控角色 | DMA 控制器 | DMA 控制器 | DMA 控制器 | 外设或 DMA |
| 典型场景 | 内存拷贝、帧缓冲复制 | LCD 刷新、DAC 输出 | UART 接收、ADC 采样 | SPI 环回、I2S 音频路由 |
| 驱动配置要点 | 设置 src/dst 递增,软件触发 | src 递增,dst 固定,绑定 DREQ | src 固定,dst 递增,绑定 DREQ | 双固定地址,双通道绑定 |
驱动开发提示: 在 Linux DMA Engine 框架中,
dma_slave_config 结构体的 src_addr_width 和 dst_addr_width 必须与硬件 FIFO 数据宽度严格一致。外设到外设传输时,两个外设的 maxburst 值应设置为相同,否则可能导致数据错位。
3.3 内存到内存传输详解
内存到内存(Mem2Mem)是最简单的 DMA 传输形式,数据在系统内存的两个区域之间移动,不涉及任何外设硬件握手。该类型通常用于快速内存拷贝、帧缓冲复制或数据整理。
3.3.1 配置要点
- 触发方式: 软件写 DMA 通道使能位,无需等待 DREQ 信号。
- 地址模式: 源地址和目标地址均设置为递增模式(
DMA_MEM_TO_MEM)。 - 数据宽度: 通常与系统总线宽度一致(32 位或 64 位),以获得最佳带宽。
- 传输完成: 依赖传输计数(Transfer Count)归零中断。
3.3.2 典型代码片段(伪寄存器配置)
/* 内存到内存 DMA 通道配置伪代码 */
dma_channel->src_addr = (uint32_t)src_buf;
dma_channel->dst_addr = (uint32_t)dst_buf;
dma_channel->transfer_size = 1024; /* 传输字节数 */
dma_channel->src_inc = DMA_ADDR_INC; /* 源地址递增 */
dma_channel->dst_inc = DMA_ADDR_INC; /* 目标地址递增 */
dma_channel->data_width = DMA_WIDTH_32; /* 32 位传输 */
dma_channel->flow_ctrl = DMA_FLOW_DMA; /* DMA 流控 */
dma_channel->trigger_mode = TRIGGER_SOFT; /* 软件触发 */
dma_channel->enable = 1; /* 启动传输 */
注意: 内存到内存传输会占用系统总线带宽,在 SMP(对称多处理)系统中可能导致缓存一致性问题。建议在传输前后执行
dma_map_single() 和 dma_unmap_single() 确保缓存同步。
3.4 内存到外设传输详解
内存到外设(Mem2Periph)传输将数据从系统内存搬运到外设的 FIFO 或数据寄存器。典型应用包括 LCD 显示刷新、DAC 波形输出、SPI 数据发送等。
3.4.1 硬件握手流程
- 外设准备好接收数据时,断言 DREQ(DMA Request)信号。
- DMA 控制器响应 DREQ,从内存读取一个数据单元(burst 大小由
maxburst决定)。 - DMA 将数据写入外设的数据寄存器或 FIFO。
- 外设消费数据后,撤销 DREQ 或再次断言以请求下一个数据。
3.4.2 驱动配置关键
/* Linux DMA Engine slave 配置示例:内存到外设 */
struct dma_slave_config config = {
.direction = DMA_MEM_TO_DEV,
.src_addr = 0, /* 由 DMA API 自动填充 */
.dst_addr = (dma_addr_t)uart_tx_fifo,
.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES,
.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE, /* UART 每次 1 字节 */
.src_maxburst = 16, /* 每次 burst 16 字节 */
.dst_maxburst = 1, /* 外设每次只接收 1 字节 */
.device_fc = false, /* DMA 流控 */
};
3.5 外设到内存传输详解
外设到内存(Periph2Mem)是数据采集类驱动的核心传输类型。外设产生数据后通过 DREQ 通知 DMA,DMA 将数据从外设 FIFO 搬运到系统内存缓冲区。
3.5.1 与 Mem2Periph 的对称性
Periph2Mem 与 Mem2Periph 在配置上呈镜像对称关系:
- 源地址固定(外设 FIFO),目标地址递增(内存缓冲区)。
- DREQ 由外设断言(数据就绪),而非外设请求发送。
- 中断触发时机通常为“传输完成”或“半传输完成”(用于 ping-pong 缓冲)。
3.5.2 环形缓冲区的典型配置
/* 外设到内存:ADC 采样数据搬运 */
struct dma_slave_config config = {
.direction = DMA_DEV_TO_MEM,
.src_addr = (dma_addr_t)adc_data_reg,
.dst_addr = 0, /* 由 DMA API 自动填充 */
.src_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES, /* ADC 16 位 */
.dst_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES,
.src_maxburst = 1, /* ADC 单次采样 */
.dst_maxburst = 16, /* 内存端 burst 写入 */
.device_fc = false,
};
/* 启动循环传输(环形缓冲区) */
dma_engine->dst_inc = DMA_ADDR_INC;
dma_engine->src_inc = DMA_ADDR_NOINC;
dma_engine->circular = 1; /* 使能循环模式 */
3.6 外设到外设传输详解
外设到外设(Periph2Periph)传输是最复杂的 DMA 类型,数据从一个外设的 FIFO 直接搬运到另一个外设的 FIFO,不经过系统内存。典型场景包括 SPI 环回测试、I2S 音频数据路由、数字信号处理器的数据转发。
3.6.1 特殊约束
| 约束项 | 说明 |
|---|---|
| 数据宽度匹配 | 源外设和目标外设的数据宽度必须一致(如均为 8 位或 16 位)。 |
| 时钟同步 | 两个外设通常需要共享同一时钟域,或使用异步 FIFO 桥接。 |
| 流控选择 | 可由 DMA 控制器流控,也可由外设流控(device_fc = true)。 |
| 双 DREQ 管理 | DMA 通道必须同时监听源外设的 DREQ(数据就绪)和目标外设的 DREQ(FIFO 空)。 |
| 地址固定 | 源地址和目标地址均设置为 DMA_ADDR_NOINC。 |
3.6.2 配置示例
/* 外设到外设:SPI 主机接收直接转发到 UART 发送 */
struct dma_slave_config config = {
.direction = DMA_DEV_TO_DEV,
.src_addr = (dma_addr_t)spi_rx_fifo,
.dst_addr = (dma_addr_t)uart_tx_fifo,
.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE,
.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE,
.src_maxburst = 4, /* SPI 每次 4 字节 */
.dst_maxburst = 4, /* UART 每次 4 字节 */
.device_fc = true, /* 外设流控 */
};
/* 地址模式:双固定 */
dma_engine->src_inc = DMA_ADDR_NOINC;
dma_engine->dst_inc = DMA_ADDR_NOINC;
性能对比: 外设到外设传输的延迟最低(不经过内存),但配置复杂度最高。在音频 I2S 路由场景中,Periph2Periph 模式可将延迟控制在 1 个采样周期以内,而经过内存的 Mem2Mem 模式至少增加 2-3 个总线周期。
3.7 传输类型选择决策树
在驱动开发中,选择正确的传输类型直接影响系统性能和稳定性。以下决策树可帮助快速判断:
3.8 驱动开发常见陷阱与避坑指南
| 陷阱场景 | 问题描述 | 解决方案 |
|---|---|---|
| Mem2Mem 缓存一致性问题 | DMA 读取的源数据与 CPU Cache 不一致 | 使用 dma_map_single() 并指定 DMA_TO_DEVICE 方向 |
| Periph2Mem 数据丢失 | 外设 FIFO 溢出,DMA 未及时响应 DREQ | 增大 maxburst 值,或提高 DMA 通道优先级 |
| Mem2Periph 外设未就绪 | DMA 写入时外设 FIFO 已满 | 检查外设 DREQ 信号极性,配置 device_fc = true |
| Periph2Periph 数据错位 | 两个外设数据宽度或 burst 大小不匹配 | 强制设置 src_addr_width == dst_addr_width,src_maxburst == dst_maxburst |
| 循环模式地址回绕错误 | 环形缓冲区边界处地址计算错误 | 确保缓冲区大小是 maxburst * data_width 的整数倍 |
调试技巧: 在 Linux 中,可以通过
/sys/kernel/debug/dmaengine/summary 查看当前 DMA 通道的传输类型、状态和未完成传输计数。对于外设到外设传输,建议先使用逻辑分析仪抓取 DREQ/DACK 信号,确认握手时序正确后再调试驱动代码。