拓冰建站拓冰建站
首页 / 资讯中心 / 正文

I2C总线核心机制详解:开漏结构、多主机仲裁与时钟延展实战

1. 总线架构与设计背景1.1 SDA/SCL与开漏结构I2C总线上只有两根线SDA数据线和SCL时钟线但这两根线上可以挂几十个设备且支持多个主机同时工作。我最早接触I2C时觉得它很神奇芯片之间通信不都得有片选、使能、独立的读写线吗SPI老老实实拉一根CS线来区分设备UART至少还得先约定好谁先说可I2C两根线就能把所有设备串在一起靠地址来寻址还能多主机并行——这套设计到今天已经用了三十多年依旧在传感器、EEPROM、触摸屏、电源管理芯片里大量出现。理解I2C第一步必须是开漏结构。I2C的SDA和SCL都不是推挽输出而是开漏open-drain——芯片只能把线拉低不能主动拉高高电平全靠外部上拉电阻。所以总线空闲时两根线都是高任何设备想发送低电平就打开内部的漏极把线拉低。为什么非要开漏因为推挽输出会打架一个设备想输出高另一个设备想输出低两个推挽输出直接怼在一起就是短路。开漏下低电平是“强制”的哪个设备把线拉低了总线就是低任何想送高电平的设备都只能松开手等所有设备都松开了线才会被上拉电阻拉回高。这个“线与”行为是I2C能做多主机仲裁、能做时钟延展的物理基础也是整个协议最精妙的地方。上拉电阻的取值也影响很大。标准模式100kHz一般用4.7kΩ快速模式400kHz用2.2kΩ左右高速模式1MHz可能用到1kΩ甚至更小。阻值太小低电平下拉电流过大芯片会受不了阻值太大上升沿太慢时序容易超标。1.2 多主机仲裁解决什么问题传统总线里多主机是个麻烦事。比如UART两个设备同时往外发数据线上一阵乱码只能靠上层协议做碰撞检测SPI更直接从结构上就不允许两个主机同时操作一根总线谁都用独立的CS。但嵌入式系统里多主机是刚需。一片主控MCU要读取传感器数据另一片协处理器也可能要访问同一个EEPROM或者系统休眠时一个低功耗协处理器接管总线做数据采集主控醒来后也要操作同一根I2C总线。如果没有多主机能力就必须加外部总线切换器增加成本和复杂度。I2C的做法是允许两个主机在物理上同时发起传输。它们会在总线上“打架”但这个“打架”是良性的、可预测的、不会破坏数据和总线状态的。这个过程就是多主机仲裁。简单说多个主机谁能在仲裁里赢下来就看谁在数据位上先送出了低电平。1.3 时钟延展解决什么问题经常有人问我“I2C的时钟不是主机产生的吗从机怎么还能控制节奏”这就说到第二个精妙点——时钟延展。实际场景里从机的处理速度千差万别。一颗MCU主控跑着48MHz的时钟I2C总线跑到400kHz毫无压力但挂在总线上的从机可能是一个慢速单片机它收到一个字节的指令后内部要跑算法、要更新寄存器、要做ADC转换这些操作需要几百微秒甚至几毫秒。如果主机不管不顾地继续发下一个字节从机来不及处理数据就丢了。时钟延展的设计思路非常朴素却极其聪明从机可以在它忙的时候主动把SCL这条时钟线拉低。SCL是开漏的主机只要检测到SCL变高才能继续推进下一个数据位所以从机拉低SCL这段时间里I2C总线就像被按下了暂停键——主机一直在等但不会丢数据、不会产生错误。等从机处理完毕释放SCL时钟线恢复高电平传输继续。要手动模拟时钟延展只需要注意一点做I2C主机时不能默认SCL一定能按时拉高必须检测SCL电平。一个简单的逻辑是发送完一位后把SCL置1然后加一个while循环去等SCL电平恢复。这就是很多人写GPIO模拟I2C时容易忽略的关键点——不用检测SCL的代码接上带时钟延展的从机大概率会出问题。2. 多主机仲裁一根线上的公平竞争2.1 仲裁的物理基础线与逻辑多主机仲裁不需要任何额外的仲裁器也不存在专门的仲裁帧它就藏在每个普通的数据位里。我们还是回到开漏结构。总线上所有设备都开漏因此只要有一个设备把SDA或SCL拉低总线就是低电平。这时候如果两个主机同时开始传输它们发出的数据在线上会做“线与”一个发逻辑1释放SDA另一个发逻辑0拉低SDA线上最终电平是0。那个发1的主机在SCL高电平采样SDA时发现自己发出的1变成了0就知道自己输了立刻退出当前传输。发0的主机发现采样结果和自己发出的电平一致继续正常传输。整个过程没有任何一方破坏总线输的一方也不会把数据搞乱这就是仲裁无损的原因。相同地SCL也存在类似的同步机制。两个主机各自的时钟频率即使有差异是同一个开漏SCL线上做“线与”的结果总线低电平的持续时间由所有主机里拉低时间最长的那个决定总线变高的时间也要等所有主机都释放SCL之后才会出现。这样一来即便两个主机时钟略有偏差仲裁采样点也会自然对齐到同一个SCL边沿上。所以仲裁不是靠“先来后到”而是靠“低电平优先”。谁先送0谁就赢而且是从起始条件之后的第一位就开始比。2.2 仲裁过程逐位比较与败者退场仲裁从START起始条件之后就开始。两个主机如果几乎同时发出STARTSDA都被拉低了这个阶段分不出胜负真正的胜负是在后面的地址位和数据位上决定的。地址仲裁每个主机都会带一个目标从机地址。在发送地址的7个数据位期间仲裁随时可能发生。如果两个主机访问的是同一个从机地址那么地址段完全一样继续往下比如果访问的是不同从机地址位在不同之处就会分出输赢。例如主机A的目标从机地址是0x50二进制1010000主机B的目标地址是0x51二进制1010001发送到最后一位时主机A发出0主机B发出1主机B在采样时看到线上的0和自己的1不一致仲裁失败退出。主机A则赢下总线继续完成后续读写。数据位仲裁即使两个主机发的是完全相同的地址后面传输数据时也可能分胜负。这种情况通常发生在多主机共享同一从机、同时想写入不同数据的时候。仲裁失败的主机不是无事可做它需要立刻停止驱动SDA和SCL撤出总线并且不能再产生STOP条件因为总线已经归赢家所有了。它只能标记自己的传输失败等待当前事务结束后再重试。实用经验仲裁失败退出后不要立刻重发。两个主机如果各自立即重试很可能又撞在一起陷入死循环。比较简单的做法是加入随机退避或者等几毫秒重试实测下来稳定性会好很多。2.3 仲裁失败后的重试策略重试的设计我在实际项目里踩过坑。当时用两片MCU通过I2C互访还需要同时读写一片EEPROM经常出现大量通信错误。一开始重试逻辑写得特别简单检测到仲裁失败就马上重发结果偶发死锁偶尔还会有数据错乱。后来分析逻辑分析仪抓到的波形才明白失败后立刻重试两个主机很可能再次同时抢占总线仲裁又失败如此反复。加上随机退避后两个主机的重试时机错开通信就顺畅了。还有一个容易忽略的点仲裁失败时输的那一方可能在地址段、也可能在数据段才失败无论如何它都不需要回NACK也不需要通知从机或赢家只需要安静退出。这也是I2C仲裁很重要的特性——失败的传输不会污染总线赢家完全不知道自己刚才和谁竞争过协议对赢家是透明的。当然如果系统中只有一个主机仲裁机制永远用不上所以很多只做从机通信的开发者完全没有接触过仲裁。但一旦系统升级为多主机这套机制的价值就显现出来了。2.4 用逻辑分析仪捕捉仲裁现场把两个主机接在总线上想让它们同时发起传输还真不是每次都能复现因为数据冲突是随机的。我常用的办法是用一个主机连续快速读EEPROM另一个主机连续快速写EEPROM总线负载高了仲裁冲突概率会明显上升。用逻辑分析仪抓SDA和SCL采样率至少设为I2C时钟的10倍100kHz的I2C用1MHz采样率就够了打开协议解码器就能看到地址帧或者数据帧中间出现了一个“仲裁丢失Arbitration Lost”事件。观察波形时有个小技巧仲裁发生的那一位SCL高电平期间SDA的波形往往会比正常位稍宽或稍窄因为输的一方中途释放SDASDA电平在SCL高电平中间发生了变化这种“SDA在半拍翻转”就是典型的仲裁现场。逻辑分析仪还能帮你验证从机的时钟延展。正常主机产生的SCL是比较整齐的方波但如果有从机在延展你会看到SCL低电平期间明显被拉长低电平时间远大于高电平时间。出现这种情况首先别怀疑主机坏了先查从机为什么忙。3. 时钟延展慢从机的“暂停键”3.1 时钟延展的物理实现时钟延展看起来很玄实际也是开漏结构带来的天然能力。SCL线本身可以被任何开漏设备拉低。主机按照自己的内部时钟产生SCL正常流程是拉低SCL→在SDA上放数据→释放SCL等线上变高→采样SDA→循环。关键就在“释放SCL等线上变高”这一步。如果从机因为忙在SCL上升沿之前抢先拉低了SCL那么主机即使释放了输出SCL也变不高。主机检测到SCL仍然是低就不能继续下一步只好一直等。这个等待不是死锁而是协议允许的合法状态。从机忙完了松开SCL时钟线恢复高主机发现电平变化继续往前走。很多新手不理解的一点是从机为什么能拉低SCL从机不是只有SDA有输出能力吗其实I2C规范里SCL同样是用开漏结构驱动的任何一个挂在总线上的设备都可以把它拉低。主机负责产生时钟但从机在特定情况下可以“暂停”时钟这就是两者之间的一种默契。3.2 主机侧的两种处理方式主机面对时钟延展有两种不同的应对场景分别是硬件I2C外设和GPIO模拟。如果你用的是MCU内部的硬件I2C外设比如STM32、NXP、Microchip等大部分外设内部逻辑已经处理了时钟延展。硬件外设在每个数据位传输后会自动检测SCL电平如果SCL一直被从机拉低状态机会停在原地等待直到SCL恢复。对使用者来说几乎是透明的你只要正常读写寄存器就行。但如果用GPIO模拟I2C就必须自己在软件里处理。我见过很多人写的模拟I2C代码SCL置1后加了个几微秒延时接着就去采样SDA了。一旦遇到会延展时钟的从机数据位就会错。正确的是在SCL置1之后加入一个检测SCL电平的循环scl_output(1); while (scl_read() 0) { /* 等待从机释放时钟延展 */ } // 此时才允许采样SDA、继续后续时序这样一个简单的while循环就能兼容绝大多数带时钟延展的从机。注意这个循环一定要配上超时保护防止从机异常拉死SCL导致系统卡死。超时时间可以根据目标任务设定比如10ms到50ms超过就报错复位总线。硬件实现方面FPGA或Verilog驱动I2C时也很容易遇到延展问题因为你自己负责状态机的时序推进。设计状态机时最好把“SCL拉高后检测电平”作为一个独立的等待状态让位传输和字节传输都走这个公共状态不要用固定延时替代。3.3 从机忙的典型场景哪些从机喜欢用时钟延展我重点说几个实战中常遇到的ADC转换芯片很多I2C接口的ADC在发起转换后需要一定时间内部转换期间芯片会延展SCL。你读转换结果时主机可能被延展几十到几百微妙不等。带内部算法的传感器比如某些温湿度传感器、气体传感器读取校准数据或触发测量后内部要跑算法这时候从机会拉住SCL。需要内部EEPROM写操作的芯片这类芯片在做内部存储写入时会通过不响应ACKSDA保持高或者延展SCL来表达“忙”具体方式取决于芯片手册。触摸控制IC我在调试GT911这类触摸屏控制器时遇到过一会儿就是GT911在固件升级模式或内部处理参数时也会延展时钟。有一点需要注意时钟延展既可以发生在字节边界也可以发生在位边界。多数芯片在某个ACK位之后延展延展一个或几个SCL周期少数特别慢的芯片会延展几十毫秒。所以超时时间不能设得太短同时也别指望逻辑分析仪里SCL是完美方波。3.4 和仲裁机制如何配合时钟延展和多主机仲裁在总线上会同时生效它们配合得相当巧妙。仲裁时两个主机在竞争总线其中一个输了会立即撤出。但如果在仲裁过程中从机同时拉低SCL延展时钟这并不会改变仲裁结果因为所有主机都要等待SCL变高才能继续采样等待期间总线上电平时序是稳定的仲裁仍会在下一个有效电平上正常判定。时钟延展也不会因为多主机就失效。从机延展SCL时所有主机都会在“SCL高电平等待”这个环节停下。因此时钟延展天然就是全局的不管是单主机还是多主机从机都可以暂停整个总线。这一点在一些设计里很关键如果你实现了多主机其中一个主机正在等待从机释放时钟延展而另一个主机还傻乎乎地想在总线上发数据那么它只会发现SCL一直是低。正常I2C主机在SCL低电平时启动START条件是非法的这会违反协议所以正确实现的多主机主机在启动传输前也要确保总线空闲。4. 经典应用从EEPROM到接口扩展4.1 EEPROM读写与写轮询I2C总线最经典的应用就是EEPROM读写AT24C02、AT24C256这些芯片几乎每个嵌入式项目都见过。EEPROM读写要特别注意内部写周期。你写完一个字节或一页数据后EEPROM需要几毫秒时间把数据从缓存真正写入非易失存储这段时间芯片不响应I2C操作。多数EEPROM在这个忙周期里是用“不响应ACK”来表达的而不是延展SCL。主机发送下一个命令的地址时如果EEPROM忙SDA在ACK位会保持高电平主机收到NACK。这时候正确的做法不是报错丢弃而是反复发送写命令去轮询直到收到ACK为止。这个技巧叫“写轮询”很多人处理EEPROM驱动快了就容易漏掉这一步导致写入后立刻读取时拿到的还是旧数据。写轮询的典型代码流程do { i2c_start(); if (i2c_write_slave_addr(addr | WRITE_BIT) ACK) { i2c_stop(); break; // 写周期结束芯片就绪 } i2c_stop(); delay_ms(1); } while (retries--);这里每次轮询都以START开始紧跟着发送从机写地址。收到ACK说明EEPROM内部写周期结束了。轮询间隔至少要1ms太频繁反而浪费时间。4.2 Verilog驱动EEPROM的时序要点做FPGA项目时经常需要用Verilog直接驱动EEPROM。这种驱动跟软件驱动有个重要区别软件里可以用while循环等一个IO电平Verilog状态机里必须把等待拆成显式状态。比如时钟延展可以给状态机设计一个公共的“等待SCL变高”状态任何一位传输在SCL拉高后都进入这个状态检测到SCL为高才继续。用计数器作为超时比如SCL超过64个周期还没变高就判定为I2C故障。EEPROM的读写状态机一般包括IDLE、START、发送地址、发送数据、发送停止条件、等待写周期、轮询ACK等状态。实现过程中最坑的地方之一是地址页边界。AT24C02的页大小是8字节写入一页数据时如果在页边界不换页地址计数器会回卷到页首把数据写到错误的位置。驱动里要根据写入地址和剩余数据长度自动拆分一次不能跨越页边界。写EEPROM时连续写入长度超过页大小就会踩这个坑数据明明发成功了读出来却错位。读EEPROM则相对简单可以顺序读了连续读但也有边界条件要考虑。有些EEPROM的读操作是通道式的连续读超过芯片末尾会回卷到起始地址如果你读的长度没控制好最后几字节可能是垃圾数据。4.3 I2C扩展器与多路复用I2C上除了EEPROM和传感器还有一个很常见的类别是I/O扩展器比如PCF8574、TCA9534用两根线换八个IO省IO脚特别好用。I/O扩展器本身没什么技术难点但总线上如果挂了多片同样地址的扩展器就会冲突。PCF8574的地址引脚A0-A2能提供8种组合理论上一根总线最多可以挂8片同样型号的扩展器。实际部署时经常不够用这时候就需要I2C多路复用器比如TCA9548A它可以把一根总线扩展成8路每一路都可以单独导通或关闭适合处理多片同地址设备的隔离场景。使用TCA9548A时有一个经验切换通道之后立刻发送目标设备访问中间最好加一个微小延时让复用器内部开关稳定。否则在某些MCU上会偶发第一个字节的ACK读不到。通道隔离还有个好处是可以把不同电平的总线分开。I2C需要上拉电阻到对应电平1.8V的传感器和3.3V的MCU不能直接挂同一根总线但通过多路复用器分到不同通道后每路用各自的电平上拉就不冲突了。4.4 从I2C到PMBusPMBus是电源管理领域常用的协议它物理层基于I2C/SMBus但定义了一套完整的电源管理命令集比如输出电压设定、电流读取、状态查询、故障记录等。PMBus设备通常是数字电源芯片例如TI的TPS536系列、MPS的MP2886等。PMBus和I2C的区别主要在软件层PMBus要求命令格式固定很多命令带数据长度和参数PMBus还广泛使用PEC包错误校验用CRC8对整帧做校验防止通信误码。I2C本身没有CRC概念普通I2C调试经验不能直接套用到PMBus上你往PMBus设备地址发送自定义数据芯片大概率不会响应。跟PMBus设备调试时最好先用i2cdetect扫描地址再用i2ctransfer按PMBus规范发送标准命令。很多PMBus芯片支持SMBus的ARA提醒响应地址故障时主动拉低SMBALERT#引脚。这块逻辑在用普通I2C主控访问时经常被忽略导致故障信息读不到。4.5 I2C的自由数据模式与从机主动更新这两年有些芯片开始支持一种更灵活的I2C模式热词里提到的“自由数据模式”大致指这样的场景主机不再严格按照固定的寄存器地址数据长度访问而是通过特定位来区分是命令还是数据比如OLED驱动SSD1306就是这样的思路。你在驱动SSD1306时每个传输帧里有一个控制字节最高位是Co位次高位是D/C#位这两位决定了后续字节是命令还是显存数据。这种结构比传统“寄存器地址值”的方式更能表达“一长串数据”的语义。“从机主动更新主机寄存器”这个提法本质上也是灵活的I2C交互。传统从机是被动的主机读它才给数据。但某些I2C从机芯片可以通过中断引脚比如INT)通知主机有数据更新主机收到中断后主动读取。这不算真正的“从机写主机寄存器”而是中断读取机制但在嵌入式里你完全可以把它模拟成从机主动上报。调试这类从机时别忘了初始化中断引脚的GPIO和上拉否则从机永远不告诉你状态变化。5. 实战排查与常见问题速查5.1 逻辑分析仪分析I2C数据的正确姿势I2C调试逻辑分析仪是必须的标准18件套都不如一支逻辑分析仪实在。我常用的是几十块钱的24MHz采样逻辑分析仪配合DSView或PulseView使用完全够用。设置上注意几点采样率I2C标准模式100kHz至少要1MHz采样建议设到10MHz以上波形细节更清楚。触发一般触发在SDA下降沿也就是START条件可以精准抓到一帧的开始。协议解码在协议解码器里选择I2C把SDA、SCL通道映射好解码器会自动识别START、地址、读写位、ACK/NACK、数据、停止条件。抓到波形后首先看ACK位正常情况下每个地址和数据字节后都有ACK低电平。如果看到NACK多半是目标从机不存在、从机忙或者写周期没结束。其次看SCL高电平期间SDA是否稳定如果SDA在SCL高电平期间出现跳变不是DDR问题就是仲裁丢失或时序违规。有一个经常被忽略的地方逻辑分析仪的通道悬空会导致波形乱跳。接I2C总线的通道一定要和GND共地否则从机地址可能被解码成错误值。还有逻辑分析仪自带的输入电阻会轻微拉低总线电平如果要抓的是高速I2C和弱上拉抓到的波形上升沿会有明显变慢这时不能盲目怀疑上拉电阻。5.2 GT911触摸屏通信失败排查实录GT911是一款常用的电容触摸控制芯片I2C通信失败的问题太常见了。我调试GT911时踩过的坑可以列成一张排查清单地址7位和8位混用GT911的I2C地址是可以通过复位时序和外部引脚配置的常见有0x5D、0x28等。这里有个天然的坑很多芯片手册写的是8位地址比如0xBA而LInux用户态驱动或i2cdetect里用的是7位地址0x5D。如果你手册照抄8位地址就会从0xBA变成十四进制怎么读都失败。要统一换算。复位和中断引脚时序GT911对复位时序很挑剔INT引脚的电平状态和复位顺序会影响地址选择。上电时序不对芯片会以错误配置醒来I2C直接没有响应。I2C总线没有正确空闲GT911上电后需要几十毫秒初始化如果主机过早发起访问芯片还没准备好不响应是正常的需要在驱动里加延时或重试。时钟延展处理GT911在某些情况下会延展SCL如果你的裸机驱动没有处理SCL等待在某些时间点会出现位错乱。此时逻辑分析仪上能看到SCL低电平时间被拉长解码出现异常帧。我实际遇到过一次特别邪门的情况同一型号的两块板子一块I2C完全正常一块完全不行。最后查出来是触摸屏FPC排线接触不良导致SDA上升沿过慢解码器判定电平不稳定。所以硬件问题排查优先级要排在软件前面先用示波器看波形质量再调代码。5.3 Linux下不依赖MDIO的I2C方案在Linux设备上管理以太网PHY一般都会用MDIO总线也就是MII管理接口。但有些场景是不用MDIO的比如SFP光模块的寄存器访问它本质上就是I2C。SFP模块内部有一片EEPROM挂在I2C总线上地址一般是A0h和A2h。A0h存放模块基本信息型号、序列号、传输距离等A2h存放诊断信息温度、电压、光功率等。Linux下可以用i2c-tools直接读取i2cdetect -y 0 i2ctransfer -y 0 w20x50 0x00 0x00 r1这种方式就绕开了MDIO。很多PHY芯片也提供I2C管理接口比如某些工业级PHY芯片设计上允许通过I2C读改写PHY寄存器用于没有MDIO控制器的主控。如果要在Linux内核驱动里访问这类I2C设备需要在设备树里声明i2c节点然后通过i2c_client访问。调试时注意地址的单位差别i2cdetect里显示的是7位地址实际驱动i2c_transfer时地址要用8位地址左移后的值很多新手第一次接触会搞错。5.4 I2C HID“代码12”及其他典型故障Windows设备管理器里有个常见报错“I2C HID设备找不到足够资源可以使用。代码12”。这个错误经常出现在I2C触摸屏、I2C触控板上。原因多数不是I2C总线本身而是ACPI资源分配冲突BIOS给I2C控制器配置的中断或内存资源和系统里其他设备冲突Windows索性不给这个设备分配资源。这种问题一般优先更新BIOS然后在设备管理器中把冲突的I2C控制器删除、重启让它重新枚举。如果还是报代码12可以查一下系统日志里是否有IRQ共享冲突或者在BIOS里调整“IO资源保留”。还有一类典型的I2C故障是“时钟低电平卡死”。可能是上拉电阻焊掉、总线被短路也可能是某个从机异常拉低SCL不释放。排查方法很简单断电后测SCL对地对阻值正常读出上拉电阻的值量级。上电后用示波器看SCL是否一直为低如果一直低逐一摘掉从机挂载来定位元凶。做一个速查表方便现场排查现象可能原因排查方法总线上完全无响应上拉电阻脱落、电源没到测SCL、SDA静态电压扫描不到从机地址从机地址错误、复位时序问题查手册地址换算检查复位引脚偶发NACK从机忙、写周期未结束增加写轮询/重试数据漂移错乱时钟延展未处理、采样率太低示波器看SCL低电平时间上升沿过缓上拉电阻过大、总线电容太大示波器测tr换小电阻仲裁丢失频率高多主机重试策略不当增加随机退避写在最后的经验之谈我自己把I2C彻底吃透靠的是反复调一块双主机加时钟延展从机的板子。仲裁和时钟延展看着是协议规范里的两个小章节但真正理解了那套开漏“线与”的物理逻辑很多问题就迎刃而解总线上为什么能你让我让、为什么慢设备能拖住整条总线、为什么仲裁失败不会污染数据。如果只让我给一条建议我想说写I2C驱动时永远不要在SCL高电平阶段用固定延时替代电平检测。这是兼容性最重要的分水岭。遇到疑难问题先上逻辑分析仪用波形说话。I2C这套设计虽然老但正因为物理层机制扎实它在低功耗、低速控制场景里还会活很多年。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门