3. V4L2核心数据结构:struct video_device、struct v4l2_device、struct v4l2_subdev
好,咱们今天来啃V4L2框架里最核心的三个数据结构。说实话,我刚接触V4L2那会儿,看到这三个结构体互相嵌套、指针满天飞,头都大了。但后来我发现,只要理清了它们各自扮演的角色,整个框架就豁然开朗。
打个比方:v4l2_device是整个摄像头系统的“总指挥部”,video_device是用户空间看到的“设备节点”,而v4l2_subdev则是藏在底层的“功能模块”。
3.1 struct v4l2_device —— 总指挥部
这个结构体代表一个完整的V4L2设备实例。说白了,它就是所有子设备的“爹”。
struct v4l2_device {
struct device *dev; // 指向物理设备
struct list_head subdevs; // 子设备链表
spinlock_t lock; // 自旋锁
char name[V4L2_DEVICE_NAME_SIZE]; // 设备名
// ... 还有其他成员
};
我个人习惯,在驱动入口函数里第一个做的事情就是分配并初始化这个结构体。为什么?因为后面所有的子设备都要挂到它下面。
关键点:v4l2_device不直接和用户空间打交道,它是个“幕后管理者”。你想想看,一个摄像头模组可能包含传感器、ISP、闪光灯等多个芯片,v4l2_device就是把这些东西组织起来的“容器”。
我的经验:初始化v4l2_device时,记得调用v4l2_device_register()。我曾经在一个项目里忘了这步,结果子设备注册时死活找不到父设备,排查了半天才发现是初始化顺序搞反了。
3.2 struct video_device —— 用户空间的窗口
这个结构体才是用户程序通过/dev/videoX看到的那个“设备”。它负责把内核驱动能力暴露给应用程序。
struct video_device {
const struct v4l2_file_operations *fops; // 文件操作
struct v4l2_device *v4l2_dev; // 指向父设备
struct device *dev; // 字符设备
// ... 还有v4l2_ioctl_ops等
};
嗯,这里要注意:video_device和v4l2_device是父子关系。一个v4l2_device可以对应多个video_device。比如有的摄像头同时提供视频流和元数据流,那就需要注册两个video_device节点。
避坑指南:我曾经在注册video_device时忘记设置v4l2_dev指针,结果应用程序打开设备后,ioctl调用全部返回-ENODEV。调试了两天才发现,原来video_device需要通过这个指针找到父设备才能正常工作。
注册video_device的标准流程是这样的:
- 分配video_device结构体(用video_device_alloc())
- 设置fops和ioctl_ops回调函数
- 设置v4l2_dev指向父设备
- 调用video_register_device()注册
3.3 struct v4l2_subdev —— 底层功能模块
这个结构体代表一个具体的硬件子模块。比如摄像头传感器、自动对焦马达、闪光灯控制器等,每个都是一个subdev。
struct v4l2_subdev {
struct v4l2_device *v4l2_dev; // 所属的父设备
struct module *owner; // 所属模块
const struct v4l2_subdev_ops *ops; // 操作函数集
char name[V4L2_SUBDEV_NAME_SIZE];
// ... 还有pad、link等媒体控制器相关成员
};
为什么要有subdev?你想想看,一个摄像头模组里,传感器负责采集图像,ISP负责处理图像,它们各自有独立的寄存器、独立的中断、独立的电源管理。如果全部揉在一个驱动里,代码会乱成一锅粥。
核心思想:subdev实现了“高内聚、低耦合”。每个子设备只关心自己的事情,通过v4l2_subdev_ops暴露标准接口给上层调用。
常用的subdev操作接口包括:
| 操作集 | 用途 |
|---|---|
| core ops | 初始化、电源管理、IOCTL直通 |
| video ops | 视频流控制(s_stream、g_parm等) |
| pad ops | 媒体控制器相关(get_fmt、set_fmt等) |
| sensor ops | 传感器特有操作(曝光、增益等) |
3.4 三者如何协同工作?
好,现在咱们把这三个结构体串起来。一个典型的摄像头驱动初始化流程是这样的:
static int my_camera_probe(struct platform_device *pdev)
{
struct v4l2_device *v4l2_dev;
struct video_device *vdev;
struct v4l2_subdev *sensor_subdev;
// 1. 创建总指挥部
v4l2_dev = kzalloc(sizeof(*v4l2_dev), GFP_KERNEL);
v4l2_device_register(&pdev->dev, v4l2_dev);
// 2. 注册传感器子设备
sensor_subdev = kzalloc(sizeof(*sensor_subdev), GFP_KERNEL);
v4l2_subdev_init(sensor_subdev, &sensor_ops);
sensor_subdev->v4l2_dev = v4l2_dev;
// 挂到父设备的子设备链表上
list_add(&sensor_subdev->list, &v4l2_dev->subdevs);
// 3. 注册视频设备节点
vdev = video_device_alloc();
vdev->v4l2_dev = v4l2_dev;
vdev->fops = &my_fops;
vdev->ioctl_ops = &my_ioctl_ops;
video_register_device(vdev, VFL_TYPE_VIDEO, -1);
return 0;
}
我的建议:实际项目中,subdev通常用i2c_client来注册,而不是像上面这样手动分配。但核心思想是一样的——先有v4l2_device这个“家”,再把subdev和video_device放进去。
当用户程序调用open("/dev/video0")时,内核通过video_device找到对应的fops,然后fops里的函数可以通过v4l2_dev找到所有subdev,进而控制具体的硬件模块。这就是整个V4L2框架的数据流走向。
再次提醒:这三个结构体的生命周期管理要特别小心。我曾经在一个项目中,subdev已经被卸载了,但video_device还在,结果用户程序一调用ioctl就直接内核崩溃。正确的做法是:先注销video_device,再注销subdev,最后注销v4l2_device。顺序不能乱。
好了,这一章的内容就到这里。记住这三个结构体的关系:v4l2_device是容器,video_device是接口,v4l2_subdev是功能模块。搞懂了它们,V4L2框架你就掌握了半壁江山。