3. DMA 传输类型:内存到内存、内存到外设、外设到内存、外设到外设传输模式详解

DMA 控制器作为系统总线上的主设备,其核心价值在于能够在不占用 CPU 的情况下,在源地址与目的地址之间搬运数据。根据源和目的设备的不同,DMA 传输被划分为四种基本模式:内存到内存 (M2M)、内存到外设 (M2P)、外设到内存 (P2M) 以及 外设到外设 (P2P)。理解这四种模式的硬件路径、触发机制和适用场景,是编写高效 DMA 驱动的基础。

3.1 四种传输模式的核心差异

下表从数据流向、触发源、地址增长方式以及典型应用四个维度,对四种模式进行了对比。

传输模式 数据流向 触发源 源/目的地址增长 典型应用
内存到内存 (M2M) 内存 → 内存 软件触发 (立即启动) 源递增 / 目的递增 内存拷贝、帧缓冲移动、数据搬移
内存到外设 (M2P) 内存 → 外设 FIFO 外设请求 (DMA_REQ) 源递增 / 目的固定 LCD 显示、DAC 输出、SPI 发送
外设到内存 (P2M) 外设 FIFO → 内存 外设请求 (DMA_REQ) 源固定 / 目的递增 ADC 采集、UART 接收、I2S 录音
外设到外设 (P2P) 外设 FIFO → 外设 FIFO 源外设请求 源固定 / 目的固定 SPI 到 UART 桥接、数字信号重映射
驱动开发提示: 在配置 DMA 描述符时,src_addr 和 dst_addr 的增长属性(DMA_ADDR_INCREMENT / DMA_ADDR_NO_CHANGE)必须与传输模式严格匹配。例如,M2P 模式下目的地址通常设置为固定,指向外设的数据寄存器地址。

3.2 内存到内存 (M2M) 传输详解

M2M 模式是最纯粹的 DMA 传输形式,数据在系统内存的两个区域之间移动。该模式通常由软件显式触发,无需等待外设硬件信号。驱动开发者通过写 DMA 控制器的使能寄存器或描述符的 SW 触发位来启动传输。

3.2.1 硬件路径与地址行为

  • 源地址:指向内存中的源缓冲区,传输完成后地址自动递增。
  • 目的地址:指向内存中的目的缓冲区,传输完成后地址自动递增。
  • 传输粒度:支持字节 (8-bit)、半字 (16-bit)、字 (32-bit) 对齐传输。
  • 突发传输:支持突发 (Burst) 模式,提高总线利用率。
关键配置: M2M 模式下,DMA 控制器的通道优先级通常设置为最低,因为该操作对实时性要求不高。同时,需要确保源和目的缓冲区在物理地址空间内连续,或使用 SG 描述符链处理非连续内存。
图 3-1:内存到内存 (M2M) 传输架构 源内存缓冲区 地址递增 (SRC_ADDR++) DMA 引擎 软件触发 目的内存缓冲区 地址递增 (DST_ADDR++) CPU 写触发寄存器 特点:无需外设参与,纯内存操作,适合大数据块搬移 典型场景:图像帧拷贝、协议栈数据包移动、内存清零

3.3 内存到外设 (M2P) 传输详解

M2P 模式用于将内存中的数据发送到外设。外设(如 SPI 发送器、UART 发送器、LCD 控制器)在准备好接收新数据时,会向 DMA 控制器发送硬件请求信号 (DMA_REQ)。DMA 控制器响应请求,从内存读取一个数据单元,写入外设的 FIFO 或数据寄存器。

3.3.1 流控与地址配置

  • 流控:由外设控制传输节奏。外设每发出一个请求,DMA 传输一个数据单元。
  • 源地址:内存缓冲区,地址递增。
  • 目的地址:外设数据寄存器地址,地址固定不变。
  • 数据宽度:必须与外设 FIFO 宽度匹配(如 32-bit 外设不能配置为 8-bit 传输)。
常见陷阱: 在 M2P 模式下,如果外设请求信号是边沿触发而非电平触发,驱动必须在描述符中正确配置请求极性。此外,当使用循环模式(Circular Mode)时,需要确保源缓冲区不会在传输过程中被 CPU 修改,否则会导致数据不一致。
图 3-2:内存到外设 (M2P) 传输架构 内存缓冲区 地址递增 DMA 引擎 外设 (SPI/UART) 地址固定 (FIFO) 外设请求 (DMA_REQ) 特点:外设驱动传输节奏,DMA 被动响应请求 典型场景:LCD 显示数据刷新、SPI 发送波形数据、UART 发送字符串

3.4 外设到内存 (P2M) 传输详解

P2M 模式是 M2P 的逆过程,用于将外设接收到的数据存入内存。当外设(如 ADC、UART 接收器)的 FIFO 中有数据就绪时,会向 DMA 发送请求。DMA 读取外设数据寄存器,并将数据写入内存缓冲区。

3.4.1 关键配置参数

