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 读写操作流程
写寄存器:
- 主机发起始条件
- 发设备地址+写位(0),等应答
- 发寄存器地址,等应答
- 发数据字节,每字节等应答
- 发停止条件
读寄存器:
- 主机发起始条件
- 发设备地址+写位,等应答
- 发寄存器地址,等应答
- 发重复起始条件
- 发设备地址+读位(1),等应答
- 读数据字节,主机发NACK表示读完
- 发停止条件
嗯,这里要注意:读操作中间的重复起始条件不能省。我见过有人直接发停止再发起始,虽然也能用,但不符合规范,某些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 逻辑分析仪抓包实战
理论说再多,不如看波形。我建议每个做驱动的工程师,桌上常备一个逻辑分析仪。便宜的几十块,贵的几百,但都能用。
抓包步骤:
- 把SCL/SDA(或SCLK/MOSI/MISO/CS)接到逻辑分析仪通道上
- 设置采样率,至少4倍于总线频率
- 触发方式选下降沿(起始条件或CS拉低)
- 运行抓包,分析波形
看波形时,重点检查:
- 起始条件是否干净,有没有毛刺
- 地址字节是否正确,应答位有没有拉低
- 数据字节顺序对不对,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和SPI就讲到这里。记住:协议是死的,波形是活的。多抓包,多分析,比看一百页手册都管用。