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)。这是协议可靠性的基石。
工作机制很简单:
- 发送方在第9个SCL时钟周期释放SDA(拉高)
- 接收方如果拉低SDA,表示ACK(应答成功)
- 接收方如果保持SDA高,表示NACK(不应答)
什么情况下会出现NACK?我总结了几种常见场景:
| 场景 | 原因 | 处理方式 |
|---|---|---|
| 地址不匹配 | 总线上没有该地址的设备 | 主机应发送停止条件,重试或报错 |
| 从机忙 | 从机正在处理内部操作(如EEPROM写操作) | 主机等待后重试 |
| 读操作结束 | 主机读完最后一个字节后发送NACK | 这是正常流程,告诉从机“别发了” |
| 从机不支持该命令 | 寄存器地址或命令无效 | 检查数据手册,确认命令格式 |
关键点:读操作的最后,主机必须发送NACK,然后跟停止条件。如果主机发了ACK,从机会继续发送下一个字节,导致总线卡死。这个细节我见过好几个人翻车。
4.5 多主机仲裁:谁抢到总线谁说话
I2C支持多个主机挂在同一条总线上。但同一时刻只能有一个主机控制总线。这就引出了仲裁(Arbitration)机制。
仲裁的原理,说白了就是“谁先拉低SDA,谁就赢”。
具体过程是这样的:
- 多个主机同时发送起始条件
- 它们各自在SDA上输出自己的地址+读写位
- 每个主机在发送每个bit后,都会读取SDA的实际电平
- 如果某个主机发送了高电平,但读到的是低电平,说明有其他主机在拉低总线
- 这个主机就知道自己“仲裁失败”了,立即释放总线,转为从机模式
我画了一张图,帮你理解仲裁的流程:
仲裁有几个重要特点:
- 无损仲裁:仲裁失败的主机不会丢失已经发送的数据,因为失败的bit之前的所有bit都与获胜方一致
- SCL由获胜方控制:仲裁只发生在SDA上,SCL由所有主机共同“线与”驱动
- 地址匹配的从机不受影响:从机只关心自己的地址是否被匹配,不关心谁赢了仲裁
我曾经遇到的一个真实案例:在一个多主机的系统中,两个主机偶尔会同时访问同一个从机。按理说仲裁机制能处理,但其中一个主机的驱动写得有问题——仲裁失败后没有释放SCL,导致SCL一直被拉低,整个总线瘫痪。后来我在驱动里加了超时检测,如果SCL被拉低超过10ms,就强制复位总线。这个经验后来被我写进了公司的I2C驱动规范里。
4.6 总结:I2C协议的精髓
回顾一下,I2C协议的核心就这几件事:
- 物理层:开漏+上拉,实现线与逻辑
- 起始/停止:SCL高电平时SDA的特殊跳变
- 地址+读写:7位地址左对齐,读写位决定数据方向
- ACK/NACK:第9个时钟的握手信号
- 多主机仲裁:谁先拉低SDA谁赢,无损切换
嗯,这些内容看起来多,但只要你亲手抓一次波形,跑一遍逻辑分析仪,就全通了。我当年就是在示波器前蹲了一下午,把起始条件、ACK、停止条件一个个对出来,才真正理解I2C的。