I2C 总线上的多主机仲裁和时钟延展,是我个人认为整个嵌入式通信协议里最值得反复琢磨的两个机制。很多人用 I2C 用了好几年,能顺利读写 EEPROM、能驱动 OLED、能跟传感器正常通信,但一旦被问到"两个主机同时发数据会怎样""从机来不及处理时怎么办",就答不上来了。这两个问题恰恰是 I2C 区别于 SPI、UART 的核心设计,也是它能用两根线挂载几十个设备还保持可靠的关键。
这篇文章面向的是已经会用 I2C 但想真正搞懂底层机制的开发者,不管你是写 STM32 HAL 库的、调 Linux 内核驱动的,还是用 ESP32 做低功耗项目的,理解仲裁和时钟延展之后,你排查总线异常的能力会有一个明显的提升。下面我从开漏输出这个物理层基础讲起,把仲裁和时钟延展的每一个细节拆开,再结合实测中遇到的坑,把这两个机制讲透。
1. 开漏输出:仲裁和时钟延展能成立的物理前提
1.1 为什么 I2C 必须用开漏而不是推挽
要理解仲裁和时钟延展,得先把开漏输出这件事说清楚。推挽输出的引脚内部有上下两个 MOS 管,输出高电平时上管导通、下管截止,引脚被主动拉到 VCC;输出低电平时反过来。这种结构驱动力强、边沿陡峭,但有一个致命问题:如果两个推挽输出的引脚接在一起,一个输出高、一个输出低,那就等于 VCC 和 GND 之间直接接了一条低阻通路,瞬间大电流烧毁引脚。
I2C 的场景恰恰就是多个设备共享同一根线。SDA 和 SCL 上挂的不只是一个主机,可能有多个主机、几十个从机。如果用推挽,只要有两个设备同时想输出不同电平,硬件就废了。所以 I2C 规范强制要求所有设备的 SDA 和 SCL 引脚都配置为开漏输出(Open-Drain),有些资料也叫开集电极(Open-Collector)。
开漏输出的结构是:内部只有一个下管(NMOS),漏极接到引脚,源极接地。输出低电平时下管导通,把线拉到 GND;输出高电平时下管截止,引脚处于高阻态,什么都不做。那高电平从哪来?靠外部的上拉电阻把线拉到 VCC。
这个设计带来一个非常优雅的性质:任何一个设备都只能主动把线拉低,不能主动把线拉高。线的高电平是所有设备都"放手"之后,被上拉电阻慢慢拉上去的。这就意味着,只要有一个设备拉低,线就是低;只有所有设备都放手,线才是高。这个性质是后面仲裁和时钟延展的全部基础。
1.2 上拉电阻的取值不是随便选的
上拉电阻的阻值直接决定了总线的上升沿时间和功耗,选错了要么通信不稳定,要么功耗超标。计算逻辑是这样的:
上升时间由 RC 充电曲线决定,I2C 标准模式(100kHz)要求上升时间不超过 1000ns,快速模式(400kHz)不超过 300ns,快速模式+(1MHz)不超过 120ns。总线电容 Cb 是所有挂在总线上的设备引脚电容加上走线电容的总和,经验值每个设备引脚约 10pF,走线每厘米约 1pF 左右。
用公式 t_r ≈ 0.847 × Rp × Cb(从 0.3VDD 到 0.7VDD 的上升时间),可以反推最大上拉电阻。比如总线电容 200pF、快速模式要求 300ns,那么 Rp ≤ 300ns / (0.847 × 200pF) ≈ 1.77kΩ。同时上拉电阻也不能太小,否则设备拉低时灌电流过大,超过 IOL 最大值(通常 3mA),会拉不到 VOL 要求的低电平。
实际选型时,我一般这样处理:
| 总线速率 | 总线电容 | 推荐上拉电阻 | 备注 |
|---|---|---|---|
| 100kHz | < 200pF | 4.7kΩ | 最常用,稳妥 |
| 100kHz | 200~400pF | 2.2kΩ | 设备多时用 |
| 400kHz | < 200pF | 2.2kΩ | 快速模式标配 |
| 400kHz | 200~400pF | 1.5kΩ | 注意灌电流 |
| 1MHz | < 100pF | 1kΩ | 走线要短 |
提示:很多开发板上的 I2C 上拉电阻默认是 10kΩ,这个值在 100kHz 下勉强能用,但如果你挂的设备多、走线长,或者想跑 400kHz,10kΩ 会导致上升沿太慢,波形变成"圆角",通信时好时坏。我遇到过好几次"换个传感器就不通信"的问题,最后都是上拉电阻太大导致的。
1.3 开漏输出与推挽输出的对比实测
用逻辑分析仪抓波形时,开漏和推挽的区别一眼就能看出来。推挽输出的上升沿几乎是垂直的,因为上管主动拉高,电流大;开漏输出的上升沿是一条指数曲线,因为靠上拉电阻给电容充电。这也是为什么 I2C 的波形看起来"软"一些。
有些 MCU 的 I2C 引脚可以配置成"开漏+内部上拉",我不建议依赖内部上拉。内部上拉通常几十 kΩ,太弱了,只适合单设备、短走线、低速的场合。正规做法是外部加上拉电阻,内部上拉关掉。
还有一个容易踩的坑:STM32 的硬件 I2C 引脚必须配置为复用开漏模式(GPIO_MODE_AF_OD),如果你配成了复用推挽(GPIO_MODE_AF_PP),单主机单从机时可能也能通信,但一旦总线上有第二个主机,或者从机做时钟延展,就会出问题。这个配置错误在 HAL 库的 CubeMX 里如果手动改引脚,很容易漏掉。
2. 多主机仲裁:两根线怎么决定谁说话
2.1 仲裁发生在每一个 bit 上
多主机仲裁的核心思想是"线与"(Wired-AND)逻辑。前面说了,所有设备都只能拉低不能拉高,所以总线上的电平等于所有设备输出的逻辑与:只要有一个输出低,线就是低;全输出高,线才是高。
仲裁的过程是这样的:每个准备发送数据的主机,在发送每一位的同时,也在读回 SDA 上的实际电平。如果它想发高电平(放手),但读回来是低电平,说明有另一个主机正在拉低这根线,它就知道自己仲裁失败了,立刻退出,转为从机接收模式,停止驱动 SDA。而那个拉低线的主机,因为读回的电平和自己发的一致,继续正常发送,根本感知不到发生了仲裁。
这个过程是逐位进行的,从地址字节的第一位开始,一直到某个主机退出为止。仲裁失败的主机不会丢失数据,因为它退出时总线上的数据是获胜主机发的,它只要切换成接收模式继续听就行。
用一个具体例子说明。假设两个主机 A 和 B 同时启动传输:
- A 要发地址 0x50(二进制 1010000),B 要发地址 0x60(二进制 1100000)
- 第 1 位:A 发 1,B 发 1,都放手,线为高,一致,继续
- 第 2 位:A 发 0,B 发 1。A 拉低线,B 放手。线为低。A 读回低,和自己发的一致,继续;B 读回低,但自己发的是高,B 知道自己输了,退出
- 结果 A 获胜,继续发送剩余位
整个过程没有任何数据冲突,也没有时间浪费,仲裁在几个 bit 内就完成了。
2.2 仲裁的边界条件:哪些情况不会触发仲裁
仲裁只在"多个主机同时发起 START 条件"时才会发生。如果两个主机发起 START 的时间差超过一个 bit,那么后发起的主机会发现总线忙(SDA 或 SCL 被拉低),它会等待,不会参与仲裁。所以仲裁是"同时开始"的多个主机之间的竞争。
这里有一个关键点:仲裁不仅发生在地址字节,也发生在数据字节。如果两个主机发往同一个从机、地址相同,那么地址阶段不会分出胜负,仲裁会继续到数据阶段。如果数据也完全相同,那仲裁会一直进行到 STOP,两个主机都以为自己是赢家——但这种情况实际上不会造成问题,因为它们发的内容完全一样,从机收到的数据是正确的。
但要注意,I2C 规范要求:仲裁失败的主机必须立即切换到从机接收模式,并且不能在这个传输中再发送 START 或 STOP。如果仲裁失败的主机在退出后还试图发 STOP,就会破坏获胜主机的传输。这是软件实现软件 I2C 多主机时最容易出错的地方。
2.3 仲裁失败后的重试策略
仲裁失败不是错误,只是"这次没抢到"。失败的主机应该等待总线空闲后重新发起传输。这里有一个设计选择:是立即重试还是等一段时间再重试?
立即重试的问题是,如果两个主机总是同时想发,可能会反复冲突。虽然仲裁本身不会浪费总线时间,但反复仲裁失败会让某个主机一直发不出去(饥饿)。实际系统中,我一般建议在仲裁失败后加一个随机的退避时间,比如退避 1~10 个字节时间再重试。这个策略和以太网的 CSMA/CD 退避类似。
不过大多数 MCU 的硬件 I2C 外设会自动处理仲裁失败:检测到 ARLO(Arbitration Lost)标志后,硬件会自动释放总线,软件只需要在中断里清除标志、稍后重试即可。STM32 的 HAL 库会返回 HAL_BUSY 或 HAL_ERROR,你需要根据返回值决定是否重试。
2.4 用逻辑分析仪观察仲裁过程
仲裁过程用逻辑分析仪抓波形非常直观。你可以用两个 MCU 同时向同一个从机发不同地址,然后抓 SDA 和 SCL。你会看到:
- 两个主机同时发 START
- 前几个 bit 两个主机都在驱动 SCL,SCL 波形正常
- 到某一位时,SDA 上出现"一个主机想拉高、另一个拉低"的情况,线保持低
- 之后 SCL 继续,但 SDA 上只剩获胜主机在驱动
- 失败的主机不再驱动 SDA,但可能还在驱动 SCL(如果它还没完全退出)
这里有个细节:仲裁失败的主机在退出时,可能还在驱动 SCL。如果它继续驱动 SCL,会干扰获胜主机的时钟。所以硬件 I2C 在检测到仲裁失败后,会立即释放 SCL 和 SDA。软件 I2C 实现时,必须在检测到 SDA 读回值与发送值不一致的那一刻,立即停止驱动两根线。
注意:仲裁失败和时钟延展经常同时出现。如果获胜主机在仲裁后继续发送,而某个从机做时钟延展拉低了 SCL,那么获胜主机也会被延展。这两个机制是独立但会叠加的,分析波形时要分开看。
3. 时钟延展:从机说"等一下"的机制
3.1 时钟延展的本质是主机让出 SCL 控制权
I2C 的时钟虽然名义上由主机产生,但实际上 SCL 线是开漏的,从机也可以拉低它。时钟延展(Clock Stretching)就是从这个性质来的:当从机需要更多时间处理数据时,它把 SCL 拉低,主机发现 SCL 没有按预期变高,就知道从机还没准备好,于是等待,直到从机释放 SCL。
具体过程是这样的:主机发送完一个字节后,会释放 SCL 让它变高(准备下一个时钟)。如果从机此时拉低 SCL,那么 SCL 保持低,主机检测到 SCL 还是低,就不会继续产生时钟,而是等待。从机处理完数据后释放 SCL,SCL 被上拉电阻拉高,主机看到 SCL 变高,继续正常发送时钟。
这个机制的美妙之处在于,它不需要任何额外的握手线,完全靠 SCL 本身的电平来传递"我还没准备好"的信息。从机的处理速度可以比主机慢很多,只要它能拉低 SCL,主机就会等它。
3.2 时钟延展发生在哪些时刻
时钟延展可以发生在传输的多个阶段:
- 地址字节之后:从机收到地址后需要时间比较、决定是否响应,可能拉低 SCL
- 每个数据字节之后:从机收到数据后需要时间写入内部寄存器,可能拉低 SCL
- 读操作的数据字节之前:从机需要时间从内部寄存器读出数据放到 SDA 上,可能拉低 SCL
- ACK/NACK 位之前:从机需要时间决定是 ACK 还是 NACK
其中最常见的是读操作时的时钟延展。比如 EEPROM 读数据,从机收到读命令后,需要从存储阵列里把数据读出来,这个时间可能是微秒级甚至毫秒级,远慢于 I2C 的时钟周期,所以从机会拉低 SCL 等待。
3.3 主机必须支持时钟延展
这里有一个非常重要的实践问题:不是所有主机都支持时钟延展。有些 MCU 的硬件 I2C 外设不支持时钟延展,或者需要特殊配置。如果主机不支持,而从机做了时钟延展,主机会继续发时钟,导致数据错乱。
STM32 的硬件 I2C 是支持时钟延展的,但有一个坑:如果你用的是软件 I2C(GPIO 模拟),必须自己实现时钟延展检测。软件 I2C 的典型实现是:拉低 SCL,延时,释放 SCL,然后读 SCL 确认它真的变高了,如果没变高就继续等。很多网上抄来的软件 I2C 代码省略了"读回 SCL"这一步,导致不支持时钟延展,遇到会延展的从机就通信失败。
我实测过几个常见的会做时钟延展的器件:
| 器件类型 | 是否时钟延展 | 典型延展时长 |
|---|---|---|
| AT24C02 EEPROM | 是 | 写周期内可达 5ms |
| SSD1306 OLED | 否 | - |
| BMP280 气压计 | 是 | 几百微秒 |
| GT911 触摸屏 | 是 | 视固件而定 |
| AS5600 磁编码器 | 是 | 几十微秒 |
GT911 这个触摸屏芯片是时钟延展的重灾区,很多人在调试 GT911 时遇到 I2C 通信失败,最后发现是主机不支持时钟延展或者延展超时设置太短。
3.4 时钟延展的超时处理
从机拉低 SCL 的时间理论上没有上限,但实际系统中不能无限等。如果从机死机了,一直拉低 SCL,主机会永远卡住。所以主机需要设置一个超时时间,超过就报错、复位总线。
STM32 的 HAL 库 I2C 有超时参数,比如HAL_I2C_Master_Transmit(&hi2c1, addr, data, len, 1000)里的 1000 是毫秒超时。如果从机延展超过 1 秒,HAL 会返回 HAL_TIMEOUT。这个值要根据从机的最长延展时间设置,比如 EEPROM 写周期 5ms,那超时设 10ms 就够了;但如果从机固件升级时可能延展几百毫秒,就要设大一些。
Linux 内核的 I2C 驱动也有超时机制,在设备树里可以配置clock-frequency和超时相关参数。如果遇到 GT911 这类器件,可能需要调整i2c-scl-falling-time-ns和i2c-scl-rising-time-ns来匹配实际波形。
4. 仲裁与时钟延展的交互:最容易被忽略的组合场景
4.1 多主机 + 时钟延展同时发生
这是最复杂的场景:两个主机同时发起传输,仲裁正在进行,同时某个从机在做时钟延展。这时候 SCL 被从机拉低,两个主机都在等 SCL 变高,仲裁暂停。等从机释放 SCL 后,仲裁继续。
这个场景下,仲裁失败的主机在退出时,必须确保它不再驱动 SCL。如果它还在驱动 SCL,而从机也在驱动 SCL,那么 SCL 会被两个设备同时拉低,等从机释放后,失败主机如果还没释放,SCL 仍然是低,获胜主机会一直等,导致死锁。
硬件 I2C 外设通常能正确处理这个场景,因为仲裁失败时硬件会自动释放两根线。但软件 I2C 实现时,必须在检测到仲裁失败的那一刻,同时释放 SDA 和 SCL,并且不能再读回 SCL 来判断时钟延展(因为此时它已经不该参与总线了)。
4.2 时钟同步:仲裁的"副产品"
时钟同步(Clock Synchronization)是另一个和仲裁密切相关的机制。当多个主机同时产生 SCL 时,SCL 的实际周期等于所有主机中"低电平时间最长"和"高电平时间最长"的组合。具体来说:
- SCL 变低:任何一个主机拉低,SCL 就低。所以低电平时间等于所有主机中低电平最长的那个
- SCL 变高:所有主机都释放,SCL 才高。所以高电平时间等于所有主机中高电平最长的那个
这个机制保证了即使多个主机的时钟频率略有差异,SCL 上也会产生一个统一的、周期不短于任何单个主机时钟的波形。这样仲裁才能逐位进行,不会因为时钟不同步而错位。
时钟同步和时钟延展的区别在于:时钟同步是多个主机之间的 SCL 竞争,时钟延展是从机对 SCL 的拉低。两者都利用了 SCL 开漏、线与的特性,但参与者和目的不同。
4.3 实测:用两个 STM32 做仲裁实验
我做过一个实验,用两个 STM32F103 同时向一个 AT24C02 发不同地址,用逻辑分析仪抓波形。配置如下:
- 两个 STM32 都配置为硬件 I2C,100kHz,开漏输出,4.7kΩ 上拉
- 主机 A 发地址 0xA0(写 EEPROM),主机 B 发地址 0x50(假设的另一个设备)
- 两个主机的 START 由同一个 GPIO 触发,尽量同时
抓到的波形显示:两个 START 几乎同时出现,前两位地址都是 1,第三位 A 发 0、B 发 1,SDA 被 A 拉低,B 读回低但自己发的是高,B 的 ARLO 标志置位,B 释放总线。A 继续完成传输,从机正常 ACK。整个过程在 3 个 bit 内完成仲裁,总线没有出现任何毛刺。
这个实验说明硬件 I2C 的仲裁是可靠的,但前提是上拉电阻和总线电容匹配,否则上升沿太慢会导致仲裁误判。我试过把上拉换成 10kΩ,仲裁就开始偶尔出错,因为 SDA 上升太慢,主机在采样点读到的电平不确定。
4.4 软件 I2C 实现仲裁和时钟延展的要点
如果你要自己写软件 I2C 支持多主机和时钟延展,核心逻辑是这样的:
// 发送一位并检测仲裁 bool i2c_send_bit(bool bit) { if (bit) { release_sda(); // 放手,让上拉拉高 } else { pull_low_sda(); // 拉低 } delay_half_period(); pull_low_scl(); // 拉低 SCL delay_half_period(); bool sda_actual = read_sda(); // 读回 SDA release_scl(); // 释放 SCL // 等待 SCL 真正变高(处理时钟延展) while (!read_scl()) { if (timeout()) return false; } if (bit && !sda_actual) { // 想发高但读回低,仲裁失败 release_sda(); return false; } return true; }关键点有三个:一是发送后要读回 SDA 比较;二是释放 SCL 后要等它真正变高,处理时钟延展;三是仲裁失败后要立即释放 SDA,并且不再参与后续时钟。
提示:软件 I2C 的延时函数精度很重要。如果延时太长,时钟频率上不去;如果太短,可能还没等 SDA 稳定就采样了。我一般用
__NOP()或者 DWT 计数器做精确延时,不用HAL_Delay(),因为后者是毫秒级,太粗。
5. 常见故障排查:仲裁和时钟延展相关的坑
5.1 总线死锁:SCL 或 SDA 被永久拉低
最常见的 I2C 故障是总线死锁,表现为 SCL 或 SDA 一直是低电平,所有通信都失败。原因通常是:
- 从机在传输过程中被复位(比如 MCU 复位但从机没复位),从机还在等时钟,拉低 SDA
- 主机在从机做时钟延展时超时退出,但从机还在拉低 SCL
- 仲裁过程中主机异常复位,SDA 被某个设备拉低
解决方法是在初始化时发送 9 个时钟脉冲,让从机把剩余的数据位发完,然后发 STOP。具体代码:
void i2c_bus_recovery(void) { // 配置 SCL 和 SDA 为开漏输出 gpio_config_open_drain(SCL_PIN); gpio_config_open_drain(SDA_PIN); // 如果 SDA 是低,发 9 个时钟 if (!read_sda()) { for (int i = 0; i < 9; i++) { pull_low_scl(); delay_us(5); release_scl(); delay_us(5); } // 发 STOP 条件 pull_low_sda(); delay_us(5); release_scl(); delay_us(5); release_sda(); } }这个恢复流程在 STM32 的 I2C 初始化里很常见,尤其是从低功耗唤醒后。ESP32 休眠后 I2C 复位也是类似的问题,需要在唤醒后重新初始化 I2C 并做总线恢复。
5.2 时钟延展导致的超时误判
有些从机的时钟延展时间比较长,如果主机超时设得太短,会误判为通信失败。比如 AT24C02 的写周期是 5ms,如果你在写操作后立即读,从机会延展 5ms,主机超时如果设 1ms 就会失败。
解决办法是查从机数据手册,找到最长的延展时间,把主机超时设为它的 2 倍以上。对于 EEPROM,写操作后可以用"应答查询"(Acknowledge Polling)代替固定延时:反复发 START + 地址,如果从机 ACK 说明写完了,如果 NACK 说明还在忙。这样比固定延时 5ms 效率高。
5.3 仲裁失败后的状态机混乱
硬件 I2C 仲裁失败后,外设状态机会回到就绪状态,但软件如果没清除标志、没重新初始化,可能会卡在错误状态。STM32 的 HAL 库在仲裁失败时返回 HAL_ERROR,你需要检查hi2c->ErrorCode里的HAL_I2C_ERROR_ARLO标志,然后调用HAL_I2C_Init()重新初始化。
我遇到过一个问题:两个主机反复仲裁失败,其中一个主机的 HAL 状态机卡在 BUSY,后续所有传输都返回 HAL_BUSY。最后发现是仲裁失败后没有正确复位 I2C 外设。加上HAL_I2C_DeInit()+HAL_I2C_Init()之后问题解决。
5.4 上拉电阻和总线电容导致的仲裁误判
前面提到过,上拉电阻太大导致上升沿太慢,会让仲裁在采样点读到不确定的电平。这种情况的波形特征是 SDA 上升沿明显变圆,在 SCL 高电平中间还没到 VDD 的 70%。解决方法是减小上拉电阻,或者降低总线速率。
用逻辑分析仪抓波形时,重点看 SCL 高电平期间 SDA 是否稳定。如果 SDA 在 SCL 高电平中间还在上升,说明上升时间不够,需要调整上拉或降速。
6. 不同平台上的仲裁与时钟延展支持情况
6.1 STM32 硬件 I2C
STM32 的硬件 I2C 完整支持多主机仲裁和时钟延展,但不同系列有差异。F1 系列的 I2C 外设有已知的硬件 bug(比如某些情况下会卡死),F4 和后续系列好很多。使用 HAL 库时,仲裁失败会触发HAL_I2C_ER_IRQHandler,你需要实现错误回调。
配置要点:GPIO 必须设为GPIO_MODE_AF_OD,I2C 的OwnAddress1要设置(多主机时用于接收),时钟频率按实际设置。如果只做单主机,可以忽略仲裁相关的中断,但建议还是打开错误中断,便于排查。
6.2 ESP32 的 I2C
ESP32 的 I2C 外设也支持仲裁和时钟延展,但 ESP-IDF 的驱动对时钟延展的支持需要确认。ESP32 在低功耗休眠后 I2C 引脚状态可能丢失,唤醒后需要重新配置。如果从机在休眠期间拉低了 SDA,唤醒后要做总线恢复。
ESP32 的 I2C 时钟源可以选 APB 或 RC,用 RC 时频率精度差,可能影响仲裁。建议用 APB 时钟。
6.3 Linux 内核 I2C
Linux 的 I2C 子系统通过i2c-algo-bit支持软件 I2C,通过i2c-adapter支持硬件 I2C。软件 I2C 的i2c-algo-bit实现了时钟延展检测(在i2c_outb和i2c_inb里有等待 SCL 变高的逻辑),但仲裁支持取决于具体实现。硬件 I2C 控制器的仲裁由硬件处理,驱动只需要处理错误中断。
设备树里可以配置i2c-scl-falling-time-ns、i2c-scl-rising-time-ns、i2c-scl-internal-delay-ns等参数来匹配实际波形。如果遇到 GT911 这类器件通信失败,可以尝试调整这些参数。
6.4 软件 I2C 的通用实现建议
如果你要写一个通用的软件 I2C 库,建议把仲裁和时钟延展做成可配置选项。单主机场景可以关掉仲裁检测(省时间),多主机场景打开。时钟延展检测建议默认打开,因为很多从机都会延展,关掉会出问题。
延时函数用 DWT 或定时器,不要用空循环,因为空循环的延时随编译器优化等级变化,不稳定。我一般用 DWT 的 CYCCNT 做微秒级延时,精度足够。
7. 几个实测中的经验教训
第一个教训是关于上拉电阻的。我曾经在一个项目里用 10kΩ 上拉,100kHz 通信一直很稳,后来加了几个传感器,总线电容上去了,就开始偶尔通信失败。用逻辑分析仪一看,SDA 上升沿在 SCL 高电平中间才到 2V 左右,采样点读到的是不确定值。换成 2.2kΩ 后问题消失。所以上拉电阻要根据总线电容算,不能照抄开发板。
第二个教训是关于时钟延展超时的。有个项目用 GT911 触摸屏,I2C 偶尔失败,查了很久发现是 GT911 固件在特定情况下会延展 SCL 超过 100ms,而主机超时设的是 50ms。把超时改成 500ms 后稳定。所以超时值要留足余量,尤其是从机固件行为不确定时。
第三个教训是关于仲裁失败重试的。两个主机同时发数据时,如果都立即重试,可能会反复冲突。我后来加了一个基于设备地址的退避时间,地址小的退避短、地址大的退避长,冲突概率大大降低。这个策略在多个同优先级主机竞争时很有效。
第四个教训是关于总线恢复的。从机在传输中被复位后,SDA 可能被拉低,导致总线死锁。我在所有 I2C 初始化代码里都加了 9 时钟恢复流程,从低功耗唤醒后也调用一次,之后再没遇到过死锁。
第五个教训是关于逻辑分析仪的。调试 I2C 问题时,逻辑分析仪是必备的,但要注意采样率。100kHz 的 I2C,逻辑分析仪采样率至少要 10MHz 才能看清上升沿。如果采样率不够,看到的波形会失真,误导判断。我一般用 24MHz 采样率抓 I2C,波形很清晰。
I2C 的仲裁和时钟延展这两个机制,初看觉得复杂,但理解了开漏输出这个物理基础之后,一切都很自然。仲裁是"线与"逻辑在地址和数据上的应用,时钟延展是 SCL 开漏特性给从机的"发言权"。把这两个机制搞懂,再回头看 I2C 的各种异常,排查思路会清晰很多。