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 数据结构关系图
下面这张图是我手绘的,帮你一眼看明白它们怎么挂接:
1.3 初始化顺序——这个顺序错了会崩
我个人习惯的初始化流程是这样的,你照着做基本不会出问题:
- 先注册 v4l2_device —— 调用
v4l2_device_register(),传入父设备指针。 - 再注册 v4l2_subdev —— 每个子设备调用
v4l2_subdev_init()或v4l2_i2c_new_subdev_board()。 - 初始化 vb2_queue —— 填充
vb2_queue结构体,调用vb2_queue_init()。 - 最后注册 video_device —— 调用
video_register_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 数据流怎么走?
说白了,这四个结构体配合起来,数据流是这样的:
- 用户空间调用
ioctl(VIDIOC_QBUF),把 buffer 交给驱动。 video_device的ioctl_ops收到请求,转给vb2_queue。vb2_queue调用buf_queue回调,把 buffer 挂到硬件队列。- 硬件采集完数据,触发中断,驱动在中断处理里调用
vb2_buffer_done()。 v4l2_subdev负责控制传感器参数(曝光、增益等),通过v4l2_device的ctrl_handler传递。
你看,每个结构体各司其职,配合得挺默契的。只要初始化顺序对了,后面基本不会出大问题。
📌 我的建议: 刚开始做 V4L2 驱动时,先别急着写复杂功能。把四个结构体的初始化跑通,用 v4l2-compliance 工具测一遍,确保所有 ioctl 都返回正确。这一步走稳了,后面加功能就顺了。