I2C精髓:多主机仲裁与时钟延展机制详解
做了这么多年嵌入式我越来越觉得 I2C 是那种“看着简单、用起来全是细节”的协议。两根线 SDA 和 SCL加上两颗上拉电阻几十个芯片就能挂在同一总线上谁都能当主机谁都能当从机。但正因为这种“公共总线”的架构I2C 才必须解决一个 SPI 和 UART 根本不用碰的问题当两个主机同时想发数据时总线到底听谁的答案就是今天要聊的第一件事——多主机仲裁。而时钟延展则是从机在读写跟不上时反过来“喊停”主机的办法。这两个机制在我看来就是 I2C 协议里最精妙、也最容易被忽视的设计。这篇内容适合两类人看一类是刚学完 I2C 基础时序、正准备接手多设备总线调试的初学者另一类是被“总线卡死”“SCL 一直低电平”“从机不回 ACK”“两个主机打架”这类问题折磨过、想彻底弄明白原理的老手。看完你至少能说清楚仲裁和时钟延展到底是怎么发生的、硬件如何保证它们不出错以及出问题时该怎么用逻辑分析仪定位。1. 为什么“多主机仲裁”是绕不开的设计题1.1 一根总线上同时说话的物理定律先把场景摆出来。一块板子上有主控 A 和协处理 B两个芯片都要周期性往同一个 EEPROM比如 AT24C32里写日志。假如 A 和 B 同时在总线上发数据SDA 上一会儿是 A 的电平一会儿是 B 的电平从机根本没法判断谁是谁整个总线就乱套了。所以协议必须规定一套规则总线空闲时谁都可以发起 START真出现“同时发起”的情况就通过仲裁决定谁继续、谁退出。仲裁的关键物理前提是 I2C 用了开漏open-drain输出。所谓开漏就是器件只能主动把引脚拉低到 GND不能主动拉高拉高完全靠外部上拉电阻典型 1k~10kΩ。这样一来SDA 线上的电平天然就是“线与”只要有一个器件输出低电平总线就是低电平哪怕另一个器件想输出高电平也没用。这句话是整个仲裁机制的基石理解了它后面所有逻辑都能顺下来。这种架构到底有多普及看一下现在的器件市场就有数了。从温湿度传感器、实时时钟、EEPROM到触摸屏控制器、OLED 屏甚至连旋钮编码器这种简单器件都有封装成 I2C 从机的小模块。可以说凡是需要“板级低速配置”的功能厂商都会优先留一组 I2C 接口。也正因为挂的设备多多主机仲裁才从“纸上特性”变成了“每天都在跑”的机制——设备一多两个主机同时发起传输的概率就不是零了。1.2 “线与”为什么能同时当通信和裁决器很多人在面试或培训时被问I2C 的仲裁为什么不会损坏数据答案就在上面这句话里。别的协议里“两个主机抢总线”会直接造成电平冲突、短路甚至报文损坏但在 I2C 里两个主机同时拉低 SDA 并不是灾难它只是在执行一次“按位的逻辑与运算”。每个主机发出的 1 和 0都在总线上合并成 0 或 1谁和总线结果不一致谁就输掉仲裁。把“线与”的结果列出来看就非常直观主机 A 输出主机 B 输出总线上实际电平仲裁结果000双方继续010发 1 的一方输100发 1 的一方输111双方继续这里有一个常见的误区以为仲裁只在地址阶段发生。事实上仲裁可以发生在数据帧的任意一位上包括地址、寄存器地址、数据内容甚至重复起始位。只要两个主机发送的位序列完全一致它们就能“并行”地继续往下走直到某一位出现分歧。这也是为什么 I2C 仲裁被形容为“输家静默退场赢家毫无察觉”。顺带提醒一下上拉电阻的选型会直接影响仲裁和时钟延展的可靠性。电阻越小信号上升沿越陡仲裁窗口越短总线能跑更高的速率但电阻太小IO 口的灌电流会偏大。常规做法是 100kHz 总线用 4.7k~10kΩ400kHz 总线用 1k~2.2kΩ具体还要看挂载设备数量和端口的驱动能力。调试时最忌随手换电阻很多人改了一颗料之后总线就间歇性抽风多半就是这个原因。2. 逐位仲裁赢家通吃输家主动退场2.1 逐位仲裁的完整过程我拿一个最简单的例子展开。两个主机都想读地址 0x50 的 EEPROM。假设它们同时发起 START那么接下来都要发送“从机地址 读/写位”这 8 个位。发送每一位时都是 SCL 低电平期间拉高或拉低 SDASCL 高电平期间保持稳定主机自己边发边回读总线电平。比如第 1 位地址位A 和 B 都发 0SDA 被拉低总线显示 0和两者预期一致继续。第 2 位A 发 0、B 也发 0依旧一致继续。直到某一位出现分歧比如地址第 3 位A 发 1、B 发 0。A 在 SCL 高电平期间没有主动拉低 SDA理论上 SDA 应该被上拉到 1但 B 此时在拉低 SDA最终总线电平是 0。A 回读发现“我发的 1 变成了 0”立刻知道自己仲裁失败。这里注意几个要点。第一仲裁失败的主机不是马上“一拍两散”而是要等到当前字节发送完在产生下一个时钟脉冲前悄悄把 SDA 释放成高阻不再参与后续传输。第二仲裁失败方不能发出 STOP 条件因为总线当前并不归它“所有”此时发 STOP 会把整个总线状态搞乱正确的做法是静默退出让赢家继续完整地完成这次传输。第三赢家从头到尾都不会察觉发生了什么它以为总线一直是自己的——从机的视角也一样它只看到一个主机在正常通信。下面给一个软件模拟bit-bang场景下的仲裁检测思路方便你在没有硬件 I2C 外设的情况下理解“边发边读”// 发送一个位返回是否赢得仲裁 // 1 表示赢得仲裁0 表示仲裁失败 int i2c_send_bit(int bit) { sda_dir OUTPUT; // 准备驱动 SDA sda_write(bit); // 输出自己想要的电平 delay(SCL_LOW_HALF); scl_write(1); // 拉高 SCL此时线上的电平由所有主机共同决定 delay(SCL_HIGH_SETUP); int level sda_read(); // 回读实际总线电平 if (level ! bit) { // 我发 1 但线上是 0说明有人把我拉低了 sda_dir INPUT; // 释放 SDA退出竞争 return 0; // 注意不要发 STOP } scl_write(0); // 拉低 SCL进入下一位 return 1; }这个“边发边读”的方式就是硬件 I2C 控制器内部仲裁器做的事情只不过硬件把比较和退出做在了状态机里。你在数据手册里看到的“Arbitration Lost”中断标志就是这里“我发 1 但线上是 0”那一瞬间被打出来的。2.2 为什么仲裁过程不会损坏数据这一点值得单独讲因为很多人担心“两个主机同时拉 SDA高低电平打架会不会烧 IO 口”。开漏结构决定了不会。IO 口内部只是把引脚接到地或者不接并没有推挽输出里的“攻防战”。两个主机一个想拉高其实是释放、一个在拉低低的那方获胜线上电平干净地变为 0不存在“半高电平”也不存在灌电流打架。这也是为什么 I2C 仲裁几乎是所有总线仲裁机制里实现成本最低的。从协议层面看仲裁和“重传”是严格分开的。输家不拥有总线所以它不需要回滚任何东西——只要它不是赢家它这次“事务”就不算开始过而赢家看到的总线状态和预期一致数据照样按帧接收。唯一需要小心的是输家在仲裁失败后如果已经发出了部分位那就必须立刻退出不能继续发完自己的整个帧。实际项目中仲裁失败后最常见的错误重试逻辑是“马上再抢一次”。如果两个主机都这么做就会形成反复碰撞逻辑分析仪上看到的现象是总线繁忙、有效传输率暴跌。解决方法是让输家退避一段随机延时再重试类似以太网的退避思想。2.3 仲裁失败后主机的正确行为很多初学者在写软件模拟 I2C 时会忽略一个问题仲裁失败后到底要不要发 STOP答案当然是“不要”。原因很简单总线此刻由赢家接管输家发 STOP 会直接打断赢家的传输造成从机状态错乱。正确的退出路径是把 SDA 置为输入高阻然后等赢家完成整个事务总线再次空闲后再决定是否重试自己的事务。另外还有一点值得展开。仲裁失败的主机如果之前已经把某个从机“叫醒”了怎么办比如输家已经完成了 START 部分地址匹配从机已经开始响应此时输家退出赢家继续发同一个地址的后续内容从机只会看到一次完整的“地址命中 数据传输”不会出现半截事务。这正是“仲裁发生在每一帧的每一位上”这个设计带来的好处——只要地址部分相同从机甚至意识不到发生了仲裁。3. 时钟延展从机手里的“暂停键”3.1 从机什么时候会拉低 SCL如果说多主机仲裁解决的是“多个主机抢总线”的问题那么时钟延展解决的就是“从机跟不上主机节奏”的问题。时钟延展的机制一句话就能说清楚在 I2C 总线上SCL 同样是开漏结构当主机把 SCL 拉高之后从机如果觉得自己还需要时间准备数据它可以主动把 SCL 拉低主机检测到 SCL 没有按预想变为高电平就会暂停产生下一个时钟脉冲一直等 SCL 被释放。换句话说时钟是由“慢的那一方”控制的。主机认为自己在产生时钟但从机在它拉高 SCL 的窗口里“按”住了 SCL时钟周期就被拉长了。这个行为对总线上其他器件完全透明因为它们都在同步地看着 SCL 的边沿“暂停”对所有人一视同仁。哪些场景最容易出现时钟延展典型的有三类第一从机内部在做模数转换或测量运算比如 I2C 温湿度传感器 SHT3x 在读测量结果时会一直拉低 SCL 直到数据准备好第二从机的寄存器或内部缓冲区还没搬运完比如某些实时时钟芯片在读取内部时间时需要额外时间锁存时间值第三MCU 以软件模拟 I2C 从机时中断响应不过来于是靠拉低 SCL 硬生生“踩刹车”让主机等一等它的软件忙完。严格说并不是所有从机都靠时钟延展来减速。以 AT24Cxx 这类经典 EEPROM 为例它在内部写周期典型 5ms内根本不响应任何命令对外是直接回 NACK主机要用“ACK 轮询”的方式反复试探直到写入成功并没有去拉长时钟。这是另一套“从机减速”的思路两者目的相同实现方式不同很多人在一个项目里同时遇到这两件事容易把它们搞混。3.2 几个会“伸时钟”的真实器件说几个我实际调试过的例子让“时钟延展”四个字落到具体型号上。SHT3x 系列温湿度传感器是教科书式的时钟延展选手。你发完读取命令后它要花几毫秒做测量这段时间内你看到的 SCL 会被它一直拉低等测量完成它才释放 SCL把数据依次送出来。如果你用不支持时钟延展的主机去读它最常见的现象就是“读出来的数据全是 0xFF”或者主机直接卡死在等 SCL 变高上。我最早在这颗芯片上翻过车后来才养成了“时序异常先抓波形”的习惯。MLX90614 红外测温芯片也很有名它在 SMBus 兼容模式下会主动延展时钟。很多人用普通 I2C 主机去读它时明明地址和寄存器都对却总是收不到有效数据最后用逻辑分析仪一看SCL 上出现了额外的一大段低电平才意识到是时钟延展在作祟。这类器件的共性是它们内部真实有“测量/计算”动作不愿意在数据没准备好时被主机硬生生地把数据“挤”出来。另外一个容易忽略的是 MCU 自带的硬件 I2C 外设。很多芯片的硬件 I2C 从机模式在 CPU 来不及响应中断时会自动延展 SCL。这其实是保护机制而不是 bug。在调试时看到 SCL 出现额外低电平先别急着怀疑从机坏了先确认一下主机的读时序是否允许这种延展。SSD1306 这类 OLED 屏本身很少延展时钟但很多人用软件模拟 I2C 刷屏时出现花屏大半原因其实是主机侧 SCL 高电平时间不达标造成从机采样失败——这属于“主机时序不行”不是“从机延展过度”定位时要把两者分开。3.3 主机端必须做的适配与超时保护时钟延展对主机最大的要求有两个一是要在等待 SCL 变高时设置超时保护二是不能把“SCL 一直低”简单当成总线错误。先说超时。如果没有超时保护一旦从机因为内部故障一直拉低 SCL主机就永远等下去整个总线瘫痪连带影响其他器件。所以所有正经的 I2C 驱动都该在“拉高 SCL 后等待变高”这一步加一个超时判断。I2C 协议本身没有规定标准超时值SMBus 规定了最低 25ms、典型 35ms 的总线超时作为参考普通 I2C 系统一般取 5ms~50ms 都可以具体看从机数据手册里“最大时钟延展时间”一栏。有些传感器最大延展时间能到几十毫秒量级超时给 1ms 就会误伤正常通信。主机侧常见的问题还有两种。一种是软件模拟 I2C 时主机在“释放 SCL”后立刻去读 SDA没有检查 SCL 是不是真的变高了——这会导致采样点落在从机延展的窗口里拿到错误数据。正确的 bit-bang 顺序必须是先释放 SCL轮询等待 SCL 回到高电平再延时一小段满足数据建立时间然后采 SDA。另一种是硬件 I2C 外设不支持时钟延展。绝大多数硬件 I2C 主机是天然支持时钟延展的状态机里会自动等待 SCL 被释放但个别精简版 IP 会忽略它。遇到这类外设最实用的办法是换用另一组 I2C、改用 GPIO 模拟或者在从机侧选择不支持时钟延展的通信模式——这也是很多传感器手册里专门写“时钟延展可关闭”的原因。4. 实操排坑仲裁与时钟延展的翻车现场4.1 用逻辑分析仪抓时序的三个要点排 I2C 的 bug逻辑分析仪比示波器好用得多因为它能直接解码出 START、地址、ACK/NACK、数据字节还能量出仲裁位置和时钟延展的长度。我建议至少选采样率 100MHz 以上、通道数够用的型号双通道能抓但体验差不少。抓仲裁现场时一定要同时挂 SCL 和 SDA 两个通道采样率设高一些因为仲裁发生的窗口只在 SCL 高电平那一段。如果采样率太低两个主机在几个微秒内的位竞争根本看不出来。抓到之后先看总线空闲和 START 条件再看前 8 个位里有没有“某一位 SDA 变化和 SCL 边沿对不上”的地方——那往往就是仲裁失败的现场好的分析仪软件会直接标出 Arbitration Lost。抓时钟延展时直接看 SCL 通道上的低电平时间。拿解码出来的每一帧的 SCL 高电平时间对比如果发现个别位的高电平时间明显长出一截或者某个 ACK 位之后 SCL 长时间保持低基本可以判定为从机延展。再用数据手册给出的最大延展时间对比就能确定从机是否“延展过度”。这里分享一个比较隐蔽的经验很多逻辑分析仪的协议解析器默认没有打开“等待时钟延展”功能导致从机延展时钟时解析器会把后面几个位解析成地址或数据错误看起来像乱码。排查时如果发现解码结果面目全非先到解码设置里把“支持时钟延展”打开再重新解析往往瞬间就正常了。4.2 常见故障与对策速查表把我在项目里遇到过的、和网上高频出现的 I2C 故障列成一张表基本覆盖了仲裁和时钟延展相关的常见坑现象深层原因对策主从机初始化正常但偶尔整帧丢失两个主机同时发起传输仲裁静默发生输家重试策略不佳输家退避后随机延时重试避免“输了立刻再抢”造成反复碰撞SDA 一直低总线“卡死”某主机停机前没发 STOPSDA 停在低电平用逻辑分析仪定位是谁占着总线必要时冷复位所有设备SCL 长时间为低主机无响应从机时钟延展超时常见于传感器测量期间给等待加超时确认从机是否处在测量或写周期读传感器数据全是 0xFF主机不支持时钟延展采 SDA 采早了改用支持延展的硬件 I2C 或修正 bit-bang 时序EEPROM 写后读回还是旧值内部写周期未完成从机在写周期内 NACK用 ACK 轮询或固定延时一个写周期再读两个主机“轮流失败”吞吐暴跌都用了固定重试间隔反复碰撞重试延时改为随机退避如 1ms~10ms触摸屏 GT911 I2C 通信失败地址选错、复位时序不满足、上拉阻值不当核对地址0x14/0x5D、检查 INT/RST 时序、用逻辑分析仪看是否有地址应答HID-over-I2C 设备报“代码 12”资源不足Windows 下 I2C HID 控制器与设备资源冲突更新 BIOS 和触控驱动、禁用并重新扫描设备、检查中断冲突最后一行说的是 OS 层面的 I2C 问题。最近搜 I2C 的高频词里有一半是“i2c hid 该设备找不到足够资源”这类问题通常不是我们调总线的人直接改代码能解决的但理解 I2C 控制器在系统里也是“总线资源”的一种会有助于判断是不是硬件占用冲突——先禁用触控设备再在设备管理器里扫描新硬件常常能恢复。GT911 这类触摸屏控制器在嵌入式侧也很常见我排查下来十有八九不是协议本身的问题而是地址或者复位时序没伺候好先抓波形确认从机有没有正常应答再谈别的。4.3 “自由数据模式”和 Verilog 仿真里的延展处理顺带提两个热词里容易混淆的点。有人搜“i2c 自由数据模式”这个词在主流协议规范里其实不是一个标准术语更多是某些逻辑分析仪软件里的“自由运行/原始数据模式”专门用来抓那些不符合标准帧结构的裸数据比如某个从机在异常状态下发出的半截字节。遇到协议解析器死活解不出来的波形可以切到原始数据模式先把每个 bit 的时序量出来再按 START、地址、ACK 的规则手工对一遍往往能发现是解析器没认出来而不是总线上真的发错了。还有人搜“i2c 读写 eeprom 代码 verilog”说明不少人在用 FPGA 实现 I2C 主机控制器。写 RTL 时最容易漏的就是“等待时钟延展”这个状态很多初学者拉高 SCL 后在同一个 always 块里立即采 SDA完全没有考虑 SCL 被从机拉低的情况。正确的状态机里SCL 拉高后应该有一个专门的等待状态检查 SCL 是否真的为高不是就自循环等待并且配一个计数器做超时超时后回到 IDLE 并置错误标志。这个细节不到十行代码却决定了你的控制器能不能和各种慢从机兼容。// 伪代码示意拉高 SCL 后等待从机释放时钟延展检测 scl_out 1b0; scl_out 1b1; // 关键轮询 SCL直到它真的变高或者超时 while (scl_in ! 1b1) begin if (stretch_timeout_cnt 0) begin error_flag 1b1; // 延展超时 state I2C_IDLE; end end // 只有此时采样 SDA 才是安全的 sda_sample sda_in;5. 设计哲学为什么 I2C 能靠两根线活到今天5.1 和 SPI/UART 相比的取舍把仲裁和时钟延展放在一起看你会发现 I2C 的设计哲学特别有意思它把“谁说话”“什么时候说话”“对方听没听清”这些事全部用两根线、一个上拉电阻、一个“线与”逻辑搞定了。对比一下其他总线就能体会深一层。SPI 有专门的片选信号 CS一个主机带多个从机就得分配多个引脚天然就没有“多主机仲裁”的需求但代价是引脚多、多主机协作麻烦。UART 更是一对一通信多机时要引入 485 的方向控制或组网协议复杂度直线上升。I2C 选择在极低引脚开销下允许任意设备竞争总线所以它才能成为板级低速器件的事实标准。代价当然也存在。I2C 没有 SPI 那样清晰的片选和流控容错全靠“线与 仲裁 应答”一旦有人违反规则比如主机在总线忙时强行发数据或者从机没有释放 SCL整个总线就会被拖死而且很难定位。这也是为什么“i2c 时序图”“I2C 数据帧格式”这类搜索永远是刚需——光是 START/STOP、ACK/NACK、重复起始位这几件套就够新手折腾一阵子。搜索热词里还经常出现“linux phy 不使用 mdio”这类问题不少网卡 PHY 芯片都可以配置 I2C 作为管理接口本质还是把 I2C 当低速配置总线用。这恰好说明 I2C 的定位已经远远超出当初“连接外围芯片”的范畴它变成了一种无处不在的板级管理语言。5.2 SMBus、PMBus 与 I2C 的“血脉关系”再往上游看I2C 的仲裁和时钟延展机制被 SMBus 和 PMBus 完整继承了下来但加了一套“增强纪律”。SMBus 规定了更严格的超时最低 25ms、典型 35ms、固定的电平阈值、以及 ARP 地址解析协议PMBus 则在 SMBus 之上定义了电源管理器的寄存器语义。很多人纠结“PMBus 和 I2C 区别”其实 PMBus 物理层就是 SMBus/I2C区别主要在报文语义、超时要求和地址管理。如果你把 I2C 的多主机仲裁和时钟延展搞透了再上手 PMBus 只是换一套“带约束的 I2C”代价很小。多主机环境下还有一类常用器件值得提一下就是 I2C 多路开关/复用器比如 TCA9548A。它可以把总线分成多条下游支路用来解决多个从机地址冲突的问题。这类器件本质上是把“仲裁和地址空间”的复杂性做了一次隔离让每一条支路都有自己的地址空间你不需要让所有设备挤在同一条总线上玩“抢地址”的游戏。这里也涉及一个我常被问到的点既然 I2C 从机不能主动发消息那“从机主动更新主机寄存器”是怎么实现的答案就是多主机。想让从机“主动”上报数据要么给它加一条中断线要么让它在一段时间内切换成主机角色发起传输要么用 SMBus 的 ALERT 线或主机通知协议。标准 I2C 的从机角色真的只是“被叫才应答”所有“主动”都是靠多主机能力绕出来的。理解这一点你对 I2C 仲裁的价值会有更深体会——仲裁不只是“抢总线”它还是从机翻身成为主机、主动上报数据的基础设施。我自己实际调试下来最大的体会是I2C 仲裁和时钟延展不是书上的理论而是几乎每天都会遇到的现象。以前遇到“SCL 拉不高”总是先怀疑从机坏了或者上拉电阻选错后来用逻辑分析仪抓到从机延展时钟的波形才反应过来是主机自己没有等它。反过来两个 MCU 抢同一根总线导致偶发丢帧也是靠调整输家重试策略才彻底解决的。把“线与”和“边发边读”这两件事刻在脑子里I2C 对你来说就真的没有秘密了。最后再送一个小技巧排查 I2C 问题之前先数一下板子上哪些设备会延展时钟把它们的型号和数据手册里关于延展上限的页码整理到一张便签上贴在工位旁省下的调试时间绝对比你贴这张便签花的时间多得多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →