4、Sensor驱动核心代码实现:上电时序、初始化序列、分辨率切换、帧率控制

驱动代码写得好不好,直接决定Sensor能不能正常工作。我见过不少方案,硬件设计没问题,就是驱动里时序写错了,结果图像出不来。说白了,驱动就是Sensor的“翻译官”——你要用代码告诉它:什么时候供电、怎么初始化、怎么切分辨率、跑多快。

这一节,我们就把这四个核心模块拆开揉碎了讲。

4.1 上电时序:Sensor的“开机仪式”

每个Sensor都有严格的上电要求。你先给哪路电、等多久、再拉哪个GPIO,差一点都不行。我早期踩过一个坑:某款OV的Sensor,DVDD和AVDD上电间隔差了0.5ms,结果图像一直有条纹。查了两天才发现是时序问题。

⚠️ 注意:上电时序不满足,轻则图像异常,重则烧坏Sensor。千万别图省事把各路电源同时拉起来。

典型的MIPI Sensor上电流程是这样的:

  1. 拉高DVDD(数字核心电压,通常1.2V或1.8V)
  2. 等待T1(通常1~5ms)
  3. 拉高AVDD(模拟电压,通常2.8V)
  4. 等待T2(通常1~5ms)
  5. 拉高IOVDD(IO电压,通常1.8V或2.8V)
  6. 等待T3(通常1~10ms)
  7. 拉高MCLK(主时钟)
  8. 等待T4(通常1ms)
  9. 拉高RST(复位引脚)
  10. 等待T5(通常10~20ms)
  11. 通过I2C写入初始化序列

代码实现上,我习惯用gpiod_set_value_cansleep配合msleep来控制。举个例子:

static int sensor_power_on(struct device *dev) {
    struct sensor_dev *sensor = dev_get_drvdata(dev);

    // 1. 拉高DVDD
    regulator_enable(sensor->dvdd);
    msleep(2);

    // 2. 拉高AVDD
    regulator_enable(sensor->avdd);
    msleep(2);

    // 3. 拉高IOVDD
    regulator_enable(sensor->iovdd);
    msleep(5);

    // 4. 提供MCLK
    clk_prepare_enable(sensor->mclk);
    msleep(1);

    // 5. 释放复位
    gpiod_set_value_cansleep(sensor->reset_gpio, 1);
    msleep(20);

    return 0;
}
💡 个人习惯:我会在驱动里把每个时序步骤的等待时间都做成宏定义,方便调试时快速调整。比如 #define T_POWER_UP_DVDD_AVDD 2。

4.2 初始化序列:Sensor的“出厂设置”

上电完成后,Sensor还处于“裸机”状态。你需要通过I2C写入一串寄存器配置,告诉它输出什么格式、多少分辨率、增益范围等等。这就是初始化序列。

初始化序列通常由Sensor厂商提供,但你不能直接照搬。为什么?因为厂商给的配置是针对他们的评估板的。你的板子走线长度、电源纹波、时钟质量都不一样,直接套用很可能出问题。

我一般把初始化序列组织成结构体数组:

struct sensor_reg {
    u16 addr;
    u8 val;
};

static const struct sensor_reg sensor_init_regs[] = {
    // 软件复位
    {0x0103, 0x01},
    {0x0103, 0x00},
    msleep(10),

    // 设置输出格式:RAW10
    {0x0100, 0x00},  // 先进入待机模式
    {0x0340, 0x04},  // 输出高度高字节
    {0x0341, 0x38},  // 输出高度低字节 (1080)
    {0x0342, 0x07},  // 输出宽度高字节
    {0x0343, 0x80},  // 输出宽度低字节 (1920)

    // 模拟增益设置
    {0x0204, 0x00},  // 增益高字节
    {0x0205, 0x10},  // 增益低字节 (1x)

    // 其他配置...
    {0x0100, 0x01},  // 退出待机模式
};

写入时,我习惯用一个通用函数来遍历这个数组:

static int sensor_write_regs(struct i2c_client *client,
                             const struct sensor_reg *regs, int len) {
    int i, ret;
    for (i = 0; i < len; i++) {
        ret = sensor_write_reg(client, regs[i].addr, regs[i].val);
        if (ret < 0) {
            dev_err(&client->dev, "写寄存器失败: addr=0x%04x\n", regs[i].addr);
            return ret;
        }
    }
    return 0;
}
🔑 关键点:初始化序列中如果有msleep等待,不要写在数组里。我通常把需要等待的步骤单独抽出来,在驱动代码里显式调用msleep。这样更清晰,也方便调试。

