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地址。
DMA地址与CPU虚拟地址映射关系 DMA地址与CPU虚拟地址映射关系 CPU 虚拟地址空间 0xC000_0000 ~ 0xCFFF_FFFF DMA 地址空间 0x8000_0000 ~ 0x8FFF_FFFF 物理内存(DDR) 物理地址:0x4000_0000 ~ 0x4FFF_FFFF MMU 页表映射 IOMMU/SMMU 映射 驱动通过 dma_alloc_coherent() 同时获得 CPU 虚拟地址和 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);
DMA流式映射生命周期 DMA流式映射生命周期(以摄像头采集为例) ① dma_map_single() ② 启动DMA传输 ③ DMA传输完成 ④ dma_sync_single_for_cpu() ⑤ CPU处理帧数据 关键说明 • 步骤①建立DMA地址映射 • 步骤②③硬件自动完成 • 步骤④刷新缓存行 使CPU看到最新数据 • 步骤⑤CPU读取帧数据 • 下次DMA前需再次调用 dma_sync_single_for_device() • 最终调用 dma_unmap_single() 释放

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掩码。
DMA映射常见问题对比 DMA映射常见问题与解决方案 ❌ 缓存未同步 现象:花屏/数据错乱 原因:未调用 sync_for_cpu 解决:在DMA完成后同步 ❌ 映射方向错误 现象:数据被缓存污染 原因:FROM/TO 用反 解决:按数据流向选择 ❌ DMA地址越界 现象:DMA传输无响应 原因:地址超出设备范围 解决:设置 dma_set_mask()