2. DMA 控制器架构

DMA(Direct Memory Access)控制器是系统中负责在外设与内存之间、或内存与内存之间直接传输数据的硬件引擎。理解其内部结构、通道与请求线机制以及传输模式,是掌握 DMA 缓冲区管理的基础。本章从硬件架构视角拆解 DMA 控制器的核心组件与工作流程。

2.1 DMA 控制器内部结构

一个典型的 DMA 控制器由多个功能单元组成,各单元协同完成数据传输的调度与执行。下图展示了 DMA 控制器的内部逻辑架构:

DMA控制器内部结构图 DMA 控制器内部结构 控制与状态寄存器 地址生成器 字节计数器 通道仲裁器 数据缓冲 FIFO 总线接口单元 通道 0 通道 1 通道 2 通道 3 说明: • 控制与状态寄存器:存储传输方向、模式、使能位等配置信息。 • 地址生成器与字节计数器:管理源/目的地址递增逻辑及剩余传输长度。 • 通道仲裁器:多通道并发时按优先级裁决总线使用权。

各内部单元的功能如下表所示:

单元名称 功能描述 关键寄存器/参数
控制与状态寄存器 配置传输方向(读/写)、传输模式、中断使能、启动/停止控制 DMA_CR, DMA_SR
地址生成器 管理源地址与目的地址的初始值、递增/递减模式、地址对齐 DMA_SAR, DMA_DAR
字节计数器 记录剩余待传输字节数,传输完成后触发中断或链式跳转 DMA_BCR
通道仲裁器 多通道请求时按固定优先级或轮询策略分配总线访问权 优先级寄存器
数据缓冲 FIFO 暂存数据以匹配外设与内存之间的速率差异,支持突发传输 FIFO 深度(通常 4~16 字)
总线接口单元 负责与系统总线(AHB/AXI/PCIe)握手,驱动地址/数据/控制信号 总线协议状态机
内核视角: 在 Linux 内核中,DMA 控制器通常通过 dma_device 结构体抽象,每个通道对应一个 dma_chan 实例。驱动通过 dma_request_channel() 获取通道,并通过 dmaengine_slave_config() 配置地址与传输参数。

2.2 通道与请求线

DMA 通道是独立的硬件数据通路,每个通道可服务于一个外设或内存区域。请求线(DMA Request Line)是外设向 DMA 控制器发起传输请求的物理信号线。通道与请求线的映射关系决定了系统的并发传输能力。

DMA通道与请求线映射 DMA 通道与请求线映射 UART I2C SPI ADC REQ0 REQ1 REQ2 REQ3 通道
映射
矩阵 通道 0 通道 1 通道 2 通道 3 映射规则: • 固定映射:外设 请求线固定连接 到特定通道。 • 可编程映射:通过 寄存器动态选择 请求线与通道的 对应关系。 • 逻辑通道:一个 物理通道可服务 多个外设(需 软件切换)。

通道与请求线的关键特性:

  • 固定映射: 每个外设请求线直接连接到指定通道,硬件布线简单,但灵活性差。常见于低功耗 MCU 中的 DMA。
  • 可编程映射: 通过 DMA 控制器内部的映射寄存器,将任意请求线路由到任意通道。例如 STM32 的 DMA 控制器支持 8 个通道与 16 个请求线之间的灵活映射。
  • 逻辑通道: 一个物理通道可通过软件分时复用服务多个外设,适用于外设数量多于通道数量的场景。
  • 请求线类型: 包括硬件请求(由外设硬件信号触发)和软件请求(由 CPU 写寄存器触发)。
Android 实践: 在 Qualcomm / MediaTek 平台中,DMA 通道通常与硬件 IP 块(如 UART、I2C、SPI、SDIO)的请求线一一对应。内核设备树(Device Tree)中的 dmas 属性用于描述外设与 DMA 通道的绑定关系,例如:
uart0: serial@fdd0c000 {
    compatible = "qcom,msm-uart";
    reg = <0xfdd0c000 0x1000>;
    interrupts = <0 112 0>;
    dmas = <&dma0 0 2>;  /* 使用 dma0 的通道 2 */
    dma-names = "tx";
};

2.3 DMA 传输模式

DMA 控制器支持多种传输模式,以适应不同场景下的数据吞吐量、实时性和 CPU 干预需求。三种核心模式为:单次传输、块传输和链式传输。

