尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

I2C多主机仲裁与时钟延展:原理、工程坑与调试实战

I2C多主机仲裁与时钟延展:原理、工程坑与调试实战 去年调一块双主控板子时我踩过一个诡异的问题两片MCU同时通过I2C访问同一条总线上的EEPROM按照常理这种“并发抢占”总得来一次数据错乱才算正常可逻辑分析仪抓到的波形非常干净——一场交锋之后其中一片悄悄退场另一片完整跑完了读写流程数据一个字都没坏。回头翻《I2C总线规范》才知道这正是I2C多主机仲裁Arbitration在起作用也是I2C总线最容易被“会用但没吃透”的设计之一。这篇文章想聊的就是I2C里最精妙的两个机制多主机仲裁和时钟延展Clock Stretching。仲裁解决的是“多个主机抢总线、谁来说了算”的问题时钟延展解决的是“从机跟不上节奏、如何让主机暂停”的问题。适合刚入门I2C、正在写驱动、或者被总线上莫名其妙的通信失败折磨过的工程师们。我会从物理层开始把仲裁和延展的原理、边界、工程坑全部过一遍最后聊聊怎么用逻辑分析仪把这两个机制从波形上看出来。1. 一根线上挂几十个设备开漏输出和线与逻辑得先解释清楚1.1 开漏输出每个设备只有一个“接地开关”I2C的SDA和SCL引脚绝大多数芯片内部都是开漏Open-Drain输出。开漏是什么概念就是引脚内部只有一个MOS管用来把引脚拉到GND没有主动推高电平的管子。引脚要变高得靠外部上拉电阻把电压“补”回去。可以想象成一根线上挂着几个按钮按钮按下就接地谁都不按时靠弹簧上拉电阻把线恢复成高位。这根线上挂十个、二十个设备都没问题原因是多个设备同时按下按钮不会打架GND就是GND谁拉低都不会短路。相比之下推挽输出如果同时有设备输出高、有设备输出低那就是电源到地的直接短路轻则波形畸变重则烧毁驱动。这个开漏结构是I2C整个体系的基石。仲裁、时钟同步、时钟延展全都建立在“谁都能拉低总线但谁都不能独占拉高”这个物理事实之上。理解了这一点后面所有的机制都会顺理成章。1.2 线与逻辑一个天然裁判一个关键事实只要总线电平为低就说明至少有一个设备在拉低。因此对I2C设备来说“读总线”永远读到的是所有设备输出的逻辑与——这就是“线与”Wired-AND。这意味着当某个主机想输出高电平也就是释放SDA让上拉电阻把总线拉高如果它读到的总线却仍是低那一定是有别人在拉低。不需要额外的仲裁寄存器也不需要优先级表硬件在物理层就判定了谁会赢。更妙的是这个判定是瞬间完成的。主机在读回总线的瞬间就能感知到冲突然后立即停止驱动不会有半分延迟。我在写代码时总爱把I2C比作一群人开会开漏和线与就是“话筒”——谁按下谁说话同时按下去声音低的会听到自己被盖住自动闭嘴。1.3 为什么SPI、UART做不到这种仲裁多主机并发这件事在别的总线里很难做到。SPI的MOSI如果两个主机同时驱动推挽输出一方输出高、一方输出低就会短路所以要实现SPI多主机得靠外部仲裁、片选互锁这些“额外电路”。UART本身是点对点设计多主机通信需要地址帧、冲突检测流程复杂得多。I2C把仲裁做进了物理层硬件自动完成。这也是它从一开始设计就定的路低速率、低成本、简单可靠。像现在一些系统里两个MCU共挂一颗EEPROM、一颗触摸屏、几颗传感器靠I2C的仲裁机制硬件上天然就不会因为“同时抢总线”而出错软件上只需要做好事务级别的规划就行。2. 多主机仲裁逐位推演两个主机同时抢线会发生什么2.1 同时发START从机的视角一片平静当主机A和主机B同时决定“我要占用总线”时它们在SCL高电平期间把SDA从高拉低产生START条件。从总线上看这个START跟单主机发出的没有区别从机也不会意识到“发起方有两个”。两个主机正式开抢是第一个SCL脉冲开始之后的事。也就是说仲裁不是在START瞬间就分出胜负的而是沿着地址位、数据位一路“边发边看”下去直到出现不一致的位。2.2 位对位PK发1读到0就当场出局I2C仲裁是逐位进行的。规范要求主机在输出每一个数据位时必须在SCL为高电平期间读回SDA总线状态如果自己写的是1读回来是0说明另一个主机已经在总线上打了低电平自己输掉了仲裁。举个例子主机A要发0x55主机B要发0x4A仲裁过程是这样位序主机A发送主机B发送总线实际电平仲裁结果1000继续2111继续3010主机B输掉4-8继续发送释放SDA按主机A数据走主机A获胜关键就在第3位。主机B想发1所以它释放了SDA等上拉电阻把总线拉高但主机A同时拉低了SDA于是总线被死死压在地上。主机B在SCL高电平窗口读回总线发现是0与自己想发的1不一致立即宣告仲裁失败。代码层面仲裁逻辑可以用这样一段伪代码描述static int i2c_send_bit(struct i2c_host *host, int bit) { if (bit) sda_set_input(); /* 释放SDA表示输出1 */ else sda_set_output(0); /* 拉低SDA表示输出0 */ scl_high(); delay_half_period(); if (bit sda_read() 0) return ARBITRATION_LOST; /* 发1读到0当场出局 */ scl_low(); delay_half_period(); return ARBITRATION_WON; }注意平时我们发0的时候也要读回总线只是发0读到0是正常情况只有发1读到0才能判定仲裁失败。如果发0读到1那就有大问题了说明总线上有设备在异常驱动或者总线被拉高了这通常属于硬件故障。2.3 输家退场不会破坏获胜方的任何数据仲裁失败的设备在发现自己输掉的那一个位就把SDA释放成高阻之后不再参与本次传输。它不能突然发一个STOP“表示抗议”因为在多主机总线上STOP是释放总线的控制信号输家没有控制权。从机的视角更简单它只是收到一个主机的完整消息完全无法意识到刚才有两位主机争夺过。仲裁失败对获胜方和从机完全是透明的数据在“线与”的保护下没有撕裂、没有覆盖、没有半截帧。这也是为什么我开头说“数据一个字都没坏”的原因。我在项目里还见过一种误解有人觉得仲裁失败会收到一个“失败的ACK”。不会的。仲裁失败是主机之间的竞争从机从头到尾看到的都是一个主机的完整流程ACK照常回复什么都不受影响。2.4 重复START和STOP的仲裁容易被忽略的特殊规则数据位仲裁之外START和STOP条件也参与仲裁。主机发出重复START时如果另一个主机正在发数据0SDA被拉低重复START的“高到低跳变”就发不出来主机想发STOP时如果SDA为低同样立即失去仲裁因为它无法产生“低到高跳变”。这三个条件的仲裁规则可以总结成一张表仲裁对象双方动作失败判断数据位发1 vs 发0发1者失败START/重复START产生SDA高到低 vs 发数据0想产生START者失败SDA已是低STOP产生SDA低到高 vs 发数据0想产生STOP者失败SDA已是低这个细节在实际项目里经常被软件工程师忽略因为数据位仲裁好理解很多人默认“仲裁只发生在数据位上”结果遇到重复START或者STOP场景开始乱抓瞎。我的建议是除非真的有强需求不要指望在总线上跑“两个主机并发高频抢占”硬件仲裁是保底软件尽量还是做时间片错峰否则即便不损坏数据调试成本也会很高。3. 从机的“暂停键”时钟延展到底是什么以及为什么需要它3.1 从机把SCL拉低主机就得等着SCL引脚同样是开漏的这意味着从机在“被动应答”之外也完全有能力把SCL拉低。所谓时钟延展Clock Stretching就是从机在某个位或者ACK位把SCL保持为低迫使主机暂停时钟一直等到从机准备好再释放SCL让通信继续。为什么不在SDA上暂停因为SDA是数据线从机如果在那里拉低主机可能误解为数据0或者仲裁失败。SCL作为时钟线上的“暂停键”再合适不过主机只要检测到SCL不为高就不会继续产生下一个时钟边沿整个总线就被冻结住。3.2 延展常发生在哪ACK位、数据位、字节间隙最典型的延展位置是ACK位。从机接收完一个字节后需要内部处理比如把数据搬进FIFO、刷新内部状态、启动一次测量它会在ACK槽位上把SCL拉低处理完了才释放。主机这边看起来就是“SCL高电平迟迟不来”于是耐心等。第二种是位级延展发生在数据位之间。有些从机每个bit都要处理精度要求高的场合会在每一位上延展。第三种是字节间隙延展发生在ACK之后、下一个字节开始之前实际上是前面两种的组合形式。EEPROM写周期是个非常直观的例子。EEPROM写一个字节后内部需要tWR时间典型值是3到5毫秒。如果主机连续写多个字节EEPROM会在每次内部写期间通过时钟延展拖住主机而不是向主机抛出一个“NACK”或者干脆丢数据。主机不需要额外写延时函数不需要查状态寄存器总线自动“慢下来”这就是硬件设计上的优雅之处。3.3 时钟同步别把延展理解得太狭隘很多人不知道的是时钟同步Clock Synchronization和时钟延展本质是同一个机制。多个主机同时发时钟时SCL低电平的持续时长由“最后一个释放SCL”的设备决定高电平持续时长由“第一个拉低SCL”的设备决定。于是较慢的主机会自动拉长整个时钟周期让快主机跟着自己的节奏走。从机的时钟延展也是这样它不产生主时钟而是把低电平时间拉长所有主机都得跟随它的节奏。这种设计意味着I2C总线上不需要统一的主时钟频率慢设备和快设备可以共存谁慢谁说了算——这点和SPI、UART那种“波特率必须预协商”的机制完全不一样。我在实际调试中经常用一句话跟人解释“I2C的速度不是协商出来的是争斗出来的谁更慢总线就更慢。” 虽然有点调侃但逻辑是对的。4. 时钟延展没有上限主机的超时机制必须认真设4.1 规范不管延展上限好机制的背面是风险I2C规范规定了SCL频率范围规定了各段时序参数但对时钟延展的持续时间没有设定一个强制上限从机理论上可以无限延展。这在从机正常工作时没有问题因为延展总会结束一旦从机异常比如固件卡死、内部状态机跑飞、外设时钟没起来SCL就会被拉在地上整条总线永久停摆。这就是时钟延展最容易被忽视的工程风险它在设计上给了从机无限的“暂停权”却没有给主机一个默认的“中止权”。所以主机侧必须自己设置总线超时否则一个坏从机就能拖死整个系统的I2C总线。4.2 主机侧超时设置别拍脑袋先实测再留余量所有能在多主机环境混的I2C控制器基本都提供了总线超时检测。STM32的I2C外设里可以配置Timeout A/BLinux下I2C控制器驱动里也有bus timeout相关的处理逻辑。代码大概长这样/* 以STM32 I2C为例配置时钟延展超时单位是I2C内核时钟周期 */ I2C-TIMEOUTR (I2C_TIMEOUTR_TIDLE | (50000U I2C_TIMEOUTR_TIMEOUTA_Pos));实际数值要查对应芯片的参考手册不同芯片寄存器差异很大但我更想强调的是设置思路。设置超时前最好用逻辑分析仪实测从机正常工作时最大的延展时间然后留出至少10倍余量。太紧会误报“总线超时”太松会让故障悬挂很久才暴露。一旦触发超时常见恢复手段是把SCL连续输出9个脉冲让卡住的从机状态机跳出来然后再发STOP复位总线状态。如果这招还不行那就只能复位从机电源了。4.3 实战案例GT911触摸屏I2C通信失败的排查GT911这类电容触摸IC是I2C从机工程中很常见“读不到坐标、通信偶发失败、主机报bus error”的问题。用逻辑分析仪挂在SDA/SCL上抓到的现象往往是SCL被拉低后持续了十几毫秒甚至几十毫秒才释放而主机I2C控制器的超时窗口只有百微秒级于是在从机还没释放SCL之前主机就已经判定总线超时关闭外设或者切入错误恢复流程。处理方向有这几条确认从机固件/驱动代码是否在触摸扫描期间频繁访问I2C寄存器GT911在固件加载阶段尤其容易拖时钟。把主机延展超时调大或者关掉过于激进的bus timeout策略。如果控制器实在太死板干脆用GPIO模拟I2C想等多久就等多久但代价是CPU开销上升。这类问题最难的一点不是“知道有延展”而是“知道从机在什么状态下会延展那么久”所以调试时一定要结合从机的状态机一起看不要只盯着主机报错。5. 仲裁碰到时钟延展协同工作的几个隐藏深坑5.1 延展发生时仲裁就地暂停仲裁和延展不是两个独立事件而是同一套硬件机制的两面。当从机把SCL拉低时所有主机都停在当前bit上SDA电平保持谁也不能继续发送。SCL释放后主机从暂停的位置继续仲裁也从那个bit重新开始竞争。这意味着仲裁可以在一个字节中间的某个位被“冻结”很长时间。比如主机A在发地址位从机突然延展了1毫秒主机B也同时等在这延展结束后A和B继续竞争下一个位好像中间那1毫秒根本不存在一样。这个特性在多主机系统里非常有用但也带来一个隐患主机的等待超时如果设置不当可能在延展期间误触发复位。5.2 两个主机同时访问同一从机再加延展隐藏死锁如果两个主机同时访问同一个从机都成功发送地址、从机也响应了但接着从机需要处理数据于是延展时钟两边主机会一起等。等从机释放后SDA上的仲裁继续最后总有一个主机赢得后续字节的发送权。坑在于主机A可能使用了“超时自动复位总线”的策略。A因为等待超过阈值开始复位然后产生START/STOP或者恢复时序这时候B可能正在继续原来的传输两边的“复位动作”混在一起现场会非常难看。我在项目里做过一次这么诡异的事故定位最后发现根本不是数据错误而是主机A的复位时序把主机B的传输给截断了。我的经验是多主机系统中只有在确认总线空闲有足够的bus idle时间后主机才可以做恢复动作。不要在等待延展期间贸然判定总线故障否则就是自己给自己挖坑。5.3 从机“主动”和更高层协议SMBus/PMBus与Linux里的I2C PHY管理严格说I2C从机不能主动发起传输但SMBus/PMBus这类高层的“报警响应地址”ARA机制允许从机通过拉低SDA等方式请求注意。总线上的主机收到后会通过仲裁决定谁去处理这个报警。这算从机参与的“伪多主机”场景仲裁机制同样管用。Linux下PHY不用MDIO而用I2C管理时也会遇到时钟延展的问题。PHY内部寄存器访问路径和MDIO不同I2C从机如果在忙期延展SCL内核I2C子系统里的timeout设得太短会出现偶发读PHY寄存器失败。实测中把I2C控制器超时调大后问题基本消失。顺便说一句SSD1306这类OLED的I2C驱动偶尔被归因于“速度慢导致花屏”其实大概率是初始化参数问题不一定是时钟延展但如果你在SCL上确实看到异常的低电平时间别急着怪驱动刷新慢先用逻辑分析仪确认是不是延展对症下药。6. 用逻辑分析仪复现一次真实的仲裁与延展6.1 接线与采样先保证能看清波形的底线I2C总线上挂逻辑分析仪是最简单的调试方式但条件要注意。400kHz快速模式至少要2M采样率实际我习惯用10M以上因为仲裁失败往往只持续几十纳秒到一两个微秒采样率不够很容易漏掉关键细节。通道接SCL/SDA地线必须和系统共地上拉电阻不用动。6.2 抓仲裁找到地址字节里的那次“穿帮”让两个主机人为同时发数据比如开机时都去轮询同一个寄存器逻辑分析仪解码I2C后观察地址字节里的某一位SDA在SCL高电平期间出现了“本应拉高却保持低”的短毛刺那个位置就是仲裁失败点。用事件触发或者循环采样多抓几次很容易看到。两侧主机分别接上不同的指示灯逻辑在软件里把“仲裁失败”这个事件打出来再和波形对上基本就能确认是谁输掉了竞争。这一步跑通之后你会对“发1读到0”这句话有切身体感。6.3 抓延展SCL低电平时长突然失控延展的特征非常明显SCL低电平时间明显拉长相对正常位宽可能拉长几倍甚至几十毫秒。好的逻辑分析仪软件会直接标记出stretch事件。把这段时间记下来作为超时配置的依据。如果一个从机的延展时间本身就在毫秒级而你的主机控制器超时只有微秒级那基本就能提前预判到“偶发通信失败”的问题会出现了。我见过最夸张的一次从机正常工作时延展就达到了40毫秒这已经是典型的主机超时10倍以上如果不去实测根本想象不到。6.4 多主机调试的一些习惯和工具我自己的做法是把逻辑分析仪当“总线看门狗”先跑完整测试用例记录每次延展最大值和仲裁失败次数然后根据这个数据设置软件超时留足余量再跑一轮确认没有误报。这比捧着数据手册猜参数靠谱得多。上拉电阻的选择也值得注意。多主机、长走线场景下上拉电阻太大容易让上升沿变慢仲裁窗口会被压缩总线电容大、设备多时建议选1k到2.2k普通桌面场景用4.7k也没问题。最后留一个小技巧设计PCB时在SCL/SDA上各留一个测试点不拆线就能夹逻辑分析仪探针。我这几年调I2C三分之二的问题都是靠波形而不是靠猜解决的。多主机仲裁和时钟延展这两个设计只有亲眼在波形上看到一次才算真正理解I2C为什么敢让一堆设备挂在同一根线上。
返回列表