3、DMA缓冲区基础:DMA一致性映射、流式映射、DMA地址与CPU虚拟地址
在V4L2驱动中,DMA缓冲区是连接摄像头传感器与系统内存的核心数据载体。理解DMA映射机制是正确使用videobuf2框架的前提。本章将深入剖析DMA一致性映射与流式映射的本质区别,并厘清DMA地址与CPU虚拟地址之间的转换关系。
3.1 DMA地址与CPU虚拟地址
在支持MMU(内存管理单元)的系统中,CPU通过虚拟地址访问内存,而DMA控制器直接使用物理地址访问总线。这种地址空间的差异导致驱动开发者必须处理两种地址的转换。
| 地址类型 | 使用者 | 特点 | 典型获取方式 |
|---|---|---|---|
| CPU虚拟地址 | CPU(驱动软件) | 经过MMU映射,连续但可能不物理连续 | dma_alloc_coherent() 返回 |
| DMA地址(总线地址) | DMA控制器/外设 | 物理地址,用于外设直接访问内存 | dma_map_single() 返回 |
| 物理地址 | 内核底层 | 真实的硬件内存地址 | virt_to_phys() |
关键认知:在V4L2驱动中,
vb2_dma_contig 内存类型操作的核心就是维护“CPU虚拟地址 ↔ DMA地址”的映射关系。驱动通过 vb2_plane_vaddr() 获取CPU可访问的虚拟地址,通过 vb2_plane_dma_addr() 获取外设可使用的DMA地址。
3.2 DMA一致性映射
DMA一致性映射(Coherent Mapping)保证CPU与DMA控制器在访问同一块内存时,不需要软件干预缓存一致性维护。这种映射通常用于控制结构体、描述符环等需要双方频繁读写的数据区域。
3.2.1 核心特性
- 缓存一致性:硬件保证CPU缓存与DMA数据始终同步,无需手动调用
dma_cache_sync()。 - 内存属性:映射的内存页被标记为不可缓存(Uncacheable)或写合并(Write-Combine),避免缓存一致性问题。
- 典型用途:视频帧的元数据缓冲区、硬件描述符环、状态寄存器映射。
3.2.2 核心API
// 分配一致性DMA缓冲区
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
dev_err(dev, "Failed to allocate coherent DMA buffer\n");
return -ENOMEM;
}
// 释放一致性DMA缓冲区
dma_free_coherent(dev, size, cpu_addr, dma_handle);
性能警告:一致性映射虽然使用简单,但由于禁用了CPU缓存,对大块数据(如视频帧)的读写性能较差。在V4L2驱动中,视频帧数据缓冲区通常不使用一致性映射,而是采用流式映射配合缓存同步操作。
3.3 DMA流式映射
DMA流式映射(Streaming Mapping)允许CPU缓存保持启用状态,通过显式的缓存同步操作来保证数据一致性。这种映射方式更适合大块数据传输,是V4L2视频帧缓冲区的首选方案。
3.3.1 映射方向与缓存同步
| DMA方向 | 枚举值 | 典型场景 | 同步操作 |
|---|---|---|---|
| 设备到CPU | DMA_FROM_DEVICE |
摄像头采集帧数据 | DMA完成后:dma_sync_single_for_cpu() |
| CPU到设备 | DMA_TO_DEVICE |
显示输出帧数据 | DMA开始前:dma_sync_single_for_device() |
| 双向 | DMA_BIDIRECTIONAL |
帧缓冲复用(编解码) | 每次切换访问方时同步 |
3.3.2 流式映射API使用流程
// 1. 映射用户空间或内核分配的缓冲区
struct sg_table *sgt; // 分散/聚集表
dma_addr_t dma_addr;
dma_addr = dma_map_single(dev, cpu_addr, size, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
dev_err(dev, "DMA mapping failed\n");
return -EIO;
}
// 2. 启动DMA传输(硬件操作)
// ...
// 3. 传输完成后,同步缓存供CPU访问
dma_sync_single_for_cpu(dev, dma_addr, size, DMA_FROM_DEVICE);
// 4. 处理数据(CPU访问)
process_frame(cpu_addr);
// 5. 下次DMA前,同步给设备
dma_sync_single_for_device(dev, dma_addr, size, DMA_FROM_DEVICE);
// 6. 不再使用时解除映射
dma_unmap_single(dev, dma_addr, size, DMA_FROM_DEVICE);
3.4 一致性映射 vs 流式映射对比
| 对比维度 | 一致性映射 | 流式映射 |
|---|---|---|
| 缓存策略 | 禁用CPU缓存(Uncacheable) | 启用CPU缓存,显式同步 |
| 数据一致性 | 硬件自动保证 | 软件通过API保证 |
| 分配/映射开销 | 较高(分配时建立页表) | 较低(仅建立映射) |
| 大块数据性能 | 差(无缓存加速) | 好(利用CPU缓存) |
| 典型数据大小 | KB级别(控制结构) | MB级别(视频帧) |
| V4L2适用场景 | vb2_queue 的元数据 | 视频帧数据缓冲区 |
3.5 在V4L2 videobuf2中的实际应用
videobuf2 框架对DMA映射进行了抽象。以 VB2_MEMORY_DMABUF 和 VB2_MEMORY_MMAP 为例,底层的内存操作回调函数(ops->alloc()、ops->prepare()、ops->finish())内部封装了DMA映射的细节。
videobuf2 回调与DMA映射的对应关系:
buf_prepare()→ 内部调用dma_map_sg()或dma_map_single()建立流式映射buf_finish()→ 内部调用dma_sync_single_for_cpu()同步缓存buf_cleanup()→ 内部调用dma_unmap_sg()解除映射
// videobuf2-dma-contig.c 中的典型实现片段
static int vb2_dc_prepare(void *buf_priv)
{
struct vb2_dc_buf *buf = buf_priv;
struct device *dev = buf->dev;
if (buf->dma_dir != DMA_NONE) {
// 流式映射:同步缓存给设备
dma_sync_single_for_device(dev, buf->dma_addr,
buf->size, buf->dma_dir);
}
return 0;
}
static void vb2_dc_finish(void *buf_priv)
{
struct vb2_dc_buf *buf = buf_priv;
struct device *dev = buf->dev;
if (buf->dma_dir != DMA_NONE) {
// 流式映射:同步缓存给CPU
dma_sync_single_for_cpu(dev, buf->dma_addr,
buf->size, buf->dma_dir);
}
}
最佳实践:在编写V4L2驱动时,除非有特殊需求(如硬件需要连续物理内存且频繁访问控制寄存器),否则应优先使用
videobuf2 默认的流式映射机制。驱动开发者通常不需要直接调用 dma_alloc_coherent(),而是通过 vb2_plane_vaddr() 和 vb2_plane_dma_addr() 间接操作DMA缓冲区。
3.6 常见问题与避坑指南
- 缓存一致性问题:使用流式映射时忘记调用
dma_sync_single_for_cpu(),导致CPU读到陈旧数据。表现为视频帧出现“花屏”或“撕裂”。 - 映射方向错误:摄像头采集使用
DMA_TO_DEVICE,导致DMA写入时缓存未失效,数据被缓存污染。 - 内存泄漏:调用
dma_map_single()后未配对调用dma_unmap_single(),导致DMA映射表资源耗尽。 - 地址空间限制:某些DMA控制器只能访问32位地址空间,需要使用
dma_set_mask()设置DMA掩码。