3. Sensor驱动框架:Linux V4L2驱动框架解析

做Sensor驱动,绕不开V4L2。说白了,它就是Linux内核里给视频设备定的一套标准接口。我刚开始接触时也觉得这东西很绕,又是video_device又是v4l2_subdev,关系理不清。后来调了两年驱动,慢慢就摸出门道了。

V4L2框架分两层:上层是video_device,面向用户空间的/dev/videoX节点;下层是v4l2_subdev,面向具体的硬件设备,比如Sensor、ISP、MIPI桥接芯片。Sensor驱动就属于subdev这一层。

核心思路:Sensor驱动本质上是向V4L2框架注册一个subdev,然后通过控制接口(I2C/SPI)操作寄存器,配置输出格式、帧率、增益等参数。

3.1 V4L2框架的分层结构

先看一张整体框架图,我画了张SVG帮你理清关系:

V4L2驱动框架分层结构 用户空间应用层 open /dev/videoX → ioctl → mmap → queue V4L2核心层 (videodev.ko) video_register_device / video_unregister_device video_device /dev/videoX 节点 v4l2_device 顶层设备容器 v4l2_subdev Sensor驱动 v4l2_subdev ISP驱动 ... 其他subdev 硬件层:I2C/SPI总线 → Sensor芯片 / ISP芯片 用户空间 (应用层) 内核空间 (V4L2框架) 硬件层 (物理设备)

这张图你看懂了吗?用户空间通过/dev/videoX发ioctl,V4L2核心层把请求路由到对应的video_device,video_device再调用注册好的subdev回调函数。Sensor驱动就在最下面的subdev位置。

3.2 Sensor子设备注册流程

Sensor驱动的入口是probe函数。我习惯把注册流程拆成5步,每一步都有对应的内核API:

步骤 操作 关键API/结构体 说明
1 分配subdev v4l2_subdev_alloc() 分配并初始化v4l2_subdev结构体
2 设置subdev ops v4l2_subdev_core_ops
v4l2_subdev_video_ops
v4l2_subdev_pad_ops
注册回调函数,核心是s_stream、s_fmt、g_frame_interval
3 初始化控制 v4l2_ctrl_handler_init()
v4l2_ctrl_new_std()
注册曝光、增益、白平衡等控制项
4 注册subdev v4l2_async_register_subdev() 异步注册,等待与bridge绑定
5 媒体实体注册 media_entity_pads_init() 定义sink/source pad,用于pipeline管理

嗯,这里要注意第4步。早期内核用v4l2_i2c_subdev_init()同步注册,现在主流平台都用异步方式。为什么?因为Sensor上电时序和MIPI时钟可能依赖其他设备先就绪,异步注册能解耦这种依赖关系。

我的经验:我在调试IMX335时遇到过一个问题——probe成功了,但s_stream就是报错。查了两天才发现是异步注册时,bridge还没准备好就调了s_stream。后来在subdev的registered回调里加了个标志位,等bridge绑定完成再允许stream on,问题解决。

3.3 I2C/SPI控制接口实现

Sensor芯片的控制接口,99%是I2C,少数高速或特殊场景用SPI。我主要讲I2C,SPI原理类似。

I2C读写函数是Sensor驱动的基石。每个寄存器操作都要通过它。看一个典型实现:

// I2C写寄存器函数
static int sensor_write_reg(struct i2c_client *client, u16 reg, u8 val)
{
    struct i2c_msg msg;
    u8 buf[3];
    int ret;

    buf[0] = (reg >> 8) & 0xFF;  // 寄存器高字节
    buf[1] = reg & 0xFF;         // 寄存器低字节
    buf[2] = val;                // 写入值

    msg.addr = client->addr;
    msg.flags = 0;               // 写操作
    msg.len = 3;
    msg.buf = buf;

    ret = i2c_transfer(client->adapter, &msg, 1);
    if (ret < 0) {
        dev_err(&client->dev, "write reg 0x%04x failed: %d\n", reg, ret);
        return ret;
    }
    return 0;
}

