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 实战经验总结
讲了这么多,我给大家总结几条实战经验:
- 上拉电阻别省。我见过有人用MCU内部上拉,结果总线波形惨不忍睹。老老实实加外部上拉电阻。
- 时序要留余量。别卡着极限频率跑。400kHz的快速模式,我一般跑300kHz左右,稳定第一。
- 加超时处理。I2C通信可能因为从机死机、总线冲突等原因卡死。驱动里一定要加超时,别让系统死等。
- 示波器是好朋友。遇到I2C问题,先上示波器看波形。看SCL和SDA的时序对不对,应答位有没有,时钟拉伸正不正常。
- 软件模拟更可控。如果硬件I2C搞不定,别死磕。软件模拟I2C虽然慢点,但每个细节都在你掌控之中。
好了,I2C协议就讲到这里。下一章我们讲SPI,那个速度更快,但线也更多。到时候再聊。