3. I2C总线详解:时序、寻址、读写操作与多主机仲裁
各位同学,今天我们来啃一块硬骨头——I2C总线。说实话,我入行那会儿,第一次调I2C驱动,被时序折腾得够呛。示波器一挂上去,波形乱七八糟,愣是调了两天才跑通。后来我才明白,I2C这东西,看着协议简单,但细节里全是坑。
I2C全称是Inter-Integrated Circuit,飞利浦公司发明的。它只用两根线——SCL(时钟)和SDA(数据),就能挂一堆设备。你想想看,这得多省IO口?我做过一个项目,主控只有8个GPIO,要接温度、湿度、气压、加速度计四个传感器,不用I2C根本搞不定。
3.1 I2C总线物理层
先说说物理层。I2C总线是开漏输出,加上拉电阻。什么意思呢?就是芯片只能把总线拉低,不能主动拉高。高电平全靠上拉电阻提供。嗯,这里要注意:上拉电阻的阻值很关键。
I2C总线上每个设备都有唯一地址。7位地址模式最多挂127个设备,10位地址模式能挂1023个。不过实际项目中,7位地址就够用了。我很少见到谁真挂上百个传感器。
3.2 I2C时序详解
时序是I2C的核心。说白了,就是SCL和SDA两根线在什么时间点该干什么事。我画了一张时序图,你们看看:
时序里最核心的就是起始条件和停止条件。起始条件是SCL高电平时,SDA从高变低。停止条件是SCL高电平时,SDA从低变高。这两个条件之外的任何SDA变化都不算数。
3.3 I2C寻址机制
I2C寻址分7位和10位两种。7位地址最常见。每个从机设备出厂时,厂家会分配一个固定地址,比如MPU6050的地址是0x68。但有些芯片会留出几个引脚,让你自己配置地址位。
举个例子,温度传感器LM75的地址是0x48~0x4F,具体取决于A0、A1、A2三个引脚的电平。我做过一个项目,用了8个LM75,每个引脚配置不同,地址从0x48到0x4F全用上了。这样一根I2C总线上就能挂8个温度传感器。
寻址过程是这样的:主机发送起始条件后,先发7位从机地址,再加1位读写位(0表示写,1表示读)。然后等待从机应答。如果从机拉低SDA,表示应答(ACK),否则表示非应答(NACK)。
3.4 I2C读写操作
读写操作是I2C协议的核心。我直接上代码,你们看看实际怎么写:
// I2C写操作示例(伪代码)
// 向从机地址0x68的寄存器0x3B写入数据0x01
void i2c_write_byte(uint8_t slave_addr, uint8_t reg_addr, uint8_t data) {
i2c_start(); // 发送起始条件
i2c_send_byte(slave_addr << 1 | 0); // 发送从机地址 + 写位
i2c_wait_ack(); // 等待从机应答
i2c_send_byte(reg_addr); // 发送寄存器地址
i2c_wait_ack(); // 等待从机应答
i2c_send_byte(data); // 发送数据
i2c_wait_ack(); // 等待从机应答
i2c_stop(); // 发送停止条件
}
// I2C读操作示例(伪代码)
// 从从机地址0x68的寄存器0x3B读取1字节数据
uint8_t i2c_read_byte(uint8_t slave_addr, uint8_t reg_addr) {
uint8_t data;
// 先写寄存器地址
i2c_start(); // 发送起始条件
i2c_send_byte(slave_addr << 1 | 0); // 发送从机地址 + 写位
i2c_wait_ack(); // 等待从机应答
i2c_send_byte(reg_addr); // 发送寄存器地址
i2c_wait_ack(); // 等待从机应答
// 再读数据(重复起始条件)
i2c_start(); // 发送重复起始条件
i2c_send_byte(slave_addr << 1 | 1); // 发送从机地址 + 读位
i2c_wait_ack(); // 等待从机应答
data = i2c_read_byte(); // 读取数据
i2c_send_nack(); // 发送非应答(表示读完了)
i2c_stop(); // 发送停止条件
return data;
}
读操作里有个关键点——重复起始条件。为什么不用停止条件再发起始条件?因为这样会释放总线,其他主机可能抢占总线。重复起始条件保证整个操作是原子的,不会被中断。
3.5 多主机仲裁
多主机仲裁是I2C最精妙的设计。多个主机可以同时挂在总线上,谁先拉低SDA,谁就赢得仲裁。输的那个主机自动退出,不破坏总线状态。
仲裁原理很简单:每个主机在发送数据时,会同时监控SDA电平。如果自己发送的是高电平,但检测到SDA是低电平,说明有其他主机在拉低总线。这时候,这个主机就输了,立即停止发送。
我画个流程图,你们看看仲裁过程:
仲裁过程中,SCL时钟是由所有主机共同驱动的。每个主机都会输出自己的时钟,但最终SCL的低电平时间由最长的那个决定,高电平时间由最短的那个决定。嗯,这个细节很多人不知道。
3.6 实际项目中的I2C调试技巧
最后,我分享几个实战中总结的调试技巧:
- 用示波器抓波形:别光看代码,波形会告诉你真相。我习惯抓起始条件、地址、ACK三个关键点。
- 注意总线电容:长距离走线或挂太多设备,总线电容会变大,导致波形畸变。这时候要么降低速率,要么换小阻值上拉电阻。
- 处理NACK:从机不应答时,主机要能正确处理。我见过不少代码,NACK后直接死循环,导致系统卡死。
- 超时机制:I2C总线可能被从机拉死(比如从机死机一直拉低SCL)。加个超时机制,超时后复位总线。
好了,I2C总线的核心内容就这些。时序、寻址、读写、仲裁,每个点都值得你花时间吃透。下次调I2C驱动时,记得先看波形,再查代码。波形对了,代码基本没问题。
可可精品资料库