2. videobuf2 核心概念:vb2_buffer、vb2_queue、vb2_ops 回调函数体系

videobuf2(简称 vb2)是 V4L2 框架中负责视频数据缓冲区管理的核心模块。它向上对接 V4L2 应用层的 VIDIOC_REQBUFS、VIDIOC_QBUF、VIDIOC_DQBUF 等标准 IOCTL,向下通过回调函数与硬件 DMA 引擎交互。理解 vb2 的三个核心概念——vb2_buffer、vb2_queue、vb2_ops——是编写正确且高效 V4L2 驱动的基础。

学习提示: 本章节将围绕“一个队列管理多个缓冲区,通过回调函数驱动硬件 DMA”这一主线展开。请重点关注三个概念之间的数据流和职责划分。

2.1 vb2_buffer:单个视频帧的抽象

vb2_buffer 是 vb2 框架中最基本的数据单元,它代表一个用于存放视频帧数据的缓冲区。驱动开发者通常不直接操作 vb2_buffer,而是操作其派生结构 vb2_v4l2_buffer,后者包含了 V4L2 应用层所需的元数据。

2.1.1 核心数据结构

struct vb2_buffer {
    struct vb2_queue        *vb2_queue;   // 所属队列
    unsigned int            index;        // 缓冲区索引
    unsigned int            type;         // 缓冲区类型 (V4L2_BUF_TYPE_VIDEO_CAPTURE 等)
    unsigned int            memory;       // 内存类型 (MMAP/USERPTR/DMABUF)
    struct vb2_plane        planes[VB2_MAX_PLANES]; // 平面数组
    enum vb2_buffer_state   state;        // 缓冲区状态
    // ... 其他内部字段
};

struct vb2_v4l2_buffer {
    struct vb2_buffer       vb2_buf;      // 基类
    struct v4l2_buffer      v4l2_buf;     // V4L2 应用层缓冲区描述
    // ... 其他 V4L2 特定字段
};

2.1.2 缓冲区状态机

每个 vb2_buffer 在生命周期内会经历多个状态,驱动通过状态判断缓冲区当前归属(应用层还是驱动层)以及可用性。

状态 枚举值 说明
VB2_BUF_STATE_DEQUEUED 0 缓冲区在应用层,等待入队
VB2_BUF_STATE_QUEUED 1 应用层已入队,等待驱动处理(准备 DMA)
VB2_BUF_STATE_ACTIVE 2 驱动正在使用,DMA 传输进行中
VB2_BUF_STATE_DONE 3 DMA 完成,等待应用层出队
VB2_BUF_STATE_ERROR 4 DMA 出错,缓冲区作废
关键点: 驱动在 buf_prepare 回调中完成 DMA 映射后,缓冲区进入 ACTIVE 状态;在硬件中断处理函数中调用 vb2_buffer_done() 将状态切换为 DONE,通知框架缓冲区已可用。

2.1.3 缓冲区与 DMA 地址

vb2_buffer 中的 planes[] 数组存储了每个平面的物理地址(dma_addr)和长度(length)。驱动在 buf_prepare 回调中通过 vb2_dma_contig_plane_dma_addr() 或 sg_dma_address() 获取 DMA 地址,并将其配置到硬件 DMA 引擎的地址寄存器中。

// 获取 DMA 地址示例(连续内存模式)
dma_addr_t dma_addr = vb2_dma_contig_plane_dma_addr(&buf->vb2_buf, 0);
// 将地址写入硬件 DMA 描述符
hw_dma_set_src_addr(dma_addr);

2.2 vb2_queue:缓冲区管理与调度中心

vb2_queue 是 vb2 框架的核心管理结构,它负责维护一个缓冲区队列,协调应用层与驱动层之间的缓冲区流转,并持有所有回调函数指针。每个视频设备节点(如 /dev/video0)通常对应一个 vb2_queue 实例。

2.2.1 核心数据结构

struct vb2_queue {
    unsigned int            type;         // 队列类型 (V4L2_BUF_TYPE_VIDEO_CAPTURE)
    unsigned int            io_modes;     // 支持的 IO 模式 (VB2_MMAP | VB2_USERPTR | VB2_DMABUF)
    unsigned int            buf_struct_size; // 驱动自定义缓冲区结构体大小
    const struct vb2_ops    *ops;         // 回调函数指针
    const struct vb2_mem_ops *mem_ops;    // 内存操作函数指针
    void                    *drv_priv;    // 驱动私有数据指针
    // ... 内部队列、锁、状态字段
};

