3. DMA 传输类型详解:内存到内存、内存到外设、外设到内存、外设到外设传输的差异与配置

DMA 控制器支持的传输类型决定了数据在系统总线上的流动路径。根据源端和目标端的不同,DMA 传输可分为四种基本类型:内存到内存、内存到外设、外设到内存和外设到外设。每种类型在硬件触发方式、地址递增规则、传输粒度以及驱动配置上存在显著差异。

核心要点: 传输类型的本质是“谁控制流控(Flow Controller)”以及“地址是否自动递增”。内存到内存由软件触发,外设相关传输由硬件握手信号触发。

3.1 四种传输类型的架构对比

下图展示了四种 DMA 传输类型在系统总线上的数据流向。箭头方向代表数据移动方向,控制信号线(DREQ/DACK)仅在外设参与时出现。

DMA 四种传输类型架构对比 类型 1:内存 → 内存 源内存 目标内存 软件触发 无 DREQ/DACK 类型 2:内存 → 外设 内存 外设 DREQ 触发 DMA 读内存 → 写外设 类型 3:外设 → 内存 外设 内存 DREQ 触发 DMA 读外设 → 写内存 类型 4:外设 → 外设 外设 A 外设 B DREQ 触发 双外设握手 DMA 控制器(DMAC) 系统总线(AHB / AXI / 本地总线) 外设接口层(UART / SPI / I2S / ADC / 定时器) 说明:内存到内存传输不需要外设握手信号,由软件直接启动;外设相关传输依赖 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 硬件握手流程

  1. 外设准备好接收数据时,断言 DREQ(DMA Request)信号。
  2. DMA 控制器响应 DREQ,从内存读取一个数据单元(burst 大小由 maxburst 决定)。
  3. DMA 将数据写入外设的数据寄存器或 FIFO。
  4. 外设消费数据后,撤销 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 传输类型选择决策树

在驱动开发中,选择正确的传输类型直接影响系统性能和稳定性。以下决策树可帮助快速判断:

DMA 传输类型选择决策树 数据源和目标都是内存? 是 → 内存到内存 软件触发,双递增 适用:内存拷贝、帧缓冲 否 → 外设参与 外设是源还是目标? 两端都是外设? 外设 → 内存 源固定,目标递增 内存 → 外设 源递增,目标固定 外设 → 外设 双固定,双 DREQ

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 信号,确认握手时序正确后再调试驱动代码。