2. I2C协议深度解析:从物理层到仲裁机制

I2C协议,说实话,是嵌入式开发里最常用的串行总线之一。我做了这么多年驱动,跟它打交道最多。它只有两根线,却能挂一堆设备,这个设计本身就很有智慧。

咱们这一章,就把I2C从物理层到仲裁机制彻底讲透。你以后写驱动、调bug,心里就有底了。

2.1 I2C总线物理层:两根线的艺术

I2C总线就两根线:SCL(时钟线)和SDA(数据线)。所有设备都挂在这两根线上。

这两根线都是开漏输出结构。什么意思?说白了,设备只能把线拉低,不能主动拉高。拉高靠的是上拉电阻。

关键点:空闲时,SCL和SDA都是高电平。谁要通信,谁就把线拉低。

上拉电阻的取值有讲究。我早期一个项目,选了10kΩ上拉,总线长度半米,结果通信老出错。后来换成4.7kΩ,问题就解决了。

总线电容 推荐上拉电阻 最大速率
< 100 pF 10 kΩ 100 kHz
100 - 200 pF 4.7 kΩ 400 kHz
200 - 400 pF 2.2 kΩ 1 MHz

注意:上拉电阻太小,功耗大;太大,信号上升沿变缓,高速通信会失败。我一般先按4.7kΩ起步,再根据波形微调。

2.2 起始条件与停止条件

I2C通信的开始和结束,有专门的信号。这两个信号是唯一允许SDA在SCL高电平时变化的情况。

  • 起始条件(S):SCL高电平时,SDA从高变低。
  • 停止条件(P):SCL高电平时,SDA从低变高。

你想想看,如果SCL低电平时SDA变了,那是在传数据。只有SCL高电平时SDA跳变,才表示开始或结束。这个设计很巧妙,不会混淆。

经验:我调试时,习惯用逻辑分析仪抓起始和停止条件。如果波形里这两个信号不干净,基本可以断定是上拉电阻或时序问题。

2.3 数据帧格式:字节与应答

I2C的数据传输,以字节为单位。每传一个字节,后面跟一个应答位(ACK)。

流程是这样的:

  1. 主机发送起始条件
  2. 主机发送7位从机地址 + 1位读写位(0写1读)
  3. 从机在第9个SCL周期拉低SDA,表示应答(ACK)
  4. 主机发送/接收数据字节,每字节后跟应答
  5. 主机发送停止条件

应答位是I2C的可靠性保障。如果从机没拉低SDA(即SDA保持高),那就是NACK,表示从机没准备好或地址不对。

我遇到过:一次从机地址写错了,主机发地址后一直收不到ACK。逻辑分析仪一看,SDA在第9个时钟还是高。改地址后就好了。所以,检查ACK是调试的第一步。

2.4 7位地址与10位地址

I2C地址有两种格式:7位和10位。7位地址最多挂127个设备(地址0x00保留),实际够用。但有些场景设备多,就需要10位地址。

10位地址怎么传?它用两个字节:

  • 第一个字节:1111 0XX + R/W(XX是10位地址的高2位)
  • 第二个字节:10位地址的低8位

说白了,就是用一个特殊的前缀来标识这是10位地址。从机看到1111 0开头,就知道后面还有一字节地址。

地址类型 地址范围 最大设备数 应用场景
7位地址 0x08 - 0x77 112 大多数传感器、EEPROM
10位地址 0x00 - 0x3FF 1024 复杂系统、多设备总线

建议:除非必要,优先用7位地址。10位地址的驱动实现稍复杂,而且不是所有从机都支持。

2.5 时钟同步与仲裁

这两个机制是I2C多主机通信的核心。我当年第一次接触时,觉得挺绕,但弄懂了就觉得设计得很精妙。

时钟同步

多个主机同时发时钟时,SCL的低电平时间由最长的那个决定,高电平时间由最短的那个决定。结果就是,所有主机的时钟被同步到一个共同的节奏上。

为什么会这样?因为开漏结构。只要有一个主机拉低SCL,SCL就是低。只有所有主机都释放SCL,SCL才会变高。所以,慢设备会拖慢总线。

仲裁

多个主机同时发数据时,谁先拉低SDA,谁就赢得仲裁。输的那个主机,必须退出并重试。

仲裁过程是无损的。赢的主机继续通信,输的检测到SDA与自己发出的不一致,就知道输了,立即释放总线。

注意:仲裁只在数据位上进行。起始条件和停止条件不参与仲裁。如果两个主机同时发起始条件,那就会产生一个合法的起始条件,没问题。

我有个项目,两个MCU通过I2C通信,偶尔数据错乱。查了半天,发现是仲裁逻辑没处理好。一个主机发完数据后,没等总线空闲就又开始发,导致冲突。后来加了总线空闲检测,问题解决。

2.6 知识体系总览

下面这张图,把I2C协议的核心知识点串起来了。你可以把它当一张地图,哪里不懂就回来看。

I2C协议知识体系 I2C协议知识体系 物理层 SCL + SDA 开漏输出 上拉电阻 4.7kΩ 起始/停止条件 S: SCL高→SDA下降沿 P: SCL高→SDA上升沿 数据帧格式 地址 + R/W + ACK 数据字节 + ACK/NACK 地址格式 7位地址: 0x08-0x77 10位地址: 0x00-0x3FF 时钟同步与仲裁 时钟同步: 慢设备拖节奏 仲裁: 先拉低SDA者赢 调试要点 1. 检查ACK 2. 抓起始/停止 3. 确认上拉电阻 4. 仲裁冲突处理

2.7 避坑指南

最后,分享几个我踩过的坑:

  • 上拉电阻选型:别照搬参考设计。根据你的总线长度和速率,实际测试一下波形。
  • 地址冲突:两个从机地址一样,总线就乱了。选型时注意地址可配置性。
  • 时钟延展:有些从机处理慢,会拉低SCL让主机等。驱动里要处理这种情况,否则会超时。
  • 总线锁死:从机异常时可能一直拉低SDA。我遇到过,后来加了超时复位逻辑才解决。

我的习惯:每个I2C驱动都加一个超时参数。默认100ms,如果总线没响应,就复位从机或重试。这个习惯救了我好几次。