4. I2C/SPI 控制总线实战:Sensor 寄存器读写、时序分析、Linux 下 I2C 设备驱动编写、调试技巧

摄像头 Sensor 的控制总线,说白了就是它的“神经”。你给 Sensor 上电、配分辨率、调增益、设帧率,全得通过 I2C 或 SPI 往寄存器里写值。我见过不少新手,Sensor 死活不出图,最后发现是 I2C 时序不对,或者地址写错了。这一章,咱们就把这根“神经”彻底打通。

4.1 为什么 Sensor 离不开 I2C/SPI?

Sensor 的像素阵列只管光电转换,但怎么转、转成多大、要不要开 HDR,这些配置信息都存在内部的寄存器里。主控(SoC)必须通过串行总线去读写这些寄存器。I2C 和 SPI 就是最常用的两种。

特性 I2C SPI
引脚数 2 根(SCL, SDA) 4 根(SCLK, MOSI, MISO, CS)
通信方式 半双工,主从架构 全双工,主从架构
速率 标准 100k/400k,高速 1M~3.4M 通常 10M~50M,甚至更高
寻址 7 位或 10 位设备地址 片选信号(CS)选择设备
适用场景 配置寄存器、低速控制 高速数据流、多设备级联

我个人习惯,对于摄像头 Sensor 的寄存器配置,99% 都用 I2C。因为 Sensor 寄存器读写频率不高,I2C 的 400kHz 完全够用,而且省引脚。只有极少数需要高速 OTP 烧录或特殊配置的 Sensor,才会用到 SPI。

4.2 I2C 时序分析:从波形到寄存器

很多驱动工程师把 I2C 当成黑盒子,调不通就换电阻、改频率。其实你只要看懂示波器上的波形,问题就解决了一半。

4.2.1 标准 I2C 写时序

写一个 Sensor 寄存器,通常需要 3 个阶段:

  1. 起始条件:SCL 高电平时,SDA 从高拉低。
  2. 发送设备地址 + 写位:7 位地址 + 0(写),等待 ACK。
  3. 发送寄存器地址 + 数据:先发 16 位或 8 位寄存器地址,再发数据,每字节后跟 ACK。
  4. 停止条件:SCL 高电平时,SDA 从低拉高。

举个例子,写 Sensor 的 0x3012 寄存器为 0x10:

// 伪代码:I2C 写操作
Start();
WriteByte(0x30 << 1 | 0);  // 设备地址 0x30,写操作
WaitAck();
WriteByte(0x30);            // 寄存器高字节
WaitAck();
WriteByte(0x12);            // 寄存器低字节
WaitAck();
WriteByte(0x10);            // 数据
WaitAck();
Stop();
避坑指南:我曾经在一个项目中,Sensor 的寄存器地址是 16 位的,但驱动里只发了 8 位。结果 Sensor 一直不回 ACK。查了半天示波器才发现,地址发少了。记住,一定要看 Sensor 的 datasheet,确认寄存器地址是 8 位还是 16 位。

4.2.2 时序参数与上拉电阻

I2C 是开漏输出,必须外接上拉电阻。电阻值选不对,波形就会变形。

参数 标准模式 (100kHz) 快速模式 (400kHz)
上拉电阻 4.7kΩ ~ 10kΩ 1.5kΩ ~ 4.7kΩ
上升时间 (tr) ≤ 1000ns ≤ 300ns
下降时间 (tf) ≤ 300ns ≤ 300ns

嗯,这里要注意:如果总线电容太大(比如走线过长),上升时间会变慢。我建议用示波器实测一下 SCL 的上升沿,如果太缓,就减小上拉电阻。但别小于 1kΩ,否则驱动能力不够,反而拉不低。

4.3 Linux 下 I2C 设备驱动编写

在 Linux 里写 Sensor 驱动,本质上就是注册一个 I2C 客户端,然后通过 i2c_transfer 或 smbus 接口读写寄存器。

4.3.1 驱动框架

一个标准的 Sensor I2C 驱动,核心结构如下:

