第3章:Sensor驱动编写——基于RK官方SDK的实战

好,咱们进入正题。这一章要干的事,说白了就是让RK平台认识你的Sensor。我见过不少新手,拿到SDK就急着往上堆代码,结果点不亮、调不通,最后回头查才发现是驱动层就没写对。咱们今天就把这块掰开揉碎了讲清楚。

3.1 驱动框架概览

RK官方的Sensor驱动,其实是一个标准的V4L2子设备驱动。它挂在I2C总线上,通过I2C寄存器读写来控制Sensor。整个驱动需要实现三个核心能力:

  • 分辨率/帧率配置 —— 告诉Sensor输出多大画面、多快速度
  • 曝光控制 —— 调节积分时间,说白了就是让Sensor“看”多久
  • 增益控制 —— 放大信号,但也会放大噪声,这是个平衡活

我习惯把驱动拆成三层来看:

层级 职责 对应文件/函数
平台适配层 注册V4L2子设备、处理DTS匹配 probe()、remove()
控制逻辑层 实现s_ctrl回调、曝光/增益计算 sensor_s_ctrl()
硬件操作层 I2C读写、寄存器配置序列 sensor_write_array()

关键点:驱动不是从头造轮子。RK SDK里已经有完整的模板,你只需要填充Sensor特有的寄存器配置和控制逻辑。

3.2 驱动入口与DTS匹配

先看驱动入口。每个Sensor驱动都有一个probe函数,它负责初始化I2C设备、读取电源时序、注册子设备。我一般这么写:

static const struct i2c_device_id sensor_id[] = {
    {"imx415", 0},
    {}
};

static const struct of_device_id sensor_of_match[] = {
    { .compatible = "sony,imx415" },
    {}
};

static struct i2c_driver sensor_i2c_driver = {
    .driver = {
        .name = "imx415",
        .of_match_table = sensor_of_match,
    },
    .probe = sensor_probe,
    .remove = sensor_remove,
    .id_table = sensor_id,
};

module_i2c_driver(sensor_i2c_driver);

这里要注意compatible字符串,它必须和DTS里的节点完全一致。我曾经遇到一个坑,DTS里写的是"imx415",驱动里写的是"imx415_v2",结果内核根本不会调用你的probe。嗯,这种低级错误排查起来最浪费时间。

3.3 分辨率与帧率配置

Sensor的寄存器配置,通常以数组形式存放在驱动里。每个分辨率对应一组寄存器序列。我习惯用结构体来组织:

struct sensor_regval {
    u16 reg;
    u8 val;
};

static const struct sensor_regval sensor_1920x1080_30fps[] = {
    {0x3000, 0x00},  // 软复位
    {0x3001, 0x01},  // 开始配置
    {0x3010, 0x43},  // 水平输出尺寸高字节
    {0x3011, 0x20},  // 水平输出尺寸低字节 (0x4320 = 1920)
    {0x3012, 0x02},  // 垂直输出尺寸高字节
    {0x3013, 0x58},  // 垂直输出尺寸低字节 (0x0258 = 600)
    // ... 省略中间配置
    {0x3014, 0x2E},  // 帧率控制
    {0x3015, 0x1C},  // PLL配置
    {0x3030, 0x01},  // 开始输出
};

驱动里通过一个枚举来管理这些模式:

enum sensor_mode {
    SENSOR_MODE_1920X1080_30FPS,
    SENSOR_MODE_1920X1080_60FPS,
    SENSOR_MODE_3840X2160_30FPS,
    SENSOR_MODE_MAX,
};

在s_fmt回调里,根据用户请求的分辨率,选择对应的寄存器数组,然后通过I2C批量写入。我建议把写入函数封装一下:

static int sensor_write_array(struct i2c_client *client,
                              const struct sensor_regval *regs,
                              u32 size)
{
    int i, ret;
    for (i = 0; i < size; i++) {
        ret = sensor_write_reg(client, regs[i].reg, regs[i].val);
        if (ret < 0)
            return ret;
    }
    return 0;
}

小技巧:写入寄存器序列时,最好在每两个寄存器之间加一个1ms的延时。有些Sensor对写入速度有要求,写太快会丢寄存器。我踩过这个坑,画面出现条纹,排查了半天才发现是写入太快导致的。

3.4 曝光与增益控制接口

