2. videobuf2核心概念
videobuf2(简称 vb2)是 V4L2 框架中负责视频数据缓冲区管理的核心模块。它向上对接 V4L2 应用层的 VIDIOC_REQBUFS、VIDIOC_QBUF、VIDIOC_DQBUF 等标准 IOCTL,向下通过回调函数与硬件 DMA 引擎交互。理解 vb2 的三个核心概念——vb2_buffer、vb2_queue、vb2_ops——是编写正确且高效 V4L2 驱动的基础。
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 的初始化,核心步骤如下:
- 填充结构体字段: 设置 type、io_modes、buf_struct_size 等参数。
- 赋值回调函数: 将
vb2_ops结构体指针赋给ops字段。 - 调用 vb2_queue_init(): 完成内部锁、链表等初始化。
- 注册到 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 引擎交互。
2.5 典型数据流场景
以视频采集(capture)为例,一次完整的缓冲区流转过程如下:
- 应用层调用 VIDIOC_REQBUFS: vb2 框架调用
queue_setup回调,驱动返回支持的缓冲区数量和大小。 - 应用层调用 VIDIOC_QBUF: 缓冲区入队,vb2 框架调用
buf_prepare回调,驱动完成 DMA 映射和描述符配置。 - 应用层调用 VIDIOC_STREAMON: vb2 框架调用
start_streaming回调,驱动启动硬件 DMA 引擎。 - 硬件 DMA 完成一帧: 触发中断,驱动在中断处理函数中调用
vb2_buffer_done(),将缓冲区状态设为 DONE。 - 应用层调用 VIDIOC_DQBUF: 获取已完成缓冲区,vb2 框架调用
buf_finish回调,驱动解除 DMA 映射。 - 应用层调用 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 宏会计算出错误的偏移地址,导致内存访问越界。