4. DMA基础概念:DMA控制器工作原理、DMA传输类型与Cache一致性挑战

DMA(Direct Memory Access,直接存储器访问)是嵌入式Linux系统中实现高性能数据搬运的核心硬件机制。它允许外设或内存之间直接交换数据,而无需CPU逐字节参与,从而将CPU从繁重的数据拷贝任务中解放出来,专注于控制逻辑与协议处理。本章将深入剖析DMA控制器的工作原理、三种典型传输类型,以及驱动开发中必须面对的Cache一致性挑战。

4.1 DMA控制器工作原理

DMA控制器是系统中独立于CPU的总线主控设备,它拥有自己的地址寄存器、字节计数寄存器和控制状态寄存器。一次典型的DMA传输由以下阶段构成:

  1. 初始化阶段:CPU配置DMA控制器的源地址、目的地址、传输长度和传输方向。
  2. 请求阶段:外设通过硬件信号(DMA请求线)或软件触发启动传输。
  3. 传输阶段:DMA控制器获得总线控制权,在源与目的之间直接搬运数据,每传输一个单元(字节/字/双字)后更新地址和计数。
  4. 完成阶段:当计数寄存器归零时,DMA控制器向CPU发出中断信号,通知传输完成。
关键指标: DMA传输性能取决于总线带宽、通道数、地址对齐方式和传输粒度。在嵌入式系统中,通常使用分散-聚集(Scatter-Gather)DMA来支持非连续物理内存的传输。
DMA控制器内部架构与传输流程 CPU DMA控制器 源地址 / 目的地址 / 计数 控制状态寄存器 系统内存 外设 ① 配置 ② 搬运 ③ 请求 ④ 传输完成中断 源地址寄存器 目的地址寄存器 字节计数寄存器 控制状态寄存器 DMA传输流程 1. CPU 配置源/目的地址和长度 2. 外设发起 DMA 请求 3. DMA 控制器接管总线搬运数据 4. 传输完成,触发中断通知 CPU

4.2 DMA传输类型

根据数据传输的源和目的设备类型,DMA传输可分为三种基本模式。理解这些模式对于驱动工程师选择正确的DMA映射方式和API至关重要。

传输类型 源设备 目的设备 典型场景 CPU参与度
内存到内存 系统内存 系统内存 数据拷贝、缓冲区移动、内存清理 仅初始化
外设到内存 外设FIFO/寄存器 系统内存 ADC采样、网络接收、摄像头数据采集 仅初始化+中断
内存到外设 系统内存 外设FIFO/寄存器 DAC输出、网络发送、显示刷新 仅初始化+中断
驱动开发提示: 在Linux内核中,dma_alloc_coherent() 用于分配一致性DMA缓冲区(适合外设到内存/内存到外设),而 dma_map_single() 配合 DMA_TO_DEVICE / DMA_FROM_DEVICE 标志用于流式DMA映射。内存到内存的DMA通常由 dmaengine 框架的 DMA_MEMCPY 事务实现。
三种DMA传输类型对比 内存 → 内存 内存A 内存B DMA 数据拷贝/缓冲区移动 外设 → 内存 外设 内存 DMA ADC/网络/摄像头采集 内存 → 外设 内存 外设 DMA DAC/网络发送/显示 传输类型选择依据 • 内存到内存:使用 dmaengine_prep_dma_memcpy(),需要两个缓冲区都映射为 DMA 可访问 • 外设到内存:使用 dma_map_single(dev, buf, size, DMA_FROM_DEVICE),注意 Cache 无效化 • 内存到外设:使用 dma_map_single(dev, buf, size, DMA_TO_DEVICE),注意 Cache 回写

4.3 Cache一致性挑战

Cache一致性是DMA驱动开发中最容易出错、最难调试的问题之一。CPU通过Cache缓存内存数据以加速访问,而DMA控制器直接访问物理内存,不经过Cache。这种“CPU看到的数据”与“DMA看到的数据”之间的差异,就是Cache一致性问题。

4.3.1 问题场景分析

