☰
I2C多主机仲裁与时钟延展:开漏输出、逐位比较及调试实战
2026/9/27 10:29:15 网站建设 项目流程

I2C 总线用两根线就能挂一堆设备,很多人第一次接触时觉得它比 SPI 简单,但真正把多个主机同时挂上去之后,问题就来了:两个主机同时开口说话怎么办?从机跟不上速度怎么办?这两个问题对应的正是 I2C 协议里最精妙的两套机制——多主机仲裁和时钟延展。我调试过不少 I2C 相关的板子,从 EEPROM 读写到 OLED 驱动,再到多 MCU 共享总线的场景,踩过的坑基本都绕不开这两个机制。这篇就把它们拆开讲透,包括开漏输出为什么是这一切的物理基础、仲裁到底怎么逐位比较、时钟延展在什么场景下会咬人,以及实际调试时怎么用逻辑分析仪看出问题。

1. 开漏输出:仲裁和时钟延展能成立的物理前提

1.1 推挽输出为什么在 I2C 上会出事

先说一个很多人忽略的根本问题:I2C 的 SDA 和 SCL 两根线,为什么必须用开漏输出加外部上拉电阻,而不是像 SPI 那样用推挽输出直接驱动?

推挽输出的结构是上下两个 MOS 管互补工作,输出高电平时上管导通、下管截止,把线拉到 VCC;输出低电平时反过来,把线拉到 GND。问题在于,如果总线上有两个设备同时驱动这根线,一个想输出高、一个想输出低,那上管和下管就会形成一条从 VCC 到 GND 的低阻通路,瞬间大电流烧管子。这在单主机的 SPI 里不是问题,因为片选信号保证了同一时刻只有一个从设备在驱动 MISO,但 I2C 没有片选线,任何时刻都可能有多个设备在操作同一根线。

开漏输出就解决了这个问题。开漏结构只有下面一个 NMOS 管,漏极开路,只能把线拉低或者释放(高阻态)。释放的时候线不会自己变高,必须靠外部上拉电阻把线拉回 VCC。这样一来,任何设备都只能"拉低"或"放手",永远不可能出现两个设备一个推高一个拉低的短路情况。总线的电平状态是所有设备"线与"的结果:只要有一个设备拉低,线就是低;所有设备都放手,线才被上拉电阻拉高。

注意:上拉电阻的取值不是随便选的。典型值 4.7kΩ 对应 100kHz 标准模式,2.2kΩ 到 1kΩ 对应 400kHz 快速模式。阻值太大,上升沿变缓,高速下波形还没到高电平阈值就被下一个时钟沿打断了;阻值太小,低电平时灌电流过大,可能超过器件的驱动能力(标准要求 3mA,多数器件能扛 6mA 到 20mA)。

1.2 线与逻辑如何让"多设备同时说话"变得安全

理解了开漏,就能理解 I2C 最核心的设计哲学:总线上的电平是所有设备输出的逻辑与。这个特性看起来是个限制,实际上恰恰是仲裁机制能工作的基础。

每个设备在发送数据时,都会同时读回总线上的实际电平。如果它想发高(放手),但读回来是低,说明有别的设备正在拉低这根线——它就知道自己"输了"。这个"边发边听"的能力,就是仲裁的全部秘密。没有开漏的线与特性,一个设备发高另一个发低就会短路,根本没法做这种比较。

时钟线 SCL 也是同样的道理。主机拉低 SCL 表示时钟低电平,释放 SCL 让上拉电阻拉高表示时钟高电平。多个主机同时拉低 SCL,线就是低;只要有一个主机还在拉低,线就一直是低。这就引出了时钟同步和时钟延展两个机制。

1.3 上拉电阻与总线电容的定量关系

实际选上拉电阻时,上升时间 $t_r$ 和总线电容 $C_b$ 的关系是:

$$t_r \approx 0.847 \times R_p \times C_b$$

I2C 标准规定,标准模式(100kHz)上升时间不超过 1000ns,快速模式(400kHz)不超过 300ns。假设你的总线挂了 8 个设备,PCB 走线加引脚电容总共约 200pF,那快速模式下:

