Linux内核DMA与缓存一致性编程实战工程 - 第1节

1.1 DMA 定义

DMA(Direct Memory Access,直接存储器访问) 是一种允许硬件子系统(外设、存储控制器、加速器)在不经过 CPU 参与的情况下,直接与系统内存进行数据读写的技术。DMA 控制器(DMAC)作为总线上的主设备,接管总线控制权,完成数据搬运任务。

核心思想:将数据搬运工作从 CPU 卸载到专用硬件,使 CPU 可以专注于计算、控制逻辑和中断处理,从而提升系统吞吐量。
概念 说明
DMA 控制器(DMAC) 负责管理 DMA 传输的硬件模块,包含源地址、目的地址、传输长度、控制寄存器
DMA 请求(DREQ) 外设向 DMAC 发出的传输请求信号
DMA 应答(DACK) DMAC 对外设请求的确认信号
总线主控(Bus Master) DMAC 在传输期间获得总线控制权,成为总线主设备
传输粒度 单次传输的数据宽度:字节(8-bit)、半字(16-bit)、字(32-bit)
DMA 系统架构图 CPU DMA 控制器 (DMAC) 系统内存 外设 DMA 数据通路(无 CPU 参与) CPU 配置 DMA 控制寄存器

1.2 DMA 工作原理

DMA 传输通常遵循以下步骤:

  1. CPU 初始化 DMAC:设置源地址、目的地址、传输长度、传输方向、触发方式。
  2. 外设发起请求:外设通过 DREQ 信号通知 DMAC 需要数据传输。
  3. DMAC 获取总线控制权:DMAC 向 CPU 发出总线请求(HOLD/BREQ),CPU 在完成当前总线周期后释放总线(HLDA/BGNT)。
  4. DMAC 执行传输:DMAC 作为总线主控,直接在外设和内存之间搬运数据。
  5. 传输完成:DMAC 释放总线控制权,向 CPU 发送中断信号(DMA_IRQ),通知传输结束。
关键点:DMA 传输期间 CPU 可以继续执行指令(如果系统支持分离事务或缓存命中),但无法访问被 DMA 占用的总线资源。现代 SoC 使用多总线矩阵和 AXI 交叉开关来缓解总线争用。
DMA 传输工作流程 ① CPU 配置 DMAC ② 外设发起 DREQ 请求 ③ DMAC 获取总线控制权 ④ DMAC 执行数据传输 ⑤ 传输完成,触发中断

1.3 DMA 与 CPU 数据搬运对比

传统 CPU 使用 PIO(Programmed I/O) 模式搬运数据,即 CPU 通过 load/store 指令逐字读写外设或内存。DMA 与 PIO 在性能、CPU 占用率和延迟方面存在显著差异:

对比维度 CPU PIO 搬运 DMA 搬运
CPU 占用率 100%(搬运期间无法执行其他任务) 仅初始化 + 中断处理(通常 < 5%)
吞吐量 受限于 CPU 主频和指令执行速度 受限于总线带宽,通常高 2~10 倍
延迟 低(无总线仲裁开销) 中等(需总线仲裁和 DMA 设置)
适用场景 小数据量(< 64 字节)、控制寄存器访问 大数据块(≥ 512 字节)、流式外设
硬件复杂度 无需额外硬件 需要 DMAC 或 SoC 内置 DMA 引擎
缓存一致性 天然一致(CPU 直接访问) 需要软件或硬件维护一致性
工程经验:在嵌入式 Linux 中,对于 SPI、UART、I2C 等低速外设,PIO 模式往往延迟更低;对于 SD/MMC、Ethernet、GPU 等高速外设,必须使用 DMA 才能达到线速。选择 DMA 还是 PIO 需要根据数据块大小和实时性要求综合评估。
CPU PIO vs DMA 数据搬运对比 CPU 外设 内存 PIO: CPU 逐字搬运 PIO: CPU 逐字搬运 CPU 外设 内存 DMA 控制器 DMA 请求 DMA 写入 中断通知

1.4 DMA 传输类型

根据数据源和目的地的不同,DMA 传输分为三种基本类型:

1.4.1 内存到内存(Memory-to-Memory)

数据从一块内存区域复制到另一块内存区域,不涉及外设。典型应用:memcpy 硬件加速、帧缓冲拷贝、数据搬移。

  • 源和目的都是系统内存地址空间
  • 通常由软件显式触发(软件触发 DMA)
  • 不需要外设请求信号
  • 在 Linux 中通过 dmaengine 的 DMA_MEMCPY 操作实现

1.4.2 内存到外设(Memory-to-Device)

数据从内存传输到外设的 FIFO 或寄存器。典型应用:LCD 显示数据刷新、音频播放(I2S 发送)、以太网数据发送。

  • 源地址:内存缓冲区(通常是 DMA 可访问区域)
  • 目的地址:外设数据寄存器地址(固定)
  • 外设通过 DREQ 信号控制传输节奏(流控模式)
  • 在 Linux 中对应 DMA_DEV_TO_MEM 方向(从设备角度看是读)

1.4.3 外设到内存(Device-to-Memory)

数据从外设传输到系统内存。典型应用:摄像头图像采集、音频录音(I2S 接收)、网络数据接收。

  • 源地址:外设数据寄存器地址(固定)
  • 目的地址:内存缓冲区
  • 外设产生数据时发起 DREQ
  • 在 Linux 中对应 DMA_MEM_TO_DEV 方向
传输类型 源 目的 触发方式 典型场景
内存→内存 内存地址 A 内存地址 B 软件触发 memcpy、帧缓冲拷贝
内存→外设 内存缓冲区 外设 FIFO/寄存器 外设 DREQ LCD 刷新、音频播放
外设→内存 外设 FIFO/寄存器 内存缓冲区 外设 DREQ 摄像头采集、音频录音
三种 DMA 传输类型 内存 A 内存 B 内存→内存 内存缓冲区 外设 FIFO 内存→外设 外设 FIFO 内存缓冲区 外设→内存 DMA 控制器(DMAC)
Linux 内核 DMA 方向枚举:在 <linux/dma-direction.h> 中定义:
enum dma_data_direction {
    DMA_BIDIRECTIONAL    = 0,  // 双向传输
    DMA_TO_DEVICE        = 1,  // 内存→外设
    DMA_FROM_DEVICE      = 2,  // 外设→内存
    DMA_NONE             = 3,  // 无传输
};
注意:DMA_TO_DEVICE 表示数据从内存流向设备(CPU 视角的"写"),DMA_FROM_DEVICE 表示数据从设备流向内存(CPU 视角的"读")。
实战要点:在编写 DMA 驱动程序时,必须根据实际数据流向正确选择 DMA 方向。方向错误会导致数据损坏或 DMA 传输失败。对于内存到内存的拷贝,Linux 使用 dma_async_memcpy_buf_to_buf() 接口,不需要指定方向枚举。