4. Sensor驱动开发

好,咱们进入正题。Sensor驱动,说白了就是让主控芯片和图像传感器能“说话”。怎么说话?通过I2C或者SPI总线。你写一个值到某个寄存器,Sensor就改变一个行为;你读一个寄存器,就知道Sensor当前的状态。

我刚开始做驱动时,总觉得这玩意儿简单——不就是读写寄存器嘛。直到有一次,一个Sensor死活出不了图,查了三天,最后发现是初始化序列里一个寄存器的写入时序不对。嗯,从那以后,我再也不敢小看驱动开发了。

核心认知: Sensor驱动不是简单的寄存器读写,而是硬件时序、芯片手册、流控逻辑三者的精确配合。

4.1 I2C vs SPI:选哪个?

先看个对比表,帮你快速决策:

特性 I2C SPI
引脚数 2线(SCL, SDA) 4线(SCLK, MOSI, MISO, CS)
速度 标准100kHz~400kHz,快模式可达1MHz 通常10MHz~50MHz,甚至更高
寻址方式 7位/10位设备地址 片选信号CS选择设备
多设备支持 总线挂载多个设备,地址不冲突即可 每个设备需要独立CS,引脚开销大
典型场景 配置寄存器、读取状态 高速配置、固件下载、数据流

我个人习惯:配置类用I2C,数据类用SPI。大部分手机摄像头Sensor的寄存器配置走I2C,因为引脚少、布线简单。但如果你在做高速工业相机,或者需要下载固件到Sensor,SPI是更好的选择。

经验之谈: 有些Sensor同时支持I2C和SPI,通过某个引脚的电平选择模式。选型时一定要确认你的主控I2C控制器是否支持400kHz以上,否则初始化时间会很长。

4.2 寄存器读写:基础操作

先写一个最基础的I2C读写函数。别小看这几行代码,它是整个驱动的基石。

// sensor_i2c.c
#include "sensor_drv.h"

static int sensor_i2c_write(struct sensor_device *sdev, 
                            uint16_t reg_addr, 
                            uint8_t *data, 
                            uint16_t len)
{
    struct i2c_msg msg;
    uint8_t buf[2 + len];
    
    // 构造发送缓冲区:寄存器地址 + 数据
    buf[0] = (reg_addr >> 8) & 0xFF;  // 高字节
    buf[1] = reg_addr & 0xFF;         // 低字节
    memcpy(&buf[2], data, len);
    
    msg.addr = sdev->i2c_addr;
    msg.flags = 0;  // 写
    msg.len = 2 + len;
    msg.buf = buf;
    
    return i2c_transfer(sdev->i2c_adap, &msg, 1);
}

static int sensor_i2c_read(struct sensor_device *sdev,
                           uint16_t reg_addr,
                           uint8_t *data,
                           uint16_t len)
{
    struct i2c_msg msgs[2];
    uint8_t addr_buf[2];
    
    addr_buf[0] = (reg_addr >> 8) & 0xFF;
    addr_buf[1] = reg_addr & 0xFF;
    
    // 先写寄存器地址
    msgs[0].addr = sdev->i2c_addr;
    msgs[0].flags = 0;
    msgs[0].len = 2;
    msgs[0].buf = addr_buf;
    
    // 再读数据
    msgs[1].addr = sdev->i2c_addr;
    msgs[1].flags = I2C_M_RD;
    msgs[1].len = len;
    msgs[1].buf = data;
    
    return i2c_transfer(sdev->i2c_adap, msgs, 2);
}

注意看,我用了组合传输(先写地址再读数据)。为什么?因为大部分Sensor的读操作需要先告诉它你要读哪个寄存器,然后才能拿到数据。有些驱动偷懒,分两次调用,中间可能被其他设备打断——我曾经就被这个坑过,读回来的数据全是错的。

4.3 初始化序列:从手册到代码

拿到Sensor的datasheet,你会看到一张长长的寄存器配置表。比如:

寄存器地址 值 说明
0x0103 0x01 软件复位
0x3034 0x1A PLL配置
0x3010 0x00 MIPI Lane设置
0x4800 0x24 曝光控制
... ... ...

我的做法是:把初始化序列定义成结构体数组,而不是散落在代码里。

// sensor_ov5640.h
struct sensor_regval {
    uint16_t reg;
    uint8_t val;
    uint8_t delay_ms;  // 写入后的等待时间
};

static const struct sensor_regval ov5640_init_seq[] = {
    {0x0103, 0x01, 10},   // 软件复位,等10ms
    {0x3034, 0x1A, 0},    // PLL配置,无需等待
    {0x3010, 0x00, 0},
    {0x4800, 0x24, 0},
    // ... 可能几百行
    {0xFFFF, 0xFF, 0},    // 结束标记
};

然后写一个通用的加载函数:

