3. V4L2核心数据结构

好,我们直接进入正题。V4L2 框架里,有三个结构体你绕不开:struct video_device、struct v4l2_device 和 struct v4l2_subdev。说白了,它们就是整个摄像头驱动的骨架。我刚开始接触 V4L2 时,光看头文件里的结构体定义,看得一头雾水。后来在项目里踩了几个坑,才真正搞明白它们各自该干什么。

今天我就把这三个结构体掰开揉碎了讲。你记住一句话:v4l2_device 是顶层容器,video_device 是用户态接口,v4l2_subdev 是内部功能单元。这三者的关系,就像一栋楼、前台和各个办公室。

核心关系速记:

  • v4l2_device:整个摄像头设备的抽象,管理所有子设备。
  • video_device:暴露给用户空间的 /dev/videoX 节点,处理 ioctl。
  • v4l2_subdev:代表一个内部硬件模块(如 sensor、ISP、MIPI 桥接芯片)。
V4L2 核心数据结构关系图 struct v4l2_device 顶层容器,管理所有子设备 struct video_device 用户态接口 /dev/videoX struct v4l2_subdev 内部硬件模块抽象 subdev: sensor (ov5640) subdev: ISP (rkisp) subdev: MIPI D-PHY 一个 v4l2_device 可以包含多个 video_device 和多个 v4l2_subdev

3.1 struct v4l2_device:整个驱动的大管家

struct v4l2_device 是 V4L2 框架里最顶层的结构体。你可以把它理解成整个摄像头设备的根节点。它不直接跟硬件打交道,而是负责协调所有子设备。

来看它的核心定义(我精简了,只留关键字段):

struct v4l2_device {
    struct device *dev;           // 指向 struct device,用于设备模型
    struct list_head subdevs;     // 挂载所有 v4l2_subdev 的链表头
    spinlock_t lock;              // 内部锁,保护 subdevs 链表
    char name[V4L2_DEVICE_NAME_SIZE]; // 设备名称,调试用
    void (*notify)(struct v4l2_subdev *sd, unsigned int notification, void *arg);
    // 通知回调:subdev 通过它向顶层报告事件
};

我个人习惯,在 probe 函数里第一件事就是分配并初始化 v4l2_device:

struct v4l2_device *v4l2_dev;
int ret;

v4l2_dev = devm_kzalloc(dev, sizeof(*v4l2_dev), GFP_KERNEL);
if (!v4l2_dev)
    return -ENOMEM;

snprintf(v4l2_dev->name, sizeof(v4l2_dev->name), "my-camera");
ret = v4l2_device_register(dev, v4l2_dev);
if (ret) {
    dev_err(dev, "Failed to register v4l2_device\n");
    return ret;
}

我的经验: 记得在 remove 函数里调用 v4l2_device_unregister()。我曾经漏掉这一步,导致模块卸载后重新加载时,内核报了一堆 "device already registered" 的错误。嗯,那感觉挺尴尬的。

3.2 struct video_device:用户态看到的那个 /dev/videoX

这个结构体,说白了就是用户空间和内核空间的桥梁。应用程序打开 /dev/video0,调用的所有 ioctl,最终都会落到 video_device 注册的回调函数上。

关键字段:

struct video_device {
    const struct v4l2_file_operations *fops; // 文件操作函数集
    struct v4l2_ioctl_ops *ioctl_ops;        // ioctl 回调
    struct device dev;                       // 内核设备模型
    struct v4l2_device *v4l2_dev;            // 指向所属的 v4l2_device
    unsigned long flags;                     // 标志位,如 V4L2_FL_REGISTERED
    int minor;                               // 次设备号
    // ... 还有 video buffer 相关字段
};

初始化时,我建议你这样写:

struct video_device *vdev;

vdev = devm_kzalloc(dev, sizeof(*vdev), GFP_KERNEL);
vdev->fops = &my_camera_fops;      // 你的 file_operations
vdev->ioctl_ops = &my_camera_ioctl_ops; // 你的 ioctl 处理
vdev->v4l2_dev = v4l2_dev;         // 关联到顶层设备
vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
strscpy(vdev->name, "My Camera", sizeof(vdev->name));

ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
if (ret) {
    dev_err(dev, "Failed to register video device\n");
    return ret;
}

