3. I2C协议进阶:寻址、仲裁、时钟同步与时钟拉伸

各位同学,咱们今天聊点硬核的。I2C 的基础时序大家应该都摸过了,但真正到了多设备、多主机的复杂系统里,光会发 Start 和 Stop 可不够。我个人觉得,I2C 协议最精彩的部分,恰恰是它怎么解决「谁跟谁说话」、「谁抢到总线」、「谁等谁」这几个问题。

说白了,I2C 的进阶内容就是一套优雅的交通规则。咱们一个一个拆开看。

3.1 I2C 寻址方式:从机怎么知道自己被点名了?

每次主机发起通信,第一个字节的高 7 位就是从机地址。我刚开始做项目时,总觉得地址就是个编号,后来踩了坑才发现,这里面的门道不少。

3.1.1 7 位地址与 10 位地址

标准模式用 7 位地址,理论上支持 112 个从机(0x00 是广播地址,0x01~0x07 保留)。但实际项目中,7 位地址经常不够用。比如一个传感器板子上挂了 8 个同样的芯片,每个芯片只有 2 个地址引脚,最多只能设 4 个不同地址。怎么办?

这时候就得用 10 位地址了。10 位地址的格式有点绕,我画个图你就明白了。

10位地址格式 7位地址: S A6 ~ A0 (7位地址) R/W ACK 10位地址: S 11110 A9~A8 R/W ACK 第二个字节: A7 ~ A0 (低8位地址) ACK 关键点 • 第一个字节高5位固定为11110 • 第一个字节低2位是A9~A8 • 第二个字节是A7~A0 • 10位地址兼容7位地址模式

你看,10 位地址的第一个字节高 5 位是 11110,这相当于一个「暗号」,告诉所有从机:后面还有 8 位地址要传。低 2 位是地址的高 2 位,第二个字节才是剩下的 8 位。

我的经验: 10 位地址虽然好用,但不是所有从机都支持。选型时一定要看 datasheet 里有没有写「10-bit addressing supported」。我曾经在一个项目里用了 10 位地址,结果有个从机芯片根本不认,排查了两天才发现是地址模式不匹配。

3.1.2 广播地址与保留地址

地址 0x00 是广播地址,所有从机都必须响应。这个功能在批量配置时很有用,比如你想让所有传感器同时进入低功耗模式,发一个广播命令就行。

但要注意,广播地址不能随便用。我见过有人把广播地址当成普通地址用,结果一呼百应,总线直接乱套。

地址范围 用途 说明
0x00 广播地址 所有从机必须响应
0x01~0x07 保留 用于特殊总线协议
0x08~0x77 7位地址可用范围 标准从机地址
0x78~0x7F 10位地址前缀 高5位为11110

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

I2C 允许多个主机挂在同一条总线上。但问题来了:如果两个主机同时想发数据,总线不就打架了吗?

I2C 的解决方案很巧妙——仲裁。它不是靠「谁先说话谁赢」,而是靠「谁先拉低谁赢」。

3.2.1 仲裁原理

你想想看,I2C 的 SDA 是开漏输出,多个设备同时拉低时,总线就是低电平。如果某个主机想发高电平,但总线上已经被别的设备拉低了,它就会检测到「我发的和总线上的不一样」,然后自动退出。

说白了,仲裁就是一场「谁更持久地拉低」的比赛。谁先发 0,谁就占上风。

核心规则: 仲裁只发生在 SDA 线上,SCL 由所有主机共同同步。仲裁失败的主机必须释放 SDA 并进入从机模式,等待下一次总线空闲。

3.2.2 仲裁过程

  1. 开始条件: 两个主机几乎同时检测到总线空闲,都发出 Start 条件。
  2. 地址竞争: 第一个字节的地址位开始逐位比较。谁先输出 0,谁就赢。
  3. 数据竞争: 如果地址相同,继续在数据位上仲裁。
  4. 失败退出: 仲裁失败的主机释放 SDA,转为从机模式。

我记得有一次调试一个双主机的系统,两个 MCU 都试图读取同一个传感器。其中一个 MCU 总是读不到数据,后来发现是它的地址优先级太低,每次都被另一个 MCU 抢走总线。解决办法是给低优先级的 MCU 加一个随机延时,错开启动时间。

