第3章:Sensor驱动开发——上电时序与I2C读写

各位好,我是老张。今天咱们聊聊Sensor驱动里最基础、也最容易翻车的一块——上电时序和I2C读写。

说实话,我见过太多工程师在Sensor初始化上栽跟头。明明代码逻辑没问题,Sensor就是不出图。最后查出来,要么是上电顺序搞反了,要么是I2C时序差那么一点点。嗯,咱们今天就把这些坑一个个填上。

3.1 上电时序:AVDD、DVDD、IOVDD

Sensor要工作,得先“吃饱饭”。这个“饭”就是三路电源:

  • AVDD:模拟电源,一般2.8V。给像素阵列、模拟电路供电。
  • DVDD:数字核心电源,常见1.2V或1.8V。给数字逻辑、PLL供电。
  • IOVDD:IO接口电源,通常1.8V。给I2C、MIPI等接口供电。

这三路电,谁先谁后?

标准顺序是:AVDD → DVDD → IOVDD。或者反过来,IOVDD → DVDD → AVDD。具体看Sensor datasheet。我个人习惯是先上AVDD,再上DVDD,最后IOVDD。为什么?因为模拟电路稳定需要时间,先给它供电,等数字部分起来时,模拟已经稳了。

⚠️ 注意: 千万别同时上电!我遇到过一块板子,设计时把三路电源用同一个LDO使能脚控制,结果上电瞬间电流过大,Sensor直接烧了。血的教训。

上电时序的典型代码实现(以GPIO控制LDO使能为例):

// 上电时序:AVDD -> DVDD -> IOVDD
void sensor_power_on(void) {
    // 1. 先开AVDD
    gpio_set_value(GPIO_AVDD_EN, 1);
    mdelay(5);  // 等待5ms,让AVDD稳定
    
    // 2. 再开DVDD
    gpio_set_value(GPIO_DVDD_EN, 1);
    mdelay(2);  // 等待2ms
    
    // 3. 最后开IOVDD
    gpio_set_value(GPIO_IOVDD_EN, 1);
    mdelay(10); // 等待IOVDD稳定,准备I2C通信
}

下电顺序正好反过来:IOVDD → DVDD → AVDD。而且下电后要等一段时间(至少10ms)才能再次上电。为什么?因为电容放电需要时间,没放干净就重新上电,电压可能还没掉到0,Sensor会进入不确定状态。

💡 小技巧: 我习惯在驱动里加一个“上电状态机”。每次上电前检查当前状态,避免重复上电或下电不完整。说白了,就是加个flag变量,记录电源状态。

3.2 I2C读写时序

电源搞定后,就该通过I2C和Sensor“对话”了。I2C读写看似简单,但坑也不少。

标准I2C写时序:

// 写Sensor寄存器
// 格式:START + 设备地址(写) + ACK + 寄存器地址高字节 + ACK + 寄存器地址低字节 + ACK + 数据 + ACK + STOP
int sensor_i2c_write(struct i2c_client *client, u16 reg, u8 val) {
    u8 buf[3];
    struct i2c_msg msg;
    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 != 1) {
        dev_err(&client->dev, "I2C write failed: reg=0x%04x, val=0x%02x\n", reg, val);
        return -EIO;
    }
    return 0;
}

标准I2C读时序:

// 读Sensor寄存器
// 格式:START + 设备地址(写) + ACK + 寄存器地址高字节 + ACK + 寄存器地址低字节 + ACK + 
//       RESTART + 设备地址(读) + ACK + 数据 + NACK + STOP
int sensor_i2c_read(struct i2c_client *client, u16 reg, u8 *val) {
    u8 reg_buf[2];
    struct i2c_msg msgs[2];
    int ret;
    
    reg_buf[0] = (reg >> 8) & 0xFF;
    reg_buf[1] = reg & 0xFF;
    
    // 第一条消息:写寄存器地址
    msgs[0].addr = client->addr;
    msgs[0].flags = 0;           // 写
    msgs[0].len = 2;
    msgs[0].buf = reg_buf;
    
    // 第二条消息:读数据
    msgs[1].addr = client->addr;
    msgs[1].flags = I2C_M_RD;   // 读
    msgs[1].len = 1;
    msgs[1].buf = val;
    
    ret = i2c_transfer(client->adapter, msgs, 2);
    if (ret != 2) {
        dev_err(&client->dev, "I2C read failed: reg=0x%04x\n", reg);
        return -EIO;
    }
    return 0;
}