static const struct i2c_device_id sensor_id[] = {
    { "sensor_ov5640", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, sensor_id);

static const struct of_device_id sensor_of_match[] = {
    { .compatible = "ovti,ov5640" },
    { }
};
MODULE_DEVICE_TABLE(of, sensor_of_match);

static struct i2c_driver sensor_driver = {
    .driver = {
        .name = "sensor_ov5640",
        .of_match_table = sensor_of_match,
    },
    .probe = sensor_probe,
    .remove = sensor_remove,
    .id_table = sensor_id,
};

module_i2c_driver(sensor_driver);

probe 函数里,你要做三件事:

  1. 分配并初始化 v4l2_subdev。
  2. 读取 Sensor 的 Chip ID,确认硬件连接正确。
  3. 注册 subdev 到媒体框架。

4.3.2 寄存器读写封装

我习惯把读写操作封装成两个函数,方便调用:

static int sensor_read_reg(struct i2c_client *client, u16 reg, u8 *val)
{
    struct i2c_msg msg[2];
    u8 reg_buf[2] = { reg >> 8, reg & 0xFF };
    u8 val_buf;

    msg[0].addr = client->addr;
    msg[0].flags = 0;       // 写
    msg[0].len = 2;
    msg[0].buf = reg_buf;

    msg[1].addr = client->addr;
    msg[1].flags = I2C_M_RD; // 读
    msg[1].len = 1;
    msg[1].buf = &val_buf;

    if (i2c_transfer(client->adapter, msg, 2) != 2)
        return -EIO;

    *val = val_buf;
    return 0;
}

static int sensor_write_reg(struct i2c_client *client, u16 reg, u8 val)
{
    u8 buf[3] = { reg >> 8, reg & 0xFF, val };
    int ret;

    ret = i2c_master_send(client, buf, 3);
    if (ret != 3)
        return -EIO;

    return 0;
}
注意:读操作必须用 i2c_transfer 发送两个消息(先写寄存器地址,再读数据),不能直接用 i2c_master_recv。因为 Sensor 需要先知道你要读哪个地址。我见过有人直接用 i2c_master_recv,结果读回来的永远是 0xFF。

4.4 调试技巧:让问题无处遁形

驱动写好了,上电一试,Sensor 没反应。怎么办?别慌,按下面的步骤排查。

4.4.1 硬件层排查

  • 测电压:用万用表量 Sensor 的 VDD、VDDIO、DVDD 是否正常。我遇到过 VDDIO 只有 1.6V,但 Sensor 要求 1.8V,结果 I2C 死活不通。
  • 看波形:用示波器抓 SCL 和 SDA。看起始条件、地址、ACK 位是否完整。如果 SDA 一直高,说明 Sensor 没拉低 ACK。
  • 检查地址:Sensor 的 I2C 地址通常由引脚电平决定。确认硬件上的 ADDR 引脚接法跟驱动里写的一致。

4.4.2 软件层调试

  • 使用 i2c-tools:在用户态用 i2cdetect 扫描总线,看 Sensor 是否在预期地址上响应。再用 i2cget 和 i2cset 手动读写寄存器,验证驱动逻辑。
  • 打印调试信息:在 probe 函数里加 dev_info,打印 Chip ID。如果读回来的 ID 不对,大概率是 I2C 通信有问题。
  • 检查 DTS:确保设备树里 i2c 节点的时钟频率、pinctrl 配置正确。我曾经因为 pinctrl 没配好,I2C 引脚被复用成 GPIO,导致通信失败。
核心思路:调试 I2C 驱动,就是“从波形到代码”的闭环验证。示波器看到波形正确,再怀疑软件;波形不对,先查硬件。

4.5 知识体系总览

下面这张图,把本章的核心逻辑串起来了。从总线选型,到时序分析,再到驱动编写和调试,每一步都环环相扣。

I2C/SPI 控制总线实战知识体系 I2C/SPI 控制总线实战知识体系 总线选型 I2C 总线 SPI 总线 时序分析 驱动编写 高速配置 OTP 烧录 调试技巧 核心:从波形到代码 闭环验证

这张图把 I2C 和 SPI 的选型、时序、驱动、调试串在了一起。你写驱动时,脑子里要有这张图:先选总线,再抠时序,然后写代码,最后用示波器和 i2c-tools 验证。每一步都走扎实了,Sensor 控制就稳了。