$$R_p \leq \frac{300ns}{0.847 \times 200pF} \approx 1.77k\Omega$$

所以 1.5kΩ 到 2.2kΩ 是比较稳妥的选择。但如果你挂的设备少、走线短,电容只有 50pF,那 4.7kΩ 也完全够用,还能降低功耗。我一般会在板子上预留两种阻值的焊盘,实测波形后再决定焊哪个。

2. 多主机仲裁:逐位比较的完整过程拆解

2.1 仲裁发生在哪些时刻

多主机仲裁不是全程都在进行的,它只在总线空闲后、多个主机同时发起传输时才会触发。具体来说,仲裁发生在两个层面:

第一个层面是起始条件(START)的竞争。两个主机如果在相近的时刻都检测到总线空闲,都想发 START,那它们会同时把 SDA 从高拉低。这时候两个 START 是重叠的,总线正常进入传输状态,仲裁继续。

第二个层面是数据位的竞争。START 之后,每个主机开始逐位发送地址和数据。每发一位,主机都会读回总线电平,和自己的输出比较。一旦发现自己发的是高(释放)但读回是低(被别人拉低),这个主机就输了仲裁,立即退出,转为从机模式或者等待下一次总线空闲。

2.2 逐位仲裁的时序细节

假设主机 A 要发地址 0x50(二进制 1010000),主机 B 要发地址 0x60(二进制 1100000)。两个主机同时发 START 后,开始逐位发送:

位序主机 A 发送主机 B 发送总线实际电平结果
第1位1(释放)1(释放)1都继续
第2位0(拉低)1(释放)0B 读回 0,B 输
后续继续退出A 控制A 赢得仲裁

关键点在于,仲裁是在数据位这一级完成的,输的那一方不会破坏赢的那一方的数据。因为输的一方在发现自己输了的那一刻,它发的是高(释放状态),并没有拉低总线,所以总线上正在传输的数据完全不受影响。这就是 I2C 仲裁"非破坏性"的含义。

提示:仲裁只发生在地址和数据阶段,不会发生在 ACK 阶段。因为 ACK 是接收方拉低 SDA,如果多个主机在竞争,ACK 阶段已经决出胜负了,只有一个主机还在发送。

2.3 仲裁失败后主机怎么处理

仲裁失败的主机必须立即做三件事:第一,把 SDA 释放(如果还在驱动的话);第二,切换到从机接收模式,因为赢的那一方可能正在寻址它;第三,继续跟随 SCL 时钟,直到当前传输结束(STOP 或重复 START)。

这里有个容易踩的坑:仲裁失败的主机不能立即停止跟随 SCL。如果它直接放手不管,可能会在赢的一方发 ACK 或者数据时误判总线状态。正确做法是继续采样 SCL,直到看到 STOP 条件或者重复 START,才认为总线空闲,可以重新发起传输。

我在用两颗 MCU 共享一条 I2C 总线做冗余控制时就遇到过这个问题。当时其中一颗 MCU 仲裁失败后直接进了低功耗模式,结果另一颗 MCU 后续的传输它完全没跟上,导致状态机错乱。后来改成仲裁失败后继续监听总线直到 STOP,问题才解决。

2.4 多主机仲裁的实际应用场景

仲裁机制在实际系统里最常见的场景是多主冗余。比如工业控制里两颗 MCU 互为备份,都挂在同一条 I2C 总线上,正常时只有主 MCU 发命令,备 MCU 只监听。一旦主 MCU 挂了,备 MCU 立即接管,因为总线是共享的,它不需要切换任何硬件通道。

另一个场景是多传感器融合。比如一个系统里有主控 MCU 和一个专用的传感器协处理器,两者都需要读取同一组传感器数据。通过 I2C 仲裁,谁先发起谁先读,另一个自动退让,不需要额外的总线切换逻辑。

3. 时钟延展:从机"喊停"主机的机制

3.1 时钟延展的本质是拉低 SCL

