2、I2C/SPI总线协议精讲:I2C时序、寻址、读写操作;SPI模式、全双工通信;逻辑分析仪抓包实战;Sensor寄存器读写封装。

做Sensor驱动,说白了就是跟寄存器打交道。而读写寄存器,绕不开I2C和SPI这两条总线。我见过不少新手,上来就调代码,波形都不看,结果卡在设备没应答这种低级问题上。今天咱们就把这两条总线彻底讲透。

2.1 I2C总线:两根线搞定所有Sensor

I2C只有两根线:SCL(时钟)和SDA(数据)。但别小看它,一根线上能挂几十个设备。怎么区分?靠地址。

2.1.1 时序与寻址

先看起始条件和停止条件:

  • 起始条件:SCL高电平时,SDA从高变低。
  • 停止条件:SCL高电平时,SDA从低变高。

每次传输都以起始开始,以停止结束。中间传数据时,SDA必须在SCL低电平时变化,高电平时采样。这是硬规矩,违反了一定出问题。

寻址呢?标准7位地址,加上读写位,凑成第一个字节。比如Sensor地址0x76,写操作就是0xEC(0x76左移1位,最低位补0)。

关键点:I2C地址是7位还是10位,看数据手册。大部分Sensor用7位,但别想当然,我遇到过一颗气压计用10位地址,坑了我半天。

2.1.2 读写操作流程

写寄存器:

  1. 主机发起始条件
  2. 发设备地址+写位(0),等应答
  3. 发寄存器地址,等应答
  4. 发数据字节,每字节等应答
  5. 发停止条件

读寄存器:

  1. 主机发起始条件
  2. 发设备地址+写位,等应答
  3. 发寄存器地址,等应答
  4. 发重复起始条件
  5. 发设备地址+读位(1),等应答
  6. 读数据字节,主机发NACK表示读完
  7. 发停止条件

嗯,这里要注意:读操作中间的重复起始条件不能省。我见过有人直接发停止再发起始,虽然也能用,但不符合规范,某些Sensor会不认。

2.2 SPI总线:速度至上

SPI比I2C快得多,全双工,四根线:SCLK、MOSI、MISO、CS。每个设备独占一根片选线,所以没有地址冲突问题。

2.2.1 四种模式

SPI模式由时钟极性(CPOL)和时钟相位(CPHA)决定:

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

我个人习惯先看数据手册的时序图,确认模式再写代码。曾经有一次,我默认用模式0,结果Sensor死活读不对,抓了波形才发现它要模式3。

2.2.2 全双工通信

SPI牛逼的地方在于,发数据的同时能收数据。比如你发一个字节0xAA,同时MISO线上会收到Sensor返回的一个字节。所以读寄存器时,通常先发地址字节(同时收垃圾数据),再发一个0x00(同时收有效数据)。

小技巧:SPI的CS片选要拉低整个传输过程,不能中间拉高。有些Sensor对CS时序很敏感,拉高一次就复位了。

2.3 逻辑分析仪抓包实战

理论说再多,不如看波形。我建议每个做驱动的工程师,桌上常备一个逻辑分析仪。便宜的几十块,贵的几百,但都能用。

抓包步骤:

  1. 把SCL/SDA(或SCLK/MOSI/MISO/CS)接到逻辑分析仪通道上
  2. 设置采样率,至少4倍于总线频率
  3. 触发方式选下降沿(起始条件或CS拉低)
  4. 运行抓包,分析波形

看波形时,重点检查:

  • 起始条件是否干净,有没有毛刺
  • 地址字节是否正确,应答位有没有拉低
  • 数据字节顺序对不对,MSB还是LSB first
  • 停止条件是否完整

避坑指南:我曾经调试一颗陀螺仪,I2C总线上有上拉电阻,但Sensor的SDA引脚内部也有弱上拉,结果电平冲突,应答位一直拉不高。逻辑分析仪一看就明白了,换了个外部上拉电阻值搞定。

2.4 Sensor寄存器读写封装

写驱动不能每次都裸调I2C/SPI。我习惯封装一层,上层代码只管读/写寄存器地址,底层自动处理总线差异。

看一个简单的I2C读写封装:

// i2c_sensor.h
typedef struct {
    int (*read)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len);
    int (*write)(uint8_t dev_addr, uint8_t reg_addr, const uint8_t *data, uint16_t len);
} sensor_bus_t;

// i2c_sensor.c
int i2c_sensor_read(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len) {
    // 发设备地址+写位
    // 发寄存器地址
    // 发重复起始
    // 发设备地址+读位
    // 读len个字节
    // 发停止
    return 0;
}

int i2c_sensor_write(uint8_t dev_addr, uint8_t reg_addr, const uint8_t *data, uint16_t len) {
    // 发设备地址+写位
    // 发寄存器地址
    // 发len个数据字节
    // 发停止
    return 0;
}

SPI的封装类似,只是不需要设备地址,换成CS片选操作:

// spi_sensor.c
int spi_sensor_read(uint8_t cs_pin, uint8_t reg_addr, uint8_t *data, uint16_t len) {
    // 拉低CS
    // 发reg_addr(最高位置1表示读)
    // 发len个0x00,同时收数据
    // 拉高CS
    return 0;
}

int spi_sensor_write(uint8_t cs_pin, uint8_t reg_addr, const uint8_t *data, uint16_t len) {
    // 拉低CS
    // 发reg_addr(最高位清0表示写)
    // 发len个数据字节
    // 拉高CS
    return 0;
}

这样上层Sensor驱动调用时,只需要传入总线结构体指针,不用关心底层是I2C还是SPI。你想想看,换Sensor或者换总线,改一行初始化代码就行,多清爽。

核心思路:把总线操作抽象成read/write两个函数指针,驱动代码只操作寄存器地址和数据,不直接调HAL层API。这样移植性极强。

2.5 知识体系总览

下面这张图把I2C和SPI的核心知识点串起来了,方便你对照复习:

I2C/SPI总线知识体系 I2C总线 2线:SCL + SDA 7/10位地址 起始/停止条件 应答位(ACK/NACK) 读:重复起始 + 写→读切换 SPI总线 4线:SCLK/MOSI/MISO/CS 全双工通信 4种模式(CPOL/CPHA) 片选控制 读:发地址+发0x00收数据 对比 寄存器读写封装层 sensor_bus_t:read() / write() 函数指针 调用 调用 调试工具:逻辑分析仪 抓波形 → 检查时序 → 确认数据正确性 验证

这张图把I2C和SPI的核心要素、对比关系、封装层和调试工具都串起来了。你写驱动时,脑子里要有这张图,遇到问题就知道该查哪一块。

好了,I2C和SPI就讲到这里。记住:协议是死的,波形是活的。多抓包,多分析,比看一百页手册都管用。