4. Sensor驱动框架(Linux V4L2):核心数据结构、注册、subdev与ioctl

做Sensor驱动,说白了就是跟Linux V4L2框架打交道。V4L2这玩意儿,我刚开始接触的时候也觉得头大——结构体套结构体,回调函数满天飞。但摸爬滚打几年下来,发现核心就那么几个东西:video_device、v4l2_subdev、ioctl回调。把这几个啃透了,驱动框架就稳了。

今天咱们就掰开揉碎,把V4L2驱动骨架讲清楚。嗯,这里要注意,我不会给你堆砌API文档,而是告诉你实际项目中怎么用、坑在哪儿。

4.1 V4L2核心数据结构:驱动的地基

V4L2框架里,最核心的数据结构就三个。我习惯把它们比作“三层楼”:

  • video_device —— 用户空间看到的设备节点(/dev/videoX)
  • v4l2_device —— 顶层设备实例,管理所有子设备
  • v4l2_subdev —— Sensor、ISP等硬件模块的抽象

先看video_device。每个Sensor驱动至少得注册一个。它的定义在include/media/v4l2-dev.h里,关键字段我列一下:

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;                      // 标志位
    int minor;                                // 次设备号
    char name[32];                            // 设备名称
    // ... 还有不少,但上面这些是必填的
};

我个人习惯,在probe函数里先分配video_device,再填充fops和ioctl_ops。千万别忘了设v4l2_dev指针,否则注册时会崩——我在项目里见过同事踩这个坑,调试了一整天。

核心要点:video_device的生命周期管理要小心。用video_device_alloc()分配,用video_device_release()释放。我建议用video_set_drvdata()把私有数据挂上去,这样回调里取数据方便。

4.2 video_device注册:让内核认识你的Sensor

注册video_device,流程其实不复杂。但顺序错了,系统可能直接Oops。我一般按这个步骤来:

  1. 分配video_device:struct video_device *vdev = video_device_alloc();
  2. 填充字段:fops、ioctl_ops、name、release回调
  3. 设置v4l2_dev:指向已经初始化的v4l2_device实例
  4. 调用video_register_device():指定类型(VFL_TYPE_VIDEO)和编号

代码示例:

static int sensor_probe(struct i2c_client *client) {
    struct sensor_dev *sensor = devm_kzalloc(&client->dev, sizeof(*sensor), GFP_KERNEL);
    struct video_device *vdev;

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

    // 2. 填充
    vdev->fops = &sensor_fops;
    vdev->ioctl_ops = &sensor_ioctl_ops;
    vdev->release = video_device_release;
    snprintf(vdev->name, sizeof(vdev->name), "%s", "imx290");

    // 3. 关联v4l2_device
    vdev->v4l2_dev = &sensor->v4l2_dev;

    // 4. 注册
    ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
    if (ret) {
        video_device_release(vdev);
        return ret;
    }

    video_set_drvdata(vdev, sensor);
    sensor->vdev = vdev;
    return 0;
}

注意:注册时第三个参数是“期望的次设备号”,传-1表示让内核自动分配。我曾经手写了一个固定编号,结果跟其他驱动冲突,/dev/video节点乱跳。后来学乖了,永远传-1。

4.3 subdev模型:把Sensor拆成模块

V4L2的subdev模型,说白了就是“分而治之”。一个Camera模组可能有Sensor、ISP、闪光灯、马达好几个硬件块。每个块都抽象成一个v4l2_subdev,通过media_entity连接起来。

subdev的核心结构:

struct v4l2_subdev {
    struct v4l2_device *v4l2_dev;       // 所属的v4l2_device
    struct module *owner;               // 模块所有者
    const struct v4l2_subdev_ops *ops;  // 操作函数集
    struct v4l2_subdev_pad_config *config; // pad配置
    struct media_entity entity;         // media实体
    char name[V4L2_SUBDEV_NAME_SIZE];   // 名称
};

subdev的ops分为几类:

ops类型 用途 关键回调
core 核心操作 s_power、g_chip_ident
video 视频流控制 s_stream、g_frame_interval
pad pad操作 enum_mbus_code、set_fmt、get_fmt
sensor Sensor专用 g_skip_frames、s_exposure

注册subdev的流程:

static int sensor_subdev_init(struct i2c_client *client) {
    struct v4l2_subdev *sd = &sensor->subdev;

    v4l2_subdev_init(sd, &sensor_subdev_ops);
    sd->owner = THIS_MODULE;
    sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
    snprintf(sd->name, sizeof(sd->name), "%s %d-%04x",
             client->name, i2c_adapter_id(client->adapter), client->addr);

    // 注册到v4l2_device
    ret = v4l2_device_register_subdev(&sensor->v4l2_dev, sd);
    if (ret) {
        dev_err(&client->dev, "subdev register failed\n");
        return ret;
    }
    return 0;
}

经验之谈:subdev的s_power回调里,我习惯做Sensor的硬件初始化。上电时配置寄存器,下电时关掉时钟。别把初始化放在probe里——probe只做软件准备,硬件控制交给subdev回调。

为什么用subdev模型?你想想看,如果Sensor、ISP、闪光灯都揉在一个驱动里,代码会变成什么样?维护起来简直是噩梦。subdev让每个硬件模块各司其职,通过media框架连接,清晰得很。