2.2.2 队列初始化流程

驱动在 probe 或 open 阶段完成 vb2_queue 的初始化,核心步骤如下:

  1. 填充结构体字段: 设置 type、io_modes、buf_struct_size 等参数。
  2. 赋值回调函数: 将 vb2_ops 结构体指针赋给 ops 字段。
  3. 调用 vb2_queue_init(): 完成内部锁、链表等初始化。
  4. 注册到 V4L2 设备: 通过 video_device 的 queue 字段关联。
static struct vb2_queue capture_queue = {
    .type          = V4L2_BUF_TYPE_VIDEO_CAPTURE,
    .io_modes      = VB2_MMAP | VB2_USERPTR | VB2_DMABUF,
    .buf_struct_size = sizeof(struct my_buffer),
    .ops           = &my_vb2_ops,
    .mem_ops       = &vb2_dma_contig_memops,
    .drv_priv      = &my_dev,
};

int ret = vb2_queue_init(&capture_queue);
if (ret)
    return ret;

// 关联到 video_device
vdev->queue = &capture_queue;

2.2.3 缓冲区生命周期管理

vb2_queue 内部维护了三个链表:queued_list(已入队待处理)、done_list(已完成待出队)、allocated_bufs(已分配缓冲区)。应用层调用 VIDIOC_QBUF 时,缓冲区从应用层移动到 queued_list;驱动通过 vb2_buffer_done() 将缓冲区从 queued_list 转移到 done_list;应用层调用 VIDIOC_DQBUF 时,缓冲区从 done_list 返回应用层。

注意事项: 驱动在中断上下文中调用 vb2_buffer_done() 时,必须确保不会持有队列的自旋锁,否则可能引发死锁。推荐使用 vb2_buffer_done() 的原子版本或在 tasklet/workqueue 中调用。

2.3 vb2_ops 回调函数体系

vb2_ops 是驱动与 vb2 框架之间的契约接口。驱动通过实现这些回调函数,将硬件 DMA 操作注入到 vb2 的缓冲区管理流程中。vb2 框架在合适的时机调用这些回调,驱动在其中完成硬件配置、DMA 启动、中断处理等操作。

2.3.1 回调函数总览

回调函数 调用时机 驱动职责
queue_setup 应用层调用 VIDIOC_REQBUFS 或 VIDIOC_CREATE_BUFS 验证请求的缓冲区数量和大小,返回驱动支持的配置
buf_prepare 缓冲区从应用层入队后,DMA 启动前 完成 DMA 映射、缓存同步、配置硬件描述符
buf_finish 缓冲区完成 DMA 后,返回应用层前 解除 DMA 映射、缓存同步(读方向)、清理硬件状态
buf_cleanup 缓冲区被释放时 释放驱动私有资源,确保 DMA 已停止
start_streaming 应用层调用 VIDIOC_STREAMON 启动硬件 DMA 引擎,使能中断
stop_streaming 应用层调用 VIDIOC_STREAMOFF 或出错时 停止硬件 DMA,释放未完成的缓冲区

2.3.2 关键回调实现要点

queue_setup 回调是驱动与框架协商缓冲区参数的入口。驱动需要根据当前硬件状态(如分辨率、格式)返回所需的平面数量和每个平面的最小大小。

static int my_queue_setup(struct vb2_queue *q,
                          unsigned int *num_buffers,
                          unsigned int *num_planes,
                          unsigned int sizes[],
                          struct device *alloc_devs[])
{
    struct my_device *dev = q->drv_priv;

    // 单平面格式
    *num_planes = 1;
    sizes[0] = dev->width * dev->height * 2; // YUYV 格式

    // 限制最小缓冲区数量
    if (*num_buffers < 2)
        *num_buffers = 2;

    return 0;
}

buf_prepare 回调是 DMA 准备的关键环节。驱动在此函数中获取 DMA 地址,配置硬件描述符链表,并执行必要的缓存同步操作。

static int my_buf_prepare(struct vb2_buffer *vb)
{
    struct my_device *dev = vb->vb2_queue->drv_priv;
    struct my_buffer *buf = container_of(vb, struct my_buffer, vb);

    // 获取 DMA 地址(连续内存模式)
    buf->dma_addr = vb2_dma_contig_plane_dma_addr(vb, 0);

    // 配置硬件 DMA 描述符
    hw_desc_setup(&dev->dma_channel, buf->dma_addr, dev->buffer_size);

    // 写方向缓存同步(确保 CPU 写入对 DMA 可见)
    vb2_dma_sync_for_device(vb, 0, VB2_DMA_SYNC_PREPARE);

    return 0;
}

