3、Sensor驱动开发:Linux V4L2 Sensor驱动框架、s_power/s_g_mbus_config实现、常用Sensor寄存器配置

好,咱们进入正题。Sensor驱动开发,说白了就是让Linux内核认识你的摄像头传感器。我刚开始做这行的时候,总觉得V4L2框架很神秘,后来发现它其实就是一套标准接口——你按规矩填好回调函数,内核就知道怎么跟你家Sensor打交道了。

3.1 V4L2 Sensor驱动框架概览

Linux内核里,Sensor驱动通常挂在drivers/media/i2c/目录下。你打开看看,全是ov5640.cimx219.c这种文件。它们都遵循同一个套路:

  • 注册一个i2c_driver结构体
  • 实现v4l2_subdev_ops里的回调函数
  • 通过v4l2_async_register_subdev挂到系统中

我个人习惯先把框架搭起来,再慢慢填函数。就像盖房子,先立柱子再砌墙。

static const struct v4l2_subdev_core_ops my_sensor_core_ops = {
    .s_power = my_sensor_s_power,
    .init = my_sensor_init,
    // 其他回调...
};

static const struct v4l2_subdev_video_ops my_sensor_video_ops = {
    .s_stream = my_sensor_s_stream,
    .g_mbus_config = my_sensor_g_mbus_config,
    // 其他回调...
};

static const struct v4l2_subdev_ops my_sensor_ops = {
    .core = &my_sensor_core_ops,
    .video = &my_sensor_video_ops,
    // 其他...
};

static int my_sensor_probe(struct i2c_client *client) {
    // 分配私有结构体
    // 初始化i2c通信
    // 读取Sensor ID确认硬件
    // 注册subdev
    v4l2_i2c_subdev_init(&sensor->sd, client, &my_sensor_ops);
    v4l2_async_register_subdev(&sensor->sd);
    return 0;
}

嗯,这里要注意:probe函数里千万别做耗时的初始化操作,比如配置寄存器序列。为什么?因为系统启动时,所有设备都是并行探测的,你卡太久会影响整体启动速度。

3.2 s_power 回调实现——电源管理的艺术

s_power是V4L2框架里最常用的回调之一。它负责控制Sensor的电源状态。说白了,就是告诉Sensor:「醒醒,要干活了」或者「睡吧,没你事了」。

我在项目中遇到过一个问题:某个Sensor在s_power(1)之后立刻读寄存器,结果返回全0。查了半天,发现是电源稳定需要时间。后来加了个msleep(10)才搞定。

核心要点:s_power(1) 要保证Sensor完全就绪,s_power(0) 要彻底断电省电。

static int my_sensor_s_power(struct v4l2_subdev *sd, int on) {
    struct my_sensor *sensor = to_my_sensor(sd);
    int ret;

    if (on) {
        // 1. 使能电源引脚
        ret = regulator_enable(sensor->avdd_reg);
        if (ret) return ret;
        ret = regulator_enable(sensor->dvdd_reg);
        if (ret) return ret;
        ret = regulator_enable(sensor->iovdd_reg);
        if (ret) return ret;

        // 2. 等待电源稳定
        usleep_range(5000, 10000);  // 5-10ms

        // 3. 拉高复位引脚
        gpiod_set_value_cansleep(sensor->reset_gpio, 1);
        usleep_range(1000, 2000);

        // 4. 初始化寄存器序列
        ret = my_sensor_init_regs(sensor);
        if (ret) return ret;

        sensor->power_on = true;
    } else {
        // 1. 拉低复位引脚
        gpiod_set_value_cansleep(sensor->reset_gpio, 0);

        // 2. 关闭电源
        regulator_disable(sensor->iovdd_reg);
        regulator_disable(sensor->dvdd_reg);
        regulator_disable(sensor->avdd_reg);

        sensor->power_on = false;
    }

    return 0;
}

个人经验:电源时序很重要。有些Sensor要求先上AVDD再上DVDD,顺序反了可能烧芯片。我建议你在数据手册里找到「Power Up Timing」那张图,照着时序图写代码,别自己瞎猜。

3.3 s_g_mbus_config 回调——告诉ISP你的数据格式