4.4 ioctl回调实现:用户空间怎么跟你对话

用户程序调用open()、ioctl()、mmap(),最终都会落到驱动里的回调函数。V4L2定义了一套标准的ioctl命令,驱动只需要实现对应的回调就行。

ioctl回调集v4l2_ioctl_ops里,我挑几个必实现的:

ioctl命令 回调函数 说明
VIDIOC_QUERYCAP vidioc_querycap 查询设备能力,必须实现
VIDIOC_ENUM_FMT vidioc_enum_fmt 枚举支持的格式
VIDIOC_S_FMT vidioc_s_fmt 设置格式,这里要配置Sensor寄存器
VIDIOC_REQBUFS vidioc_reqbufs 申请缓冲区,V4L2内存管理入口
VIDIOC_QBUF vidioc_qbuf 将缓冲区放入队列
VIDIOC_DQBUF vidioc_dqbuf 从队列取出已填满的缓冲区
VIDIOC_STREAMON vidioc_streamon 启动视频流,调用subdev的s_stream

举个例子,vidioc_s_fmt的实现:

static int sensor_vidioc_s_fmt(struct file *file, void *priv,
                               struct v4l2_format *f) {
    struct sensor_dev *sensor = video_drvdata(file);
    struct v4l2_mbus_framefmt mbus_fmt;
    int ret;

    // 1. 检查格式是否支持
    if (f->fmt.pix.pixelformat != V4L2_PIX_FMT_SGRBG10)
        return -EINVAL;

    // 2. 转换为media bus格式
    mbus_fmt.width = f->fmt.pix.width;
    mbus_fmt.height = f->fmt.pix.height;
    mbus_fmt.code = MEDIA_BUS_FMT_SGRBG10_1X10;
    mbus_fmt.field = V4L2_FIELD_NONE;

    // 3. 通过subdev设置Sensor
    ret = v4l2_subdev_call(&sensor->subdev, pad, set_fmt, NULL, &mbus_fmt);
    if (ret)
        return ret;

    // 4. 更新当前格式
    sensor->fmt = f->fmt.pix;
    return 0;
}

关键点:ioctl回调里不要做耗时操作。配置Sensor寄存器时,I2C通信可能几十毫秒,但用户空间等得起。真正要小心的是——不要在中断上下文或持有锁时调用ioctl回调。

我曾经在调试一个项目时,VIDIOC_STREAMON回调里调了msleep(100),结果整个系统卡住。为什么?因为当时持有了一个spinlock。嗯,从那以后我养成了习惯:ioctl回调里能用mutex就别用spinlock。

4.5 整体框架:一张图看懂

说了这么多,咱们用一张SVG图把整个框架串起来。这张图展示了从用户空间到硬件寄存器的完整调用链:

V4L2 Sensor驱动框架结构图 用户空间应用程序 系统调用 (open/ioctl/mmap) V4L2核心框架 video_device / v4l2_ioctl_ops / vb2 Sensor驱动 (video_device回调实现) s_fmt / s_stream / qbuf / dqbuf 回调 v4l2_subdev (Sensor控制) vb2缓冲区管理 Sensor硬件 (I2C/SPI寄存器) DMA内存 / 硬件FIFO 调用流程 1. 用户调用ioctl 2. V4L2分发到驱动 3. 驱动调用subdev 4. subdev操作硬件

这张图里,从上到下是调用链,从左到右是功能拆分。你写驱动时,重点关注中间两层——驱动回调实现和subdev控制。V4L2核心框架帮你做了大部分脏活累活,比如ioctl命令分发、缓冲区队列管理。

4.6 避坑指南:我踩过的几个坑

最后,分享几个实战中容易翻车的地方:

  • video_device未设置release回调:内核会在卸载时调用release释放内存。不设置的话,video_device_release不会被调用,造成内存泄漏。我见过一个驱动,卸载后/proc/slabinfo里kmalloc-2k只增不减,查了两天才发现是这个问题。
  • subdev ops未初始化:注册subdev前,ops里的回调必须全部初始化。哪怕某个回调用不到,也要设为NULL。否则内核在遍历ops时会访问野指针——直接Oops。
  • ioctl回调中睡眠:有些ioctl(比如VIDIOC_DQBUF)可能在中断上下文被调用。如果你在里面调了msleep或wait_event_interruptible,系统可能死锁。我建议:拿不准的时候,用might_sleep()检查一下当前上下文。
  • 格式协商不严谨:用户空间传过来的格式,驱动必须做合法性检查。宽度、高度、像素格式,一个都不能少。我曾经偷懒没检查宽度,结果用户传了个1920x1088(1088?),Sensor输出花屏,ISP那边还以为是硬件坏了。

调试小技巧:在ioctl回调入口加dev_dbg()打印命令码和参数。用v4l2-ctl -d /dev/video0 --all可以快速验证驱动是否正常工作。我调试新Sensor时,第一件事就是跑一遍v4l2-ctl --list-formats,看枚举格式对不对。

好了,V4L2驱动框架的核心内容就这些。说白了,就是video_device + subdev + ioctl回调这三板斧。框架本身不复杂,复杂的是你往回调里塞的业务逻辑——配置Sensor寄存器、管理缓冲区、处理中断。把这些基础打牢了,后面调ISP才能得心应手。