第三章 Sensor驱动开发:I2C/SPI通信协议、sensor寄存器配置、V4L2 subdev注册、sensor控制接口实现(曝光、增益、帧率)

好,我们直接进入正题。Sensor驱动,说白了就是让摄像头芯片听话。你得告诉它怎么曝光、怎么放大信号、多久出一帧画面。而这些指令,全靠I2C或SPI总线传过去。今天我就把这块掰开了讲清楚。

3.1 I2C/SPI通信协议:选对路子,少走弯路

先说说通信协议。Sensor和主控之间,最常见的就两种:I2C和SPI。我个人习惯,能走I2C就不走SPI,除非sensor寄存器太多或者帧率要求极高。

I2C的特点:两根线(SCL、SDA),地址7位或10位,速度一般400kHz或1MHz。我遇到过不少新手,上来就把I2C时钟配成3.4MHz,结果sensor根本不响应。为什么?因为很多老款sensor只支持标准模式(100kHz)或快速模式(400kHz)。

核心要点:I2C通信时,一定要先发设备地址(7位左移1位,最后一位是读写位),再发寄存器地址,最后发数据。每次传输后都要检查ACK信号。

SPI的特点:四根线(SCLK、MOSI、MISO、CS),全双工,速度可以跑到几十MHz。适合寄存器多、需要批量配置的场景。但要注意,SPI的时序模式(CPOL、CPHA)必须和sensor手册完全一致,否则数据全是乱的。

我的经验:调试SPI时,先用逻辑分析仪抓波形。我曾经花了两天时间查一个sensor初始化失败的问题,最后发现是CS片选信号拉低后没等足够时间就发数据了。sensor手册上写的是CS拉低后至少等1us,我代码里只等了500ns。

3.2 Sensor寄存器配置:从手册到代码

拿到一款新sensor,第一件事就是看寄存器手册。别急着写代码,先把关键寄存器梳理清楚。我一般会列个表格:

寄存器地址 寄存器名称 默认值 功能描述
0x0100 MODE_SELECT 0x00 软件复位/进入待机模式
0x0202 EXPOSURE_H 0x00 曝光时间高8位
0x0203 EXPOSURE_L 0x00 曝光时间低8位
0x0204 GAIN_H 0x00 增益高8位
0x0205 GAIN_L 0x00 增益低8位
0x0300 FRAME_LENGTH_H 0x0E 帧长高8位(决定帧率)
0x0301 FRAME_LENGTH_L 0x10 帧长低8位

配置sensor时,我建议按这个顺序来:

  1. 先软件复位(写0x0103为0x01)
  2. 等待至少10ms,让sensor内部稳定
  3. 配置PLL和时钟分频
  4. 配置输出格式(RAW8、RAW10、YUV等)
  5. 配置分辨率(窗口裁剪、binning等)
  6. 配置帧率(通过帧长和行时间计算)
  7. 最后退出待机模式(写0x0100为0x01)

注意:很多sensor在待机模式下才能修改关键寄存器。如果你在streaming状态下写曝光值,可能不会生效。我曾经踩过这个坑,sensor手册里写得很清楚,但我没仔细看,结果曝光死活调不动。

3.3 V4L2 subdev注册:让内核认识你的sensor

在Linux内核里,sensor被抽象成一个V4L2 subdev。注册过程其实不复杂,但有几个关键点要注意。

先看核心代码框架:

static struct v4l2_subdev_ops sensor_subdev_ops = {
    .core = &sensor_core_ops,
    .video = &sensor_video_ops,
    .pad = &sensor_pad_ops,
};

static int sensor_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    struct sensor_dev *sensor;
    struct v4l2_subdev *sd;

    sensor = devm_kzalloc(&client->dev, sizeof(*sensor), GFP_KERNEL);
    if (!sensor)
        return -ENOMEM;

    sd = &sensor->subdev;
    v4l2_i2c_subdev_init(sd, client, &sensor_subdev_ops);

    // 注册到V4L2框架
    ret = v4l2_async_register_subdev(sd);
    if (ret < 0) {
        dev_err(&client->dev, "failed to register subdev\n");
        return ret;
    }

    return 0;
}

这里有个细节:v4l2_i2c_subdev_init会自动把i2c_client和subdev关联起来。你后面读写寄存器时,直接用i2c_smbus_write_byte_datai2c_smbus_read_byte_data就行。

我的习惯:在probe函数里,我会先读sensor的chip ID寄存器,确认硬件连接正确。比如:

