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

各位同学,今天我们来聊聊I2C。这个协议在嵌入式系统里太常见了,传感器、EEPROM、ADC/DAC,十有八九都用它。我最早接触I2C是在一个温湿度采集项目里,当时觉得不就是两根线嘛,能有多复杂?结果被坑得挺惨。嗯,咱们今天就把I2C从头到尾捋一遍。

4.1 I2C物理层:就两根线,但别小看它

I2C总线就两根线:SDA(串行数据线)SCL(串行时钟线)。但这两根线是开漏输出,不是推挽输出。什么意思?说白了,芯片只能把线拉低,不能主动拉高。拉高靠的是外部的上拉电阻。

关键点:开漏结构决定了I2C的电气特性——谁先拉低谁说了算。这也是多主机仲裁的基础。

上拉电阻怎么选?我一般这么估算:

  • 标准模式(100kHz):4.7kΩ 左右
  • 快速模式(400kHz):2.2kΩ 左右
  • 高速模式(3.4MHz):1kΩ 左右

电阻太小,功耗大;电阻太大,上升沿太慢,信号会变形。我在一个项目里用过10kΩ上拉,结果400kHz下波形圆得像馒头,通信老是丢包。后来换成2.2kΩ,问题就解决了。

我的习惯:不确定的时候先焊4.7kΩ,用示波器看上升沿。如果上升时间超过SCL周期的10%,就换小一点的电阻。

4.2 起始条件与停止条件

I2C通信的开始和结束,靠的是SDA和SCL的时序配合。你想想看,总线空闲时,SDA和SCL都是高电平。怎么告诉从机“我要开始说话了”?

  • 起始条件(S):SCL为高时,SDA从高切换到低
  • 停止条件(P):SCL为高时,SDA从低切换到高

这两个条件都是唯一的。也就是说,在SCL高电平期间,SDA不允许变化。如果变了,要么是起始,要么是停止。我曾经见过一个新手工程师,在SCL高时不小心抖了一下SDA,结果从机以为收到了停止条件,直接终止了通信。嗯,调试了一下午才发现。

// 起始条件示例(伪代码)
void i2c_start(void) {
    SDA_HIGH();
    SCL_HIGH();
    delay();          // 保持一段时间
    SDA_LOW();        // SCL高时,SDA拉低 → 起始条件
    delay();
    SCL_LOW();        // 准备传输数据
}

4.3 地址与读写操作

起始条件之后,主机要发送7位从机地址(也有10位地址模式,但用得少)加1位读写位。总共8位,也就是一个字节。

  • 读写位 = 0:主机写数据到从机
  • 读写位 = 1:主机从从机读数据

每个字节后面跟着一个应答位(ACK)。从机收到地址后,如果匹配,就拉低SDA表示应答。如果不匹配,就保持高电平(NACK)。

注意:应答位是第9个时钟周期。很多初学者会忽略这个细节,导致通信失败。

举个例子,你要往地址0x50的EEPROM写一个字节:

  1. 发送起始条件
  2. 发送0xA0(0x50左移1位 + 写位0)
  3. 等待从机ACK
  4. 发送寄存器地址(比如0x00)
  5. 等待从机ACK
  6. 发送要写的数据(比如0x55)
  7. 等待从机ACK
  8. 发送停止条件

读操作稍微复杂一点,需要先写寄存器地址,再重新发送起始条件(这叫重复起始条件),然后发送读命令。我刚开始做的时候,忘了发重复起始条件,直接发了停止再发起始,结果从机状态机乱了。后来养成习惯:能用重复起始就别用停止+起始。

4.4 多主机仲裁

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

答案是仲裁。因为I2C是开漏结构,谁先拉低SDA,谁就赢了。仲裁发生在数据位级别:

  • 每个主机在发送数据位的同时,会监测SDA的实际电平
  • 如果自己发送的是1,但SDA被拉低了(说明另一个主机发了0),那它就输了
  • 输的主机立即释放总线,转为从机模式

避坑指南:我曾经在一个多主机项目里,两个主机发送的地址不同,仲裁时输的那个主机直接退出了。但问题在于,输的主机没有检测到自己的数据被覆盖,导致后续逻辑出错。解决办法是:每个主机在发送完地址后,必须检查是否赢得了仲裁。如果输了,要重新调度。

仲裁的规则其实很简单:低电平优先。所以从机地址的设计也有讲究——优先级高的设备,地址应该偏小(二进制低位多0)。

场景 主机A发送 主机B发送 SDA实际电平 仲裁结果
地址位 1 0 0 主机A输
地址位 0 0 0 平局,继续
数据位 1 1 1 平局,继续

多主机仲裁在实际项目中用得不多,但一旦用上,就要特别注意时序。我记得有个项目,两个主机都是MCU,一个发数据,一个发时钟。结果时钟线也被仲裁了,搞得整个总线乱套。后来加了总线管理协议,才稳定下来。

4.5 总结与经验

I2C协议看着简单,但坑不少。我总结几条实战经验:

  • 上拉电阻别省:内部上拉通常太弱,外部必须加
  • 时序要严格:起始/停止条件、应答位,一个都不能错
  • 多主机要谨慎:仲裁机制虽然完善,但软件设计要跟上
  • 示波器是好朋友:遇到通信问题,先看波形,别瞎猜

好了,I2C就讲到这里。下一章我们聊SPI,那个协议速度更快,但线也多。到时候再对比一下两者的适用场景。