2. V4L2 核心数据结构:struct video_device、struct v4l2_device、struct v4l2_subdev

V4L2(Video for Linux 2)框架的核心在于三个关键数据结构:struct video_device、struct v4l2_device 和 struct v4l2_subdev。理解它们各自的职责、成员以及三者之间的协作关系,是掌握 V4L2 驱动开发的基础。本章将深入剖析这三个结构体,并通过初始化流程图和关系图帮助读者建立清晰的认知。

2.1 数据结构概览与职责

在 V4L2 框架中,这三个结构体分别代表了不同层次的硬件与软件抽象:

数据结构 所属头文件 核心职责 典型代表
struct v4l2_device include/media/v4l2-device.h 顶层设备实例,管理整个视频硬件(如一个 USB 摄像头或 SoC ISP 模块) 一个摄像头传感器 + 控制器 + 视频接口的整体
struct v4l2_subdev include/media/v4l2-subdev.h 子设备抽象,代表一个独立的功能单元(如传感器、解码器、调焦马达) OV5640 传感器、ADV7180 视频解码器
struct video_device include/media/v4l2-dev.h 用户空间接口,通过 /dev/videoX 设备节点与应用程序交互 /dev/video0、/dev/video1
设计思想: 这种分层设计遵循了 Linux 内核“高内聚、低耦合”的原则。v4l2_device 作为容器,v4l2_subdev 负责硬件控制逻辑,而 video_device 专注于字符设备操作。驱动开发者可以分别实现各层,最后通过注册机制将它们绑定在一起。

2.2 struct v4l2_device:顶层设备容器

struct v4l2_device 是整个 V4L2 设备的根节点。它通常嵌入在一个更大的设备结构体中(如 struct usb_device 或 struct platform_device),并负责管理所有关联的子设备。

关键成员:

  • struct list_head subdevs:链表头,用于挂载所有注册到该设备的 v4l2_subdev 实例。
  • spinlock_t lock:自旋锁,保护子设备链表操作。
  • char name[V4L2_DEVICE_NAME_SIZE]:设备名称,用于调试和日志。
  • struct device *dev:指向底层 Linux 通用设备结构体,用于电源管理和 sysfs。
  • void (*notify)(struct v4l2_subdev *sd, unsigned int notification, void *arg):通知回调,用于子设备向父设备发送事件(如传感器状态变化)。

初始化流程:

#include <media/v4l2-device.h>

struct my_camera_dev {
    struct v4l2_device v4l2_dev;
    /* 其他驱动私有成员 */
};

static int my_probe(struct platform_device *pdev) {
    struct my_camera_dev *camera;
    int ret;

    camera = devm_kzalloc(&pdev->dev, sizeof(*camera), GFP_KERNEL);
    if (!camera)
        return -ENOMEM;

    /* 1. 初始化 v4l2_device */
    ret = v4l2_device_register(&pdev->dev, &camera->v4l2_dev);
    if (ret < 0) {
        dev_err(&pdev->dev, "Failed to register v4l2_device\n");
        return ret;
    }

    /* 2. 后续注册子设备、video_device 等 */

    return 0;
}

static int my_remove(struct platform_device *pdev) {
    struct my_camera_dev *camera = platform_get_drvdata(pdev);

    /* 注销顺序与注册相反 */
    v4l2_device_unregister(&camera->v4l2_dev);
    return 0;
}

2.3 struct v4l2_subdev:子设备抽象

struct v4l2_subdev 代表一个独立的硬件功能块。它封装了该子设备的操作函数集(通过 v4l2_subdev_ops)和内部状态。

关键成员:

  • struct v4l2_device *v4l2_dev:指向所属的父 v4l2_device。
  • struct module *owner:所属内核模块,防止模块被意外卸载。
  • const struct v4l2_subdev_ops *ops:操作函数集,包括视频、音频、控制、传感器等回调。
  • struct v4l2_subdev_internal_ops *internal_ops:内部操作,如注册/注销通知。
  • struct device *dev:指向该子设备对应的物理设备结构体。
  • struct list_head list:用于挂入 v4l2_device.subdevs 链表。
  • char name[V4L2_SUBDEV_NAME_SIZE]:子设备名称。
  • unsigned int flags:标志位,如 V4L2_SUBDEV_FL_HAS_DEVNODE 表示该子设备拥有独立的设备节点。

初始化与注册示例:

#include <media/v4l2-subdev.h>

static const struct v4l2_subdev_ops my_sensor_ops = {
    .core   = &my_sensor_core_ops,
    .video  = &my_sensor_video_ops,
    .pad    = &my_sensor_pad_ops,
};

