2、I2C总线协议详解:物理层、时序、起始/停止条件、数据帧格式

各位同学,今天我们聊聊I2C总线。说实话,在嵌入式系统里,I2C是我用得最多的串行通信协议之一。为什么?因为它简单、省引脚,而且几乎所有的传感器、EEPROM、RTC芯片都支持它。我记得刚入行那会儿,第一次用I2C去读一个温度传感器,折腾了一下午没读出数据,后来发现是上拉电阻没焊……嗯,这种坑,你们以后也会遇到的。

2.1 物理层:两根线搞定一切

I2C总线只需要两根线:SCL(时钟线)SDA(数据线)。你想想看,一个总线上可以挂几十个设备,每个设备都有自己的地址,主机通过地址来寻址。这比SPI那种一主多从还要多几条片选线的方案,省了不少引脚。

物理层有几个关键点,我挑重要的说:

  • 开漏输出:SCL和SDA都是开漏结构。这意味着它们只能拉低,不能主动拉高。拉高靠的是外部的上拉电阻。所以,I2C总线必须要有上拉电阻,一般4.7kΩ到10kΩ之间,具体看总线电容和速率。
  • 线与逻辑:因为开漏,多个设备可以同时拉低总线。如果有一个设备拉低了,总线就是低电平。这就是“线与”逻辑。仲裁就是靠这个实现的。
  • 上拉电阻的选择:我个人习惯,标准模式(100kHz)用10kΩ,快速模式(400kHz)用4.7kΩ。如果总线太长或者设备太多,电阻要适当减小。我曾经在一个项目里,挂了8个I2C设备,总线长度超过30cm,结果用10kΩ电阻死活通信不稳定,换成2.2kΩ就好了。

重要提醒:I2C总线的最大电容负载是400pF(标准模式)。超过这个值,信号边沿会变缓,导致通信错误。设计时一定要估算总线电容。

2.2 时序:时钟是怎么工作的

I2C的时序其实不复杂。主机负责产生时钟信号SCL,从机只是被动地响应。数据在SCL的低电平时变化,在高电平时采样。说白了,就是时钟上升沿的时候,数据必须是稳定的。

这里有几个时序参数,你们写驱动的时候会用到:

参数 标准模式(100kHz) 快速模式(400kHz) 说明
f_SCL 100 kHz 400 kHz 时钟频率
t_HD;STA 4.0 μs 0.6 μs 起始条件保持时间
t_SU;STO 4.0 μs 0.6 μs 停止条件建立时间
t_HD;DAT 0 μs 0 μs 数据保持时间
t_SU;DAT 250 ns 100 ns 数据建立时间

这些参数在写底层驱动时,尤其是用GPIO模拟I2C时,一定要心里有数。我曾经用GPIO模拟I2C去读一个加速度计,延时没给够,结果读出来的数据全是0xFF。后来查了数据手册,发现那个芯片对建立时间要求比较严格。

2.3 起始条件和停止条件

这两个条件是I2C通信的“开关”。没有它们,总线上的设备不知道什么时候开始、什么时候结束。

  • 起始条件(S):SCL为高电平时,SDA从高电平切换到低电平。这个信号告诉所有从机:“注意,要开始通信了!”
  • 停止条件(P):SCL为高电平时,SDA从低电平切换到高电平。这个信号告诉所有从机:“通信结束,释放总线。”

为什么要在SCL高电平时切换?因为数据在SCL高电平时是稳定的,这时候SDA的变化才具有“控制”意义。你想想看,如果SCL低电平时SDA变化,那只是正常的数据传输,不是起始或停止。

个人经验:我建议在写驱动时,起始条件和停止条件的代码单独封装成函数。这样调试的时候,可以在每个函数入口加个打印,方便定位问题。我曾经在一个项目里,就是因为起始条件没拉稳,导致总线一直卡在“忙”状态。

2.4 数据帧格式:字节传输与应答

I2C的数据传输是以字节为单位的。每个字节8位,后面跟着一个应答位(ACK/NACK)。所以实际上每传输一个有效字节,总线上要跑9个时钟周期。

数据帧的格式如下:

起始条件 | 7位从机地址 + R/W位 | 应答位 | 数据字节1 | 应答位 | ... | 数据字节N | 应答位 | 停止条件

这里有几个细节要注意:

  • 地址与读写位:主机先发送7位从机地址,然后第8位是读写位(0表示写,1表示读)。所以实际上地址字节是8位。
  • 应答机制:每个字节传输完后,接收方必须拉低SDA表示应答(ACK)。如果接收方不拉低(即SDA保持高电平),那就是非应答(NACK)。主机收到NACK后,通常会发送停止条件结束通信。
  • 数据方向:写操作时,主机发送数据,从机应答;读操作时,从机发送数据,主机应答。主机在读完最后一个字节后,应该发送NACK,告诉从机“不要再发了”。

避坑指南:我曾经在一个项目里,读一个EEPROM时,主机在最后一个字节后忘了发NACK,结果从机继续发数据,导致总线冲突。从那以后,我每次写读操作,都会在循环里判断是不是最后一个字节,如果是,就配置主机发送NACK。

2.5 实际应用中的注意事项

说了这么多理论,最后分享几个实战中的坑:

  1. 总线锁死:如果从机在某个时刻异常拉低了SCL或SDA,主机可能无法再产生起始条件。解决办法是:在初始化时,先产生9个时钟脉冲,让从机复位状态机。我习惯在每次通信前都做这个操作。
  2. 时钟拉伸:有些从机(比如某些传感器)在收到数据后,需要时间处理,会主动拉低SCL,告诉主机“慢点,我还没准备好”。主机必须检测SCL是否被拉低,如果是,就等待。很多初学者写的I2C驱动没有处理时钟拉伸,导致通信不稳定。
  3. 多主机仲裁:如果总线上有多个主机同时发起通信,它们会通过“线与”逻辑进行仲裁。谁先拉低SDA,谁就赢得总线。丢失仲裁的主机必须释放总线,等待下次机会。

好了,I2C的物理层和协议层就讲到这里。下一节我们会动手写一个I2C驱动,到时候你们会发现,理解了这些底层细节,写驱动其实很简单。