3. Sensor驱动开发

Sensor驱动,说白了就是让摄像头芯片“活过来”的那层代码。很多刚入行的朋友觉得这模块简单——不就是配几个寄存器嘛?嗯,我当年也这么想,直到第一次调不通图像,拿着示波器抓了一整天波形才发现是上电时序差了2毫秒。

这一章,我把这些年踩过的坑和总结的方法论都摊开来讲。从硬件时序到软件流控,咱们一步步来。

3.1 Sensor上电时序——芯片的“生命开关”

每个Sensor芯片都有自己的“起床流程”。你想想看,它内部有模拟电路、数字电路、PLL、MIPI PHY……这些模块不能同时上电,否则轻则锁死,重则烧毁。

3.1.1 典型上电时序要求

我见过最标准的时序图长这样:

信号 动作 时序要求 典型值
AVDD (模拟电源) 先上电 稳定后等待T1 1ms
DVDD (数字电源) 第二步 AVDD稳定后T2 0.5ms
IOVDD (IO电源) 第三步 DVDD稳定后T3 0.5ms
MCLK (主时钟) 提供时钟 电源稳定后T4 1ms
RESET (复位) 释放复位 时钟稳定后T5 2ms
I2C/SPI 开始通信 复位释放后T6 5ms
我曾经踩过的坑:某款OV Sensor的AVDD和DVDD之间要求至少1ms间隔,我图省事用同一个LDO同时供电。结果芯片初始化成功率只有70%,低温下直接降到30%。后来老老实实加了延时,问题解决。

3.1.2 代码实现要点

上电时序在驱动里通常用GPIO控制LDO使能脚来实现。我个人习惯把时序参数定义成宏,方便调试时微调:

/* 上电时序参数(单位:us) */
#define SENSOR_POWERON_T1_AVDD2DVDD    1000
#define SENSOR_POWERON_T2_DVDD2IOVDD    500
#define SENSOR_POWERON_T3_IOVDD2MCLK    500
#define SENSOR_POWERON_T4_MCLK2RESET   1000
#define SENSOR_POWERON_T5_RESET2I2C    2000

static int sensor_power_on(struct device *dev)
{
    /* 1. AVDD上电 */
    gpiod_set_value(avdd_gpio, 1);
    usleep_range(SENSOR_POWERON_T1_AVDD2DVDD,
                 SENSOR_POWERON_T1_AVDD2DVDD + 100);

    /* 2. DVDD上电 */
    gpiod_set_value(dvdd_gpio, 1);
    usleep_range(SENSOR_POWERON_T2_DVDD2IOVDD,
                 SENSOR_POWERON_T2_DVDD2IOVDD + 100);

    /* 3. IOVDD上电 */
    gpiod_set_value(iovdd_gpio, 1);
    usleep_range(SENSOR_POWERON_T3_IOVDD2MCLK,
                 SENSOR_POWERON_T3_IOVDD2MCLK + 100);

    /* 4. 提供MCLK */
    clk_prepare_enable(sensor_clk);
    usleep_range(SENSOR_POWERON_T4_MCLK2RESET,
                 SENSOR_POWERON_T4_MCLK2RESET + 100);

    /* 5. 释放复位 */
    gpiod_set_value(reset_gpio, 1);
    msleep(SENSOR_POWERON_T5_RESET2I2C / 1000);

    return 0;
}
小技巧:usleep_range()比usleep()更友好,给调度器一点弹性空间。我一般给+100us的余量,既保证时序又不会太死板。

3.2 I2C/SPI寄存器读写——与Sensor对话的“语言”

Sensor内部有几百个寄存器,控制着曝光、增益、帧率、输出格式等等。我们要通过I2C或SPI总线去读写它们。

3.2.1 I2C vs SPI 怎么选?

