4. DMA基础概念:DMA控制器工作原理、DMA传输类型与Cache一致性挑战
DMA(Direct Memory Access,直接存储器访问)是嵌入式Linux系统中实现高性能数据搬运的核心硬件机制。它允许外设或内存之间直接交换数据,而无需CPU逐字节参与,从而将CPU从繁重的数据拷贝任务中解放出来,专注于控制逻辑与协议处理。本章将深入剖析DMA控制器的工作原理、三种典型传输类型,以及驱动开发中必须面对的Cache一致性挑战。
4.1 DMA控制器工作原理
DMA控制器是系统中独立于CPU的总线主控设备,它拥有自己的地址寄存器、字节计数寄存器和控制状态寄存器。一次典型的DMA传输由以下阶段构成:
- 初始化阶段:CPU配置DMA控制器的源地址、目的地址、传输长度和传输方向。
- 请求阶段:外设通过硬件信号(DMA请求线)或软件触发启动传输。
- 传输阶段:DMA控制器获得总线控制权,在源与目的之间直接搬运数据,每传输一个单元(字节/字/双字)后更新地址和计数。
- 完成阶段:当计数寄存器归零时,DMA控制器向CPU发出中断信号,通知传输完成。
关键指标: DMA传输性能取决于总线带宽、通道数、地址对齐方式和传输粒度。在嵌入式系统中,通常使用分散-聚集(Scatter-Gather)DMA来支持非连续物理内存的传输。
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 事务实现。
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维护(回写或无效化)。适用于短时、频繁的传输,性能更高但需要驱动工程师手动管理同步点。
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平台上的正确性。