int sensor_load_init_seq(struct sensor_device *sdev,
                         const struct sensor_regval *seq)
{
    int ret;
    
    while (seq->reg != 0xFFFF) {
        ret = sensor_i2c_write(sdev, seq->reg, &seq->val, 1);
        if (ret < 0) {
            dev_err(sdev->dev, "写寄存器 0x%04X 失败\n", seq->reg);
            return ret;
        }
        
        if (seq->delay_ms > 0)
            msleep(seq->delay_ms);
        
        seq++;
    }
    
    return 0;
}
注意: 有些寄存器写入后需要等待一段时间才能生效,比如PLL锁定、DSP启动。手册里通常会写“Wait for N ms”,千万别省略。我见过有人把delay全写成0,结果Sensor输出全是雪花。

4.4 流控:Stream On / Stream Off

流控是驱动里最容易出bug的地方。Stream On让Sensor开始输出图像数据,Stream Off让它停止。但这里有个关键点:不是所有寄存器都能随便写。

我画了一张流控的状态图,帮你理清逻辑:

Sensor 流控状态机 Power Off 未上电 Standby 已上电,未配置 Configured 初始化完成 Streaming 正在输出图像 上电 加载初始化序列 Stream On Stream Off 下电 关键约束 • Stream On/Off 期间 禁止写入配置寄存器 • 必须先Stop再改配置 常见错误 • 未等待VSYNC边界 • 重复发送初始化序列 • 漏掉PLL稳定时间

Stream On的典型实现:

int sensor_stream_on(struct sensor_device *sdev)
{
    int ret;
    uint8_t val = 0x01;
    
    // 1. 检查当前状态
    if (sdev->state != SENSOR_STATE_CONFIGURED) {
        dev_err(sdev->dev, "状态错误,无法启动流\n");
        return -EINVAL;
    }
    
    // 2. 写入Stream On寄存器
    // 不同Sensor的寄存器不同,常见的有0x0100、0x3000等
    ret = sensor_i2c_write(sdev, 0x0100, &val, 1);
    if (ret < 0)
        return ret;
    
    // 3. 等待VSYNC稳定
    msleep(100);  // 至少等一帧
    
    sdev->state = SENSOR_STATE_STREAMING;
    dev_info(sdev->dev, "Stream On 成功\n");
    
    return 0;
}

int sensor_stream_off(struct sensor_device *sdev)
{
    int ret;
    uint8_t val = 0x00;
    
    if (sdev->state != SENSOR_STATE_STREAMING) {
        dev_warn(sdev->dev, "当前不在Streaming状态\n");
        return 0;
    }
    
    // 写入Stream Off寄存器
    ret = sensor_i2c_write(sdev, 0x0100, &val, 1);
    if (ret < 0)
        return ret;
    
    // 等待最后一帧输出完毕
    msleep(50);
    
    sdev->state = SENSOR_STATE_CONFIGURED;
    dev_info(sdev->dev, "Stream Off 成功\n");
    
    return 0;
}
避坑指南: 我曾经在Stream Off后立刻重新配置寄存器,结果Sensor死锁了。后来发现,有些Sensor需要等待几个VSYNC周期才能完全停止内部流水线。建议在Stream Off后加一个至少50ms的延迟,或者轮询某个状态寄存器确认Sensor已停止。

4.5 通用Sensor驱动框架

好了,把上面的东西串起来,就是一个通用的驱动框架。我把它分成三层:

层级 文件 职责
硬件抽象层 sensor_i2c.c / sensor_spi.c 封装I2C/SPI读写,处理时序
核心驱动层 sensor_core.c 初始化序列加载、流控、电源管理
Sensor适配层 sensor_ov5640.c 具体型号的寄存器表、特殊配置

核心数据结构:

struct sensor_device {
    struct device *dev;
    struct i2c_adapter *i2c_adap;
    uint16_t i2c_addr;
    
    enum sensor_state state;  // 当前状态
    const struct sensor_regval *init_seq;  // 初始化序列
    const struct sensor_regval *stream_on_seq;  // 流控序列
    const struct sensor_regval *stream_off_seq;
    
    int (*write)(struct sensor_device *, uint16_t reg, uint8_t *data, uint16_t len);
    int (*read)(struct sensor_device *, uint16_t reg, uint8_t *data, uint16_t len);
};

这个框架的好处是:换Sensor只需要换适配层。核心驱动和硬件抽象层基本不用动。我在一个项目里用这个框架同时支持了OV5640、IMX219、AR0230三个Sensor,切换时只需要改设备树和加载不同的适配文件。

框架设计原则:
  • 硬件操作与业务逻辑分离
  • 初始化序列数据驱动,不写死在代码里
  • 流控状态机清晰,防止非法操作
  • 错误处理完整,每个I2C操作都要检查返回值

嗯,到这里,一个通用的Sensor驱动框架就搭起来了。你可能会问:这就能用了吗?说实话,这只是基础。实际项目中还有电源时序、MIPI D-PHY配置、时钟树、中断处理等等。但有了这个骨架,后面的事情就是往里面填肉了。