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) |
1.2 DMA 工作原理
DMA 传输通常遵循以下步骤:
- CPU 初始化 DMAC:设置源地址、目的地址、传输长度、传输方向、触发方式。
- 外设发起请求:外设通过 DREQ 信号通知 DMAC 需要数据传输。
- DMAC 获取总线控制权:DMAC 向 CPU 发出总线请求(HOLD/BREQ),CPU 在完成当前总线周期后释放总线(HLDA/BGNT)。
- DMAC 执行传输:DMAC 作为总线主控,直接在外设和内存之间搬运数据。
- 传输完成:DMAC 释放总线控制权,向 CPU 发送中断信号(DMA_IRQ),通知传输结束。
关键点:DMA 传输期间 CPU 可以继续执行指令(如果系统支持分离事务或缓存命中),但无法访问被 DMA 占用的总线资源。现代 SoC 使用多总线矩阵和 AXI 交叉开关来缓解总线争用。
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 需要根据数据块大小和实时性要求综合评估。
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 | 摄像头采集、音频录音 |
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() 接口,不需要指定方向枚举。