I2C协议详解:从物理层到数据帧

I2C协议,说白了就是一根时钟线加一根数据线,把一堆芯片串起来聊天的办法。我最早接触I2C是在一个步进电机驱动项目里,当时觉得这东西简单,结果被地址冲突坑了一把——嗯,后面我会细说。

I2C总线的物理层

I2C总线只有两条线:SCL(串行时钟线)和SDA(串行数据线)。所有设备都挂在这两条线上,主设备负责产生时钟,从设备响应数据。

物理层有几个关键点:

  • 开漏输出:SCL和SDA都是开漏结构,只能拉低,不能主动拉高。高电平靠上拉电阻实现。
  • 上拉电阻:阻值一般在1kΩ~10kΩ之间。我习惯用4.7kΩ,除非总线电容特别大才换2.2kΩ。
  • 线与逻辑:多个设备可以同时拉低总线,谁先拉低谁占主导。这是仲裁的基础。
  • 最大电容:标准模式下总线电容不能超过400pF。线长了、设备多了,信号就变形。
经验之谈:我曾经在一个项目里把I2C总线拉了30cm的排线,结果通信时好时坏。后来加上4.7kΩ上拉电阻,把速率降到50kHz才稳定。所以,I2C不适合远距离传输,板内通信才是它的主场。

起始条件与停止条件

I2C总线的数据传输,由两个特殊信号控制:

信号 SCL状态 SDA变化 说明
起始条件(S) 高电平 从高到低跳变 总线从空闲变为忙碌
停止条件(P) 高电平 从低到高跳变 总线从忙碌变为空闲

你想想看,为什么要在SCL高电平时检测SDA的变化?因为数据采样也是在SCL高电平时进行的。如果SDA在SCL高电平时变化,就会被误认为数据位。所以起始和停止条件故意选在SCL高电平时改变SDA,就是为了和普通数据位区分开。

注意:起始条件之后,总线就被主设备占用了。如果主设备不发送停止条件,从设备会一直等待。我曾经见过一个同事在调试时忘了发停止条件,结果从设备卡死在半路,怎么复位都不管用——最后发现是代码里少了一行。

数据帧格式

I2C的数据帧格式很规整。每个字节8位,高位在前(MSB first)。每发完一个字节,从设备必须回复一个ACK位。

完整的数据传输流程是这样的:

  1. 主设备发送起始条件
  2. 主设备发送7位从设备地址 + 1位读写位
  3. 从设备回复ACK
  4. 主设备发送/接收数据字节(每字节后跟ACK)
  5. 主设备发送停止条件

这里有个细节:ACK位是接收方在第9个SCL周期拉低SDA。如果接收方没拉低,就表示NACK。NACK通常意味着:地址不对、设备忙、或者数据接收完毕。

核心要点:I2C的每个字节后面必须跟ACK/NACK。没有ACK,数据就不算传完。这是协议层面的硬性要求,不是可选项。

地址与读写位

I2C的7位地址,最多支持112个设备(保留地址占16个)。地址由设备厂商固定,但有些芯片可以通过引脚配置。

读写位紧跟在地址后面:

  • 0:主设备写数据到从设备
  • 1:主设备从从设备读数据

举个例子:一个I2C温度传感器的地址是0x48(二进制1001000)。如果要读它的温度值,主设备先发0x48左移1位+读位1,也就是0x91。从设备匹配到地址后回复ACK,然后开始传数据。

我遇到过最头疼的问题是什么?地址冲突。两个不同型号的传感器用了同一个地址,挂在同一根总线上。解决办法要么换芯片,要么加一个I2C多路复用器(比如PCA9548)。从那以后,我选型时第一件事就是看地址能不能改。

I2C协议核心流程

下面这张图把I2C的完整通信流程画出来了,从起始到停止,每一步都标清楚了:

I2C 通信流程 起始条件 (S) 地址 + R/W 从设备 ACK 数据字节 还有数据? 是 停止条件 (P) 否 说明: • 起始条件后,总线被主设备占用,直到停止条件释放 • 每个数据字节后必须跟ACK/NACK,否则通信异常

常见问题与避坑指南

做I2C开发,有几个坑我几乎每次都会遇到:

  • 总线锁死:从设备拉低SDA不放,主设备无法产生起始条件。解决办法是给SCL发9个时钟脉冲,让从设备复位。
  • 上拉电阻选错:阻值太大,信号上升沿太慢;阻值太小,功耗高且可能拉不低。我一般先按4.7kΩ试,不行再调。
  • 地址冲突:两个设备地址一样,谁都不理你。选型时一定要确认地址是否可配置。
  • 速率不匹配:主设备跑400kHz,从设备只支持100kHz,通信就会出错。降速到100kHz最保险。
曾经踩过的坑:有一次我在电机驱动板上用了I2C,总线长度不到5cm,结果还是偶尔通信失败。查了半天,发现是电机启动时的大电流干扰了SDA信号。最后在SDA和SCL上各加了一个100pF的对地电容,问题解决。所以,I2C虽然简单,但在电机控制这种强干扰环境下,滤波和布局都不能马虎。