// I2C读寄存器函数
static int sensor_read_reg(struct i2c_client *client, u16 reg, u8 *val)
{
    struct i2c_msg msg[2];
    u8 reg_buf[2];
    int ret;

    reg_buf[0] = (reg >> 8) & 0xFF;
    reg_buf[1] = reg & 0xFF;

    msg[0].addr = client->addr;
    msg[0].flags = 0;            // 写寄存器地址
    msg[0].len = 2;
    msg[0].buf = reg_buf;

    msg[1].addr = client->addr;
    msg[1].flags = I2C_M_RD;     // 读数据
    msg[1].len = 1;
    msg[1].buf = val;

    ret = i2c_transfer(client->adapter, msg, 2);
    if (ret < 2) {
        dev_err(&client->dev, "read reg 0x%04x failed: %d\n", reg, ret);
        return ret < 0 ? ret : -EIO;
    }
    return 0;
}

这段代码看着简单,但坑不少。我列几个常见的:

  • 寄存器地址宽度:有些Sensor是8位地址,有些是16位。我遇到过一款芯片手册写16位,实际是8位,读出来的值全错。
  • I2C速率:默认100kHz,但很多Sensor支持400kHz甚至1MHz。速率不匹配会导致通信不稳定。
  • 批量读写:初始化时往往要写几十个寄存器,用单次读写效率太低。我习惯加一个sensor_write_array()函数,把reg_addr + reg_val做成表,一次i2c_transfer搞定。

我曾经踩过的坑:有一次Sensor在低温下初始化失败,查了三天发现是I2C时序在低温下变差,需要调整I2C总线的上升时间。后来在设备树里加了i2c-scl-rising-time-ns参数,问题解决。所以,别小看I2C时序,它跟温度、PCB走线都有关系。

3.4 设备树与驱动绑定

现代Linux驱动都走设备树(Device Tree)。Sensor的设备树节点长这样:

// imx335设备树节点示例
&i2c2 {
    status = "okay";
    clock-frequency = <400000>;  // I2C速率400kHz

    imx335: camera-sensor@1a {
        compatible = "sony,imx335";
        reg = <0x1a>;             // I2C地址
        clocks = <&clk IMX335_XVCLK>;
        clock-names = "xvclk";
        reset-gpios = <&gpio3 15 GPIO_ACTIVE_LOW>;
        powerdown-gpios = <&gpio3 16 GPIO_ACTIVE_HIGH>;

        // MIPI CSI-2 配置
        port {
            imx335_ep: endpoint {
                remote-endpoint = <&csi2_ep>;
                data-lanes = <1 2 3 4>;
                link-frequencies = /bits/ 64 <594000000>;
            };
        };
    };
};

驱动里通过of_match_table匹配设备树节点:

static const struct of_device_id sensor_of_match[] = {
    { .compatible = "sony,imx335" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, sensor_of_match);

static struct i2c_driver sensor_i2c_driver = {
    .driver = {
        .name = "imx335",
        .of_match_table = sensor_of_match,
    },
    .probe = sensor_probe,
    .remove = sensor_remove,
    .id_table = sensor_i2c_id,
};
module_i2c_driver(sensor_i2c_driver);

probe函数被调用时,i2c_client已经帮你配好了设备地址、时钟、GPIO等信息。你只需要从设备树里解析出这些资源,然后初始化Sensor硬件。

3.5 控制接口的封装策略

我见过很多新手把I2C读写函数直接暴露给上层调用。这不好。我建议做一层封装:

层级 函数 职责
底层 sensor_read_reg / sensor_write_reg 直接操作I2C/SPI总线,单寄存器读写
中间层 sensor_set_gain / sensor_set_exposure 封装增益、曝光等具体功能的寄存器操作序列
上层 v4l2_ctrl_ops.s_ctrl V4L2控制框架回调,调用中间层函数

这样做的好处是:哪天换Sensor了,只需要改中间层和底层,上层V4L2接口完全不用动。我在项目中用这个分层结构,换过三次Sensor型号,每次只改不到200行代码。

总结一下:Sensor驱动框架的核心就是V4L2 subdev + I2C控制接口。subdev负责对接V4L2框架,I2C负责操作硬件。把这两块搞明白,Sensor驱动就拿下了一半。另一半是MIPI CSI-2的配置和同步,我们后面章节会详细讲。