1. 为什么多主机仲裁是 I2C 协议里最值得反复琢磨的部分
I2C 总线只有两根线,一根 SDA 数据线,一根 SCL 时钟线,却能挂载几十个设备,还能允许多个主机同时存在。很多人第一次接触 I2C 的时候,注意力都放在读写时序、设备地址、寄存器操作上,觉得把 EEPROM 或者传感器读出来就完事了。但真正让 I2C 从"能用"变成"精妙"的,恰恰是多主机仲裁和时钟延展这两个机制。
我见过不少工程师,用 I2C 用了好几年,碰到总线锁死、多主竞争、从机拉低时钟导致主机超时这些问题时,完全不知道底层发生了什么,只能靠复位或者重新上电来"解决"。这种处理方式在实验室里没问题,一旦到了量产环境,就是隐患。所以这一讲我想把多主机仲裁和时钟延展这两件事彻底讲透,从电气结构讲到协议层,再讲到实际调试中怎么观察、怎么排查。
先说结论:多主机仲裁之所以精妙,是因为它用极少的硬件代价,实现了"无损冲突解决"。两个主机同时发数据,不会互相破坏,输的那个自动退出,赢的那个甚至感知不到有人跟它竞争过。这种设计在 1980 年代就已经成型,放到今天看依然优雅。
这篇文章适合已经会基本 I2C 读写、但想搞清楚总线竞争和时钟同步底层逻辑的读者。如果你还在纠结"为什么 SDA 要接上拉电阻"这种问题,建议先补一下开漏输出的基础,再回来看这篇会更有收获。
2. 开漏输出:多主机仲裁能成立的物理前提
2.1 推挽输出为什么不能用在 I2C 总线上
要理解仲裁,必须先理解开漏输出(Open-Drain)。很多教程一上来就说"I2C 的 SDA 和 SCL 必须接上拉电阻",但很少讲清楚为什么。
推挽输出(Push-Pull)的结构是上下两个 MOS 管,上管导通输出高电平,下管导通输出低电平。它的特点是输出高电平时是主动驱动到 VCC,有很强的拉电流能力。问题来了:如果两个设备都接在同一根线上,一个想输出高,一个想输出低,上管和下管直接形成一条从 VCC 到 GND 的低阻通路,瞬间大电流烧毁器件。这就是所谓的总线争用短路。
开漏输出则不同,它只有下管,没有上管。输出低电平时下管导通,把线拉到 GND;输出高电平时下管截止,引脚处于高阻态,线的高电平完全靠外部上拉电阻提供。这样一来,任何设备都只能"拉低"总线,不能主动"推高"总线。多个设备同时接在一根线上,只要有一个拉低,线就是低;全部释放,线才被上拉电阻拉高。
这个特性用一句话概括:线与逻辑。总线的状态是所有设备输出的逻辑与。正是这个"只能拉低、不能推高"的约束,让多主机仲裁成为可能。
2.2 上拉电阻的取值不是随便选的
既然高电平靠上拉电阻,那这个电阻的取值就很关键。它同时受两个因素制约:
- 上升时间:I2C 总线有电容,标准模式(100kHz)要求上升时间不超过 1000ns,快速模式(400kHz)不超过 300ns。电阻越大,RC 充电越慢,上升沿越缓。总线电容一般按 10pF 到 400pF 估算,电阻太大波形就变成"圆顶",高速下直接读错。
- 灌电流能力:设备拉低时,电流从 VCC 经上拉电阻灌入下管。I2C 规范规定标准模式灌电流约 3mA,快速模式约 6mA。电阻太小,灌电流超标,长期运行会损伤器件。
实际取值通常在 1.8kΩ 到 10kΩ 之间。总线电容小、速率高,就取小一点,比如 2.2kΩ;总线长、设备多、电容大,就取大一点,但要注意上升时间是否满足。我一般会先用 4.7kΩ 打样,然后用示波器看上升沿,如果圆得厉害就往下调。
提示:很多人忽略总线电容的累积效应。每多挂一个设备、每多一段排线,电容都会增加。一个 4.7kΩ 上拉在单设备时波形很漂亮,挂到 8 个设备后可能就爬不起来了。这时候要么减小电阻,要么用 I2C 缓冲器/多路复用器把总线分段。
2.3 开漏输出与推挽输出的对比
| 特性 | 推挽输出 | 开漏输出 |
|---|---|---|
| 高电平驱动 | 主动驱动到 VCC | 靠外部上拉电阻 |
| 低电平驱动 | 下管拉低 | 下管拉低 |
| 能否多设备共线 | 不能,会短路 | 能,线与逻辑 |
| 电平转换 | 困难 | 容易,上拉到目标电压即可 |
| 上升沿速度 | 快 | 受上拉电阻和电容限制 |
| 典型应用 | SPI、UART、GPIO 输出 | I2C、SMBus、单总线 |
这张表里有个细节值得单独说:开漏输出天然支持电平转换。比如一个 3.3V 的 MCU 要和一个 5V 的从机通信,只要把上拉电阻接到 5V,总线高电平就是 5V,MCU 的开漏引脚耐压够的话直接就能用。这也是 I2C 在混合电压系统里特别受欢迎的原因。
3. 多主机仲裁的完整过程拆解
3.1 仲裁发生在哪一位
多主机仲裁不是"谁先发谁赢",也不是"谁优先级高谁赢",而是逐位比较。两个主机同时启动传输,从起始条件 S 开始,到地址字节、数据字节,每一位都在比较。
规则很简单:每个主机在发送每一位时,都会同时读回 SDA 线的实际电平。如果自己发的是高(释放总线,靠上拉),但读回来是低(被别的设备拉低了),说明有另一个主机发了低,自己输了,立刻退出,停止驱动 SDA 和 SCL,转为从机接收状态或者等待总线空闲。
如果自己发的是低,读回来也是低,那可能是自己拉的,也可能是别人拉的,无法区分,继续下一位。如果自己发的是高,读回来也是高,说明没人拉低,继续。
关键点在于:仲裁只发生在 SDA 线上,SCL 线不参与仲裁。因为所有主机在同一个 SCL 周期内采样,时钟是同步的(后面讲时钟延展时会说同步机制)。仲裁输掉的主机不会破坏赢家的数据,因为它在发现自己输的那一刻,自己发的位和总线上的位本来就是一致的——它发高、别人发低,总线是低,赢家要的就是低,数据没被破坏。
3.2 一个具体的仲裁例子
假设两个主机 A 和 B 同时想控制总线,它们的从机地址分别是:
- 主机 A 要访问地址
0x50(7 位:1010000) - 主机 B 要访问地址
0x60(7 位:1100000)
从起始条件后开始逐位比较(先发最高位):
| 位序 | 主机 A 发送 | 主机 B 发送 | 总线实际 | 结果 |
|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 都继续 |
| 2 | 0 | 1 | 0 | B 读回 0,自己发 1,B 输,退出 |
| 3 | 1 | - | 1 | A 继续 |
| ... | ... | - | ... | A 独占总线 |
在第 2 位,主机 B 发的是 1(释放),但总线被 A 拉低成 0,B 读回 0 发现自己输了,立即退出。A 完全不知道 B 存在过,继续正常传输。整个过程没有任何数据损坏,也没有时间浪费。
这就是"无损仲裁"的含义。仲裁的代价几乎为零,只是每个主机多了一个读回比较的动作,硬件上就是一个输入缓冲器。
3.3 仲裁失败后主机该做什么
仲裁输掉的主机,规范要求它立即切换到从机接收模式,并且:
- 停止驱动 SDA 和 SCL
- 继续采样 SCL,直到当前传输结束(检测到停止条件 P)
- 不能立即重试,要等总线空闲
这里有个容易踩的坑:仲裁失败的主机如果立刻重试,可能反复失败。因为赢家可能连续占用总线。正确的做法是等总线空闲后再发起。有些 MCU 的硬件 I2C 会自动处理这个状态,有些则需要软件判断。
还有一个更隐蔽的坑:仲裁失败可能发生在数据阶段,而不是地址阶段。比如两个主机访问同一个从机,地址阶段完全一致,到了写数据阶段才分叉。这时候仲裁失败的主机已经发了一部分数据,从机可能已经接收了。这种情况规范里也有说明,但因为两个主机访问同一从机本身就是设计问题,实际项目中很少遇到。
注意:多主机仲裁的前提是所有主机都使用相同的 SCL 频率和相同的时序参数。如果一个主机跑 100kHz,另一个跑 400kHz,仲裁逻辑会混乱。虽然时钟同步机制能一定程度上协调,但混速多主是自找麻烦,能避免就避免。
3.4 多主机仲裁在真实系统里的价值
有人会问:既然多主机这么复杂,为什么不用单主机加多路复用器?答案是成本和灵活性。
在服务器管理总线(如 IPMI)、电信设备背板、电池管理系统(BMS)里,经常有多个控制器需要访问同一组传感器或 EEPROM。如果每个控制器都配一套独立的 I2C 总线,引脚和走线成本会翻倍。用多主机仲裁,所有控制器共享两根线,谁需要谁发起,硬件成本最低。
另一个场景是热插拔冗余控制。主控制器和备份控制器都挂在同一条 I2C 总线上,主控制器正常时它主导,主控制器挂了备份控制器接管。仲裁机制保证了切换过程中不会出现两个主机同时驱动导致的数据混乱。
4. 时钟延展:从机反过来"卡"主机的机制
4.1 时钟延展要解决什么问题
I2C 是同步通信,SCL 由主机产生。理论上从机只要跟着时钟走就行。但现实是,从机可能是:
- 一个慢速的 MCU,处理一个字节需要时间
- 一个正在做内部操作的 EEPROM,写周期需要几毫秒
- 一个 ADC,转换需要时间
如果主机不管从机死活,按固定节奏发时钟,从机来不及处理就会丢数据。时钟延展(Clock Stretching)就是给从机一个"喊停"的能力:从机可以在需要的时候把 SCL 线拉低,主机发现 SCL 没按预期变高,就知道从机还没准备好,乖乖等着。
这个机制的本质还是开漏输出。SCL 线也是开漏的,主机和从机都能拉低。只要有一个拉低,线就是低。主机想发高电平时,如果从机还拉着低,主机读回的是低,就知道要等。
4.2 时钟延展的时序细节
正常一个 SCL 周期:主机拉低 SCL(低电平期),然后释放 SCL(高电平期),从机在高电平期采样数据。
时钟延展发生时:
- 主机释放 SCL,期望它变高
- 从机此时把 SCL 拉低(它还没准备好)
- 主机读回 SCL 是低,知道从机在延展,进入等待
- 从机处理完内部事务,释放 SCL
- SCL 被上拉电阻拉高,主机检测到高电平,继续下一个周期
从机的延展可以发生在每一位之后,也可以发生在字节之间(ACK 位之后)。规范允许从机在每个 ACK 位之后延展,这是最常见的场景。
4.3 时钟同步:多主机场景下的 SCL 协调
时钟延展是"从机卡主机",而时钟同步是"主机之间互相卡"。当多个主机同时产生 SCL 时,它们的时钟需要同步,否则采样点会错乱。
同步机制同样利用线与逻辑:
- 每个主机有自己的 SCL 低电平期和高电平期
- 所有主机的 SCL 输出是线与的,所以低电平期由最长的那个决定(谁拉低得久,线就低得久)
- 高电平期由最短的那个决定(谁先释放,线就先被拉高,但只要有别人还拉着,线还是低)
结果就是:所有主机的 SCL 被"对齐"成一个统一的时钟,低电平期取最长,高电平期取最短。这样即使两个主机频率略有差异,也能协调工作。
这个机制和仲裁配合,构成了 I2C 多主机能力的完整基础:仲裁解决"谁说话",时钟同步解决"按什么节奏说"。
4.4 时钟延展的边界与限制
时钟延展虽然好用,但不是无限制的:
- 延展时间不能无限长。主机通常有超时机制,如果从机拉低 SCL 超过一定时间(比如 25ms 或 100ms),主机会认为总线故障,复位或报错。
- 不是所有主机都支持时钟延展。有些硬件 I2C 控制器不支持从机延展,遇到从机拉低 SCL 会直接超时报错。选型时要看数据手册。
- 高速模式下时钟延展受限。I2C 高速模式(3.4MHz)对时钟延展有更严格的限制,很多高速从机不支持延展。
我遇到过最典型的问题:用某款 MCU 的硬件 I2C 读一个老式 EEPROM,写操作后 EEPROM 进入内部写周期,会拉低 SCL 延展。但这款 MCU 的 I2C 外设不支持延展,直接报总线错误。解决办法要么换支持延展的 MCU,要么用软件模拟 I2C,要么在写操作后加足够延时再读。
5. 总线锁死:仲裁和延展失控后的典型故障
5.1 总线锁死的三种常见成因
多主机仲裁和时钟延展设计得再好,实际系统里还是会出问题。总线锁死是最常见的 I2C 故障,表现为 SDA 或 SCL 被某个设备一直拉低,主机无法发起任何传输。
成因主要有三类:
- 从机在传输中途复位。比如从机正在发 ACK 时被复位,它的 SDA 下管可能停在导通状态,把 SDA 一直拉低。主机等不到释放,总线卡死。
- 主机在传输中途复位。主机发到一半复位,SCL 停在低电平,从机以为时钟还在继续,一直等,双方僵持。
- 时钟延展超时。从机拉低 SCL 时间过长,主机超时后放弃,但从机还在拉低,总线锁死。
5.2 用 GPIO 手动恢复总线
总线锁死后的标准恢复流程是手动发时钟脉冲,让从机把剩下的位移完,释放总线。具体做法:
- 把 SCL 配置为 GPIO 输出(开漏),SDA 配置为输入
- 手动发 9 个 SCL 脉冲(一个字节 8 位加 1 个 ACK)
- 每个脉冲后检查 SDA 是否释放
- 如果 SDA 释放了,发一个停止条件(SDA 低时 SCL 高,然后 SDA 拉高)
- 重新初始化 I2C 外设
// 伪代码示意,具体寄存器名按平台调整 void i2c_bus_recovery(void) { gpio_set_open_drain(SCL_PIN); gpio_set_input(SDA_PIN); for (int i = 0; i < 9; i++) { gpio_low(SCL_PIN); delay_us(5); gpio_high(SCL_PIN); // 释放,靠上拉拉高 delay_us(5); if (gpio_read(SDA_PIN) == 1) { break; // SDA 已释放 } } // 发停止条件 gpio_low(SDA_PIN); delay_us(5); gpio_high(SCL_PIN); delay_us(5); gpio_high(SDA_PIN); delay_us(5); i2c_reinit(); }这段代码的关键是用开漏方式操作 GPIO,不能推挽,否则会和从机的下拉冲突。另外脉冲数量不一定是 9 个,如果从机状态机卡得深,可能需要更多,但 9 个覆盖绝大多数情况。
5.3 预防总线锁死的设计习惯
与其事后恢复,不如事前预防。我在项目里会坚持几个习惯:
- 主机复位时先释放 I2C 引脚,把 SCL 和 SDA 配置为高阻输入,让上拉电阻把总线拉高,避免主机复位后还占着总线。
- 从机侧加看门狗,从机如果长时间收不到完整传输,自动复位 I2C 状态机。
- 关键总线加 I2C 缓冲器,缓冲器有隔离和恢复能力,一段出问题不影响另一段。
- 软件层加超时和重试,每次 I2C 操作设超时,超时后走恢复流程,而不是死等。
提示:ESP32 这类平台在休眠唤醒后,I2C 外设状态可能丢失,需要重新初始化。如果休眠前总线处于非空闲状态,唤醒后第一件事应该是总线恢复,而不是直接读写。这个坑我在低功耗项目里踩过,唤醒后第一次读传感器必失败,加了恢复流程才稳定。
6. 用逻辑分析仪看懂仲裁和延展的真实波形
6.1 抓取多主机仲裁波形
光看协议文档,仲裁是个抽象概念。真正抓一次波形,理解会深很多。做法是:
- 两个主机同时发起传输,目标地址不同
- 逻辑分析仪接在 SDA 和 SCL 上,采样率至少 10 倍于 SCL 频率
- 触发条件设在起始条件
波形上你会看到:起始条件后,两个主机的地址位逐位发出,到某一位时,SDA 上出现一个"本该是高却被拉低"的位置,之后其中一个主机的 SCL 停止,另一个继续。这个分叉点就是仲裁失败点。
6.2 识别时钟延展
时钟延展在波形上表现为SCL 高电平期被异常拉长。正常 SCL 周期是规整的方波,延展发生时,某个高电平期明显变宽,因为从机拉着 SCL 不放。
用逻辑分析仪的协议解码功能,能看到解码结果里出现"Clock Stretching"标记,或者时间轴上 SCL 高电平持续时间远超正常值。这时候对照从机数据手册,看它哪个操作会触发延展,就能定位问题。
6.3 逻辑分析仪选型与设置建议
| 参数 | 建议值 | 说明 |
|---|---|---|
| 采样率 | ≥ 10MHz | 400kHz 总线至少 10 倍采样 |
| 通道数 | ≥ 2 | SDA、SCL 必需,有条件加中断线 |
| 触发 | 起始条件/地址匹配 | 精准抓取目标传输 |
| 解码 | I2C 协议解码 | 直接看地址、数据、ACK |
| 缓冲深度 | 越大越好 | 抓偶发故障需要长缓冲 |
便宜的 8 通道逻辑分析仪(配合开源软件)就能满足大部分 I2C 调试需求。关键是采样率要够,缓冲要深,否则抓不到偶发问题。
7. 硬件 I2C 与软件模拟 I2C 在仲裁延展上的差异
7.1 硬件 I2C 的仲裁支持参差不齐
不是所有 MCU 的硬件 I2C 都完整支持多主机仲裁和时钟延展。选型时要重点看数据手册里这几个关键词:
- Multi-Master:是否支持多主机
- Arbitration:是否有仲裁丢失检测
- Clock Stretching:是否支持从机延展
- Bus Timeout:是否有总线超时检测
有些低成本 MCU 的 I2C 只支持单主机,遇到仲裁直接报错。有些支持仲裁但不支持延展。STM32 的 I2C 外设在不同系列里能力也不同,F1 系列和 F4 系列的 I2C 就有差异,用之前一定要查对应型号的参考手册。
7.2 软件模拟 I2C 的灵活性与代价
软件模拟 I2C(bit-banging)用 GPIO 手动控制 SCL 和 SDA,天然支持仲裁和延展,因为每一步都是软件控制的,可以随时读回线状态、随时等待。
代价是:
- 占用 CPU:每个位都要软件操作,高速下 CPU 占用高
- 时序精度差:受中断影响,时序可能抖动
- 速率受限:一般只能跑到 100kHz 到 400kHz,再高就吃力
但在调试阶段,软件模拟 I2C 是排查问题的利器。你可以随时插入打印、随时暂停、随时改变时序,观察从机反应。很多 I2C 疑难问题,最后都是用软件模拟 I2C 定位的。
7.3 选型建议
- 单主机、从机简单:硬件 I2C,省 CPU
- 多主机、需要仲裁:确认硬件支持,否则用软件模拟
- 从机有延展需求:确认硬件支持延展,否则软件模拟或加延时
- 调试阶段:软件模拟 I2C 优先,方便观察
8. 几个真实项目里的仲裁与延展经验
8.1 BMS 多主竞争导致采样丢失
之前做一个电池管理系统,主控和备份控都挂在同一条 I2C 上采集电池监控芯片。调试时发现偶发的采样数据丢失。抓波形发现,两个控制器偶尔同时发起传输,仲裁失败的一方没有正确等待总线空闲就重试,导致赢家的传输被打断。
解决办法是在仲裁失败后加随机退避延时,而不是立即重试。退避时间取几个毫秒的随机值,避免两个控制器反复碰撞。改完之后采样丢失率降到零。
8.2 EEPROM 写周期延展导致主机超时
另一个项目用 EEPROM 存配置,写操作后立即读,经常读失败。查手册发现 EEPROM 写周期内会拉低 SCL 延展,而主机的 I2C 外设超时设得太短(默认 10ms),EEPROM 写周期要 5ms 到 10ms,临界超时。
解决办法是把主机 I2C 超时放宽到 50ms,或者在写操作后主动延时再读。前者更优雅,后者更保险。我最后两个都做了,双保险。
8.3 从机复位导致 SDA 常低
有个传感器在电源波动时会复位,复位瞬间 SDA 下管停在导通状态,把总线拉死。主机后续所有传输都失败。加了总线恢复流程后,主机检测到超时自动发 9 个脉冲恢复,问题解决。
这个案例说明:总线恢复流程应该是 I2C 驱动的标配,不是可选项。任何量产产品,只要 I2C 总线上挂了可能复位的设备,就要有恢复机制。
8.4 时钟延展与低功耗的冲突
低功耗项目里,从机为了省电会长时间拉低 SCL 延展,等内部唤醒。但主机如果也在低功耗模式,可能等不到从机释放就进入休眠,唤醒后总线状态混乱。这种场景要仔细设计电源和时钟策略,必要时用中断唤醒代替延展。
9. 把仲裁和延展写进你的 I2C 驱动设计
9.1 驱动层要暴露的状态
一个好的 I2C 驱动,不应该只提供 read 和 write,还要暴露这些状态:
- 仲裁丢失:让上层知道发生了多主竞争
- 总线超时:让上层知道可能锁死
- 延展等待:让上层知道从机在忙
- 总线恢复次数:统计恢复频率,判断总线健康度
这些状态可以用返回值、错误码或者回调上报。有了这些信息,上层才能做正确的重试和降级策略。
9.2 重试策略的设计
I2C 操作失败后的重试不是简单循环,要区分错误类型:
| 错误类型 | 重试策略 |
|---|---|
| 仲裁丢失 | 随机退避后重试,退避时间递增 |
| 从机 NACK | 检查设备是否存在,不盲目重试 |
| 总线超时 | 先执行总线恢复,再重试 |
| 延展超时 | 放宽超时或增加延时后重试 |
盲目重试最危险,可能让总线状态更糟。我一般会设最大重试次数(比如 3 次),超过就上报错误,让上层决定是否降级。
9.3 总线健康度监控
在长时间运行的系统里,我会加一个总线健康度统计:单位时间内仲裁丢失次数、超时次数、恢复次数。如果某个指标异常升高,说明总线或某个设备有问题,可以提前告警,而不是等彻底挂了才发现。
这个统计不需要很复杂,几个计数器加一个定时上报就够了。但在工业现场,这个简单的监控能省下大量排查时间。
10. 写在最后的一点个人体会
I2C 的多主机仲裁和时钟延展,是那种"看文档觉得简单,实际用起来处处是坑"的机制。它们的精妙之处在于用最少的硬件(开漏加线与)实现了复杂的总线协调,但代价是把很多责任推给了软件和系统设计。
我的经验是:只要你的系统里 I2C 总线上挂了超过两个设备,或者有多个控制器,或者有会复位的从机,就必须认真对待仲裁和延展。不要等到量产现场出问题才回头补,那时候改硬件的成本远高于前期多花两天把驱动写扎实。
具体到操作上,我建议每个 I2C 项目都做三件事:第一,用逻辑分析仪抓一次正常波形和一次异常波形,建立直观认识;第二,驱动里实现总线恢复流程,并测试它真的能恢复;第三,加超时和重试,但重试要有策略,不能无脑循环。这三件事做完,你的 I2C 稳定性会上一个台阶。
至于时钟延展,记住一句话:从机拉低 SCL 是在说"我还没好,等等我"。主机要做的不是催,而是等,并且设一个合理的上限,等太久就认为出问题了。理解了这个,很多"莫名其妙"的读失败就都有了解释。