static int my_sensor_probe(struct i2c_client *client) {
    struct my_sensor *sensor;
    int ret;

    sensor = devm_kzalloc(&client->dev, sizeof(*sensor), GFP_KERNEL);
    if (!sensor)
        return -ENOMEM;

    /* 1. 初始化 v4l2_subdev */
    v4l2_subdev_init(&sensor->sd, &my_sensor_ops);
    sensor->sd.owner = THIS_MODULE;
    sensor->sd.dev = &client->dev;
    snprintf(sensor->sd.name, sizeof(sensor->sd.name), "%s %d-%04x",
             client->name, i2c_adapter_id(client->adapter), client->addr);

    /* 2. 注册子设备到父 v4l2_device */
    ret = v4l2_device_register_subdev(&camera->v4l2_dev, &sensor->sd);
    if (ret < 0) {
        dev_err(&client->dev, "Failed to register subdev\n");
        return ret;
    }

    return 0;
}

2.4 struct video_device:用户空间接口

struct video_device 是用户空间通过 open()、ioctl() 等系统调用访问 V4L2 设备的入口。它负责将用户请求路由到正确的 v4l2_device 或 v4l2_subdev。

关键成员:

  • const struct v4l2_file_operations *fops:字符设备文件操作函数集(open、release、ioctl、mmap 等)。
  • struct video_device *queue:指向视频缓冲队列(videobuf2 相关)。
  • struct v4l2_device *v4l2_dev:指向所属的 v4l2_device。
  • struct device *dev:指向创建的字符设备结构体。
  • int minor:次设备号。
  • unsigned long flags:标志位,如 V4L2_FL_REGISTERED。
  • char name[32]:设备名称,显示在 /sys/class/video4linux/ 中。

初始化与注册示例:

#include <media/v4l2-dev.h>

static const struct v4l2_file_operations my_fops = {
    .owner      = THIS_MODULE,
    .open       = my_open,
    .release    = my_release,
    .unlocked_ioctl = my_ioctl,
    .mmap       = my_mmap,
};

static int my_video_probe(struct platform_device *pdev) {
    struct my_camera_dev *camera;
    struct video_device *vdev;
    int ret;

    /* 假设 camera 和 v4l2_device 已初始化 */

    /* 1. 分配 video_device */
    vdev = video_device_alloc();
    if (!vdev)
        return -ENOMEM;

    /* 2. 设置核心字段 */
    vdev->v4l2_dev = &camera->v4l2_dev;
    vdev->fops = &my_fops;
    vdev->release = video_device_release;
    vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
    strscpy(vdev->name, "My Camera", sizeof(vdev->name));

    /* 3. 注册 video_device */
    ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
    if (ret < 0) {
        video_device_release(vdev);
        dev_err(&pdev->dev, "Failed to register video device\n");
        return ret;
    }

    camera->vdev = vdev;
    return 0;
}

2.5 三者关系与初始化流程

下图清晰地展示了 v4l2_device、v4l2_subdev 和 video_device 之间的层次关系与初始化顺序。

V4L2 核心数据结构关系与初始化流程 V4L2 核心数据结构关系与初始化流程 1. v4l2_device_register() 2. v4l2_device_register_subdev() 3. video_register_device() 数据结构关系说明 v4l2_device v4l2_subdev video_device subdevs 链表 v4l2_dev 指针 一个 v4l2_device 可以包含多个 v4l2_subdev 和多个 video_device video_device 通过 v4l2_dev 指针找到所属的顶层设备 v4l2_subdev 通过 list 挂入 v4l2_device 的 subdevs 链表

标准初始化顺序:

  1. 注册 v4l2_device:调用 v4l2_device_register(),传入父设备指针和已分配的 v4l2_device 实例。此函数初始化内部锁和链表。
  2. 注册 v4l2_subdev:为每个硬件子模块调用 v4l2_device_register_subdev(),将子设备挂入 v4l2_device.subdevs 链表,并建立双向引用。
  3. 注册 video_device:调用 video_register_device(),指定设备类型(如 VFL_TYPE_VIDEO)和次设备号。内核会创建 /dev/videoX 节点并关联 v4l2_device。
注意事项:
  • 注销顺序必须与注册顺序相反:先 video_unregister_device(),再 v4l2_device_unregister_subdev(),最后 v4l2_device_unregister()。
  • video_device 的 release 回调必须正确设置,否则会导致内存泄漏。
  • 子设备可以通过 v4l2_subdev_call() 宏调用其他子设备的操作,但必须确保父设备已注册。

2.6 典型拓扑结构

在实际的嵌入式摄像头系统中,这三个结构体通常形成以下拓扑:

典型摄像头驱动拓扑结构 典型摄像头驱动拓扑结构 v4l2_device v4l2_subdev: 传感器 v4l2_subdev: ISP v4l2_subdev: 闪光灯 video_device /dev/video0

在这个拓扑中:

  • 一个 v4l2_device 代表整个摄像头模组。
  • 多个 v4l2_subdev 分别控制传感器、ISP 和闪光灯,它们通过 I2C 或 SPI 与主控通信。
  • 一个 video_device 提供视频数据流接口,应用程序通过它获取图像帧。
进阶提示: 在复杂 SoC 中,一个 v4l2_device 可能注册多个 video_device(如主摄像头和深度摄像头),每个 video_device 可以共享或独占某些 v4l2_subdev。这种灵活性使得 V4L2 能够支持从简单 USB 摄像头到多传感器机器视觉系统的各种场景。