在Linux内核驱动开发中编写DMA代码,开发者必须处理好两个核心挑战。
第一,跨越地址的鸿沟:
- CPU视角: 使用的是虚拟地址(Virtual Address),即我们常见的
void * 类型指针。
- DMA硬件视角: 使用的是总线地址(Bus Address,类型为
dma_addr_t)。
通常情况下,总线地址近似于物理地址。切记,绝不能将 kmalloc 等函数返回的虚拟地址指针直接写入DMA硬件寄存器,必须通过内核提供的API进行地址映射和转换。
第二,维护缓存一致性:
CPU读写内存时通常会经过高速缓存(Cache),而DMA引擎则直接访问物理RAM,这可能导致数据不一致:
- DMA写入场景: DMA将新数据写入RAM,但CPU随后读取该地址时,可能直接从Cache中拿到了旧数据。
- CPU写入场景: CPU修改了数据,但新数据还停留在Cache中未刷入RAM,此时DMA从RAM读取数据并发送,导致发出了旧数据。
因此,必须使用Linux内核提供的专用DMA API,由内核自动处理Cache的刷新(Clean)与失效(Invalidate)操作,这也是Linux内核开发中保证硬件协同的关键。
Linux DMA映射的两种方式
根据使用场景和性能需求,Linux内核主要提供了两种DMA内存映射方式。
1. 一致性映射 (Coherent / Consistent Mapping)
特点: 映射建立后,CPU和DMA看到的内存内容始终保持一致。内核通常会为此段内存关闭Cache,或由硬件保证一致性。优点是省心,无需手动同步;缺点是因无Cache加速,CPU访问速度较慢。适用于驱动与硬件间需要长期共存、频繁访问的控制结构,如描述符环形缓冲区、状态寄存器等。其生命周期常与驱动模块绑定。
典型代码流程:
/* 1. 分配并映射 */
// cpu_addr: 给CPU用的虚拟地址
// dma_handle: 给硬件用的总线地址
dma_addr_t dma_handle;
void *cpu_addr;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
/* 错误处理 */
}
/* 2. 配置硬件 */
// 将 dma_handle 写入硬件的DMA地址寄存器
writel(dma_handle, my_dev_reg_base + REG_DMA_START_ADDR);
/* 3. 使用 */
// CPU可直接读写 cpu_addr,无需担心Cache一致性
/* 4. 释放 */
dma_free_coherent(dev, size, cpu_addr, dma_handle);
2. 流式映射 (Streaming Mapping)
特点: 性能更高,允许使用Cache,但在每次DMA传输前后,必须手动调用API来同步Cache。适用于数据量大、生命周期短的场景,如网络数据包(sk_buff)、视频帧缓冲区等。使用时需明确指定数据流向(DMA_TO_DEVICE 或 DMA_FROM_DEVICE)。
典型代码流程:
/* 假设 buffer 是已分配好的一块内存 */
void *buffer = kmalloc(size, GFP_KERNEL);
dma_addr_t dma_handle;
/* 1. 映射 (Map) */
// 此操作会处理Cache刷新,之后CPU暂时不应再访问buffer
dma_handle = dma_map_single(dev, buffer, size, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_handle)) {
/* 错误处理 */
}
/* 2. 启动DMA传输 */
writel(dma_handle, my_dev_reg_base + REG_DATA_ADDR);
writel(START_CMD, my_dev_reg_base + REG_CMD);
/* ... 等待中断,确认传输完成 ... */
/* 3. 取消映射 (Unmap) */
// 此后,CPU才能再次安全访问 buffer
dma_unmap_single(dev, dma_handle, size, DMA_TO_DEVICE);
使用DMA Engine通用API
对于支持DMA Engine框架的控制器,驱动可以作为“客户端”来申请和使用DMA通道,流程更为抽象和统一。
#include <linux/dmaengine.h>
struct dma_chan *chan;
struct dma_async_tx_descriptor *tx;
// 1. 申请通道 (通常在probe函数中)
chan = dma_request_chan(dev, "tx"); // “tx”需对应设备树中的定义
// 2. 准备数据 (使用流式映射)
dma_addr_t dma_buf = dma_map_single(dev, buffer, len, DMA_MEM_TO_DEV);
// 3. 配置从设备参数并准备传输描述符
struct dma_slave_config cfg = { ... };
dmaengine_slave_config(chan, &cfg);
tx = dmaengine_prep_slave_single(chan, dma_buf, len, DMA_MEM_TO_DEV, flags);
// 4. 设置传输完成回调函数
tx->callback = my_dma_callback;
// 5. 提交描述符并启动DMA传输
dmaengine_submit(tx);
dma_async_issue_pending(chan);
关键注意事项与常见“坑”
-
禁止映射栈内存:
切勿对函数内的局部变量(栈内存)进行DMA映射,因为其物理地址可能不连续,且函数返回后内存会失效。
void bad_func() {
char buf[100]; // 栈内存
// 错误!行为未定义
dma_map_single(..., buf, ...);
}
必须使用 kmalloc 或 dma_alloc_coherent 分配的堆内存。
-
设置DMA地址掩码:
现代SoC多为64位CPU,但外设DMA控制器可能只支持32位寻址。驱动中必须设置正确的DMA掩码:
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))) {
/* 设置失败,硬件无法访问全部内存 */
}
若设置失败,则意味着存在硬件无法访问的内存区域。
-
切勿混淆数据传输方向:
DMA_TO_DEVICE(内存 -> 硬件)与 DMA_FROM_DEVICE(硬件 -> 内存)的方向一旦搞反,内核执行的Cache同步操作将完全错误,导致数据损毁。