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的标准流程是这样的:

  1. 分配video_device结构体(用video_device_alloc())
  2. 设置fops和ioctl_ops回调函数
  3. 设置v4l2_dev指向父设备
  4. 调用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框架你就掌握了半壁江山。