场景 CPU操作 DMA操作 潜在问题
DMA写入内存(外设→内存) 读取缓冲区 向缓冲区写入数据 CPU Cache中保留旧数据,读取到的是脏数据
DMA读取内存(内存→外设) 写入缓冲区 从缓冲区读取数据 CPU写入的数据还在Cache中,DMA读到的是过时数据
典型Bug: 驱动工程师在DMA传输完成后忘记调用 dma_unmap_single() 或 dma_sync_single_for_cpu(),导致CPU读取到Cache中的旧数据。这种Bug表现为“数据有时对有时错”,极难复现和定位。

4.3.2 解决方案:一致性DMA与流式DMA

Linux内核提供两种DMA缓冲区管理策略来应对Cache一致性挑战:

  • 一致性DMA(Coherent DMA):通过 dma_alloc_coherent() 分配,保证CPU和DMA看到的是同一份数据。底层实现通常通过禁用Cache(非Cacheable页表属性)或硬件Cache一致性协议(如ARM的ACE/CHI)来达成。适用于长期共享的缓冲区,但分配开销较大。
  • 流式DMA(Streaming DMA):通过 dma_map_single() / dma_unmap_single() 管理,在每次传输前后显式地进行Cache维护(回写或无效化)。适用于短时、频繁的传输,性能更高但需要驱动工程师手动管理同步点。
Cache一致性问题:DMA与CPU的数据视图差异 CPU 视角 Cache (数据=0xAA) 内存 (数据=0x55) 读旧数据 DMA 视角 内存 (数据=0x55) 外设 (写入0x55) 写新数据 Cache一致性问题的本质 • CPU 从 Cache 读取数据 0xAA(旧值),而 DMA 已经将内存更新为 0x55(新值) • 解决方案:在 DMA 传输前执行 Cache 回写(Clean),传输后执行 Cache 无效化(Invalidate) • Linux API:dma_sync_single_for_device() / dma_sync_single_for_cpu()

4.3.3 驱动中的Cache同步操作

在流式DMA模式下,驱动工程师必须在正确的时机调用Cache同步函数。以下是一个典型的外设到内存(DMA接收)的代码模板:

/* 外设到内存:DMA接收数据 */
/* 1. 映射缓冲区给DMA使用 */
dma_addr_t dma_handle;
dma_handle = dma_map_single(dev, buffer, size, DMA_FROM_DEVICE);

/* 2. 启动DMA传输(硬件操作) */
start_dma_transfer(dma_handle, size);

/* 3. 等待DMA传输完成(中断或轮询) */
wait_for_dma_completion();

/* 4. 同步Cache:使CPU Cache无效化,确保读取到DMA写入的最新数据 */
dma_sync_single_for_cpu(dev, dma_handle, size, DMA_FROM_DEVICE);

/* 5. 现在CPU可以安全地读取buffer中的数据 */
process_data(buffer);

/* 6. 解除映射 */
dma_unmap_single(dev, dma_handle, size, DMA_FROM_DEVICE);
最佳实践: 对于长期存在的DMA缓冲区(如网络驱动中的RX ring),建议使用 dma_alloc_coherent() 分配一致性内存,避免每次传输都进行Cache同步。对于短时、一次性的传输(如音频采样),流式DMA配合 dma_sync_single_* 是更高效的选择。

4.3.4 不同架构的Cache一致性差异

架构 Cache一致性机制 驱动注意事项
x86 硬件Cache一致性(MESI协议),DMA直接窥探Cache 通常不需要显式Cache同步,但 dma_alloc_coherent() 仍建议使用
ARM32 无硬件一致性,依赖页表属性(非Cacheable)或软件同步 必须显式调用 dma_map_single / dma_sync_single_*
ARM64 支持硬件一致性(通过ACE/CHI协议),但非所有平台都启用 检查设备树中 dma-coherent 属性;若不支持,仍需软件同步
RISC-V 类似ARM32,早期无硬件一致性 与ARM32类似,需要显式Cache维护
跨平台开发建议: 始终按照“最严格”的架构(ARM32)来编写DMA驱动——即显式调用 dma_map_single 和 dma_sync_single_*。这样在x86上虽然多了一次空操作,但保证了代码在ARM/RISC-V平台上的正确性。