4、I2C协议详解:从物理层到多主机仲裁

I2C协议,说实话,是我在嵌入式开发中用得最多的串行总线之一。它只需要两根线,就能挂一堆设备,省GPIO又省PCB面积。但很多新手栽就栽在“简单”二字上——觉得两根线嘛,能有多复杂?结果波形一抓,全是问题。

今天咱们就把I2C从物理层到仲裁机制彻底捋一遍。我保证,看完这章你再调I2C,心里会踏实很多。

4.1 I2C总线物理层:两根线撑起一个世界

I2C总线只有两条线:SCL(串行时钟线)和SDA(串行数据线)。但这两根线的电气特性,决定了整个协议的基调。

核心要点:SCL和SDA都是开漏输出结构,必须外接上拉电阻到VCC。总线空闲时,两根线都被上拉至高电平。

为什么用开漏?说白了,就是为了实现“线与”逻辑——多个设备可以同时拉低总线,而不会短路。你想想看,如果用的是推挽输出,两个设备一个想拉高一个想拉低,那不就冒烟了吗?

上拉电阻的取值很关键。我早期一个项目就吃过这个亏:

  • 电阻太小(如1kΩ):电流大,功耗高,但上升沿陡,适合高速模式
  • 电阻太大(如10kΩ):省电,但上升沿变缓,总线电容大时波形会“圆角”
  • 经验值:100kHz标准模式用4.7kΩ,400kHz快速模式用2.2kΩ,基本不会翻车

我的习惯:画原理图时,我会在SCL和SDA上各预留一个0402电阻位,先焊4.7kΩ。调试时用示波器看上升沿,如果太缓就换小一点的。别嫌麻烦,这一步能省后面很多排查时间。

总线电容也是个隐形杀手。I2C规范规定,标准模式下总线电容不得超过400pF。换算一下,大概就是1米左右的走线长度。超过这个值,波形就会畸变,通信就会出错。

4.2 起始条件与停止条件:总线的“发车”和“靠站”

I2C总线上所有的通信,都以起始条件(START)开始,以停止条件(STOP)结束。这两个信号是唯一允许SDA在SCL高电平时变化的情况——其他时候,SDA必须在SCL低电平时才能变化。

信号 SCL状态 SDA变化 时序图示意
起始条件(S) 高电平 从高→低 SCL高时,SDA下降沿
停止条件(P) 高电平 从低→高 SCL高时,SDA上升沿

我刚开始学的时候,总觉得这两个条件很抽象。后来在逻辑分析仪上亲眼看到波形,才恍然大悟——原来就是两个“特殊”的跳变沿。

我曾经踩过的坑:有一次写I2C驱动,在发送停止条件之前,不小心把SDA拉高了。结果从机一直认为总线还在忙,再也不响应了。后来查了半天,发现是停止条件的时序没满足——SDA必须在SCL高电平期间完成从低到高的跳变,中间不能有毛刺。

4.3 地址与读写位:找到你要聊的设备

起始条件之后,主机发送的第一个字节就是从机地址+读写位。格式如下:

| 7位地址 (bit7~bit1) | 读写位 (bit0) |
| 0 1 1 0 1 0 0       | 0 (写)       |
| 0 1 1 0 1 0 0       | 1 (读)       |
  • 7位地址:最多挂127个设备(地址0x00保留用于通用呼叫)
  • 读写位:0表示主机写从机,1表示主机读从机
  • 10位地址模式:扩展地址空间,但用得少,我做了这么多年也就碰到过两次

每个I2C设备都有一个固定的地址,通常由芯片型号决定,部分地址引脚可以配置。比如常见的AT24C02 EEPROM,地址是0x50~0x57,由A0/A1/A2引脚电平决定。

避坑指南:注意地址是左对齐的!比如设备地址0x50,实际发送的字节是0xA0(写)或0xA1(读)。很多新手直接发0x50,结果从机根本不响应。我习惯在代码里用宏定义:#define DEV_ADDR_W 0xA0,这样一眼就能看出是写地址。

4.4 ACK/NACK机制:握手的艺术

I2C的每个字节后面,都跟着一个应答位(ACK/NACK)。这是协议可靠性的基石。

工作机制很简单:

  1. 发送方在第9个SCL时钟周期释放SDA(拉高)
  2. 接收方如果拉低SDA,表示ACK(应答成功)
  3. 接收方如果保持SDA高,表示NACK(不应答)

什么情况下会出现NACK?我总结了几种常见场景:

场景 原因 处理方式
地址不匹配 总线上没有该地址的设备 主机应发送停止条件,重试或报错
从机忙 从机正在处理内部操作(如EEPROM写操作) 主机等待后重试
读操作结束 主机读完最后一个字节后发送NACK 这是正常流程,告诉从机“别发了”
从机不支持该命令 寄存器地址或命令无效 检查数据手册,确认命令格式

关键点:读操作的最后,主机必须发送NACK,然后跟停止条件。如果主机发了ACK,从机会继续发送下一个字节,导致总线卡死。这个细节我见过好几个人翻车。

4.5 多主机仲裁:谁抢到总线谁说话

I2C支持多个主机挂在同一条总线上。但同一时刻只能有一个主机控制总线。这就引出了仲裁(Arbitration)机制。

仲裁的原理,说白了就是“谁先拉低SDA,谁就赢”。

具体过程是这样的:

  1. 多个主机同时发送起始条件
  2. 它们各自在SDA上输出自己的地址+读写位
  3. 每个主机在发送每个bit后,都会读取SDA的实际电平
  4. 如果某个主机发送了高电平,但读到的是低电平,说明有其他主机在拉低总线
  5. 这个主机就知道自己“仲裁失败”了,立即释放总线,转为从机模式

我画了一张图,帮你理解仲裁的流程:

I2C多主机仲裁流程图 多个主机同时发送起始条件 各自在SDA上发送地址位 每个主机比较“发送的电平”与“读到的电平” 仲裁失败:释放总线,转为从机 仲裁成功:继续控制总线 检测总线空闲后,重新发起传输 继续发送数据,直到停止条件 仲裁不会丢失数据:失败的从机在释放总线前,已经发送的bit与获胜方一致,所以数据是完整的

仲裁有几个重要特点:

  • 无损仲裁:仲裁失败的主机不会丢失已经发送的数据,因为失败的bit之前的所有bit都与获胜方一致
  • SCL由获胜方控制:仲裁只发生在SDA上,SCL由所有主机共同“线与”驱动
  • 地址匹配的从机不受影响:从机只关心自己的地址是否被匹配,不关心谁赢了仲裁

我曾经遇到的一个真实案例:在一个多主机的系统中,两个主机偶尔会同时访问同一个从机。按理说仲裁机制能处理,但其中一个主机的驱动写得有问题——仲裁失败后没有释放SCL,导致SCL一直被拉低,整个总线瘫痪。后来我在驱动里加了超时检测,如果SCL被拉低超过10ms,就强制复位总线。这个经验后来被我写进了公司的I2C驱动规范里。

4.6 总结:I2C协议的精髓

回顾一下,I2C协议的核心就这几件事:

  • 物理层:开漏+上拉,实现线与逻辑
  • 起始/停止:SCL高电平时SDA的特殊跳变
  • 地址+读写:7位地址左对齐,读写位决定数据方向
  • ACK/NACK:第9个时钟的握手信号
  • 多主机仲裁:谁先拉低SDA谁赢,无损切换

嗯,这些内容看起来多,但只要你亲手抓一次波形,跑一遍逻辑分析仪,就全通了。我当年就是在示波器前蹲了一下午,把起始条件、ACK、停止条件一个个对出来,才真正理解I2C的。