第三章 I2C/SPI控制总线驱动:寄存器读写时序、Linux I2C子系统、SPI模式配置、多设备总线仲裁

好,咱们进入第三章。这一章讲的是控制总线,说白了就是怎么跟CMOS传感器“说话”。

传感器再厉害,也得有人告诉它“曝光时间设多少”、“增益开多大”。这个传话的,就是I2C和SPI总线。

我个人习惯把控制总线比作传感器的“神经”。神经断了,传感器就是个瞎子。

3.1 寄存器读写时序:别小看这个“握手”

先说说I2C。I2C只有两根线:SCL(时钟)和SDA(数据)。

你想想看,两根线要传地址、传数据、还要应答,时序上稍微差一点,传感器就不理你了。

我在项目中遇到过最典型的问题:上电后读传感器ID,读回来全是0xFF。查了半天,发现是I2C起始条件没给够时间。

I2C写寄存器标准流程:

  1. 主机发送起始信号(SCL高电平时,SDA从高变低)
  2. 发送7位设备地址 + 写位(0)
  3. 等待从机应答(ACK)
  4. 发送寄存器地址(高字节)
  5. 等待从机应答
  6. 发送寄存器地址(低字节)
  7. 等待从机应答
  8. 发送要写入的数据
  9. 等待从机应答
  10. 主机发送停止信号(SCL高电平时,SDA从低变高)

嗯,这里要注意:很多传感器支持“连续读写”。你可以在发送完寄存器地址后,连续发多个字节的数据,不用每次重新发地址。这样效率高很多。

// 伪代码示例:I2C写传感器寄存器
int i2c_write_reg(uint8_t dev_addr, uint16_t reg_addr, uint8_t data) {
    uint8_t buf[3];
    buf[0] = (reg_addr >> 8) & 0xFF;  // 寄存器高字节
    buf[1] = reg_addr & 0xFF;         // 寄存器低字节
    buf[2] = data;                    // 要写入的数据
    
    // 调用Linux I2C子系统接口
    struct i2c_msg msg;
    msg.addr = dev_addr;
    msg.flags = 0;  // 写操作
    msg.len = 3;
    msg.buf = buf;
    
    return i2c_transfer(adapter, &msg, 1);
}

SPI的时序就简单粗暴多了。四根线:SCLK、MOSI、MISO、CS。CS拉低,时钟一打,数据就出去了。

但SPI有个坑:模式配置。CPOL和CPHA这两个参数,搞错了就读不到数据。

SPI模式 CPOL CPHA 数据采样边沿
模式0 0 0 上升沿采样
模式1 0 1 下降沿采样
模式2 1 0 上升沿采样
模式3 1 1 下降沿采样

我曾经犯过一个低级错误:传感器手册写的是模式0,我配成了模式3。结果读回来的数据全是乱的。折腾了两天才发现是这里的问题。

3.2 Linux I2C子系统:别自己造轮子

很多初学者喜欢在用户态直接操作GPIO模拟I2C。我建议你千万别这么干。

Linux内核已经提供了完善的I2C子系统。你只需要注册一个客户端设备,然后调用读写接口就行了。

我的经验:在设备树里把传感器节点配好,驱动里用i2c_add_driver()注册,然后通过i2c_smbus_read_byte_data()和i2c_smbus_write_byte_data()操作寄存器。省心又稳定。

// Linux I2C驱动框架示例
static int sensor_probe(struct i2c_client *client, 
                        const struct i2c_device_id *id) {
    // 读取传感器ID确认连接
    u8 chip_id = i2c_smbus_read_byte_data(client, REG_CHIP_ID);
    if (chip_id != EXPECTED_ID) {
        dev_err(&client->dev, "Chip ID mismatch: 0x%02x\n", chip_id);
        return -ENODEV;
    }
    
    // 初始化传感器配置
    i2c_smbus_write_byte_data(client, REG_MODE, MODE_NORMAL);
    i2c_smbus_write_byte_data(client, REG_EXPOSURE, 0x10);
    
    return 0;
}

static const struct i2c_device_id sensor_id[] = {
    { "cmos_sensor", 0 },
    { }
};