4.3 分辨率切换:别让画面“卡住”

分辨率切换,说白了就是让Sensor从一种输出尺寸切换到另一种。比如从1080P切到720P。这个过程如果处理不好,画面会卡住或者花掉。

为什么会这样?因为切换分辨率时,Sensor内部的PLL、行缓存、输出接口都需要重新配置。你必须先让Sensor进入待机模式,改完配置,再唤醒它。

标准流程如下:

步骤 操作 说明
1 停止视频流 写寄存器0x0100 = 0x00,进入待机
2 等待帧结束 等待当前帧传输完成,约1帧时间
3 写入新分辨率配置 修改输出宽度、高度、PLL分频等
4 重新启动视频流 写寄存器0x0100 = 0x01
5 等待稳定 等待Sensor输出新分辨率的图像

代码实现上,我习惯这样写:

static int sensor_set_resolution(struct i2c_client *client, int width, int height) {
    // 1. 停止流
    sensor_write_reg(client, 0x0100, 0x00);
    msleep(20);  // 等待帧结束

    // 2. 根据分辨率选择配置
    if (width == 1920 && height == 1080) {
        sensor_write_regs(client, sensor_1080p_regs, ARRAY_SIZE(sensor_1080p_regs));
    } else if (width == 1280 && height == 720) {
        sensor_write_regs(client, sensor_720p_regs, ARRAY_SIZE(sensor_720p_regs));
    } else {
        dev_err(&client->dev, "不支持的分辨率: %dx%d\n", width, height);
        return -EINVAL;
    }

    // 3. 重新启动流
    sensor_write_reg(client, 0x0100, 0x01);
    msleep(50);  // 等待稳定

    return 0;
}
⚠️ 我曾经踩过的坑:切换分辨率时,忘记等待帧结束就直接写寄存器。结果Sensor内部状态机乱了,输出一直花屏。后来我加了msleep(20),问题解决。记住:切换分辨率前,一定要确保当前帧已经传输完成。

4.4 帧率控制:让Sensor跑在“正确节奏”上

帧率控制,本质上是控制Sensor输出一帧图像的时间。这个时间由两部分决定:

  • 行时间(HTS):一行像素的传输时间
  • 帧行数(VTS):一帧图像包含多少行

帧率计算公式很简单:

帧率 = PCLK / (HTS × VTS)

其中PCLK是像素时钟,由PLL配置决定。HTS和VTS都可以通过寄存器调整。

举个例子,假设PCLK是72MHz,HTS是2200,VTS是1125:

帧率 = 72,000,000 / (2200 × 1125) ≈ 29.1 fps

如果你想跑到30fps,可以减小VTS:

VTS = 72,000,000 / (2200 × 30) ≈ 1091

代码实现上,我一般把帧率控制封装成一个函数:

static int sensor_set_framerate(struct i2c_client *client, int fps) {
    u16 vts;
    u16 hts = 2200;  // 固定行时间
    u32 pclk = 72000000;  // 像素时钟

    // 计算VTS
    vts = pclk / (hts * fps);

    // 写入VTS寄存器
    sensor_write_reg(client, 0x0340, (vts >> 8) & 0xFF);  // 高字节
    sensor_write_reg(client, 0x0341, vts & 0xFF);          // 低字节

    dev_info(&client->dev, "设置帧率: %d fps, VTS=%d\n", fps, vts);
    return 0;
}
💡 我建议:帧率不要设得太极限。比如Sensor标称最高30fps,你设29fps会更稳定。因为VTS太小,Sensor内部的行缓存可能来不及处理,导致丢帧。

4.5 知识结构总览

下面这张图,把四个核心模块的关系串起来了:

Sensor驱动核心代码模块 上电时序 初始化序列 分辨率切换 帧率控制 上电时序是基础 电源顺序不对 Sensor无法正常工作 分辨率切换需谨慎 必须先停止流 再改配置 帧率由HTS和VTS决定 调整VTS是最常用的方法 不要设得太极限

这四个模块是递进关系:上电时序是前提,初始化序列是基础,分辨率切换是功能扩展,帧率控制是性能调优。你想想看,任何一个环节出问题,图像质量都无从谈起。

📌 总结:写Sensor驱动,不要只盯着代码。你要理解Sensor的工作流程,知道每个寄存器的作用。我见过有人把初始化序列写对了,但上电时序搞反了,结果折腾了一周。嗯,细节决定成败。