start_streaming 回调启动整个 DMA 传输链。驱动需要确保所有已入队的缓冲区都已配置好描述符,然后使能硬件 DMA 引擎。

static int my_start_streaming(struct vb2_queue *q, unsigned int count)
{
    struct my_device *dev = q->drv_priv;

    // 使能硬件 DMA 引擎
    hw_dma_enable(dev->dma_channel);

    // 使能 DMA 完成中断
    hw_irq_enable(dev->irq);

    return 0;
}

2.4 核心概念关系图

下图展示了 vb2_buffer、vb2_queue 和 vb2_ops 三者之间的协作关系,以及它们如何与硬件 DMA 引擎交互。

vb2_buffer、vb2_queue、vb2_ops 与硬件 DMA 协作关系图 vb2 核心概念与硬件 DMA 协作关系 vb2_queue 缓冲区管理调度中心 持有 ops / mem_ops / drv_priv vb2_ops 回调函数体系 queue_setup / buf_prepare start_streaming / stop_streaming vb2_buffer 单个视频帧数据抽象 planes[] / state / index 硬件 DMA 引擎 DMA 描述符 / 地址寄存器 中断控制器 调用 操作 配置/启动/停止 DMA 地址 说明: vb2_queue 持有 vb2_ops 指针,在缓冲区状态切换时调用对应回调;驱动在回调中操作 vb2_buffer 获取 DMA 地址,并配置硬件 DMA 引擎。

2.5 典型数据流场景

以视频采集(capture)为例,一次完整的缓冲区流转过程如下:

  1. 应用层调用 VIDIOC_REQBUFS: vb2 框架调用 queue_setup 回调,驱动返回支持的缓冲区数量和大小。
  2. 应用层调用 VIDIOC_QBUF: 缓冲区入队,vb2 框架调用 buf_prepare 回调,驱动完成 DMA 映射和描述符配置。
  3. 应用层调用 VIDIOC_STREAMON: vb2 框架调用 start_streaming 回调,驱动启动硬件 DMA 引擎。
  4. 硬件 DMA 完成一帧: 触发中断,驱动在中断处理函数中调用 vb2_buffer_done(),将缓冲区状态设为 DONE。
  5. 应用层调用 VIDIOC_DQBUF: 获取已完成缓冲区,vb2 框架调用 buf_finish 回调,驱动解除 DMA 映射。
  6. 应用层调用 VIDIOC_STREAMOFF: vb2 框架调用 stop_streaming 回调,驱动停止硬件 DMA 并释放所有未完成缓冲区。
最佳实践: 在 stop_streaming 回调中,驱动必须调用 vb2_buffer_done() 将所有仍处于 ACTIVE 或 QUEUED 状态的缓冲区返回给框架,否则会导致应用层阻塞在 DQBUF 调用上。

2.6 驱动私有缓冲区结构体扩展

大多数驱动需要扩展 vb2_v4l2_buffer 以存储硬件相关的私有数据(如 DMA 描述符指针、硬件时间戳、中断上下文标志等)。通过设置 vb2_queue.buf_struct_size 字段,vb2 框架会自动分配更大的缓冲区结构体。

// 驱动自定义缓冲区结构体
struct my_buffer {
    struct vb2_v4l2_buffer  vb;           // 必须为第一个成员
    dma_addr_t              dma_addr;     // DMA 地址
    struct hw_desc          *desc;        // 硬件描述符指针
    unsigned long           timestamp;    // 硬件时间戳
};

// 在 queue_setup 中设置大小
static int my_queue_setup(struct vb2_queue *q, ...)
{
    q->buf_struct_size = sizeof(struct my_buffer);
    // ...
}

// 在 buf_prepare 中访问私有数据
static int my_buf_prepare(struct vb2_buffer *vb)
{
    struct my_buffer *buf = container_of(vb, struct my_buffer, vb.vb2_buf);
    // 使用 buf->dma_addr, buf->desc 等
}
结构体布局约束: vb2_v4l2_buffer 必须作为驱动私有结构体的第一个成员,否则 container_of 宏会计算出错误的偏移地址,导致内存访问越界。