static struct i2c_driver sensor_driver = {
    .driver = {
        .name = "cmos_sensor",
    },
    .probe = sensor_probe,
    .id_table = sensor_id,
};

module_i2c_driver(sensor_driver);

为什么推荐用子系统?因为内核帮你处理了总线仲裁、时钟延展、错误重试这些脏活。你想想看,要是自己写,光处理NACK重试就得写一堆代码。

3.3 SPI模式配置:时钟极性和相位

SPI的四种模式,说白了就是决定数据在时钟的哪个边沿采样。

我个人的记忆方法:CPOL决定空闲时时钟电平,CPHA决定采样边沿。

  • CPOL=0:空闲时时钟为低电平
  • CPOL=1:空闲时时钟为高电平
  • CPHA=0:第一个边沿采样(上升或下降取决于CPOL)
  • CPHA=1:第二个边沿采样

在Linux SPI子系统中,配置模式很简单:

// SPI设备配置示例
struct spi_device *spi;
spi->mode = SPI_MODE_0;  // CPOL=0, CPHA=0
spi->bits_per_word = 8;
spi->max_speed_hz = 10 * 1000 * 1000;  // 10MHz

// 写寄存器
u8 tx_buf[3] = {0x03, 0x00, 0x55};  // 读命令+地址+数据
u8 rx_buf[3] = {0};
struct spi_transfer t = {
    .tx_buf = tx_buf,
    .rx_buf = rx_buf,
    .len = 3,
};
spi_sync_transfer(spi, &t, 1);

注意:有些传感器对SPI时钟频率很敏感。频率太高,信号会失真。我建议先从1MHz开始调试,稳定了再往上提。

3.4 多设备总线仲裁:别让传感器“打架”

内窥镜里通常不止一个传感器。可能有主摄像头、副摄像头、甚至温度传感器。它们都挂在同一条I2C或SPI总线上。

问题来了:如果两个设备同时说话,总线就乱了。

I2C的多设备仲裁:

I2C本身支持多主机。每个设备都有唯一的7位地址。主机发送地址时,只有地址匹配的设备才会应答。

但要注意:如果两个主机同时发起传输,I2C总线会通过“线与”逻辑仲裁。谁先拉低SDA,谁就赢得总线。

我在项目中遇到过一个问题:主控和协处理器都想访问传感器。结果总线经常锁死。后来加了I2C总线锁(mutex),才解决。

// 多设备访问保护示例
static DEFINE_MUTEX(i2c_bus_lock);

int safe_i2c_write(struct i2c_client *client, u8 reg, u8 val) {
    int ret;
    mutex_lock(&i2c_bus_lock);
    ret = i2c_smbus_write_byte_data(client, reg, val);
    mutex_unlock(&i2c_bus_lock);
    return ret;
}

SPI的多设备仲裁:

SPI就简单了。每个设备有自己的片选(CS)引脚。主机想跟谁说话,就把谁的CS拉低。

但有个坑:CS拉低后,如果主机没及时拉高,其他设备可能会误判。

我的建议:SPI多设备时,每次传输完立刻拉高CS。别偷懒。我曾经因为CS拉高不及时,导致两个SPI设备互相干扰,图像出现条纹。

另外,SPI总线上所有设备的MISO引脚是“线与”的。如果某个设备没被选中,它的MISO必须处于高阻态。否则会跟正在通信的设备冲突。

嗯,这一点很多硬件工程师会忽略。选型时一定要确认传感器支持三态输出。

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

最后,分享几个我实际项目中遇到的坑:

  • I2C上拉电阻:太小了信号振铃,太大了上升沿太慢。我一般用4.7kΩ,频率400kHz时刚好。
  • SPI走线长度:超过10cm就要考虑信号完整性。内窥镜的软排线很长,我建议降低SPI频率到5MHz以下。
  • 寄存器地址宽度:有些传感器用8位地址,有些用16位。读手册时一定要看清楚,否则地址对不上。
  • 写后读验证:写完寄存器后,再读回来确认。我曾经被一个传感器坑过,它写寄存器有延迟,连续写太快会丢数据。

好了,这一章就到这里。控制总线是传感器驱动的“基本功”,看似简单,但细节很多。下一章我们讲MIPI CSI-2接口,那才是真正的“高速公路”。