4. I2C总线协议:从物理层到多主机仲裁

各位同学,今天我们来聊聊I2C。这个协议在嵌入式开发里太常见了,传感器、EEPROM、ADC/DAC,十有八九都用它。我最早接触I2C是在一个温湿度采集项目上,当时被时钟拉伸坑了一整天。嗯,咱们今天就把它彻底讲透。

4.1 I2C物理层:就两根线

I2C总线物理上就两根线:SCL(时钟线)SDA(数据线)。这两根线都是开漏输出,外部要接上拉电阻。为什么用开漏?说白了,就是为了实现多设备共用总线——任何设备都能把线拉低,但谁都不能主动拉高。

上拉电阻的取值有讲究。我习惯用4.7kΩ,标准模式100kHz没问题。如果总线电容大、设备多,可以换成2.2kΩ。但别太小,否则功耗会上去。你想想看,每个低电平都在消耗电流。

关键参数速查:

  • 标准模式:100kHz
  • 快速模式:400kHz
  • 高速模式:3.4MHz
  • 上拉电阻:1kΩ ~ 10kΩ(常用4.7kΩ)
  • 总线电容:不超过400pF

4.2 起始条件和停止条件

I2C通信的开始和结束,靠的是SCL和SDA的特定电平变化。

起始条件(START):SCL为高电平时,SDA从高切换到低。这个动作告诉所有从机:「注意,要开始通信了!」

停止条件(STOP):SCL为高电平时,SDA从低切换到高。从机收到这个信号就知道:「好了,主机要收工了。」

我曾经在调试时遇到一个奇怪问题:每次通信到一半就卡死。查了半天,发现是起始条件没满足时序要求——SDA下降沿不够陡。后来在SDA上加了个小电容整形,问题就解决了。

注意:起始和停止条件必须由主机产生。从机不能主动发起或终止通信。这是I2C的基本规则。

4.3 设备地址:7位还是10位?

起始条件之后,主机要发送设备地址。标准I2C用7位地址,最多挂127个设备。后来觉得不够用,又加了10位地址模式。

7位地址的格式是这样的:

起始条件 + 7位地址 + 读/写位 + 应答位

举个例子,假设传感器地址是0x68(二进制1101000):

  • 写操作:发送 0xD0(1101000 + 0)
  • 读操作:发送 0xD1(1101000 + 1)

很多传感器数据手册会给7位地址,但实际发送时要左移一位。我见过新手直接拿7位地址当8位发,结果设备死活不应答。嗯,这个坑我当年也踩过。

小技巧:写驱动时,我习惯把设备地址定义成7位值,然后在发送时手动左移。这样代码更清晰,不容易搞混。

4.4 读写时序:一步一步来

I2C的读写操作,说白了就是主机和从机之间的一问一答。

写操作时序

主机发送:START + 从机地址(W) + ACK + 寄存器地址 + ACK + 数据 + ACK + STOP

每一步都要等从机应答。如果从机没拉低SDA(即NACK),说明出问题了。我一般会在驱动里加超时处理,防止死等。

读操作时序

读操作稍微复杂点,要先写后读:

主机发送:START + 从机地址(W) + ACK + 寄存器地址 + ACK + 
          RESTART + 从机地址(R) + ACK + 读取数据 + NACK + STOP

注意最后那个NACK。读最后一个字节时,主机要主动发送NACK,告诉从机「够了,别发了」。然后发STOP结束通信。

我记得第一次写MPU6050驱动时,读数据总是多一个字节。后来才发现是没发NACK,从机以为我还要继续读。

4.5 时钟拉伸:从机的「慢一点」信号

时钟拉伸是I2C里一个容易被忽略的细节。当从机还没准备好数据时,它会主动拉低SCL线,告诉主机:「等等,我还没搞定。」

主机检测到SCL被拉低,就要停止时钟,直到从机释放SCL。这个机制在慢速传感器上很常见。

我曾经用STM32的硬件I2C读一个气压传感器,死活读不到数据。示波器一看,SCL被从机拉低了,但主机的I2C外设没处理时钟拉伸。后来换成软件模拟I2C,手动处理时钟拉伸,问题就解决了。

避坑指南:有些MCU的硬件I2C不支持时钟拉伸,或者支持得不完整。选型时一定要看数据手册。如果要用硬件I2C,先确认它能不能正确处理SCL被拉低的情况。

4.6 多主机仲裁:谁抢到算谁的

I2C支持多个主机共享总线。但同一时间只能有一个主机控制总线。如果两个主机同时发起通信,怎么办?

答案是:仲裁

仲裁的规则很简单:谁先拉低SDA,谁就赢。另一个主机检测到SDA电平和自己想发的不一致,就知道自己输了,自动退出。

举个例子:

  • 主机A想发「1」(高电平)
  • 主机B想发「0」(低电平)
  • 结果SDA被B拉低,A检测到冲突,退出

仲裁过程中不会丢失数据,因为赢的那个主机会继续完成通信。输的那个等总线空闲了再重试。

我实际项目中很少用多主机模式,因为太复杂了。但如果你要做冗余设计或者热插拔,这个机制就很有用。

多主机仲裁要点:

  • 仲裁在SDA线上进行,SCL由所有主机共同驱动
  • 仲裁不破坏数据,输的主机自动退出
  • 每个主机在发送前要检测总线是否空闲
  • 建议加总线空闲超时,防止死锁

4.7 实战经验总结

讲了这么多,我给大家总结几条实战经验:

  1. 上拉电阻别省。我见过有人用MCU内部上拉,结果总线波形惨不忍睹。老老实实加外部上拉电阻。
  2. 时序要留余量。别卡着极限频率跑。400kHz的快速模式,我一般跑300kHz左右,稳定第一。
  3. 加超时处理。I2C通信可能因为从机死机、总线冲突等原因卡死。驱动里一定要加超时,别让系统死等。
  4. 示波器是好朋友。遇到I2C问题,先上示波器看波形。看SCL和SDA的时序对不对,应答位有没有,时钟拉伸正不正常。
  5. 软件模拟更可控。如果硬件I2C搞不定,别死磕。软件模拟I2C虽然慢点,但每个细节都在你掌控之中。

好了,I2C协议就讲到这里。下一章我们讲SPI,那个速度更快,但线也更多。到时候再聊。