时钟延展(Clock Stretching)的机制说起来很简单:从机如果来不及处理数据,就在主机释放 SCL 之后、自己还没准备好之前,继续把 SCL 拉低。主机本来想发下一个时钟高电平,但发现 SCL 还是低,就只能等。等从机处理完了,释放 SCL,主机才继续。

这个机制的精妙之处在于,它不需要任何额外的握手信号。从机只需要控制 SCL 线,就能让主机"慢下来"。而且因为 SCL 也是开漏的,从机拉低 SCL 不会和主机冲突——主机释放 SCL 时是放手状态,从机拉低是合法的。

3.2 时钟延展的典型触发场景

不是所有从机都会做时钟延展,但以下几类场景很常见:

  • EEPROM 写操作:写一个字节后,EEPROM 需要几毫秒的内部擦写时间。这段时间它不响应任何总线活动,有些型号会通过拉低 SCL 来延展时钟,有些则直接不响应(主机需要轮询 ACK)。
  • ADC 转换:某些 ADC 在转换期间会拉低 SCL,告诉主机"我还在转换,别催"。
  • 传感器数据准备:比如温湿度传感器完成一次测量需要几十毫秒,期间可能用时钟延展来同步。
  • MCU 作为从机:如果从机 MCU 的 I2C 中断优先级不够高,或者软件处理慢,硬件会自动拉低 SCL 来争取时间。

3.3 时钟同步:多个主机同时拉低 SCL 的结果

时钟同步和时钟延展经常被混为一谈,但它们是两个不同的机制。时钟同步发生在多主机场景:两个主机同时发 SCL,各自有自己的时钟周期。因为 SCL 是线与的,只要有一个主机还在拉低,SCL 就是低。所以实际的总线时钟周期是"最慢的那个主机"决定的。

具体过程是这样的:主机 A 的 SCL 高电平周期先结束,它拉低 SCL;主机 B 的高电平周期还没结束,它还在释放 SCL。但 A 已经拉低了,所以 SCL 变低。B 检测到 SCL 变低,知道自己的高电平周期被截断了,于是也开始自己的低电平周期。低电平周期结束时,谁先释放 SCL 不重要,因为只要有一个还在拉低,SCL 就还是低。最终 SCL 的高电平从"最后一个释放的主机"开始,低电平从"第一个拉低的主机"开始。

这个机制保证了多主机场景下,所有主机看到的 SCL 是同一个时钟,不会因为时钟不同步导致数据错乱。

3.4 时钟延展带来的实际麻烦

时钟延展虽然设计精妙,但在实际调试中经常带来麻烦。最常见的问题是:主机不支持时钟延展。

很多 MCU 的硬件 I2C 外设是支持时钟延展的,但有些低端 MCU 或者用 GPIO 模拟 I2C 的代码,根本不检测 SCL 是否被从机拉低。主机按自己的节奏发时钟,从机还没准备好就被强行推进,结果就是数据错乱或者 NACK。

另一个问题是时钟延展导致的超时。如果从机因为某种原因一直拉低 SCL(比如死机了),主机会一直等,整个总线挂死。所以健壮的主机驱动必须有超时机制:等待 SCL 释放超过一定时间(比如 25ms),就认为总线故障,发 9 个时钟脉冲尝试恢复,或者直接复位 I2C 外设。

注意:用 GPIO 模拟 I2C 时,SCL 的释放操作后必须加一段延时再读回 SCL 电平,确认它真的变高了。如果直接从机拉低着,你的代码却以为时钟已经走高,后续时序全乱。这个延时不用太长,几百纳秒到几微秒,取决于你的上拉电阻和总线电容。

4. 仲裁与时钟延展的联合作用:一个完整的冲突场景

4.1 场景设定

假设一条 I2C 总线上有两个主机 MCU(A 和 B)和一个从机 EEPROM(地址 0x50)。某一时刻,A 和 B 几乎同时检测到总线空闲,都想向 EEPROM 写数据。同时,EEPROM 因为上一笔写操作还在内部擦写,暂时拉低了 SCL 做时钟延展。

