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

各位同学,今天我们来啃I2C协议这块硬骨头。说实话,I2C协议本身并不复杂,但很多人在实际项目中翻车,往往是因为对底层细节理解不够。我做了这么多年嵌入式,见过太多“I2C通信不稳定”的案例,最后发现都是些基础问题。

咱们从物理层开始,一层层往上扒。

2.1 物理层:SDA/SCL与上拉电阻

I2C总线就两根线:SDA(数据线)和SCL(时钟线)。这两根线都是开漏输出,什么意思呢?说白了,芯片只能把线拉低,不能主动拉高。那高电平谁来给?上拉电阻。

核心要点:I2C总线的高电平由上拉电阻提供,芯片只负责拉低。

上拉电阻的取值很关键。我刚开始做项目时,随便焊了个10kΩ电阻上去,结果400kHz高速模式下波形惨不忍睹。后来才明白,电阻太大,上升沿太慢;电阻太小,功耗又高。

总线模式 标准模式 (100kHz) 快速模式 (400kHz) 高速模式 (3.4MHz)
推荐上拉电阻 4.7kΩ ~ 10kΩ 1.5kΩ ~ 4.7kΩ 0.5kΩ ~ 1.5kΩ
总线电容 ≤ 400pF ≤ 400pF ≤ 100pF

我的经验:如果板子上挂多个I2C设备,总线电容会增大。我一般先用4.7kΩ,调试时用示波器看上升沿,如果太缓就换小一点的电阻。

2.2 协议层:起始、停止、数据与应答

I2C协议层其实就四个基本动作:起始条件、停止条件、数据传输、应答机制。咱们一个一个说。

2.2.1 起始条件与停止条件

这两个条件很好记:

  • 起始条件:SCL高电平时,SDA从高变低
  • 停止条件:SCL高电平时,SDA从低变高

为什么这么设计?你想想看,平时总线空闲时,两根线都是高电平。起始条件就是“我要开始通信了”的信号,停止条件就是“我结束了”。

注意:我曾经遇到过一个坑——MCU复位时,SDA线如果恰好处于低电平,会被误认为是起始条件。后来我在初始化时先让SDA和SCL输出高电平,保持几个微秒再开始通信,问题就解决了。

2.2.2 数据有效性

I2C的数据采样规则很简单:

  • SCL低电平时,SDA可以变化
  • SCL高电平时,SDA必须稳定

说白了,数据是在SCL的上升沿被采样的。所以你在写驱动时,一定要保证在SCL拉高之前,SDA已经稳定了。

2.2.3 应答机制

I2C的应答是强制性的。每发送完一个字节(8位),接收方必须在第9个时钟周期拉低SDA,表示“收到了”。

应答位有两种:

  • ACK (0):接收方拉低SDA,表示继续发送
  • NACK (1):接收方不拉低SDA,表示停止或结束

我调试LCD驱动时,经常用逻辑分析仪看应答位。如果看到NACK,八成是设备地址写错了,或者设备没上电。

2.3 7位与10位地址模式

I2C设备地址有两种模式。7位地址是最常见的,10位地址是为了扩展更多设备用的。

特性 7位地址 10位地址
地址范围 0x00 ~ 0x7F 0x000 ~ 0x3FF
最大设备数 112(保留16个) 1024
首字节格式 7位地址 + R/W 11110xx + R/W
兼容性 所有设备都支持 需要设备支持

7位地址的传输格式是这样的:

起始条件 + 7位地址 + R/W位 + 应答 + 数据字节 + 应答 + ... + 停止条件

10位地址稍微复杂一点,分两步:

  1. 先发11110xx(xx是地址的高2位)+ R/W=0(写)
  2. 再发剩下的8位地址

我的建议:除非你板子上要挂几十个I2C设备,否则用7位地址就够了。10位地址的兼容性问题比较多,有些老设备不支持。

2.4 时钟同步与仲裁

这两个机制是I2C协议的精髓,也是很多人搞不清楚的地方。

2.4.1 时钟同步

I2C是多主设备总线,多个主设备可以同时发起通信。时钟同步就是让所有主设备的时钟保持一致。

怎么做到的?靠“线与”特性。每个主设备都会拉低SCL,但只有所有主设备都释放SCL时,SCL才会变高。说白了,谁拉得慢,谁就决定时钟的低电平时间。

我记得有一次调试,两个MCU同时访问同一个传感器,结果时钟乱掉了。后来发现是其中一个MCU的时钟频率偏差太大,导致同步失败。

2.4.2 仲裁

仲裁解决的是“多个主设备同时发送数据时,谁说了算”的问题。

规则很简单:谁先拉低SDA,谁就赢。仲裁失败的设备会退出,等总线空闲后再重试。

仲裁发生在SDA线上。每个主设备在发送数据时,会同时监测SDA的电平。如果自己发送的是1,但检测到SDA是0,说明有其他设备在拉低SDA,自己就输了。

关键点:仲裁不会丢失数据,也不会破坏通信。失败的设备会在下一个起始条件时重新尝试。

下面这张图展示了I2C协议的整体框架:

I2C协议知识体系 物理层 协议层 地址模式 SDA/SCL 上拉电阻 开漏输出 起始条件 停止条件 数据有效性 应答机制 7位地址 10位地址 时钟同步 & 仲裁 线与特性 多主设备竞争 I2C协议的核心:物理层保证电气特性,协议层保证通信流程,地址模式保证设备识别,时钟同步与仲裁保证多主设备共存

2.5 实际项目中的避坑指南

最后,我把自己这些年踩过的坑总结一下,希望对你有帮助。

我曾经踩过的坑:

  • 上拉电阻选错:有一次用1kΩ电阻,结果功耗超标,电池供电的设备两天就没电了。
  • 地址搞混:7位地址左移一位才是8位地址,很多人在这里翻车。比如设备地址0x3C,实际发送时要左移成0x78。
  • 应答超时:有些慢速设备需要时间处理数据,如果主设备不等应答就发下一个字节,通信会失败。我一般加一个超时机制,等500μs还没应答就重试。
  • 总线死锁:如果从设备拉低SCL后不释放,总线就卡死了。解决办法是加一个超时复位,或者用GPIO模拟I2C时手动拉高SCL。

嗯,I2C协议就讲到这里。这些基础打牢了,后面写驱动、移植协议就会顺手很多。记住,协议本身不复杂,复杂的是各种边界情况和硬件差异。多动手、多调试,慢慢就有感觉了。