3. V4L2核心数据结构

做 V4L2 驱动开发,说白了就是跟四个结构体打交道:video_device、v4l2_device、v4l2_subdev 和 vb2_queue。我刚入行那会儿,看着这四兄弟也是一脸懵——它们谁管谁?初始化顺序是什么?今天咱们就把这事儿彻底捋清楚。

1.1 四兄弟的职责分工

先给个直观的比喻。你想想看,一个摄像头驱动就像一家公司:

  • v4l2_device —— 公司总部。负责统筹全局,管理所有资源。
  • video_device —— 前台接待。用户空间通过 /dev/videoX 访问,就是它在对接。
  • v4l2_subdev —— 各个部门。比如传感器部门、ISP 部门,各干各的活。
  • vb2_queue —— 仓库物流。负责 buffer 的分配、流转、回收。

我在项目中遇到过有人把 video_device 和 v4l2_device 搞混,结果注册顺序错了,驱动死活加载不上。嗯,这里要注意:先有总部,才有前台。

1.2 数据结构关系图

下面这张图是我手绘的,帮你一眼看明白它们怎么挂接:

struct v4l2_device(公司总部) struct video_device (前台接待 /dev/videoX) • v4l2_dev → 指向 v4l2_device • queue → 指向 vb2_queue • fops → 文件操作函数集 struct vb2_queue (仓库物流 - Buffer管理) • buf_struct_size • ops → vb2_ops 回调 • mem_ops → 内存操作 v4l2_dev 指针 queue 指针 struct v4l2_subdev(各部门) 传感器 subdev v4l2_dev 指针 ISP subdev v4l2_dev 指针 MIPI CSI subdev v4l2_dev 指针 初始化顺序:v4l2_device → v4l2_subdev → vb2_queue → video_device

1.3 初始化顺序——这个顺序错了会崩

我个人习惯的初始化流程是这样的,你照着做基本不会出问题:

  1. 先注册 v4l2_device —— 调用 v4l2_device_register(),传入父设备指针。
  2. 再注册 v4l2_subdev —— 每个子设备调用 v4l2_subdev_init() 或 v4l2_i2c_new_subdev_board()。
  3. 初始化 vb2_queue —— 填充 vb2_queue 结构体,调用 vb2_queue_init()。
  4. 最后注册 video_device —— 调用 video_register_device()。
⚠️ 我曾经踩过的坑: 先注册了 video_device 再初始化 vb2_queue,结果用户空间打开 /dev/videoX 时,queue 还没准备好,直接 kernel panic。记住:video_device 必须最后注册。

1.4 代码示例:标准初始化流程

下面是我在实际项目中用的模板,你直接套用就行:

// 1. 定义驱动结构体
struct my_camera_dev {
    struct v4l2_device      v4l2_dev;
    struct video_device     vdev;
    struct v4l2_subdev      *sensor_sd;
    struct vb2_queue        queue;
    // ... 其他成员
};

// 2. 初始化顺序
static int my_camera_probe(struct platform_device *pdev)
{
    struct my_camera_dev *dev;
    int ret;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);

    // 第一步:注册 v4l2_device
    ret = v4l2_device_register(&pdev->dev, &dev->v4l2_dev);
    if (ret < 0)
        return ret;

    // 第二步:注册 subdev(以 I2C 传感器为例)
    dev->sensor_sd = v4l2_i2c_new_subdev_board(&dev->v4l2_dev,
                                                adapter,
                                                "imx219", 0x10);
    if (!dev->sensor_sd) {
        v4l2_err(&dev->v4l2_dev, "sensor not found\n");
        return -ENODEV;
    }

    // 第三步:初始化 vb2_queue
    dev->queue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    dev->queue.io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
    dev->queue.ops = &my_vb2_ops;
    dev->queue.mem_ops = &vb2_dma_contig_memops;
    dev->queue.drv_priv = dev;
    ret = vb2_queue_init(&dev->queue);
    if (ret < 0)
        return ret;

    // 第四步:注册 video_device(最后一步!)
    dev->vdev.v4l2_dev = &dev->v4l2_dev;
    dev->vdev.queue = &dev->queue;
    dev->vdev.fops = &my_fops;
    dev->vdev.ioctl_ops = &my_ioctl_ops;
    video_set_drvdata(&dev->vdev, dev);

    ret = video_register_device(&dev->vdev, VFL_TYPE_VIDEO, -1);
    if (ret < 0) {
        v4l2_device_unregister(&dev->v4l2_dev);
        return ret;
    }

    return 0;
}

1.5 各结构体的关键字段速查表

结构体 关键字段 作用
v4l2_device dev 指向父设备(struct device *)
subdevs 链表头,挂接所有子设备
ctrl_handler 控制句柄,管理曝光、增益等
video_device v4l2_dev 指向所属的 v4l2_device
queue 指向 vb2_queue
fops 文件操作(open/close/ioctl)
v4l2_subdev v4l2_dev 指向所属的 v4l2_device
ops 子设备操作函数集
ctrl_handler 子设备自己的控制句柄
vb2_queue type buffer 类型(V4L2_BUF_TYPE_VIDEO_CAPTURE 等)
ops vb2_ops 回调(queue_setup/buf_prepare/buf_queue)
mem_ops 内存分配操作(dma_contig / vmalloc / dma_sg)

1.6 避坑指南——我这些年总结的教训

🔴 坑1:video_device 的 release 回调不能省

我曾经写驱动时忘了实现 video_device.release,结果卸载模块时内核报 refcount_t: underflow。这个回调是必须的,哪怕是个空函数也要写。

🔴 坑2:vb2_queue 的 buf_struct_size 要算对

如果你自定义了 buffer 结构体,buf_struct_size 要包含你额外字段的大小。我见过有人忘了加,结果访问自定义字段时越界,数据全乱了。

💡 小技巧: 调试时可以在 vb2_ops 的每个回调里加 dev_dbg() 打印,这样能清晰看到 buffer 的生命周期。我习惯在 buf_queue 和 buf_finish 里各打一条日志。

1.7 数据流怎么走?

说白了,这四个结构体配合起来,数据流是这样的:

  1. 用户空间调用 ioctl(VIDIOC_QBUF),把 buffer 交给驱动。
  2. video_device 的 ioctl_ops 收到请求,转给 vb2_queue。
  3. vb2_queue 调用 buf_queue 回调,把 buffer 挂到硬件队列。
  4. 硬件采集完数据,触发中断,驱动在中断处理里调用 vb2_buffer_done()。
  5. v4l2_subdev 负责控制传感器参数(曝光、增益等),通过 v4l2_device 的 ctrl_handler 传递。

你看,每个结构体各司其职,配合得挺默契的。只要初始化顺序对了,后面基本不会出大问题。

📌 我的建议: 刚开始做 V4L2 驱动时,先别急着写复杂功能。把四个结构体的初始化跑通,用 v4l2-compliance 工具测一遍,确保所有 ioctl 都返回正确。这一步走稳了,后面加功能就顺了。