4. vb2核心回调函数
videobuf2(vb2)框架通过一组预定义的回调函数与驱动程序交互,将缓冲区生命周期管理标准化。驱动开发者只需实现这五个核心回调,即可完成 DMA 缓冲区的分配、准备、入队、流启停等关键操作。本章逐一解析每个回调的语义、调用时机、参数含义及典型实现模式。
4.1 回调函数总览与调用时序
五个回调函数覆盖了从缓冲区创建到流关闭的完整生命周期。下表列出每个回调的触发条件与核心职责:
| 回调函数 | 触发时机 | 核心职责 |
|---|---|---|
queue_setup |
调用 VIDIOC_REQBUFS 或 VIDIOC_CREATE_BUFS 时 |
分配缓冲区内存,确定缓冲区数量与每帧大小 |
buf_prepare |
缓冲区从 VIDIOC_QBUF 入队时 |
验证并准备缓冲区(如缓存同步、填充元数据) |
buf_queue |
buf_prepare 成功后 |
将缓冲区加入驱动内部待处理队列 |
start_streaming |
调用 VIDIOC_STREAMON 时 |
启动硬件 DMA 传输,使能中断 |
stop_streaming |
调用 VIDIOC_STREAMOFF 或设备关闭时 |
停止硬件 DMA,归还所有未处理缓冲区 |
关键时序点:
queue_setup 仅在缓冲区分配阶段调用一次或有限次;buf_prepare 和 buf_queue 在每个缓冲区入队时成对出现;start_streaming / stop_streaming 控制整个流的启停。
4.2 queue_setup — 缓冲区分配
queue_setup 是 vb2 框架中第一个被调用的驱动回调,负责告知框架驱动需要多少个缓冲区以及每个缓冲区的大小。其原型如下:
int (*queue_setup)(struct vb2_queue *q,
unsigned int *nbuffers,
unsigned int *nplanes,
unsigned int sizes[],
struct device *alloc_devs[]);
参数说明:
q:指向vb2_queue实例的指针,驱动可通过q->drv_priv获取私有数据。nbuffers:输入时为用户请求的缓冲区数量,输出时驱动可修改为实际分配的数量。nplanes:输出参数,指定缓冲区使用的 plane 数量(单平面为 1,多平面如 YUV422 可设为 2 或 3)。sizes[]:输出数组,每个元素对应一个 plane 的大小(字节)。alloc_devs[]:输出数组,指定每个 plane 对应的 DMA 设备。
典型实现模式:
static int my_queue_setup(struct vb2_queue *q,
unsigned int *nbuffers,
unsigned int *nplanes,
unsigned int sizes[],
struct device *alloc_devs[])
{
struct my_dev *dev = vb2_get_drv_priv(q);
/* 单平面:每帧大小为 width * height * bpp */
*nplanes = 1;
sizes[0] = dev->width * dev->height * 2; /* YUYV 格式 */
alloc_devs[0] = dev->dev;
/* 确保缓冲区数量在合理范围内 */
if (*nbuffers < 2)
*nbuffers = 2;
if (*nbuffers > VIDEO_MAX_FRAME)
*nbuffers = VIDEO_MAX_FRAME;
return 0;
}
常见错误: 在
queue_setup 中分配过大内存导致系统内存不足;未正确设置 alloc_devs 导致 DMA 映射失败;nplanes 与 sizes 数组长度不匹配。
4.3 buf_prepare — 缓冲区准备
每个缓冲区在入队前都会经过 buf_prepare 处理。该回调负责验证缓冲区状态、执行缓存同步操作,并填充硬件所需的描述符信息。原型如下:
int (*buf_prepare)(struct vb2_buffer *vb);
核心职责:
- 检查缓冲区长度是否满足驱动要求(
vb2_plane_size(vb, 0) >= required_size)。 - 对于输出(OUTPUT)设备,执行
dma_sync_single_for_device将用户数据同步到 DMA 缓冲区。 - 填充硬件描述符(如 DMA 地址、长度、格式信息)。
- 设置缓冲区状态为
VB2_BUF_STATE_PREPARED。
static int my_buf_prepare(struct vb2_buffer *vb)
{
struct my_dev *dev = vb2_get_drv_priv(vb->vb2_queue);
struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
unsigned long size;
size = vb2_plane_size(vb, 0);
if (size < dev->width * dev->height * 2)
return -EINVAL;
/* 对于 OUTPUT 设备,将用户数据同步到 DMA 缓冲区 */
if (dev->direction == OUTPUT) {
dma_sync_single_for_device(dev->dev,
vb2_dma_contig_plane_dma_addr(vb, 0),
size, DMA_TO_DEVICE);
}
vbuf->field = V4L2_FIELD_NONE;
return 0;
}
性能提示:
buf_prepare 在 QBUF 路径上被调用,应避免耗时操作(如大块内存分配)。缓存同步操作应仅针对当前使用的 plane,避免全缓冲区同步。
4.4 buf_queue — 缓冲区入队
buf_queue 在 buf_prepare 成功后立即调用,将缓冲区放入驱动内部待处理队列。驱动通常在此回调中将缓冲区添加到链表或环形缓冲区,等待硬件处理。
void (*buf_queue)(struct vb2_buffer *vb);
典型实现:
static void my_buf_queue(struct vb2_buffer *vb)
{
struct my_dev *dev = vb2_get_drv_priv(vb->vb2_queue);
struct my_buffer *buf = container_of(vb, struct my_buffer, vb);
unsigned long flags;
spin_lock_irqsave(&dev->queue_lock, flags);
list_add_tail(&buf->list, &dev->pending_list);
spin_unlock_irqrestore(&dev->queue_lock, flags);
/* 如果硬件空闲,立即启动传输 */
if (dev->state == IDLE)
schedule_work(&dev->dma_work);
}
注意事项:
buf_queue在原子上下文(持有 vb2 内部锁)中被调用,不能使用msleep等阻塞调用。- 驱动应使用自旋锁保护内部队列操作。
- 如果硬件支持链式 DMA,可在此时将缓冲区描述符写入硬件链表。
4.5 start_streaming — 启动流
当用户调用 VIDIOC_STREAMON 时,vb2 框架调用 start_streaming 通知驱动启动硬件传输。驱动在此回调中使能 DMA 引擎、注册中断处理函数,并开始处理已入队的缓冲区。
int (*start_streaming)(struct vb2_queue *q, unsigned int count);
参数 count 表示当前已入队的缓冲区数量。如果 count 小于驱动所需的最小缓冲区数,驱动应返回错误(如 -ENOBUFS)。
static int my_start_streaming(struct vb2_queue *q, unsigned int count)
{
struct my_dev *dev = vb2_get_drv_priv(q);
int ret;
if (count < 2) {
dev_err(dev->dev, "need at least 2 buffers\n");
return -ENOBUFS;
}
/* 配置 DMA 寄存器 */
writel(dev->width, dev->regs + REG_WIDTH);
writel(dev->height, dev->regs + REG_HEIGHT);
writel(1, dev->regs + REG_ENABLE);
/* 使能中断 */
enable_irq(dev->irq);
dev->state = STREAMING;
return 0;
}
错误处理: 如果
start_streaming 返回错误,vb2 框架会自动调用 stop_streaming 清理已入队的缓冲区。驱动必须确保在失败路径中正确归还所有缓冲区。
4.6 stop_streaming — 停止流
stop_streaming 在流关闭时被调用,负责停止硬件 DMA、释放中断,并将所有未处理的缓冲区归还给 vb2 框架。原型如下:
void (*stop_streaming)(struct vb2_queue *q);
必须完成的操作:
- 停止硬件 DMA 传输(清除使能位、复位 DMA 引擎)。
- 禁用中断并释放中断处理程序(如果动态申请)。
- 遍历驱动内部队列,对每个缓冲区调用
vb2_buffer_done(vb, VB2_BUF_STATE_ERROR)归还。 - 将驱动状态切换为
IDLE或STOPPED。
static void my_stop_streaming(struct vb2_queue *q)
{
struct my_dev *dev = vb2_get_drv_priv(q);
struct my_buffer *buf, *tmp;
unsigned long flags;
/* 停止硬件 */
writel(0, dev->regs + REG_ENABLE);
disable_irq(dev->irq);
/* 归还所有待处理缓冲区 */
spin_lock_irqsave(&dev->queue_lock, flags);
list_for_each_entry_safe(buf, tmp, &dev->pending_list, list) {
list_del(&buf->list);
vb2_buffer_done(&buf->vb, VB2_BUF_STATE_ERROR);
}
spin_unlock_irqrestore(&dev->queue_lock, flags);
dev->state = IDLE;
}
关键原则:
stop_streaming 必须保证所有从 vb2 借出的缓冲区都被归还,否则会导致内存泄漏或后续流启动失败。使用 VB2_BUF_STATE_ERROR 状态标记因流停止而未完成的缓冲区。
4.7 回调函数协作流程
五个回调函数并非孤立运行,它们通过 vb2 框架形成完整的缓冲区生命周期管理闭环。下图展示了从缓冲区分配到流停止的完整协作流程:
4.8 回调实现检查清单
在编写驱动时,可使用以下清单验证五个回调的实现完整性:
| 回调 | 必须检查项 | 常见遗漏 |
|---|---|---|
queue_setup |
设置 nplanes、sizes[]、alloc_devs[] |
未处理 nbuffers 下限;多平面时 sizes 顺序错误 |
buf_prepare |
校验缓冲区大小;缓存同步;填充元数据 | 未调用 dma_sync_single_for_device;未检查 vb2_plane_size |
buf_queue |
加锁入队;触发硬件处理 | 在原子上下文中使用 kmalloc;未使用自旋锁 |
start_streaming |
检查最小缓冲区数;使能硬件;注册中断 | 失败时未调用 vb2_buffer_done 归还缓冲区 |
stop_streaming |
停止硬件;归还所有缓冲区 | 遗漏部分缓冲区导致内存泄漏;未禁用中断 |
调试建议: 在
start_streaming 和 stop_streaming 中添加 dev_dbg 打印,记录缓冲区计数和状态切换。使用 trace_printk 跟踪 buf_prepare 和 buf_queue 的调用频率,确保每个 QBUF 都成对出现。