3. Sensor驱动核心:I2C/SPI控制接口,寄存器读写实战

好,咱们进入正题。Sensor驱动里最基础、也最核心的活儿,就是通过I2C或SPI去读写寄存器。说白了,你写的驱动能不能让Sensor正常干活,全看这一步稳不稳。

我个人习惯,拿到一块新的Sensor模组,第一件事不是看数据手册里的那些高大上的算法,而是先把I2C/SPI的读写调通。为什么?因为这是你和Sensor对话的唯一通道。通道不通,后面全是白搭。

3.1 为什么是I2C和SPI?

Camera Sensor的控制接口,主流就两种:I2C和SPI。你可能会问,为什么不用UART?嗯,UART虽然简单,但速度慢,而且多设备挂载不方便。Sensor的控制数据量不大,但对时序要求高,I2C和SPI正好合适。

特性 I2C SPI
引脚数 2线(SCL, SDA) 4线(SCLK, MOSI, MISO, CS)
速度 标准100kHz~400kHz,快速可达1MHz 通常10MHz~50MHz
多设备 地址寻址,支持多设备 片选CS,一主多从
适用场景 寄存器配置、参数读取 高速配置、固件下载

我在项目中遇到过,有些Sensor为了省成本,只给了I2C接口。但像索尼IMX系列的高端Sensor,往往同时支持I2C和SPI。我建议,如果硬件设计允许,优先用SPI做配置,速度更快,调试也更方便。

3.2 寄存器读写的基础套路

不管I2C还是SPI,读写寄存器的套路其实差不多。核心就是:地址 + 数据

你想想看,Sensor内部有成千上万个寄存器,每个寄存器都有一个唯一的地址。你要做的就是:

  1. 告诉Sensor你要访问哪个地址
  2. 然后读或写这个地址里的数据

就这么简单。但实际写代码时,坑往往出在细节上。

3.2.1 I2C读写流程

I2C的读写,标准流程是这样的:

// 写寄存器
1. 发送起始信号
2. 发送设备地址 + 写位(0)
3. 等待ACK
4. 发送寄存器地址(高字节)
5. 等待ACK
6. 发送寄存器地址(低字节)
7. 等待ACK
8. 发送数据
9. 等待ACK
10. 发送停止信号

// 读寄存器
1. 发送起始信号
2. 发送设备地址 + 写位(0)
3. 等待ACK
4. 发送寄存器地址(高字节)
5. 等待ACK
6. 发送寄存器地址(低字节)
7. 等待ACK
8. 发送重复起始信号
9. 发送设备地址 + 读位(1)
10. 等待ACK
11. 读取数据
12. 发送NACK
13. 发送停止信号

嗯,这里要注意:很多Sensor的寄存器地址是16位的,需要分高字节和低字节发送。我刚开始做驱动时,就犯过这个错——只发了低字节,结果读出来的数据全是乱的。

关键点:I2C读操作时,需要先发一个写操作来设置寄存器地址,然后再发一个重复起始信号切换为读模式。这个重复起始信号不能省略,否则Sensor会懵掉。

3.2.2 SPI读写流程

SPI就简单多了,没有地址的概念,全靠片选信号区分设备。读写流程:

// SPI写寄存器
1. 拉低CS(片选)
2. 发送命令字节(通常写命令为0x02)
3. 发送寄存器地址(16位,分两次发送)
4. 发送数据
5. 拉高CS

// SPI读寄存器
1. 拉低CS
2. 发送命令字节(通常读命令为0x03)
3. 发送寄存器地址(16位,分两次发送)
4. 读取数据
5. 拉高CS

SPI的好处是,读写可以同时进行。你在发送地址的同时,MISO线上可能已经在返回数据了。但要注意,很多Sensor的SPI模式是模式0(CPOL=0, CPHA=0),数据在时钟上升沿采样。如果搞错了模式,读出来的数据全是0xFF或者0x00。

避坑指南:我曾经在一个项目里,SPI读寄存器总是返回0xFF。查了两天,最后发现是Sensor的SPI模式是模式3(CPOL=1, CPHA=1),而我代码里配的是模式0。改了一行代码,问题解决。所以,拿到Sensor后,第一件事就是确认SPI模式。

3.3 实战:写一个通用的I2C读写函数

好了,理论说完了,咱们直接上代码。我习惯把I2C读写封装成两个函数,这样上层调用起来特别清爽。

#include <linux/i2c.h>
#include <linux/delay.h>

#define SENSOR_I2C_ADDR 0x6C  // 假设Sensor的I2C地址

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

    buf[0] = (reg >> 8) & 0xFF;  // 寄存器地址高字节
    buf[1] = reg & 0xFF;         // 寄存器地址低字节
    buf[2] = val;                // 要写入的数据

    ret = i2c_master_send(client, buf, 3);
    if (ret < 0) {
        dev_err(&client->dev, "写寄存器失败: reg=0x%04x, val=0x%02x\n", reg, val);
        return ret;
    }

    // 有些Sensor需要一点延时才能接受下一次操作
    usleep_range(100, 200);
    return 0;
}

