3. I2C 地址与仲裁:7 位地址与 10 位地址格式、多主机仲裁机制、时钟同步与拉伸

好,咱们进入第三章。这一章讲的是 I2C 协议里最容易让人迷糊,也最容易出 Bug 的几个点:地址格式、多主机抢总线、还有时钟拉伸。说实话,我早年做项目时,在这上面栽过不止一个跟头。尤其是时钟拉伸,很多芯片手册里写得不清楚,实际调起来才发现坑很深。

咱们一个一个来拆。

3.1 7 位地址与 10 位地址格式

I2C 的地址,说白了就是给每个从机发一个“门牌号”。主机发数据前,先喊一声这个门牌号,对应的从机应答,然后才开始通信。

标准模式下,地址是 7 位的。但 7 位能表示 128 个地址,其中 0x00 是通用呼叫地址,0x01~0x07 是保留地址,真正能用的也就 120 个左右。这在复杂系统里不够用,所以后来扩展出了 10 位地址。

7 位地址格式

7 位地址的传输格式很简单:主机发送起始条件后,先发一个字节,高 7 位是地址,最低位是读写位(0 表示写,1 表示读)。

起始条件 | 地址[7:1] + R/W | 应答 | 数据... | 停止条件

举个例子:如果从机地址是 0x32(二进制 011 0010),主机要写数据,那么发送的第一个字节就是:

0x32 << 1 | 0 = 0x64

嗯,这里要注意:很多新手会把 0x32 直接当成发送字节,结果从机死活不应答。我刚开始也犯过这个错,后来养成了习惯——写驱动时一定把地址左移一位,再拼上读写位。

10 位地址格式

10 位地址的格式稍微复杂一点。主机需要发送两个字节来标识地址:

  • 第一个字节:高 5 位固定为 11110,接着是 10 位地址的高 2 位,最后是读写位。
  • 第二个字节:10 位地址的低 8 位。

格式如下:

起始条件 | 11110 + Addr[9:8] + R/W | 应答 | Addr[7:0] | 应答 | 数据... | 停止条件

举个例子:如果从机 10 位地址是 0x2B4(二进制 10 1011 0100),那么:

  • 高 2 位是 10
  • 低 8 位是 1011 0100(0xB4)
  • 第一个字节:1111 0 10 + 0(写)= 0xF4
  • 第二个字节:0xB4

你想想看,10 位地址的好处是能挂更多设备,但代价是多传一个字节,通信效率略低。我个人习惯是:如果系统里从机不超过 10 个,用 7 位地址就够了;超过 10 个或者需要兼容未来扩展,才用 10 位地址。

特性 7 位地址 10 位地址
地址范围 0x00 ~ 0x7F(保留 8 个) 0x000 ~ 0x3FF
有效设备数 最多 112 个 最多 1024 个
地址传输字节数 1 字节 2 字节
兼容性 所有设备都支持 部分旧设备不支持
提示:在 RTOS 中配置从机地址时,一定要确认芯片手册里写的是“7 位地址”还是“8 位地址”。有些厂商把左移后的值直接写在手册里,容易混淆。我建议你在驱动层统一用 7 位原始地址,左移操作封装在底层函数里。

3.2 多主机仲裁机制

I2C 支持多个主机挂在同一条总线上。但问题来了——如果两个主机同时发起通信,谁说了算?

答案是:仲裁。

仲裁的规则很简单:谁先拉低 SDA 线,谁就赢。因为 I2C 是开漏输出,多个设备同时拉低时,总线电平就是低电平。如果一个主机想发高电平,但检测到总线是低电平,说明有其他主机在抢,它就自动退出。

具体流程是这样的:

  1. 两个主机同时发送起始条件。
  2. 然后开始逐位发送地址和数据。
  3. 每个主机在发送每一位后,都会读取 SDA 线的实际电平。
  4. 如果发送的电平与读取的电平不一致,说明仲裁失败,该主机立即释放总线。
  5. 仲裁获胜的主机继续通信,失败的主机等待总线空闲后重试。

这里有个关键点:仲裁只发生在 SDA 线上,SCL 线始终由主机共同驱动。也就是说,时钟是同步的,数据是仲裁的。

核心要点:仲裁不丢失数据。失败的主机在仲裁过程中不会发送无效数据,因为它只是停止驱动总线,不会破坏正在进行的传输。