参数 配置值 说明
源地址 外设数据寄存器地址 固定不变 (NO_CHANGE)
目的地址 内存缓冲区地址 递增 (INCREMENT)
传输完成中断 使能 通知 CPU 数据已就绪
FIFO 阈值 半满 / 全满 决定请求触发时机
驱动实现要点: 在 P2M 模式下,建议使用描述符链 (Descriptor Chain) 配合乒乓缓冲 (Ping-Pong Buffer) 机制。当 DMA 正在填充一个缓冲区时,CPU 可以安全地处理另一个已满的缓冲区,从而避免数据覆盖。
图 3-3:外设到内存 (P2M) 传输架构 外设 (ADC/UART) 地址固定 (FIFO) DMA 引擎 内存缓冲区 地址递增 外设请求 (DMA_REQ) 特点:外设数据自动存入内存,减少 CPU 中断频率 典型场景:ADC 连续采样、UART 接收数据流、I2S 音频录音

3.5 外设到外设 (P2P) 传输详解

P2P 模式是最复杂的 DMA 传输类型,数据从一个外设的 FIFO 直接传输到另一个外设的 FIFO,完全绕过内存。这种模式通常用于实时数据流处理,例如将 SPI 接收的数据直接转发到 UART 发送。

3.5.1 硬件约束与挑战

  • 数据宽度匹配:源外设和目的外设的数据宽度必须一致,否则需要硬件对齐逻辑。
  • 时钟域同步:两个外设可能位于不同的时钟域,DMA 控制器需要处理跨时钟域同步。
  • 流控一致性:源外设的请求速率必须与目的外设的消费速率匹配,否则会导致 FIFO 溢出或欠载。
  • 地址配置:源地址和目的地址均为固定模式,指向各自外设的数据寄存器。
硬件限制: 并非所有 DMA 控制器都支持 P2P 模式。在实现 P2P 驱动前,必须查阅芯片参考手册,确认 DMA 控制器是否支持“外设到外设”的传输类型,以及是否需要额外的同步逻辑。
图 3-4:外设到外设 (P2P) 传输架构 源外设 (SPI RX) 地址固定 DMA 引擎 目的外设 (UART TX) 地址固定 源外设请求 特点:零内存占用,纯硬件数据流,延迟最低 典型场景:SPI 到 UART 协议转换、数字音频重采样、硬件加密加速

3.6 传输模式选择决策树

在实际驱动开发中,选择合适的 DMA 传输模式是设计的第一步。以下决策树可以帮助开发者快速确定模式:

  1. 数据是否经过内存?
    • 是 → 进入步骤 2
    • 否 → 选择 外设到外设 (P2P)
  2. 源设备是内存还是外设?
    • 内存 → 选择 内存到外设 (M2P)
    • 外设 → 选择 外设到内存 (P2M)
  3. 源和目的都是内存?
    • 是 → 选择 内存到内存 (M2M)
性能考量: M2M 模式通常使用最慢的传输优先级,因为它不涉及实时外设。M2P 和 P2M 模式需要根据外设的实时性要求设置通道优先级(例如,音频 I2S 通常设置为高优先级,而 UART 可以设置为中等优先级)。P2P 模式由于完全绕过内存,具有最低的延迟和最高的实时性,但硬件支持有限。

3.7 描述符配置示例 (伪代码)

以下伪代码展示了在描述符接口中如何根据传输模式配置关键字段:

/* DMA 描述符配置结构体 */
struct dma_descriptor {
    uint32_t src_addr;      /* 源地址 */
    uint32_t dst_addr;      /* 目的地址 */
    uint32_t control;       /* 控制字 */
    uint32_t next_desc;     /* 下一个描述符地址 */
};

/* 根据传输模式配置描述符 */
void dma_desc_config(struct dma_descriptor *desc,
                     enum dma_transfer_type type,
                     uint32_t src, uint32_t dst,
                     uint32_t size)
{
    desc->src_addr = src;
    desc->dst_addr = dst;

    switch (type) {
    case DMA_MEM_TO_MEM:
        /* 源递增,目的递增,软件触发 */
        desc->control = (size & 0xFFFF) |
                        DMA_SRC_INC |
                        DMA_DST_INC |
                        DMA_SW_TRIGGER;
        break;

    case DMA_MEM_TO_PERIPH:
        /* 源递增,目的固定,外设请求触发 */
        desc->control = (size & 0xFFFF) |
                        DMA_SRC_INC |
                        DMA_DST_NO_CHANGE |
                        DMA_HW_TRIGGER;
        break;

    case DMA_PERIPH_TO_MEM:
        /* 源固定,目的递增,外设请求触发 */
        desc->control = (size & 0xFFFF) |
                        DMA_SRC_NO_CHANGE |
                        DMA_DST_INC |
                        DMA_HW_TRIGGER;
        break;

    case DMA_PERIPH_TO_PERIPH:
        /* 源固定,目的固定,外设请求触发 */
        desc->control = (size & 0xFFFF) |
                        DMA_SRC_NO_CHANGE |
                        DMA_DST_NO_CHANGE |
                        DMA_HW_TRIGGER;
        break;
    }

    desc->next_desc = 0; /* 单次传输,无链 */
}