4.2 逐步推演

第一步,A 和 B 都发 START。因为 SDA 是线与的,两个 START 重叠,总线正常进入传输状态。

第二步,A 发地址 0x50(1010000),B 发地址 0x50(1010000)。两个地址完全一样,逐位比较下来谁也没输。这时候仲裁不会决出胜负,两个主机都以为自己赢了。

第三步,进入数据阶段。A 要写 0xAA(10101010),B 要写 0x55(01010101)。逐位比较:

位序A 发送B 发送总线电平结果
第1位100A 输,退出
后续退出继续B 控制B 赢得仲裁

第四步,B 继续发送剩余数据。但此时 EEPROM 还在擦写,它拉低 SCL 做时钟延展。B 发完一位后释放 SCL,发现 SCL 还是低,就知道从机在延展,于是等待。等 EEPROM 擦写完成,释放 SCL,B 继续发送。

第五步,B 完成传输,发 STOP。A 在仲裁失败后一直监听总线,看到 STOP 后知道总线空闲,可以重新发起自己的传输。

这个场景把仲裁和时钟延展串在了一起,实际系统里两者经常同时出现。理解了这个完整链路,调试时看到波形就不会懵。

4.3 用逻辑分析仪抓这个场景

如果你手头有逻辑分析仪(比如 Saleae 或者便宜的逻辑分析仪配合开源软件),可以这样抓:

  • 通道 0 接 SDA,通道 1 接 SCL,采样率至少 4MHz(快速模式要 10MHz 以上)。
  • 触发条件设成 SDA 下降沿(START 条件)。
  • 解码器选 I2C,设置正确的地址位宽(7 位还是 10 位)。

抓到的波形里,你能看到两个主机同时发 START 后,SDA 上的数据是两者线与的结果。仲裁失败的那一刻,SDA 上会出现一个"半高"的毛刺——其实是两个主机驱动能力不同导致的短暂竞争,但因为是开漏,不会损坏器件。SCL 上如果看到异常长的低电平周期,那就是时钟延展。

5. 调试实战:仲裁和时钟延展相关的典型故障

5.1 故障一:总线挂死,SCL 一直被拉低

这是最经典的 I2C 故障。现象是主机发 START 后一直等不到 ACK,逻辑分析仪显示 SCL 持续为低。

根因通常是某个从机在传输过程中被复位或者断电,导致它的 SCL 驱动管一直导通。或者是从机的时钟延展逻辑有 bug,拉低后忘了释放。

排查步骤:先断电,用万用表测 SCL 对地电阻。如果接近 0Ω,说明有器件在硬拉低。逐个断开从机,找到罪魁祸首。如果是软件问题,主机驱动里加超时恢复:检测到 SCL 低超过 25ms,就切换 SCL 为 GPIO 输出,手动发 9 个时钟脉冲,再发 STOP,尝试复位总线。

5.2 故障二:多主机场景下数据偶发错乱

现象是单主机时一切正常,加上第二个主机后偶发数据错误,但逻辑分析仪看波形又"看起来正常"。

这种问题往往是仲裁失败的主机没有正确退出。比如它仲裁输了之后,没有继续跟随 SCL,导致它在错误的时刻采样了总线,误以为自己还在传输。或者它的 I2C 外设仲裁失败中断没有正确处理,状态机卡死。

排查方法:在仲裁失败中断里加日志,记录失败时的位序和总线状态。同时用逻辑分析仪抓长时间波形,看是否有主机在非预期时刻驱动 SDA。

5.3 故障三:时钟延展导致主机超时

现象是主机读传感器时偶发超时,但传感器本身工作正常。

根因可能是传感器的时钟延展时间超过了主机驱动的超时阈值。比如某些温湿度传感器一次测量需要 50ms,期间一直拉低 SCL,而主机驱动的超时设的是 10ms,自然就超时了。

解决办法:查传感器数据手册,确认最大时钟延展时间,把主机超时阈值设成它的 1.5 到 2 倍。如果主机硬件不支持长延展,就改用轮询模式:主机发测量命令后立即 STOP,等足够时间后再发读命令。