我在项目中遇到过一个问题:两个主机同时访问同一个从机,结果从机收到了混乱的数据。后来排查发现,其中一个主机的 SDA 引脚配置成了推挽输出,而不是开漏输出。推挽输出在拉高时强行输出高电平,导致仲裁机制失效。嗯,这个坑我印象很深——I2C 的 SDA 和 SCL 必须配置为开漏输出,这是协议的基础。

3.3 时钟同步与时钟拉伸

时钟同步和时钟拉伸,这两个概念经常被混在一起说。其实它们干的活不一样。

时钟同步

时钟同步发生在多主机场景下。多个主机各自产生 SCL 时钟,但它们的时钟频率和相位可能不同。I2C 的同步机制是:所有主机共同驱动 SCL 线,谁先拉低 SCL,总线就进入低电平;谁最后释放 SCL,总线才进入高电平。

说白了,就是“低电平取最长,高电平取最短”。这样所有主机的时钟就被强制同步了。

举个例子:

  • 主机 A 的 SCL 低电平时间是 4μs,高电平时间是 4μs。
  • 主机 B 的 SCL 低电平时间是 5μs,高电平时间是 3μs。
  • 同步后,SCL 低电平时间是 5μs(取最长),高电平时间是 3μs(取最短)。

这样总线的时钟频率就被拉到了较慢的那个主机的水平。所以,如果你在系统里混用不同速度的 I2C 主机,整体速度会被拖慢。

时钟拉伸

时钟拉伸是从机的一种“反制”手段。从机如果处理不过来,可以主动拉低 SCL 线,告诉主机:“慢点,我还没准备好。”

主机在 SCL 高电平期间采样数据,但在低电平期间会检测 SCL 线的状态。如果从机拉低了 SCL,主机就会等待,直到从机释放 SCL 为止。

时钟拉伸的典型场景:

  • 从机需要时间处理接收到的数据(比如写入 EEPROM)。
  • 从机需要时间准备要发送的数据。
  • 从机正在处理中断,暂时无法响应 I2C 通信。
警告:不是所有 I2C 从机都支持时钟拉伸。有些从机芯片手册里明确写了“不支持时钟拉伸”,这种情况下主机必须保证足够长的等待时间,或者用轮询方式确认从机状态。我曾经在一个项目里用了不支持时钟拉伸的传感器,结果主机一直等从机释放 SCL,导致总线死锁。后来加了超时处理才解决。

在 RTOS 环境下,时钟拉伸对任务调度有直接影响。如果主机任务因为等待时钟拉伸而阻塞,其他任务可能趁机抢占 CPU,导致 I2C 通信超时。我建议的做法是:在 I2C 驱动中为时钟拉伸设置一个最大等待时间,超时后主动复位总线。

3.4 知识结构图

下面这张图把本章的核心知识点串起来了。你可以把它当成一个快速索引。

I2C 地址与仲裁知识结构图 I2C 地址与仲裁知识结构 地址格式 多主机仲裁 时钟同步与拉伸 7 位地址 10 位地址 逐位仲裁 开漏输出 时钟同步 时钟拉伸 关键说明: • 7 位地址左移 1 位 + R/W 位组成第一个字节 • 10 位地址需要 2 个字节传输,高 5 位固定为 11110 • 仲裁基于开漏输出,谁先拉低 SDA 谁获胜 实战要点 SDA/SCL 必须配置为开漏输出 | 时钟拉伸需设置超时 | 10 位地址兼容性需确认

3.5 避坑指南

最后,我把自己踩过的坑整理成几条,你写驱动时多留个心眼:

  • 地址左移:芯片手册里写的地址通常是 7 位原始地址,发送时要左移一位。别直接拿手册里的值去发。
  • 开漏输出:SDA 和 SCL 必须配成开漏,不能是推挽。否则仲裁和时钟拉伸都会失效。
  • 时钟拉伸超时:从机可能无限期拉低 SCL,主机必须设超时。我一般设 10ms,超时后复位 I2C 外设。
  • 多主机冲突:如果系统里有多主机,确保所有主机都支持仲裁。有些简化版 I2C 控制器不支持仲裁,只能当单主机用。
  • 10 位地址兼容性:部分旧从机芯片不支持 10 位地址,选型时一定要确认。