chip_id = i2c_smbus_read_word_data(client, 0x0000);
if (chip_id != 0x1234) {
    dev_err(&client->dev, "chip ID mismatch: 0x%04x\n", chip_id);
    return -ENODEV;
}

这一步能帮你快速定位硬件问题,省得后面调试时抓瞎。

3.4 Sensor控制接口实现:曝光、增益、帧率

这部分是驱动开发的核心。用户空间的ISP框架(比如Exynos的FIMC-IS)会通过V4L2的控制接口来调节sensor参数。你需要实现s_s_ctrl回调函数。

先看控制ID的定义:

static const struct v4l2_ctrl_ops sensor_ctrl_ops = {
    .s_ctrl = sensor_s_ctrl,
};

static int sensor_init_controls(struct sensor_dev *sensor)
{
    struct v4l2_ctrl_handler *hdl = &sensor->ctrl_handler;

    v4l2_ctrl_handler_init(hdl, 3);

    v4l2_ctrl_new_std(hdl, &sensor_ctrl_ops,
                      V4L2_CID_EXPOSURE_ABSOLUTE,
                      0, 0xFFFF, 1, 0x0100);

    v4l2_ctrl_new_std(hdl, &sensor_ctrl_ops,
                      V4L2_CID_GAIN,
                      0, 0xFF, 1, 0x40);

    v4l2_ctrl_new_std(hdl, &sensor_ctrl_ops,
                      V4L2_CID_PIXEL_RATE,
                      0, 100000000, 1, 72000000);

    sensor->subdev.ctrl_handler = hdl;
    return 0;
}

然后是实现控制逻辑:

static int sensor_s_ctrl(struct v4l2_ctrl *ctrl)
{
    struct sensor_dev *sensor = container_of(ctrl->handler,
                                             struct sensor_dev, ctrl_handler);
    u16 exposure, gain;
    u32 frame_length;

    switch (ctrl->id) {
    case V4L2_CID_EXPOSURE_ABSOLUTE:
        exposure = ctrl->val;
        // 写曝光寄存器
        sensor_write(sensor, 0x0202, (exposure >> 8) & 0xFF);
        sensor_write(sensor, 0x0203, exposure & 0xFF);
        break;

    case V4L2_CID_GAIN:
        gain = ctrl->val;
        // 写增益寄存器
        sensor_write(sensor, 0x0204, (gain >> 8) & 0xFF);
        sensor_write(sensor, 0x0205, gain & 0xFF);
        break;

    case V4L2_CID_PIXEL_RATE:
        // 根据像素时钟计算帧长
        frame_length = ctrl->val / sensor->line_time;
        sensor_write(sensor, 0x0300, (frame_length >> 8) & 0xFF);
        sensor_write(sensor, 0x0301, frame_length & 0xFF);
        break;
    }

    return 0;
}

关键点:曝光和增益的调节顺序很重要。我建议先调曝光,再调增益。因为增益会放大噪声,如果先调增益再调曝光,画面噪点会很明显。另外,帧率和曝光是耦合的——曝光时间不能超过帧周期。你想想看,如果帧率是30fps(33ms一帧),曝光设成50ms,那画面就乱套了。

3.5 避坑指南:我踩过的那些雷

最后分享几个实战中容易出问题的地方:

  • I2C地址不对:sensor手册上给的地址可能是7位值,但Linux驱动里要用8位值(左移1位)。我见过有人直接拿7位地址去读写,结果设备永远找不到。
  • 寄存器写入顺序:有些sensor要求先写高8位再写低8位,有些则相反。一定要看手册里的时序图。
  • 帧率计算:帧率 = 像素时钟 / (帧长 × 行长度)。其中行长度包括有效像素和blanking时间。我刚开始做时,忘了算blanking,算出来的帧率差了20%。
  • 多sensor冲突:如果系统里有多个sensor共用一个I2C总线,记得检查地址是否冲突。我曾经把两个sensor设成同一个地址,结果一个初始化成功,另一个死活不行。

我的调试技巧:在sensor驱动里加调试打印,每次写寄存器时都打印出来。比如:

dev_dbg(&client->dev, "write reg 0x%04x = 0x%02x\n", reg, val);

配合逻辑分析仪,你能快速定位是驱动没写对,还是硬件没响应。

好了,这一章的内容就到这里。Sensor驱动开发,说白了就是三件事:把数据发过去、把参数配好、把接口暴露出来。下一章我们会讲MIPI CSI-2接口的配置和调试,那是sensor和ISP之间的高速公路,跑不通的话前面全白干。