这里有个细节:读操作时,写完寄存器地址后,要发一个RESTART信号,而不是STOP再START。为什么?因为如果发STOP,其他设备可能抢占总线,导致读操作失败。我刚开始做驱动时没注意这个,结果读回来的数据全是0xFF,查了两天才发现是时序问题。

🔑 关键点: I2C读写一定要加超时处理。有些Sensor在初始化阶段响应慢,不加超时的话,驱动可能卡死。我一般设100ms超时,重试3次。

3.3 Sensor初始化序列配置

上电、I2C通信都正常后,就该给Sensor“喂”初始化序列了。说白了,就是往一堆寄存器里写特定值,让Sensor进入工作状态。

初始化序列通常包含:

  • 软件复位:让Sensor回到默认状态
  • PLL配置:设置时钟频率、分频比
  • 输出格式:设置分辨率、帧率、数据格式(RAW10/RAW12等)
  • 模拟增益:设置模拟增益、数字增益
  • 测试模式:输出测试图案,验证通路

初始化序列一般以表格形式给出:

寄存器地址 寄存器值 说明
0x0103 0x01 软件复位
0x0300 0x04 PLL配置:分频比
0x0301 0x10 PLL配置:倍频比
0x0340 0x07 输出分辨率:行数高字节
0x0341 0x80 输出分辨率:行数低字节
0x0342 0x0F 输出分辨率:列数高字节
0x0343 0xA0 输出分辨率:列数低字节

代码实现时,我习惯把初始化序列定义为一个结构体数组:

struct sensor_reg {
    u16 reg;
    u8 val;
};

static const struct sensor_reg sensor_init_seq[] = {
    {0x0103, 0x01},  // 软件复位
    {0x0300, 0x04},  // PLL分频
    {0x0301, 0x10},  // PLL倍频
    {0x0340, 0x07},  // 行数高字节
    {0x0341, 0x80},  // 行数低字节
    {0x0342, 0x0F},  // 列数高字节
    {0x0343, 0xA0},  // 列数低字节
    // ... 更多配置
};

int sensor_init(struct i2c_client *client) {
    int i, ret;
    
    for (i = 0; i < ARRAY_SIZE(sensor_init_seq); i++) {
        ret = sensor_i2c_write(client, sensor_init_seq[i].reg, sensor_init_seq[i].val);
        if (ret) {
            dev_err(&client->dev, "Init failed at reg=0x%04x\n", sensor_init_seq[i].reg);
            return ret;
        }
        // 有些寄存器写入后需要等待
        if (sensor_init_seq[i].reg == 0x0103) {
            mdelay(10);  // 复位后等待10ms
        }
    }
    return 0;
}
⚠️ 注意: 初始化序列里有些寄存器是“敏感”的,比如PLL配置。写错一个值,Sensor可能直接罢工。我建议每写一个寄存器就读回来验证一下,确保写入成功。

3.4 寄存器读写封装

好的驱动,一定要有好的封装。我见过有些工程师把I2C读写直接写在初始化函数里,代码又长又乱。我的做法是:

  • 底层封装:sensor_i2c_read/sensor_i2c_write,只处理I2C协议
  • 中间层:sensor_reg_read/sensor_reg_write,加一些校验、重试逻辑
  • 上层接口:sensor_init/sensor_start_stream,直接调用中间层

举个例子,中间层可以加个“写后读验证”:

int sensor_reg_write_verify(struct i2c_client *client, u16 reg, u8 val) {
    int ret;
    u8 read_val;
    
    // 先写
    ret = sensor_i2c_write(client, reg, val);
    if (ret) return ret;
    
    // 再读回来验证
    ret = sensor_i2c_read(client, reg, &read_val);
    if (ret) return ret;
    
    if (read_val != val) {
        dev_err(&client->dev, "Verify failed: reg=0x%04x, wrote=0x%02x, read=0x%02x\n",
                reg, val, read_val);
        return -EIO;
    }
    return 0;
}

这样封装的好处是:上层代码只需要关心“写什么值”,不用管底层怎么通信。而且验证逻辑统一,不会漏掉。

💡 个人习惯: 我还会加一个“批量写”接口。初始化序列动辄几十上百个寄存器,一个一个写太慢。批量写可以合并I2C消息,减少通信开销。不过要注意,有些Sensor不支持连续写,得看datasheet。

好了,这一章的内容就到这里。Sensor驱动开发,说白了就是“上电→通信→配置”三步走。每一步都有坑,但只要你按照datasheet来,加上我提到的那些小技巧,基本不会出大问题。

下一章咱们聊聊MIPI接口和CSI-2协议,那可是Sensor和ISP之间的“高速公路”。到时候见!