特性 I2C SPI
引脚数 2线(SDA+SCL) 4线(MOSI+MISO+SCLK+CS)
速度 标准400kHz,快模式1MHz 通常10MHz~50MHz
寻址方式 7位/10位设备地址 片选信号CS选择设备
适用场景 配置寄存器(低速) 配置寄存器+少量数据读取
驱动复杂度 较低(内核有通用框架) 中等(需处理CS时序)

我个人习惯:配置类操作用I2C就够了,除非Sensor要求SPI。你想想看,配置寄存器又不是传图像,400kHz完全够用。

3.2.2 I2C读写实战

Linux内核里I2C读写有标准接口。我封装了一个通用函数,支持16位和8位寄存器地址:

/* 写Sensor寄存器(16位地址 + 8位数据) */
static int sensor_write_reg(struct i2c_client *client,
                            u16 reg_addr, u8 val)
{
    struct i2c_msg msg;
    u8 buf[3];
    int ret;

    buf[0] = (reg_addr >> 8) & 0xFF;  /* 高8位地址 */
    buf[1] = reg_addr & 0xFF;         /* 低8位地址 */
    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 != 1) {
        dev_err(&client->dev,
                "I2C write failed: reg=0x%04x, val=0x%02x\n",
                reg_addr, val);
        return -EIO;
    }

    return 0;
}

/* 读Sensor寄存器 */
static int sensor_read_reg(struct i2c_client *client,
                           u16 reg_addr, u8 *val)
{
    struct i2c_msg msg[2];
    u8 addr_buf[2];
    int ret;

    addr_buf[0] = (reg_addr >> 8) & 0xFF;
    addr_buf[1] = reg_addr & 0xFF;

    /* 先写地址 */
    msg[0].addr  = client->addr;
    msg[0].flags = 0;
    msg[0].len   = 2;
    msg[0].buf   = addr_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,
                "I2C read failed: reg=0x%04x\n", reg_addr);
        return -EIO;
    }

    return 0;
}
避坑指南:我曾经遇到一个坑——某款Sensor的I2C地址是7位,但Linux驱动里要用8位左移后的值。写错了地址,读回来的全是0xFF。排查了整整半天才发现是地址没对齐。

3.3 Sensor寄存器配置实战——让Sensor“说出”你要的画面

寄存器配置是Sensor驱动的核心。说白了,就是告诉Sensor:你要输出多大分辨率、多少帧率、什么格式的数据。

3.3.1 常用寄存器分类

类别 典型寄存器 作用
芯片ID CHIP_ID_H, CHIP_ID_L 读取验证Sensor型号
模式选择 MODE_SELECT 软复位、待机、流模式切换
分辨率 WIDTH_H, WIDTH_L, HEIGHT_H, HEIGHT_L 设置输出图像尺寸
帧率 VTS_H, VTS_L, HTS_H, HTS_L 通过VTS/HTS调节帧率
增益/曝光 GAIN, AEC_EXPO 控制亮度和曝光时间
输出格式 FORMAT_SELECT RAW10/RAW12/YUV等
MIPI配置 MIPI_LANE_NUM, MIPI_CLK 配置MIPI通道数和时钟

3.3.2 初始化序列实战

每个Sensor厂商都会提供一份初始化寄存器表。我习惯把它组织成结构体数组:

/* 寄存器配置项 */
struct sensor_regval {
    u16 reg;
    u8  val;
};

