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写一个字节:
- 发送起始条件
- 发送0xA0(0x50左移1位 + 写位0)
- 等待从机ACK
- 发送寄存器地址(比如0x00)
- 等待从机ACK
- 发送要写的数据(比如0x55)
- 等待从机ACK
- 发送停止条件
读操作稍微复杂一点,需要先写寄存器地址,再重新发送起始条件(这叫重复起始条件),然后发送读命令。我刚开始做的时候,忘了发重复起始条件,直接发了停止再发起始,结果从机状态机乱了。后来养成习惯:能用重复起始就别用停止+起始。
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,那个协议速度更快,但线也多。到时候再对比一下两者的适用场景。