5.4 故障四:上拉电阻选错导致仲裁误判

这个坑比较隐蔽。如果上拉电阻太大,SCL 和 SDA 的上升沿很缓,在高速模式下,某个主机可能还没等到线完全变高就采样了,误判为低电平,导致仲裁逻辑出错。

判断方法:用示波器看上升沿时间。如果超过标准规定的最大值(标准模式 1000ns,快速模式 300ns),就要减小上拉电阻。我一般会把上拉电阻和总线电容一起算,确保最坏情况下上升时间也达标。

6. 从协议到代码:仲裁和时钟延展在驱动里的体现

6.1 硬件 I2C 外设里的仲裁逻辑

大多数 MCU 的硬件 I2C 外设都内置了仲裁逻辑。以常见的 STM32 为例,它的 I2C 状态寄存器里有 ARLO(仲裁丢失)标志位。当外设检测到自己发送的数据和总线实际电平不一致时,硬件自动置位 ARLO,释放 SDA 和 SCL,切换到从机模式。

软件需要做的是:在 I2C 中断里检查 ARLO 标志,如果置位,清除标志,重新初始化传输状态机,等待总线空闲后重试。注意,仲裁丢失后不能立即重试,必须等当前传输结束(STOP 或重复 START),否则会再次冲突。

6.2 GPIO 模拟 I2C 时怎么处理仲裁

用 GPIO 模拟 I2C 时,仲裁需要软件自己实现。核心逻辑是:每次写 SDA 之后,读回 SDA 电平,如果自己写的是 1 但读回是 0,说明仲裁失败。

// 简化的仲裁检测逻辑 bool i2c_write_bit(bool bit) { if (bit) { SDA_RELEASE(); // 释放 SDA,让上拉电阻拉高 } else { SDA_LOW(); // 拉低 SDA } delay_short(); bool actual = SDA_READ(); // 读回总线实际电平 SCL_HIGH(); delay_short(); SCL_LOW(); if (bit && !actual) { return false; // 仲裁失败 } return true; }

这段代码里,SDA_RELEASE()是把 GPIO 设成输入或者开漏输出高,SDA_LOW()是设成开漏输出低。关键是SDA_READ()必须在 SCL 拉高之前读,因为 SCL 拉高后数据才稳定。

6.3 时钟延展在 GPIO 模拟里的处理

GPIO 模拟 I2C 时,主机释放 SCL 后必须读回 SCL,确认它真的变高了,才能继续。