DMA传输模式对比 DMA 传输模式对比 单次传输 1. CPU 配置源/目的地址 2. 设置传输长度(1 次) 3. 启动 DMA 4. 传输完成 → 中断 5. CPU 重新配置下一次 传输(高 CPU 开销) 块传输 1. CPU 配置块参数 2. 设置块大小(N 字节) 3. 启动 DMA 4. 连续传输整个块 5. 块完成 → 中断 6. 适合大数据块搬运 链式传输 1. CPU 构建描述符链表 2. 每个描述符包含: 源地址、目的地址、 长度、控制标志、 下一描述符指针 3. 启动后硬件自动遍历 4. 零 CPU 干预完成多块 模式对比 特性 单次传输 块传输 链式传输 CPU 干预 每次传输后 块完成后 仅初始配置 传输大小 1 次(可配置) N 字节/块 多块(链表) 适用场景 低速外设轮询 音频/视频帧 网络包/批量IO 硬件复杂度 低 中 高(需描述符引擎)

2.3.1 单次传输

单次传输是最基本的模式。CPU 配置一次源地址、目的地址和传输长度后启动 DMA,完成指定数量的数据搬运后产生中断。每次传输都需要 CPU 重新配置,适用于外设数据量小且不频繁的场景(如按键扫描、传感器单次读取)。

2.3.2 块传输

块传输模式下,DMA 控制器在一次启动后连续传输整个数据块(通常为 64 字节、256 字节或更大),期间不需要 CPU 干预。块传输利用 FIFO 和突发传输(Burst Transfer)提高总线利用率,是音频帧、摄像头行缓冲等中等吞吐量场景的首选。

注意: 块传输的块大小必须与系统总线宽度和 FIFO 深度对齐。例如,32 位总线系统上,块大小建议为 4 字节的整数倍;若块大小与 FIFO 深度不匹配,可能导致总线停顿或数据溢出。

2.3.3 链式传输

链式传输(也称为描述符链或 scatter-gather)是最高效的模式。CPU 在内存中构建一个描述符链表,每个描述符包含一次传输所需的全部参数(源地址、目的地址、长度、控制标志)以及指向下一个描述符的指针。DMA 控制器启动后,自动从链表头取出描述符执行传输,完成后自动加载下一个描述符,直到链表结束。

链式传输的核心优势:

  • 零 CPU 干预: 一次配置即可完成多个不连续内存区域的传输,适用于网络数据包、文件系统 I/O 等复杂场景。
  • 描述符结构: 典型的描述符包含以下字段:
struct dma_descriptor {
    uint32_t src_addr;      // 源地址
    uint32_t dst_addr;      // 目的地址
    uint32_t length;        // 传输长度(字节)
    uint32_t ctrl;          // 控制标志(中断使能、传输方向等)
    uint32_t next_desc;     // 下一个描述符指针(物理地址)
    uint32_t reserved;      // 保留/状态
};
Linux 内核实现: 链式传输对应 dmaengine 框架中的 dma_async_tx_descriptor 结构。驱动通过 dmaengine_prep_slave_sg() 或 dmaengine_prep_dma_cyclic() 创建描述符链,并通过 dmaengine_submit() 提交到硬件队列。Android 内核中,多媒体框架(如 Codec2、ION)广泛使用链式传输实现零拷贝内存映射。

2.4 传输模式选择策略

在实际系统设计中,选择哪种传输模式取决于以下因素:

场景 推荐模式 原因
低速外设(UART 单字节、GPIO) 单次传输 传输量小,CPU 配置开销可接受
音频播放/录音(周期性缓冲) 块传输 或 循环链式 固定块大小,需连续流式传输
摄像头帧捕获(大块连续数据) 块传输 单帧数据量大,地址连续
网络数据包(分散/聚集 I/O) 链式传输 数据分布在多个不连续缓冲区
文件系统读写(页缓存与设备间) 链式传输 内存页分散,需高效聚合
性能提示: 在 Android 内核中,ION 分配器与 DMA-BUF 框架通常配合链式传输实现跨硬件模块的零拷贝。例如,相机驱动将帧数据通过链式 DMA 直接写入 GPU 或显示控制器可访问的内存区域,避免 CPU 介入拷贝。