s_g_mbus_config这个回调,名字有点绕口。其实它的作用很简单:告诉Camera ISP,你的Sensor输出数据是什么格式——几根数据线?同步信号怎么传?

你想想看,ISP那边要正确接收数据,必须知道这些信息。否则数据对不上,画面全是花的。

static int my_sensor_g_mbus_config(struct v4l2_subdev *sd,
                                    struct v4l2_mbus_config *cfg) {
    // 告诉ISP:我们用的是CSI-2接口,2条lane
    cfg->type = V4L2_MBUS_CSI2_DPHY;

    // 配置标志位
    cfg->flags = V4L2_MBUS_CSI2_2_LANE |
                 V4L2_MBUS_CSI2_CHANNEL_0 |
                 V4L2_MBUS_CSI2_CONTINUOUS_CLOCK;

    return 0;
}

常见的配置选项有:

标志位 含义 我遇到的坑
V4L2_MBUS_CSI2_1_LANE / 2_LANE / 4_LANE 数据通道数量 曾经有个项目,硬件只接了2条lane,我配成了4条,结果图像只有一半正常
V4L2_MBUS_CSI2_CONTINUOUS_CLOCK 时钟是否持续输出 非连续时钟模式省电,但有些ISP不支持,得看平台
V4L2_MBUS_CSI2_CHANNEL_0 使用的虚拟通道 多Sensor共用CSI总线时,这个要区分开

注意:s_g_mbus_config返回的信息必须和硬件实际连接一致。我曾经见过一个案例,硬件工程师把D0和D1线序接反了,驱动里怎么配都不对。最后只能改驱动里的lane映射表。

3.4 常用Sensor寄存器配置——实战经验

每个Sensor都有自己的寄存器地图。但不管什么型号,有几类寄存器是必配的:

  1. 芯片ID寄存器:probe时读取,确认硬件型号
  2. 模式选择寄存器:设置分辨率、帧率
  3. 曝光/增益寄存器:控制图像亮度
  4. 输出格式寄存器:RAW8、RAW10、YUV等
  5. PLL配置寄存器:生成内部时钟

我一般会把寄存器配置写成结构体数组,方便维护:

struct regval_list {
    uint16_t addr;
    uint8_t val;
};

static const struct regval_list my_sensor_init_regs[] = {
    // 软件复位
    {0x0103, 0x01},
    {0x0103, 0x00},

    // 设置输出尺寸 1920x1080
    {0x3808, 0x07},  // 宽度高字节
    {0x3809, 0x80},  // 宽度低字节 (0x0780 = 1920)
    {0x380A, 0x04},  // 高度高字节
    {0x380B, 0x38},  // 高度低字节 (0x0438 = 1080)

    // 设置输出格式 RAW10
    {0x4300, 0x30},  // 格式选择

    // PLL配置,目标帧率30fps
    {0x3034, 0x1A},  // PLL分频
    {0x3035, 0x21},  // PLL倍频
    {0x3036, 0x46},  // PLL后分频

    // 结束标记
    {0xFFFF, 0xFF},
};

写寄存器配置时,我有个习惯:每配完一组关键寄存器,就读回来验证一下。有些Sensor的I2C总线不稳定,写进去的值可能不对。

避坑指南:我曾经在配置一个OV系列Sensor时,发现帧率怎么都调不对。后来用示波器抓MCLK时钟,发现晶振频率是24MHz,但驱动里写死了27MHz的PLL参数。嗯,这种低级错误,查了一整天。

3.5 调试技巧——让Sensor「开口说话」

Sensor驱动写好了,怎么验证它工作正常?我一般按这个顺序排查:

  • 第一步:确认I2C通信——用i2cdetect看设备地址对不对
  • 第二步:读芯片ID——probe时打印出来,确认硬件型号
  • 第三步:检查电源和时钟——用万用表测电压,示波器看MCLK
  • 第四步:抓MIPI信号——用逻辑分析仪看CSI-2数据包

说实话,大部分问题都出在前三步。电源没上对、时钟频率不对、I2C地址写错了——这些基础问题占了80%的调试时间。

好了,Sensor驱动开发的核心框架就这些。下一章我们会讲怎么把Sensor数据流打通,让图像真正从Sensor走到ISP里去。