尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

I2C总线从开漏输出到多主仲裁:嵌入式工程师避坑指南

发布时间:2026/9/25 1:11:53

资讯中心
01
ARTICLE

I2C总线从开漏输出到多主仲裁:嵌入式工程师避坑指南

I2C总线从开漏输出到多主仲裁:嵌入式工程师避坑指南
I2C这东西刚入行的时候我觉得它特别简单——两根线一根时钟一根数据挂几个从机读读写写就完事了。直到有一次调试一块传感器板波形死活出不来示波器上SDA被拉到一个不高不低的电平既不是高也不是低我才意识到自己根本没搞懂这两根线背后到底发生了什么。后来花了整整一周时间从开漏输出的物理层一路啃到多主仲裁的时序逻辑才算把I2C真正吃透。这篇就把我这一周踩过的坑、画过的波形、想明白的原理完整地摊开讲一遍。不管你是刚接触嵌入式的学生还是做了几年硬件但一直对I2C会用不会讲的工程师这篇内容应该都能帮你把脑子里那些零散的知识点串成一条线。我会从最底层的电路结构讲起一直讲到多主机同时抢总线时仲裁是怎么工作的中间穿插大量实测波形分析和代码示例尽量做到看完就能上手验证。1. 为什么I2C非得用开漏输出推挽不行吗很多人学I2C的第一课就是I2C的SDA和SCL必须接上拉电阻配置成开漏输出。但为什么要这样如果只是记结论遇到问题的时候根本不知道怎么排查。我当初就是死记硬背结果第一次画板子忘了加上拉电阻调了半天以为是代码问题。1.1 从一根线的电平冲突说起先想一个最简单的场景总线上挂了三个设备主机、从机A、从机B。假设SDA线采用推挽输出也就是每个设备都能主动把线拉到高电平或者低电平。现在主机想发一个0于是把SDA拉低与此同时从机A想发一个1把SDA拉高。两条路径直接怼上了——一个往地灌电流一个往电源灌电流中间没有任何限流措施轻则波形畸变重则烧毁IO口。这就是推挽输出的致命问题它只能用在点对点的单向通信中一旦多个设备共享一根线就必然出现电平冲突。而I2C的设计初衷就是一根总线挂多个设备所以推挽方案从根上就被排除了。开漏输出则完全不同。开漏的意思是输出级只有一个N沟道MOS管漏极开路源极接地。MOS管导通时线被拉到地输出0MOS管截止时线处于高阻态浮空此时线的电平完全由外部上拉电阻决定。也就是说任何设备都只能主动把线拉低不能主动把线拉高。拉高这件事统一交给上拉电阻来做。这样一来多个设备同时操作同一根线也不会冲突如果有一个设备拉低线就是低如果所有设备都释放高阻态上拉电阻把线拉高。这就是所谓的线与逻辑——只要有一个输出0结果就是0。1.2 上拉电阻的取值不是随便选的知道了为什么要开漏接下来的问题就是上拉电阻选多大我见过有人直接用10kΩ也见过有人用1kΩ到底哪个对上拉电阻的取值需要同时满足两个条件条件一上升时间不能太长。I2C总线的电容负载包括PCB走线电容、器件引脚电容、连接器电容等通常在几十pF到几百pF之间。上拉电阻和总线电容构成一个RC充电回路上升时间大约是0.8473×R×C从0.3VDD充到0.7VDD。以标准模式100kHz为例上升时间不能超过1000ns。如果总线电容是200pF那么R不能超过约5.9kΩ。条件二低电平时的灌电流不能太大。当设备把线拉低时上拉电阻上的电流会灌入设备的开漏MOS管。I2C标准规定标准模式和快速模式下灌电流不能超过3mA。以3.3V供电为例R不能小于1.1kΩ。所以上拉电阻的合理范围大约在1.1kΩ到5.9kΩ之间。实际工程中2.2kΩ到4.7kΩ是最常用的选择。总线电容小、速率低的时候用4.7kΩ总线挂的设备多、走线长、速率高的时候用2.2kΩ甚至更小。注意如果你用示波器看到SDA或SCL的上升沿明显变圆、变慢大概率是上拉电阻太大了。反过来如果低电平被抬高到0.3VDD以上可能是上拉电阻太小导致灌电流过大。1.3 实测波形对比不同上拉电阻的差异我在一块STM32开发板上做了个对比实验总线挂了两个从机走线长度约8cm用示波器抓了三种上拉电阻下的SCL波形上拉电阻上升时间实测100kHz下波形质量400kHz下波形质量10kΩ约1.2μs勉强可用上升沿明显圆滑不可用时钟高电平时间不足4.7kΩ约550ns良好勉强可用2.2kΩ约260ns优秀良好从表里可以清楚看到10kΩ在100kHz下就已经很勉强了400kHz直接没法用。这也是为什么很多新手用10kΩ上拉低速能跑通一升速就挂的原因。2. 起始条件、停止条件和数据有效性时序图的每一个细节都有意义搞定了物理层接下来就是协议层。I2C的协议层看起来简单但时序图上的每一个时间参数都有它存在的理由。我当初看时序图就是扫一眼觉得大概知道怎么回事结果实际调试时发现从机不响应查了半天才发现是起始条件的保持时间不够。2.1 起始条件和停止条件的本质I2C总线在空闲状态下SCL和SDA都是高电平被上拉电阻拉高。起始条件START的定义是在SCL为高电平期间SDA从高变低。停止条件STOP的定义是在SCL为高电平期间SDA从低变高。为什么这样定义因为正常数据传输时SDA的电平变化只能发生在SCL为低电平期间。SCL为高电平期间SDA必须保持稳定这样接收方才能在SCL高电平期间正确采样数据。起始和停止条件故意违反了这条规则所以它们不会被误认为是数据。这个设计非常巧妙不需要额外的控制线只用两根线就能区分数据和帧边界。这也是I2C能用两根线实现完整通信协议的关键。2.2 数据有效性规则和采样时机数据传输时每个bit的流程是这样的SCL为低电平发送方把SDA设置成要发送的bit值0或1。发送方释放SCL或者主机控制SCL拉高SCL变为高电平。在SCL高电平期间接收方采样SDA的值。SCL被拉低准备下一个bit。这里有一个容易忽略的细节接收方是在SCL的上升沿采样还是在SCL高电平期间任意时刻采样严格来说I2C标准规定数据在SCL高电平期间必须保持稳定接收方通常是在SCL上升沿之后的某个时刻采样。实际器件中大多数是在SCL高电平的中间位置采样这样留出了足够的建立时间和保持时间余量。我在用逻辑分析仪抓波形时经常看到新手写的I2C驱动代码在SCL拉高之后立刻改变SDA这就违反了数据有效性规则。虽然有些宽容的从机可能还能工作但遇到严格的器件比如某些EEPROM就会直接出错。2.3 时钟拉伸从机也能控制SCL大多数教程讲I2C都是主机控制SCL从机被动响应但实际上I2C协议允许从机通过时钟拉伸Clock Stretching来控制SCL。具体机制是从机在接收到一个字节后如果需要更多时间处理数据比如EEPROM写入周期可以把SCL拉低并保持。主机发现SCL没有被释放明明自己已经释放了SCL但SCL还是低就知道从机在忙于是等待。直到从机释放SCL通信才继续。这个机制在实际中非常有用。比如你读写一个EEPROM写入一个字节后EEPROM需要几毫秒的内部写入时间这段时间它就会拉低SCL让主机等着。如果你的主机代码没有处理时钟拉伸就会在从机还没准备好的时候继续发时钟导致数据错乱。提示不是所有从机都支持时钟拉伸也不是所有主机都正确处理时钟拉伸。如果你用的是硬件I2C外设查一下参考手册确认它是否支持如果是软件模拟I2C记得在拉高SCL之后检测SCL是否真的变高了。3. 完整数据帧的拆解从地址到ACK的每一步理解了物理层和基本时序接下来把一帧完整的数据拆开看。I2C的每一帧都由几个固定部分组成每个部分都有明确的含义和作用。3.1 7位地址帧的完整结构一次典型的I2C写操作数据帧结构如下[START] [7位从机地址] [R/W位] [ACK] [8位数据] [ACK] ... [STOP]7位从机地址是器件的唯一标识比如常见的AT24C02 EEPROM地址是0x507位DS3231 RTC芯片地址是0x687位。R/W位表示这次操作是读1还是写0。这里有一个新手经常搞混的地方7位地址和8位地址的区别。很多数据手册上写的地址是8位的比如写地址0xA0读地址0xA1。这其实是把7位地址和R/W位拼在一起了0xA0 0x50 1 | 00xA1 0x50 1 | 1。在写代码的时候你需要根据使用的I2C库来决定传7位地址还是8位地址。我当初用STM32的HAL库时HAL_I2C_Master_Transmit函数的第二个参数要求传7位地址左移一位后的值也就是8位格式。我直接传了7位地址结果从机死活不响应。后来查了HAL库的源码才发现这个问题。3.2 ACK和NACK从机的回应机制每传输完一个字节8个bit发送方会释放SDA线然后在第9个时钟周期接收方需要把SDA拉低表示应答ACK或者保持高电平表示非应答NACK。ACK/NACK机制是I2C可靠性的重要保障。主机发送从机地址后如果从机存在且准备好就会回ACK如果从机不存在或忙SDA保持高电平主机收到NACK就知道这次通信失败了。读操作中的ACK/NACK方向是反过来的主机读从机数据时从机发送8个bit然后主机在第9个时钟周期回ACK表示我还要继续读或NACK表示我读完了准备发STOP。注意很多新手写读操作时最后一个字节忘了发NACK导致从机一直以为主机还要继续读SCL被从机一直拉低时钟拉伸总线卡死。这个坑我踩过不止一次。3.3 重复起始条件读操作的关键I2C的读操作不能直接发起必须先写后读。具体流程是主机发送START。主机发送从机地址 写位0。从机回ACK。主机发送要读取的寄存器地址。从机回ACK。主机发送重复起始条件Repeated START。主机发送从机地址 读位1。从机回ACK。从机发送数据主机回ACK/NACK。主机发送STOP。重复起始条件和普通起始条件的区别在于重复起始条件之前不发送STOP。这样总线不会被释放其他主机没有机会抢占总线保证了读操作的原子性。如果不用重复起始条件而是先STOP再START中间总线空闲期间可能被其他主机抢占导致读到的数据不是自己想要的。这在多主系统中尤其重要。4. 多主仲裁两个主机同时说话会怎样单主系统里I2C很简单但I2C协议本身是支持多主的。多主仲裁是I2C最精妙的设计之一也是很多人没搞明白的地方。我当初看协议文档时对仲裁这一节反复读了好几遍才理解。4.1 仲裁的基本规则谁先发出不同的电平谁出局多主仲裁的核心规则非常简单多个主机同时发送数据时每个主机都在每个bit上比较自己发送的电平和总线上的实际电平。如果发现自己发送的是1但总线是0说明有其他主机在发送0自己就失去仲裁退出竞争。为什么发送1的主机会退出因为开漏输出的线与特性只要有一个主机拉低发送0总线就是低。发送1的主机释放了SDA高阻态但总线被另一个主机拉低了所以它读回的是0和自己发送的1不一致就知道自己输了。发送0的主机不会退出因为它拉低了总线读回的就是0和自己发送的一致。所以仲裁的结果是发送0的主机赢得仲裁发送1的主机退出。这个机制保证了仲裁过程中不会丢失任何数据赢得仲裁的主机发送的数据和总线上实际的数据完全一致因为输掉的主机在发现自己输的那一刻就停止发送了不会影响总线。4.2 仲裁发生在哪些阶段仲裁可以发生在多个阶段地址阶段仲裁两个主机同时发起START然后各自发送不同的从机地址。在地址的某个bit上一个主机发送0另一个发送1发送1的主机退出。赢得仲裁的主机继续和从机通信。数据阶段仲裁两个主机发送了相同的从机地址都选中了同一个从机但在数据阶段发送了不同的数据。同样是在某个bit上分出胜负。读操作中的仲裁读操作时主机发送地址后从机控制SDA。如果两个主机同时读同一个从机它们发送的地址相同从机回的数据也相同所以不会发生仲裁冲突。但如果两个主机读不同的从机地址阶段就会分出胜负。4.3 仲裁失败的主机怎么恢复仲裁失败的主机需要立即切换到从机模式或者退出总线等待下一次总线空闲。具体来说仲裁失败的主机立即释放SDA和SCL。它不能再发送任何数据因为总线已经被赢得仲裁的主机控制了。它可以监听总线等待STOP条件出现后再尝试重新发起通信。这里有一个容易忽略的细节仲裁失败的主机必须能够检测到自己输了并且及时释放总线。如果它继续拉低SCL就会干扰赢得仲裁的主机的通信。所以硬件I2C外设通常都有仲裁丢失检测功能软件模拟I2C则需要自己在每个bit后检查SDA的实际电平。4.4 多主仲裁的实测验证我用两块STM32开发板做了个多主仲裁的实验。两块板子都配置成I2C主机同时向同一个从机发送数据。用逻辑分析仪抓波形可以清楚看到两个主机同时发送START。在地址阶段两个主机发送的地址不同在某个bit上分出胜负。输掉的主机立即释放总线SCL和SDA不再被它驱动。赢得仲裁的主机继续完成通信从机正常响应。这个实验让我真正理解了仲裁的无损特性整个过程中从机看到的总线波形和单主通信完全一样它根本不知道曾经有两个主机在竞争。5. 从波形到代码用逻辑分析仪反向验证I2C通信理论讲完了接下来讲怎么用逻辑分析仪实际验证。我强烈建议每个学I2C的人都买一个逻辑分析仪几十块钱的就行配合开源软件就能解码I2C协议。看着波形和代码对应起来理解会深刻很多。5.1 逻辑分析仪的接线和配置逻辑分析仪的接线很简单GND接开发板的GND通道0接SCL通道1接SDA。注意不要接反否则解码出来的数据全是乱的。配置方面采样率至少要设置为I2C速率的10倍以上。比如100kHz的I2C采样率至少1MHz400kHz的I2C采样率至少4MHz。我一般用24MHz采样率这样波形细节看得比较清楚。5.2 解码结果分析从波形到数据帧逻辑分析仪解码I2C后会直接显示每个字节的值和ACK/NACK状态。比如一次EEPROM写操作解码结果可能是这样的Start Address: 0x50 (Write) ACK Data: 0x00 ACK Data: 0xAB ACK Stop对照代码你可以清楚看到每一步操作对应的波形。如果某个字节的ACK没有出现说明从机没有响应可能是地址错了、从机没上电、或者上拉电阻有问题。5.3 常见波形异常及排查思路我整理了几种常见的波形异常和对应的排查方向波形现象可能原因排查方法SDA一直被拉低从机死锁、上拉电阻缺失断电重启检查上拉电阻上升沿太慢上拉电阻太大、总线电容太大减小上拉电阻缩短走线ACK位始终为高从机地址错误、从机未上电确认地址测量从机供电SCL被拉低不释放从机时钟拉伸、总线死锁检查从机状态发送9个时钟脉冲解锁起始条件后无波形主机配置错误、引脚复用未设置检查GPIO配置和I2C外设初始化提示如果总线死锁SDA或SCL被某个从机一直拉低可以尝试发送9个SCL脉冲让从机完成当前字节的传输然后发送STOP条件释放总线。这个方法我救过好几次假死的I2C总线。6. 那些年我踩过的I2C坑从GT911到EEPROM理论、波形、代码都讲完了最后分享几个我在实际项目中踩过的坑。这些坑在教科书上不会写但实际工作中经常遇到。6.1 GT911触摸屏的I2C地址切换GT911是一颗常见的电容触摸屏控制器它的I2C地址不是固定的而是由复位时序决定的。具体来说GT911有两个可选地址0x5D和0x14在上电复位时如果INT引脚在某个时间窗口内为低电平地址就是0x5D如果为高电平地址就是0x14。我当初调试GT911时I2C死活读不到数据用逻辑分析仪看波形发现地址发出去了但一直没有ACK。查了数据手册才发现地址不对。后来调整了复位时序把INT引脚拉低再释放地址就正确了。这个坑的教训是有些器件的I2C地址不是固定的上电时序会影响地址选择。遇到I2C设备不响应时除了检查地址和上拉电阻还要看看有没有地址配置引脚或时序要求。6.2 EEPROM的页写入边界问题AT24C系列EEPROM支持页写入Page Write一次可以写入一页数据通常是8字节或16字节。但页写入有一个限制不能跨页。如果你从页的中间开始写写到页尾时会回卷到页首覆盖之前的数据。我当初写EEPROM驱动时没注意这个问题一次性写了20个字节结果发现数据错乱。后来查了数据手册才明白超过页边界的数据会回卷覆盖。解决办法是把长数据拆分成多次页写入每次不超过页边界。6.3 软件模拟I2C的延时问题用GPIO模拟I2C时延时是关键。延时太短时序不满足I2C标准延时太长通信速率上不去。我当初用STM32的软件模拟I2C一开始用HAL_Delay做延时结果发现HAL_Delay的最小分辨率是1ms导致I2C速率只有几百Hz。后来改用__NOP()或者DWT计数器做微秒级延时速率才提上去。这里的关键是软件模拟I2C的延时必须精确到微秒级而且要根据CPU主频计算出合适的循环次数。不同主频的MCU需要不同的延时参数不能直接照搬。6.4 多设备共用总线时的地址冲突I2C总线上挂多个设备时地址冲突是最常见的问题。比如两个传感器都用0x68地址就没法挂在同一条总线上。解决办法有几种使用I2C多路复用器如TCA9548A把总线分成多路每路挂一个设备。选择地址可配置的器件通过硬件引脚改变地址。使用两个独立的I2C外设分别挂不同的设备。我在一个项目里同时用了MPU6050地址0x68和DS3231地址0x68两个设备地址冲突。最后用了TCA9548A多路复用器才解决。这个芯片用起来很简单通过I2C发送一个字节选择通道然后就可以像操作普通I2C设备一样操作对应通道上的设备了。7. 从100kHz到400kHz速率提升时需要注意什么最后聊一下速率提升的问题。很多项目一开始用100kHz跑通了想升到400kHz就各种问题。我总结了几点经验。7.1 上拉电阻和总线电容的重新评估速率提升后上升时间的要求更严格了。100kHz时上升时间允许1000ns400kHz时只有300ns。如果原来用4.7kΩ上拉400kHz时可能就不够了需要换成2.2kΩ甚至1.5kΩ。同时要检查总线电容。总线电容主要来自PCB走线、器件引脚和连接器。走线越长、挂的设备越多电容越大。如果电容超过400pF即使上拉电阻很小上升时间也很难满足要求。这时候需要考虑缩短走线、减少挂载设备或者使用I2C缓冲器。7.2 从机是否支持400kHz不是所有I2C从机都支持400kHz。有些老型号的EEPROM、传感器只支持100kHz。升速之前一定要查数据手册确认。我见过有人把100kHz的EEPROM挂到400kHz总线上结果读写全乱。7.3 硬件I2C外设的时钟配置用硬件I2C外设时速率由时钟控制寄存器配置。不同MCU的配置方法不同但基本原理是一样的I2C时钟来源于APB总线时钟通过分频系数得到SCL频率。配置时需要确保分频系数计算正确否则实际速率可能和预期不符。我一般配置完I2C后会用逻辑分析仪测一下实际SCL频率确认和预期一致。这个习惯帮我发现过好几次配置错误。7.4 信号完整性问题400kHz以上时信号完整性问题开始显现。如果走线没有做阻抗控制或者上拉电阻离器件太远波形上会出现振铃、过冲等问题。解决办法包括缩短走线、增加串联电阻通常22Ω到100Ω、优化PCB布局。我在一块四层板上跑400kHz I2C时SCL波形有轻微振铃但不影响通信。后来在SCL线上串了一个33Ω电阻振铃明显改善。这个电阻的作用是匹配阻抗吸收反射。8. 一周学习路径回顾如果重新来过我会怎么学回头看这一周的学习过程有些地方走了弯路有些地方应该更早动手实践。如果让我重新安排我会这样规划第1天物理层。搞懂开漏输出的原理用万用表和示波器测量上拉电阻对波形的影响。动手换不同阻值的上拉电阻观察上升时间的变化。第2天基本时序。用逻辑分析仪抓一次完整的I2C通信波形对照时序图逐个分析起始条件、地址、ACK、数据、停止条件。自己用GPIO模拟一次I2C通信加深理解。第3天读写操作。找一颗EEPROM实现完整的读写驱动。重点理解写操作和读操作的区别以及重复起始条件的作用。第4天多主仲裁。用两块开发板做多主仲裁实验观察仲裁过程。理解仲裁的无损特性以及仲裁失败后的恢复机制。第5天时钟拉伸和总线死锁。找一颗支持时钟拉伸的从机比如某些传感器观察时钟拉伸现象。学习总线死锁的解锁方法。第6天速率提升。把I2C速率从100kHz提升到400kHz观察波形变化调整上拉电阻和走线。记录不同配置下的波形质量。第7天综合项目。用I2C总线挂多个设备EEPROM、传感器、RTC实现一个完整的应用。处理地址冲突、时钟拉伸、错误重试等问题。这一周下来I2C就不再是两根线那么简单了而是一个从物理层到协议层都清清楚楚的完整系统。后面再遇到I2C相关的问题排查起来就有章法了不会像以前那样瞎猜。最后分享一个我常用的I2C调试小工具用Python写一个简单的I2C扫描脚本配合树莓派或者Linux开发板可以快速扫描总线上所有设备的地址。这个脚本虽然简单但在排查地址冲突和设备不响应时非常有用。代码大概长这样import smbus bus smbus.SMBus(1) found [] for addr in range(0x03, 0x78): try: bus.write_quick(addr) found.append(hex(addr)) except: pass print(Found devices:, found)跑一遍就能知道总线上挂了哪些设备、地址是多少。如果某个设备不在列表里就说明它没有正常上电或者地址配置有问题。这个脚本帮我省了很多查数据手册的时间。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。