注意: 仲裁失败不会产生任何错误标志。主机必须通过「自己发的数据 vs 总线上的数据」不一致来判断是否仲裁失败。所以,多主机系统中,每个主机都要开启「数据对比」功能。

3.3 时钟同步:大家一起走

多主机模式下,每个主机都有自己的时钟源。如果两个主机的 SCL 频率不一样,总线时序就会乱。I2C 的解决办法是——时钟同步。

时钟同步的原理很简单:所有主机共同驱动 SCL 线。谁先拉低 SCL,总线就进入低电平。谁最后释放 SCL,总线才进入高电平。

你想想看,这就像一群人一起喊口号。喊得快的要等喊得慢的,最后大家步调一致。

时钟同步过程 主机1 SCL 主机2 SCL 总线 SCL 时钟同步结果 • 总线SCL = 主机1 SCL AND 主机2 SCL • 谁先拉低,总线就低 • 谁最后释放,总线才高

你看,主机 1 的时钟周期短,主机 2 的时钟周期长。经过同步后,总线的 SCL 周期被拉长到跟主机 2 一样。这就是「慢者决定快者」的规则。

3.4 时钟拉伸:慢从机让主机等等我

时钟拉伸是 I2C 协议里一个容易被忽略但极其重要的特性。说白了,就是从机告诉主机:「我还没准备好,你先别发下一个 bit,等我一下。」

怎么实现的?从机在 SCL 低电平期间,主动拉低 SCL 线不放。主机检测到 SCL 被拉低,就会一直等待,直到从机释放 SCL。

关键点: 时钟拉伸只发生在 SCL 低电平期间。从机不能在 SCL 高电平期间拉低,否则会被视为 Start 条件。

3.4.1 什么时候需要时钟拉伸?

  • 从机处理数据: 比如 EEPROM 写入后需要时间擦除,从机拉低 SCL 让主机等待。
  • 从机缓冲区满: 比如 UART 转 I2C 的桥接芯片,内部 FIFO 满了,需要主机暂停发送。
  • 从机采样慢: 比如一些低速 ADC,转换时间较长,需要拉伸时钟。

我记得有一次调试一个温湿度传感器,读数据时总是超时。后来用逻辑分析仪一看,发现传感器每次发完地址后都会拉伸时钟大约 500μs。我用的 MCU 的 I2C 外设默认不支持时钟拉伸,直接报超时错误。换了软件模拟 I2C 才搞定。

避坑指南: 不是所有 MCU 的硬件 I2C 外设都支持时钟拉伸。选型时一定要确认外设的 datasheet 里有没有写「clock stretching support」。如果不支持,要么换 MCU,要么用 GPIO 模拟 I2C。

3.4.2 时钟拉伸的时序

正常 I2C 时序中,主机在 SCL 低电平时改变 SDA,在 SCL 高电平时采样 SDA。如果从机拉伸时钟,SCL 的低电平时间会被拉长,主机必须等待。

// 软件模拟I2C时,时钟拉伸的处理示例
uint8_t i2c_wait_clock_stretch(void) {
    uint16_t timeout = 10000;
    while (i2c_read_scl() == 0) {  // 等待SCL被释放
        if (--timeout == 0) {
            return 1;  // 超时,返回错误
        }
    }
    return 0;  // 成功
}

这段代码很简单,但很实用。实际项目中,超时时间要根据从机的最长拉伸时间设定。我一般设 10ms,够用又不至于死等。

3.5 综合对比:这些特性什么时候用?

特性 适用场景 注意事项
7位地址 从机数量少(≤112) 地址冲突风险
10位地址 从机数量多或地址不够用 需从机支持
多主机仲裁 多个MCU共享总线 需开启数据对比
时钟同步 多主机不同时钟源 自动完成,无需配置
时钟拉伸 慢速从机需要等待 需主机外设支持

嗯,到这里,I2C 的进阶内容就讲完了。这些特性平时可能用不到,但一旦遇到多主机或慢速从机的场景,它们就是救命的。下次咱们聊 I2C 的时序参数和电气特性,那才是真正决定通信可靠性的硬功夫。