4. I2C总线协议:从物理层到实战避坑
I2C协议,说实话,是我在嵌入式开发中用得最多的总线之一。它只有两根线,却能挂一堆设备,省GPIO又灵活。但很多新手一上来就被它的时序搞晕了。别急,咱们一步步拆开看。
4.1 物理层:就两根线,但别小看
I2C总线就两条线:SDA(数据线)和SCL(时钟线)。都是开漏输出,需要外部上拉电阻。我习惯用4.7kΩ,但具体要看总线电容和速率。
关键点:开漏结构意味着——任何设备都可以把线拉低,但没人能主动拉高。拉高靠上拉电阻。这就是为什么I2C支持多主机。
标准模式下速率100kHz,快速模式400kHz,高速模式能到3.4MHz。嗯,这里要注意:线越长,速率就得越低。我在一个项目里试过用1米线跑400kHz,结果数据全是错的。后来降到100kHz才稳定。
4.2 起始条件和停止条件
这两个条件定义了通信的边界。说白了就是:
- 起始条件(S):SCL高电平时,SDA从高变低
- 停止条件(P):SCL高电平时,SDA从低变高
你想想看,这个设计很巧妙。平时总线空闲时,SDA和SCL都是高电平。一旦检测到SDA在SCL高时跳变,就知道通信开始了。
我的经验:曾经有个项目,从机偶尔会丢失起始条件。排查了半天,发现是SDA线上的毛刺触发了误判。后来在SDA和SCL上各加了一个100pF的小电容滤波,问题就解决了。
4.3 数据帧格式:字节传输与应答
I2C的数据传输是以字节为单位的。每个字节8位,后面跟一个应答位(ACK)。
标准的数据帧格式是这样的:
起始条件 | 7位地址 + R/W | 应答 | 数据字节 | 应答 | ... | 停止条件
举个例子,读一个温度传感器:
- 主机发起始条件
- 主机发设备地址(7位)+ 写位(0)
- 从机应答ACK
- 主机发寄存器地址
- 从机应答ACK
- 主机发重复起始条件
- 主机发设备地址 + 读位(1)
- 从机应答ACK
- 从机发数据,主机应答ACK(或NACK表示结束)
- 主机发停止条件
注意:应答位是必须的。如果从机不拉低SDA(即NACK),主机就知道出问题了。我遇到过从机掉电的情况,主机一直等ACK,结果卡死在总线上。解决方案是加超时处理,一般5ms收不到ACK就复位总线。
4.4 多主机仲裁:谁抢到总线谁说话
I2C支持多个主机同时发起通信。但总线只有一根,怎么解决冲突?答案是仲裁。
仲裁的原理很简单:谁先拉低SDA,谁就赢了。因为SDA是开漏的,如果一个主机想发高电平,但另一个主机拉了低电平,那总线实际就是低电平。发高电平的那个主机检测到电平不对,就知道自己仲裁失败了,自动退出。
我曾经在一个多主机的项目里踩过坑。两个主机几乎同时发起始条件,结果一个赢了,另一个退出了。但退出后它没有正确释放总线,导致后续通信全乱套。后来我在每个主机的代码里加了总线空闲检测,确保前一次通信完全结束再发起下一次。
核心要点:仲裁不会丢失数据。赢的那个主机继续发,输的那个自动转为从机模式。整个过程对上层应用是透明的。
4.5 时钟拉伸:从机的“慢一点”信号
时钟拉伸是从机的一种流控机制。当从机来不及处理数据时,它会拉低SCL线,告诉主机“慢一点,我还没准备好”。主机检测到SCL被拉低,就会等待,直到从机释放SCL。
为什么需要这个?举个例子,一个EEPROM在写入数据时,内部需要几毫秒的编程时间。如果主机不管不顾地继续发数据,数据就丢了。所以EEPROM会在写入期间拉低SCL,主机乖乖等着。
我的建议:写驱动时一定要支持时钟拉伸。有些便宜的传感器对时序要求很严格,不支持时钟拉伸的话,通信会间歇性失败。我习惯在I2C驱动里加一个超时计数器,比如等待SCL释放最多10ms,超时就报错退出。
4.6 知识体系总览
下面这张图把I2C的核心知识点串起来了。你可以把它当作一个快速参考。
4.7 实战建议总结
| 知识点 | 常见坑 | 我的解决方案 |
|---|---|---|
| 物理层 | 线太长导致信号畸变 | 降速或加总线缓冲器 |
| 起始/停止条件 | 毛刺误触发 | SDA/SCL加100pF电容 |
| 数据帧格式 | 从机不应答导致死锁 | 5ms超时复位 |
| 多主机仲裁 | 退出后总线未释放 | 加总线空闲检测 |
| 时钟拉伸 | 驱动不支持导致丢数据 | 必须实现等待逻辑 |
好了,I2C的核心内容就这些。说实话,协议本身不复杂,但实际项目中坑不少。记住一点:永远不要假设从机是完美的。加超时、加检测、加容错,这是嵌入式工程师的生存法则。