3. 缓冲区队列初始化:vb2_queue_init 参数详解与 queue_setup 回调实现

缓冲区队列初始化是 videobuf2 框架中驱动与核心交互的第一个关键步骤。驱动通过 vb2_queue_init() 向 V4L2 核心注册一个 vb2_queue 实例,并绑定一系列回调函数。其中 queue_setup 回调决定了缓冲区分配的内存类型、数量与大小,直接影响后续映射与 DMA 操作的效率。

3.1 vb2_queue 结构体核心参数

struct vb2_queue 是 videobuf2 框架中管理缓冲区队列的核心数据结构。驱动在初始化时必须正确填充以下关键字段:

参数 类型 说明 典型值 / 注意事项
type enum v4l2_buf_type 缓冲区类型,决定数据流向 V4L2_BUF_TYPE_VIDEO_CAPTURE 或 V4L2_BUF_TYPE_VIDEO_OUTPUT
io_modes u32 支持的 I/O 模式位掩码 VB2_MMAP | VB2_USERPTR | VB2_DMABUF
drv_priv void * 驱动私有数据指针,通常指向设备结构体 在回调中通过 vb2_get_drv_priv(vq) 获取
buf_struct_size unsigned int 每个缓冲区私有结构体的大小(字节) 若驱动需要扩展缓冲区元数据,设为 sizeof(struct my_buffer)
ops struct vb2_ops * 回调函数集合指针 必须实现 queue_setup、buf_prepare、buf_queue 等
mem_ops struct vb2_mem_ops * 内存分配器操作集 通常使用 vb2_dma_contig_memops 或 vb2_vmalloc_memops
timestamp_flags u32 时间戳标志 V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC
min_buffers_needed unsigned int 驱动正常工作所需的最小缓冲区数 对于流水线型驱动,通常设为 2 或 3
调优要点: buf_struct_size 如果设置过小,驱动无法存储每个缓冲区的 DMA 地址或帧描述符;设置过大则浪费内存。建议在驱动私有结构中包含 struct vb2_v4l2_buffer v4l2_buf 作为第一个成员,然后扩展自定义字段。

3.2 queue_setup 回调:缓冲区分配决策中心

queue_setup 回调在 REQBUFS 或 CREATE_BUFS 时被调用,驱动必须在此回调中告知核心:

  • 需要分配多少个缓冲区(num_planes 和 plane_sizes)
  • 每个平面的字节大小
  • 是否允许分配请求数量的缓冲区(返回 0 表示同意,负值表示拒绝)

回调原型如下:

static int my_queue_setup(struct vb2_queue *vq,
                          unsigned int *num_buffers,
                          unsigned int *num_planes,
                          unsigned int sizes[],
                          struct device *alloc_devs[])
{
    struct my_device *dev = vb2_get_drv_priv(vq);

    // 1. 检查缓冲区数量是否在合理范围内
    if (*num_buffers < dev->min_buffers)
        *num_buffers = dev->min_buffers;
    if (*num_buffers > MY_MAX_BUFFERS)
        *num_buffers = MY_MAX_BUFFERS;

    // 2. 设置平面数量(单平面或双平面)
    *num_planes = 1;

    // 3. 设置每个平面的大小(对齐到 PAGE_SIZE)
    sizes[0] = PAGE_ALIGN(dev->width * dev->height * 2); // YUYV 格式

    // 4. 指定分配设备(用于 DMA 一致性内存)
    alloc_devs[0] = dev->dev;

    return 0;
}
常见错误: 在 queue_setup 中直接分配内存或申请 DMA 缓冲区。该回调只负责描述缓冲区需求,实际内存分配由 videobuf2 核心根据 mem_ops 在后续步骤完成。驱动不应在此处调用 dma_alloc_coherent。

3.3 内存分配策略对比

videobuf2 支持三种内存分配后端,驱动通过 mem_ops 选择:

内存类型 mem_ops 实现 适用场景 DMA 支持 性能特征
vmalloc vb2_vmalloc_memops USB 摄像头、模拟采集卡(非 DMA) 不支持硬件 DMA 低吞吐,CPU 拷贝开销大
DMA 连续 vb2_dma_contig_memops 大多数嵌入式摄像头、ISP、DMA 引擎 支持,物理地址连续 高吞吐,适合大块传输
DMA 分散/聚集 vb2_dma_sg_memops 支持 SG 的 DMA 引擎、高性能场景 支持,物理地址不连续 高吞吐,减少内存碎片
调优建议: 对于嵌入式平台,优先使用 vb2_dma_contig_memops。如果硬件支持 scatter-gather DMA,且系统内存碎片严重,切换到 vb2_dma_sg_memops 可以避免连续物理内存分配失败。

3.4 缓冲区队列初始化流程 SVG

下图展示了从驱动 probe 到 vb2_queue_init 完成,再到 REQBUFS 触发 queue_setup 的完整流程:

vb2_queue_init 与 queue_setup 调用流程 vb2_queue_init 与 queue_setup 调用流程 驱动 probe / 初始化 填充 vb2_queue 参数 vb2_queue_init(vq) 应用层调用 REQBUFS 核心调用 queue_setup 回调 返回 num_buffers / num_planes / sizes 关键说明 • queue_setup 不分配内存 • 只描述缓冲区需求 • 实际分配由 mem_ops 完成

3.5 多平面与格式协商

对于 YUV 422 或 NV12 等格式,驱动可能需要使用多平面缓冲区。在 queue_setup 中设置 *num_planes = 2 并分别指定 sizes[0] 和 sizes[1]:

static int my_queue_setup_mplane(struct vb2_queue *vq,
                                 unsigned int *num_buffers,
                                 unsigned int *num_planes,
                                 unsigned int sizes[],
                                 struct device *alloc_devs[])
{
    struct my_device *dev = vb2_get_drv_priv(vq);

    *num_planes = 2;
    sizes[0] = PAGE_ALIGN(dev->width * dev->height);       // Y 平面
    sizes[1] = PAGE_ALIGN(dev->width * dev->height / 2);   // UV 平面

    alloc_devs[0] = dev->dev;
    alloc_devs[1] = dev->dev;

    return 0;
}
调优提示: 多平面场景下,确保 alloc_devs[] 指向正确的 DMA 设备。如果两个平面使用不同的内存控制器(例如 ISP 的 Y 通道和 UV 通道分离),可以指向不同的 struct device *。

3.6 初始化参数检查清单

在调用 vb2_queue_init 之前,建议驱动按以下清单逐项确认:

  1. type 是否与设备能力匹配(CAPTURE vs OUTPUT)
  2. io_modes 是否包含应用层可能请求的模式(至少支持 MMAP)
  3. mem_ops 是否与硬件 DMA 能力匹配(连续 vs SG)
  4. ops 是否实现了所有必需回调(queue_setup、buf_prepare、buf_queue、start_streaming、stop_streaming)
  5. buf_struct_size 是否足够容纳驱动私有数据
  6. min_buffers_needed 是否满足流水线深度需求
  7. drv_priv 是否指向有效的设备结构体

完成上述参数填充后,调用 vb2_queue_init(vq) 返回 0 表示成功。此时队列已注册到 V4L2 核心,等待应用层通过 REQBUFS 触发缓冲区分配。