/* 1080P@30fps 初始化序列 */
static const struct sensor_regval sensor_init_1080p30[] = {
    /* 软复位 */
    {0x0103, 0x01},
    {0x0100, 0x00},
    msleep(10),

    /* 设置分辨率 1920x1080 */
    {0x3808, 0x07},  /* width high */
    {0x3809, 0x80},  /* width low  (1920) */
    {0x380A, 0x04},  /* height high */
    {0x380B, 0x38},  /* height low (1080) */

    /* 设置帧率 30fps */
    {0x380C, 0x0B},  /* HTS high */
    {0x380D, 0xA0},  /* HTS low  (2976) */
    {0x380E, 0x04},  /* VTS high */
    {0x380F, 0x60},  /* VTS low  (1120) */

    /* 输出格式 RAW10 */
    {0x4300, 0x30},  /* format select */

    /* MIPI 4-lane, 800Mbps/lane */
    {0x4800, 0x24},  /* lane num = 4 */
    {0x4801, 0x0F},  /* clk config */

    /* 其他模拟配置(省略) */
    /* ... */

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

static int sensor_load_init_seq(struct i2c_client *client)
{
    const struct sensor_regval *p = sensor_init_1080p30;
    int ret;

    while (p->reg != 0xFFFF) {
        if (p->reg == 0x0000) {
            /* 延时指令 */
            msleep(p->val);
        } else {
            ret = sensor_write_reg(client, p->reg, p->val);
            if (ret)
                return ret;
        }
        p++;
    }

    return 0;
}
经验之谈:初始化序列里经常需要插入延时(比如软复位后要等几毫秒)。我习惯用reg=0x0000作为延时指令标记,这样不用单独写延时逻辑,代码更干净。

3.4 Sensor流控(Stream On/Off)——让数据流“听话”

流控是Sensor驱动的“开关”。Stream On让Sensor开始输出图像数据,Stream Off让它停止。看似简单,但处理不好会导致MIPI时钟紊乱、帧中断丢失等问题。

3.4.1 Stream On/Off 标准流程

static int sensor_s_stream(struct v4l2_subdev *sd, int enable)
{
    struct sensor_device *sensor = to_sensor(sd);
    int ret;

    if (enable) {
        /* Stream On */
        /* 1. 确保电源和时钟已稳定 */
        /* 2. 加载初始化序列 */
        ret = sensor_load_init_seq(sensor->client);
        if (ret)
            return ret;

        /* 3. 设置输出模式 */
        ret = sensor_write_reg(sensor->client, 0x0100, 0x01);
        if (ret)
            return ret;

        /* 4. 等待MIPI时钟锁定 */
        msleep(100);

        /* 5. 使能MIPI接收端 */
        /* ... */

        sensor->streaming = true;
    } else {
        /* Stream Off */
        /* 1. 先关MIPI接收端 */
        /* ... */

        /* 2. 写寄存器停止输出 */
        ret = sensor_write_reg(sensor->client, 0x0100, 0x00);
        if (ret)
            return ret;

        /* 3. 等待Sensor进入待机 */
        msleep(50);

        /* 4. 可选:关闭电源 */
        /* ... */

        sensor->streaming = false;
    }

    return 0;
}

3.4.2 流控中的关键时序

我遇到过最头疼的问题:Stream On后MIPI时钟不稳定,导致接收端丢帧。后来发现是Sensor输出模式和MIPI接收端配置不匹配。

我曾经踩过的坑:某次Stream Off后立即重新Stream On,Sensor没有完全复位,导致输出图像出现条纹。排查后发现是两次Stream On之间间隔太短,Sensor内部状态机没回到初始态。解决方案:Stream Off后至少等待100ms再重新Stream On。

3.4.3 流控状态机

为了更直观地理解流控流程,我画了一张状态图:

Sensor流控状态机 上电完成 初始化完成 Stream On 待机状态 下电完成 关键时序要求: • Stream Off → Stream On 间隔 ≥ 100ms • 下电 → 上电 间隔 ≥ 200ms 状态说明: • 实线:正常流程 • 虚线:异常恢复流程 • 每个状态切换需检查状态

3.5 本章小结

Sensor驱动开发,说白了就是三件事:

  • 上电时序——严格按照datasheet的时序要求,差1ms都可能出问题
  • 寄存器读写——I2C/SPI接口要稳定可靠,做好错误处理
  • 流控管理——Stream On/Off的时序和状态机要严谨,避免MIPI链路异常

我个人觉得,Sensor驱动是Camera驱动里最“实在”的部分。没有那么多花哨的算法,就是老老实实配好每一个寄存器,控制好每一个时序。但恰恰是这些“实在”的活,最容易出问题。