// 读寄存器:16位地址,返回8位数据
static int sensor_read_reg(struct i2c_client *client, u16 reg, u8 *val)
{
    int ret;
    u8 addr_buf[2];

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

    // 先发送寄存器地址
    ret = i2c_master_send(client, addr_buf, 2);
    if (ret < 0) {
        dev_err(&client->dev, "发送寄存器地址失败: reg=0x%04x\n", reg);
        return ret;
    }

    // 再读取数据
    ret = i2c_master_recv(client, val, 1);
    if (ret < 0) {
        dev_err(&client->dev, "读取寄存器失败: reg=0x%04x\n", reg);
        return ret;
    }

    return 0;
}

你看,代码其实不长。但有几个细节我特别想强调:

  • 地址顺序:先高字节后低字节,这是行业惯例。但有些Sensor是反的,先低后高。一定要看数据手册确认。
  • 延时:写操作后加一个usleep,不是所有Sensor都需要,但加上更保险。我遇到过一些Sensor,连续写太快会丢数据。
  • 错误处理:每个i2c操作都要检查返回值。别偷懒,否则出了问题你都不知道是硬件没连上还是代码写错了。

3.4 实战:SPI读写函数

SPI的读写函数,套路也差不多。但要注意,SPI的读写通常是一次性完成的。

#include <linux/spi/spi.h>

#define SPI_CMD_WRITE 0x02
#define SPI_CMD_READ  0x03

// SPI写寄存器
static int sensor_spi_write_reg(struct spi_device *spi, u16 reg, u8 val)
{
    int ret;
    u8 tx_buf[4];

    tx_buf[0] = SPI_CMD_WRITE;       // 写命令
    tx_buf[1] = (reg >> 8) & 0xFF;   // 地址高字节
    tx_buf[2] = reg & 0xFF;          // 地址低字节
    tx_buf[3] = val;                 // 数据

    ret = spi_write(spi, tx_buf, 4);
    if (ret < 0) {
        dev_err(&spi->dev, "SPI写失败: reg=0x%04x\n", reg);
        return ret;
    }

    return 0;
}

// SPI读寄存器
static int sensor_spi_read_reg(struct spi_device *spi, u16 reg, u8 *val)
{
    int ret;
    u8 tx_buf[4] = {0};
    u8 rx_buf[4] = {0};

    tx_buf[0] = SPI_CMD_READ;        // 读命令
    tx_buf[1] = (reg >> 8) & 0xFF;
    tx_buf[2] = reg & 0xFF;
    tx_buf[3] = 0;                   // dummy字节,用于产生时钟

    // SPI读写同时进行
    ret = spi_write_then_read(spi, tx_buf, 4, rx_buf, 4);
    if (ret < 0) {
        dev_err(&spi->dev, "SPI读失败: reg=0x%04x\n", reg);
        return ret;
    }

    // 注意:读到的数据在rx_buf[3]里,因为前3个字节是命令和地址
    *val = rx_buf[3];
    return 0;
}

小技巧:SPI读操作时,发送的dummy字节(0x00)不能省略。因为SPI是同步通信,你不发数据,时钟就不会产生,Sensor也就不会把数据推出来。我见过有人把dummy字节写成0xFF,也能用,但0x00更规范。

3.5 调试技巧:如何验证读写是否正确?

代码写完了,怎么验证?我一般会这么做:

  1. 读芯片ID寄存器:几乎所有Sensor都有一个芯片ID寄存器(比如地址0x0000或0x0001),里面存着一个固定值。如果能正确读出来,说明I2C/SPI通路是通的。
  2. 写一个已知寄存器再读回来:比如写0xAA到某个测试寄存器,再读回来看看是不是0xAA。注意,有些寄存器是只读的,写进去也没用。
  3. 用逻辑分析仪抓波形:这是最靠谱的方法。看看SCL/SDA或者SCLK/MOSI/MISO的时序对不对。我遇到过硬件上SDA上拉电阻没焊,导致通信时好时坏,用逻辑分析仪一眼就看出来了。

嗯,说到逻辑分析仪,我建议每个做驱动的工程师都备一个。便宜的几十块钱,但能帮你省下几天的调试时间。

3.6 常见问题与避坑

最后,我总结几个我踩过的坑,希望能帮你少走弯路:

  • I2C地址不对:Sensor的I2C地址通常是7位,但Linux内核里用的是8位地址(左移一位)。别搞混了。
  • 寄存器地址长度:有些Sensor用8位地址,有些用16位。一定要看数据手册确认。
  • 时序问题:I2C的时钟频率不能太高,否则Sensor跟不上。我遇到过把I2C频率设到400kHz,结果Sensor死活不响应,降到100kHz就好了。
  • SPI的CS控制:有些Sensor要求CS在每次传输后必须拉高一段时间,否则会进入错误状态。这个时间在数据手册里通常叫"CS de-assert time"。

好了,这一章的内容就到这里。说白了,I2C/SPI的寄存器读写就是Sensor驱动的"敲门砖"。这块搞定了,后面配置Sensor的曝光、增益、帧率什么的,就只是往寄存器里填数据的事了。

下一章,咱们会聊聊如何通过I2C/SPI配置Sensor的上电序列和初始化流程。到时候,你会看到这些读写函数是怎么被串起来用的。