曝光和增益控制,是通过V4L2的s_ctrl回调实现的。RK平台会调用这个回调来设置曝光时间和增益值。核心逻辑如下:

static int sensor_s_ctrl(struct v4l2_ctrl *ctrl)
{
    struct sensor *sensor = container_of(ctrl->handler,
                                         struct sensor, ctrl_handler);

    switch (ctrl->id) {
    case V4L2_CID_EXPOSURE:
        // 曝光值单位是行数,需要转换成Sensor的寄存器值
        sensor->exposure = ctrl->val;
        sensor_set_exposure(sensor, sensor->exposure);
        break;
    case V4L2_CID_GAIN:
        // 增益值单位是dB,需要查表或计算
        sensor->gain = ctrl->val;
        sensor_set_gain(sensor, sensor->gain);
        break;
    case V4L2_CID_VBLANK:
        // 垂直消隐区,影响帧率
        sensor->vblank = ctrl->val;
        sensor_set_vblank(sensor, sensor->vblank);
        break;
    default:
        return -EINVAL;
    }
    return 0;
}

曝光控制的实现,说白了就是写两个寄存器:粗调(Coarse Integration Time)和细调(Fine Integration Time)。大部分Sensor只用粗调就够了:

static int sensor_set_exposure(struct sensor *sensor, u32 exposure)
{
    // 曝光值不能超过一帧的总行数
    u32 max_exposure = sensor->vts - 4;  // 留4行余量
    if (exposure > max_exposure)
        exposure = max_exposure;

    sensor_write_reg(sensor, 0x3020, (exposure >> 8) & 0xFF);
    sensor_write_reg(sensor, 0x3021, exposure & 0xFF);
    return 0;
}

增益控制稍微复杂一点。Sensor的增益通常分为模拟增益和数字增益。模拟增益噪声小,但范围有限;数字增益范围大,但会放大噪声。我一般优先使用模拟增益:

static int sensor_set_gain(struct sensor *sensor, u32 gain)
{
    u32 analog_gain, digital_gain;

    if (gain <= 64) {
        // 0dB ~ 12dB 用模拟增益
        analog_gain = gain;
        digital_gain = 0;
    } else {
        // 超过12dB,模拟增益固定,数字增益补足
        analog_gain = 64;
        digital_gain = gain - 64;
    }

    sensor_write_reg(sensor, 0x3030, analog_gain);
    sensor_write_reg(sensor, 0x3031, digital_gain);
    return 0;
}

注意:曝光和增益的写入顺序很重要。必须先写增益,再写曝光。因为改变增益会影响Sensor的灵敏度,如果先写曝光再写增益,会导致这一帧的曝光量不准。我调试时遇到过画面闪烁,就是顺序搞反了。

3.5 驱动注册与初始化流程

整个驱动的初始化流程,我画了一张图,方便你理解:

Sensor驱动初始化流程 probe() 被调用 读取DTS电源/时钟配置 上电 & 复位Sensor 读取Sensor ID 校验 注册V4L2子设备 & ctrl 初始化完成,等待stream on 常见失败点 • I2C通信失败 • Sensor ID不匹配 • 电源时序不对

你看,整个流程其实不复杂。但每一步都可能出问题。我调试时最常遇到的是Sensor ID校验失败,往往是I2C地址写错了,或者Sensor还没完全上电就去读寄存器。

3.6 避坑指南

最后,我总结几个实战中容易踩的坑:

  • I2C地址要确认 —— Sensor的I2C地址通常是7位还是8位?RK平台用的是7位地址,但有些Sensor手册给的是8位。记得右移一位。
  • 帧率计算要准确 —— 帧率 = 主时钟 / (HTS * VTS)。HTS和VTS要从寄存器读出来,不要想当然。
  • 曝光单位要统一 —— 有些Sensor曝光单位是行数,有些是微秒。RK平台默认用行数,如果Sensor用微秒,你得在驱动里做转换。
  • 增益表要查对 —— 很多Sensor的增益不是线性的,需要查表。我建议把增益表放在驱动里,用二分查找来匹配。

我的习惯:每次写完驱动,先用i2cdetect工具确认I2C通信正常,再用v4l2-ctl -d /dev/v4l-subdevX --list-ctrls确认控制接口注册成功。这两个命令能帮你快速定位问题。