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 位地址的第一个字节高 5 位是 11110,这相当于一个「暗号」,告诉所有从机:后面还有 8 位地址要传。低 2 位是地址的高 2 位,第二个字节才是剩下的 8 位。
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,谁就占上风。
3.2.2 仲裁过程
- 开始条件: 两个主机几乎同时检测到总线空闲,都发出 Start 条件。
- 地址竞争: 第一个字节的地址位开始逐位比较。谁先输出 0,谁就赢。
- 数据竞争: 如果地址相同,继续在数据位上仲裁。
- 失败退出: 仲裁失败的主机释放 SDA,转为从机模式。
我记得有一次调试一个双主机的系统,两个 MCU 都试图读取同一个传感器。其中一个 MCU 总是读不到数据,后来发现是它的地址优先级太低,每次都被另一个 MCU 抢走总线。解决办法是给低优先级的 MCU 加一个随机延时,错开启动时间。
3.3 时钟同步:大家一起走
多主机模式下,每个主机都有自己的时钟源。如果两个主机的 SCL 频率不一样,总线时序就会乱。I2C 的解决办法是——时钟同步。
时钟同步的原理很简单:所有主机共同驱动 SCL 线。谁先拉低 SCL,总线就进入低电平。谁最后释放 SCL,总线才进入高电平。
你想想看,这就像一群人一起喊口号。喊得快的要等喊得慢的,最后大家步调一致。
你看,主机 1 的时钟周期短,主机 2 的时钟周期长。经过同步后,总线的 SCL 周期被拉长到跟主机 2 一样。这就是「慢者决定快者」的规则。
3.4 时钟拉伸:慢从机让主机等等我
时钟拉伸是 I2C 协议里一个容易被忽略但极其重要的特性。说白了,就是从机告诉主机:「我还没准备好,你先别发下一个 bit,等我一下。」
怎么实现的?从机在 SCL 低电平期间,主动拉低 SCL 线不放。主机检测到 SCL 被拉低,就会一直等待,直到从机释放 SCL。
3.4.1 什么时候需要时钟拉伸?
- 从机处理数据: 比如 EEPROM 写入后需要时间擦除,从机拉低 SCL 让主机等待。
- 从机缓冲区满: 比如 UART 转 I2C 的桥接芯片,内部 FIFO 满了,需要主机暂停发送。
- 从机采样慢: 比如一些低速 ADC,转换时间较长,需要拉伸时钟。
我记得有一次调试一个温湿度传感器,读数据时总是超时。后来用逻辑分析仪一看,发现传感器每次发完地址后都会拉伸时钟大约 500μs。我用的 MCU 的 I2C 外设默认不支持时钟拉伸,直接报超时错误。换了软件模拟 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 的时序参数和电气特性,那才是真正决定通信可靠性的硬功夫。