注意: video_register_device 的第三个参数是 nr(设备号)。传 -1 表示让内核自动分配。我建议你这么做,除非你有非常明确的静态编号需求。否则手动指定容易跟其他驱动冲突。

你想想看,用户态调用 VIDIOC_QUERYCAP 时,内核是怎么处理的?就是通过 video_device 的 ioctl_ops 找到对应的回调函数。所以,ioctl_ops 是你必须实现的核心。

3.3 struct v4l2_subdev:藏在背后的硬件模块

这个结构体,代表一个内部硬件子模块。比如摄像头 sensor、ISP 处理器、MIPI 桥接芯片。每个 subdev 都有自己的控制逻辑,但它们不直接暴露给用户空间。

核心定义:

struct v4l2_subdev {
    struct v4l2_device *v4l2_dev;       // 所属的顶层设备
    struct module *owner;               // 所属模块
    const struct v4l2_subdev_ops *ops;  // 子设备操作函数集
    const struct v4l2_subdev_internal_ops *internal_ops;
    struct device *dev;                 // 对应的硬件设备
    struct list_head list;              // 用于挂入 v4l2_device->subdevs
    char name[V4L2_SUBDEV_NAME_SIZE];
    unsigned long flags;
    // ... 还有 pad、format、crop 等配置
};

subdev 的 ops 里包含了很多回调组:

回调组 用途 我常用的场景
core 基础操作(init、s_power、g_register) 上电/下电控制
video 视频流控制(s_stream、g_frame_interval) 启动/停止 sensor 输出
pad 多媒体 pad 操作(get_fmt、set_fmt、enum_mbus_code) 配置分辨率、像素格式
sensor sensor 专用(g_skip_frames、s_auto_focus) 跳过前几帧(AEC/AGC 稳定用)

注册 subdev 的典型流程:

struct v4l2_subdev *sd;

sd = devm_kzalloc(dev, sizeof(*sd), GFP_KERNEL);
v4l2_subdev_init(sd, &my_sensor_ops);
sd->owner = THIS_MODULE;
sd->dev = dev;
snprintf(sd->name, sizeof(sd->name), "ov5640 0-003c");

ret = v4l2_device_register_subdev(v4l2_dev, sd);
if (ret) {
    dev_err(dev, "Failed to register subdev\n");
    return ret;
}

避坑指南: 我曾经在注册 subdev 时,忘了设置 sd->owner。结果模块卸载时,subdev 的回调还在被调用,直接导致内核 panic。嗯,从那以后我每次都会检查 owner 字段。

3.4 三者如何协同工作?

我画个流程图你就明白了。用户态调用 ioctl 时,数据是怎么流转的:

  1. 用户态 打开 /dev/video0,调用 VIDIOC_S_FMT。
  2. 内核 通过 video_device 的 ioctl_ops 找到 vidioc_s_fmt 回调。
  3. 驱动 在回调里,通过 v4l2_device 找到对应的 v4l2_subdev(比如 sensor)。
  4. 驱动 调用 v4l2_subdev_call(sd, pad, set_fmt, ...),让 sensor 配置分辨率。
  5. sensor subdev 通过 I2C 写入硬件寄存器,完成配置。

你看,video_device 负责接客(用户态请求),v4l2_subdev 负责干活(操作硬件),v4l2_device 负责协调(管理生命周期和通知)。

我的建议: 在调试时,多利用 v4l2_device 的 notify 回调。subdev 可以通过 v4l2_subdev_notify() 向顶层报告事件(比如 sensor 掉电、帧同步丢失)。顶层收到通知后,可以做统一处理。这个机制在复杂系统中特别有用。

3.5 小结

这三个结构体,是 V4L2 驱动的基石。你写任何摄像头驱动,都离不开它们。记住:

  • v4l2_device:大管家,负责统筹。
  • video_device:前台,负责跟用户态打交道。
  • v4l2_subdev:员工,负责具体干活。