4. V4L2核心数据结构:struct video_device、struct vb2_queue、struct v4l2_device

好,我们直接进入正题。V4L2 框架里,有三个结构体你绕不开。它们就像摄像头驱动的骨架,撑起了整个采集流程。我个人习惯把这仨比作「设备、队列、容器」——video_device 是暴露给用户空间的设备节点,vb2_queue 是管理内存和缓冲区的调度中心,v4l2_device 则是整个驱动的顶层容器。

今天我们就一个一个拆开看。我会结合我踩过的坑,告诉你每个结构体到底怎么用、为什么这么设计。

4.1 struct video_device:用户空间的窗口

说白了,video_device 就是你在 /dev/video0 里看到那个设备节点背后的结构体。用户程序打开它、调用 ioctl,最终都会落到你注册的回调函数里。

核心成员一览:

  • fops:file_operations 指针,定义 open、release、ioctl 等操作
  • v4l2_dev:指向父级 v4l2_device
  • queue:指向 vb2_queue,用于流式 I/O
  • minor:次设备号,系统自动分配
  • name:设备名称,比如 "mt9v034"
  • release:释放回调,最后一个引用被释放时调用

我记得刚接触 V4L2 时,犯过一个低级错误——忘了设置 release 回调。结果驱动卸载时内核直接 panic。嗯,这里要注意:release 必须实现,哪怕是个空函数,否则内核会认为你占着资源不放手。

注册流程其实很固定:

struct video_device *vdev = video_device_alloc();
vdev->fops = &my_fops;
vdev->v4l2_dev = &my_v4l2_dev;
vdev->queue = &my_queue;
strscpy(vdev->name, "my_camera", sizeof(vdev->name));
vdev->release = video_device_release_empty;

ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
if (ret < 0) {
    video_device_release(vdev);
    return ret;
}

你想想看,video_register_device 第三个参数是 -1,表示让内核自动分配次设备号。我建议你永远别手动指定,除非你有非常特殊的理由。为什么?因为不同驱动之间可能冲突,自动分配最安全。

个人经验: 调试时可以用 cat /sys/class/video4linux/video0/name 查看设备名称,确认你的驱动是否注册成功。我曾经花了一下午查一个设备打不开的问题,最后发现是名字拼错了。

4.2 struct vb2_queue:缓冲区管理的核心

这个结构体,说白了就是 V4L2 内存管理的调度器。它负责分配缓冲区、管理队列状态、处理 DQBUF/QBUF 请求。你写的摄像头驱动,大部分时间都在跟它打交道。

先看初始化:

struct vb2_queue q;
memset(&q, 0, sizeof(q));

q.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
q.io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
q.drv_priv = &my_dev;
q.buf_struct_size = sizeof(struct my_buffer);
q.ops = &my_vb2_ops;
q.mem_ops = &vb2_vmalloc_memops;
q.timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;

ret = vb2_queue_init(&q);
if (ret)
    return ret;

这里有个关键点:io_modes 决定了你的驱动支持哪种内存模型。我个人习惯三种都支持,因为用户程序的需求千奇百怪。但如果你做的是嵌入式产品,只支持 MMAP 就够了,省心。

ops 是你必须实现的回调函数集,包括:

回调函数 作用 我的建议
queue_setup 分配缓冲区时调用,告诉内核你需要多少内存 这里可以动态调整缓冲区数量
buf_prepare 每次 QBUF 前调用,准备硬件 DMA 注意检查缓冲区大小是否匹配
buf_queue 将缓冲区放入硬件队列,开始采集 这里要加锁,防止竞态
start_streaming 启动视频流 记得检查硬件是否就绪
stop_streaming 停止视频流,清理未完成的缓冲区 我曾经在这里漏了释放 DMA 描述符,导致内存泄漏

避坑指南: 我曾经在 stop_streaming 里忘记调用 vb2_buffer_done 返回所有未处理的缓冲区,结果用户空间一直阻塞在 DQBUF 上。嗯,这个坑我踩了两次才记住。

还有一个容易被忽略的细节:buf_struct_size。如果你需要为每个缓冲区保存额外的私有数据(比如 DMA 地址、描述符指针),就在这里指定大小。内核会在分配缓冲区时自动为你预留空间。

4.3 struct v4l2_device:顶层容器

这个结构体相对简单,但很重要。它是整个 V4L2 驱动的根节点,所有子设备、video_device、其他资源都挂载在它下面。

初始化代码:

struct v4l2_device v4l2_dev;

v4l2_dev.dev = &pdev->dev;  // platform device
strscpy(v4l2_dev.name, "my_camera_driver", sizeof(v4l2_dev.name));

ret = v4l2_device_register(&pdev->dev, &v4l2_dev);
if (ret)
    return ret;

你想想看,为什么需要这个结构体?因为一个复杂的摄像头模组可能包含多个子设备——比如传感器、ISP、闪光灯控制器。每个子设备都是一个 v4l2_subdev,它们都注册到同一个 v4l2_device 下。这样管理起来就清晰多了。

核心作用:

  • 提供统一的设备注册/注销接口
  • 管理子设备链表,方便遍历和通知
  • 作为 debugfs 和 sysfs 的入口点
  • 提供通知链(notifier),用于子设备间的通信

我记得有一次调试一个多摄像头模组,两个传感器共用一个 I2C 总线。如果没有 v4l2_device 统一管理,光总线冲突就能让你崩溃。有了它,每个子设备独立注册,内核自动帮你协调。

4.4 三个结构体的协作关系

好,我们捋一下它们怎么配合工作的:

  1. v4l2_device 是老大,负责注册和生命周期管理
  2. video_device 是门面,负责和用户空间打交道
  3. vb2_queue 是后勤,负责内存和缓冲区的调度

典型的初始化顺序是:先注册 v4l2_device,再初始化 vb2_queue,最后注册 video_device。注销时顺序相反。

代码里体现出来就是这样:

// 1. 注册顶层设备
v4l2_device_register(&pdev->dev, &my_dev->v4l2_dev);

// 2. 初始化 vb2_queue
vb2_queue_init(&my_dev->queue);

// 3. 注册 video_device
my_dev->vdev.v4l2_dev = &my_dev->v4l2_dev;
my_dev->vdev.queue = &my_dev->queue;
video_register_device(&my_dev->vdev, VFL_TYPE_VIDEO, -1);

个人习惯: 我会在 probe 函数里把这三个结构体都放在一个大的私有结构体中,比如 struct my_camera_dev。这样管理起来方便,也方便在回调函数里通过 container_of 获取上下文。

4.5 常见问题与调试技巧

最后分享几个我实际项目中遇到的问题:

  • video_device 注册失败: 检查次设备号是否被占用,或者 v4l2_device 是否已经注册。我曾经因为 probe 顺序问题,导致 v4l2_device 还没注册就注册了 video_device,内核直接报错。
  • vb2_queue 无法启动: 检查 queue_setup 返回的缓冲区数量是否合理。太少会导致采集卡顿,太多会浪费内存。我一般设 4 个,够用。
  • 用户空间无法打开设备: 检查权限,或者 video_devicefops 是否实现了 open 回调。嗯,这个坑我帮别人排查过好几次。

好了,这一章的内容就到这里。三个结构体,说复杂也复杂,说简单也简单。你只要记住它们的角色分工,写驱动时自然就知道该往哪个结构体里塞代码了。