void i2c_clock_high(void) { SCL_RELEASE(); while (!SCL_READ()) { // 从机还在拉低 SCL,等待 // 这里要加超时计数,防止死等 } }

这个while循环就是处理时钟延展的关键。如果没有这个循环,主机在从机还没准备好时就继续发时钟,数据肯定错。但循环里必须有超时,否则从机死机时主机会卡死。

提示:超时计数不要用太小的值。我一般设成 10000 次循环,对应大概几毫秒到几十毫秒,具体取决于你的 CPU 主频。这个值要大于从机最大的时钟延展时间。

7. 几个容易被忽略的边界条件

7.1 重复 START 与仲裁的关系

重复 START(Repeated START)是在不释放总线的情况下重新发起传输。在多主机场景下,重复 START 也可能触发仲裁。如果两个主机同时发重复 START,仲裁过程和普通 START 一样,逐位比较地址。

但要注意,重复 START 期间总线不会回到空闲状态,所以仲裁失败的主机不能等 STOP,而要等当前传输完全结束(包括重复 START 后的所有数据)。这个细节在写多主机驱动时很容易漏。

7.2 10 位地址模式下的仲裁

10 位地址模式下,仲裁过程比 7 位地址多一个阶段。前两个字节都是地址,仲裁会在这两个字节的每一位上进行。如果两个主机发的前两个字节完全一样,仲裁不会决出胜负,继续比较数据字节。

10 位地址的仲裁逻辑和 7 位一样,只是比较的位数更多。实际调试时,逻辑分析仪的解码器要设成 10 位地址模式,否则解码结果会错。

7.3 时钟延展与总线超时的平衡

时钟延展是合法的协议行为,但过长的延展会拖慢整个总线。设计时要在"允许从机延展"和"防止总线挂死"之间找平衡。

我的经验是:主机驱动的超时阈值设成从机数据手册标称最大延展时间的 2 倍。如果从机没有标称值,就按最坏情况估,比如 100ms。同时,在应用层加总线恢复逻辑:连续 N 次超时后,复位 I2C 外设或者重新初始化总线。

7.4 电源域不同时的仲裁风险

如果两个主机的电源域不同,一个先上电一个后上电,先上电的主机可能在另一个还没准备好时就发起传输。这时候后上电的主机的 SDA/SCL 引脚可能是高阻态,不会拉低总线,仲裁不会触发,但后上电的主机会错过传输。

解决办法:在硬件上加电源监控,确保所有主机都上电稳定后再允许 I2C 传输。或者用一根额外的 GPIO 做总线使能信号,所有主机都准备好后才拉高使能。

8. 实测经验:我怎么验证仲裁和时钟延展是否正常工作

8.1 用两颗 MCU 做仲裁测试

我搭过一个测试平台:两颗 STM32 挂在同一条 I2C 总线上,各自跑不同的传输任务,用逻辑分析仪抓波形。测试用例包括:

  • 两颗 MCU 同时发 START,地址不同,验证仲裁是否正确决出胜负。
  • 两颗 MCU 同时发 START,地址相同,验证数据阶段仲裁。
  • 一颗 MCU 正常传输,另一颗 MCU 在传输中途发 START,验证总线冲突处理。
  • 从机拉低 SCL 做时钟延展,验证主机是否正确等待。

实测下来,STM32 的硬件 I2C 仲裁逻辑很可靠,ARLO 标志能正确置位。但要注意,仲裁丢失后如果立即重试,很容易再次冲突,最好加一个随机退避延时。

8.2 用可编程从机模拟时钟延展

时钟延展的测试比较麻烦,因为不是所有从机都支持。我用过一块 FPGA 模拟的 I2C 从机,可以编程控制拉低 SCL 的时间。测试时逐步增加延展时间,看主机驱动在什么时间点开始报错。

这个测试帮我确定了主机超时阈值的最佳值。太短会误报,太长会拖慢故障恢复。最终我设的是从机最大延展时间的 1.5 倍,实测下来既能容忍正常延展,又能在从机死机时及时恢复。

8.3 逻辑分析仪的触发技巧

抓仲裁和时钟延展的波形,触发设置很关键。我一般用两级触发:

第一级,SDA 下降沿触发(START 条件)。第二级,在触发后延迟一段时间再抓,确保抓到完整的传输过程。

如果要抓时钟延展,可以把 SCL 低电平时间作为触发条件。很多逻辑分析仪软件支持"脉宽触发",设成 SCL 低电平超过某个阈值就触发。这样能精准抓到延展事件。

9. 写多主机 I2C 驱动的几条硬经验

第一,仲裁失败中断里不要做耗时操作。仲裁失败是高频事件,中断里只做标志置位和状态切换,具体重试逻辑放到主循环里。

第二,时钟延展等待循环里必须喂看门狗。如果从机死机导致主机死等,看门狗能救你一命。

第三,多主机场景下,每个主机的 I2C 传输都要有重试机制。仲裁失败是正常现象,不是错误,重试几次总能成功。

第四,上拉电阻的选型要留余量。我一般按最坏情况(最多设备、最长走线、最高速度)算,然后实测波形微调。

第五,逻辑分析仪是调试 I2C 的必备工具。没有它,仲裁和时钟延展的问题基本靠猜。有了它,波形一看就明白。

这套机制我用了好几年,从简单的 EEPROM 读写到复杂的多主冗余系统,仲裁和时钟延展这两个机制理解透了,I2C 调试基本就没有盲区了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询