简介:《I2C总线协议详解(中文)》是一份面向嵌入式软硬件开发者、电子类专业学生及单片机爱好者的PDF技术文档,侧重解决I2C总线基础概念、通信时序与规范理解问题,适合入门学习与项目开发查阅。资料包仅含1个PDF文件,大小约872KB,篇幅集中、便于检索,内容涵盖协议版本演进、SDA/SCL信号线、主从设备与地址机制、标准/快速/高速模式、位传输有效性、起始停止条件、字节格式与应答、仲裁和时钟同步,以及7位和10位寻址等关键模块。已有842人学习,说明其在I2C入门与总线协议复习场景中具有一定参考价值。读者可据此理清I2C读写流程、时序约束与多设备总线设计要点,并借助目录快速定位规范条目,为传感器、EEPROM、实时时钟等低速外设的接口调试提供依据。
1. 两条线上百个器件:为什么它们不会同时开口说话
I2C 这个名字在嵌入式开发里出现得太多,多到很多人把它当成“两根线接上去就该通”的默认事实,直到某天示波器上那根 SDA 一直处于低电平,才发现自己对它的理解只剩下“SDA 是数据、SCL 是时钟”。真正让 I2C 能成立的,从来不是那两根线本身,而是叠加在两根线上的三层机制:开漏输出构成的线与、靠边沿先后顺序定义的帧边界、以及用“谁先发出 1 谁出局”实现的非破坏性仲裁。这三层东西合起来,才让一根共享的数据线既能挂上一百多个器件、又能允许两个主机同时抢总线而不丢一个字节。这篇内容围绕一份完整的中文 I2C 总线规范展开,从电气层一路拆到协议层,讲清每个约束背后的物理原因。
2. 开漏与上拉:为什么 I2C 的“1”必须靠外部电阻撑起来
2.1 线与逻辑:输出级为什么只能是开漏或集电极开路
I2C 的总线引脚有一条强制规定:任何器件的输出级都必须是漏极开路或者集电极开路。初看像是工艺限制,其实是拓扑决定的。假设 SDA 上挂十个器件,其中两个用推挽输出,一个想输出高、另一个想输出低,那么就是电源到地之间的一条低阻通路,轻则大电流发热、重则烧掉输出级。开漏结构把“输出高”这件事交了出去:器件只能主动拉低,想要高电平就让引脚浮空,由上拉电阻把线拽回 VDD。于是总线上所有器件的输出等效成一个多输入与门:只要有一家拉低,线就是低;全部松手,线才是高。这就是线与。
这不是一个可有可无的实现细节,I2C 后面几乎所有高级行为都建立在它之上。多主机仲裁时,某个主机输出高、另一个输出低,总线呈现低电平,输出高的那个主机读回的值和自己写出去的不一致,就知道自己输了,安静退出。时钟同步时,好几个主机同时往 SCL 上发时钟,SCL 实际呈现的是它们各自时钟的“与”,低电平被最长的那个拉长。从机拉低 SCL 做时钟拉伸,物理上也不是什么特殊功能,就是它与门的另一路输入。换句话说,把输出级做成开漏,I2C 用一条电气规则同时买到了三个协议特性。
所以当有人拿普通推挽 GPIO 去“模拟 I2C”时,问题不在于能不能跑起来,而在于它把这条总线从“线与”退化成了“谁驱动得强听谁的”。单主机、单从机、时序宽松的场合确实看不出毛病,但只要总线上有两个会拉低的东西同时驱动,就会出现短路电流;只要从机想拉伸时钟,推挽输出会直接和从机对着干。这也是常见的实现思路里,软件模拟 I2C 的引脚配置一律要求开漏加外部上拉的原因。
2.2 上拉电阻的取值:Rp、RS 与 400pF 电容的三方拉扯
上拉电阻不是随便取一个 4.7kΩ 就完事,它被两个方向同时夹住。往上取太大,上升沿被 RC 时间常数拖长,高电平还没爬到阈值下一个时钟就来了;往下取太小,器件拉低时灌进去的电流超出输出级能力,VOL 抬高,低电平又不够低。规范给出的思路是:上限由上升时间 tr 和总线电容 Cb 决定,下限由 VOL 最大允许值和灌电流 IOL 决定。
# 上拉电阻的上下限估算(对应标准模式/快速模式的时序要求) VDD = 3.3 # 总线电源 VOL_MAX = 0.4 # 器件拉低时允许的最大低电平 IOL = 3e-3 # 器件输出级的灌电流能力,3mA CB = 200e-12 # 总线总电容,含所有器件引脚与走线 TR = 300e-9 # 快速模式允许的最大上升时间 # 上限:上升时间约束,0.8473 是 RC 充电从 0 到 0.7VDD 的系数 Rp_max = TR / (0.8473 * CB) # 约 1.77 kΩ # 下限:灌电流约束 Rp_min = (VDD - VOL_MAX) / IOL # 约 967 Ω print(Rp_max, Rp_min) # 合理区间大致 1.0k ~ 1.7k注意上限公式里的 0.8473 来自 RC 充电曲线,门限取 0.7VDD 时的通用近似;不同器件的输入阈值略有差异,实做时通常还要留 30% 以上余量。另外这段代码把 CB 设成 200pF,如果挂满八个器件再加一段排线,很容易冲到 300pF 以上,Rp_max 会掉到 1.1kΩ 附近。这就是“器件一多就得减上拉电阻”的量化依据。
很多板子上还会在器件引脚和总线之间串一个小电阻 RS,作用是抑制总线上的尖峰和静电冲击,保护输出级。但 RS 和 Cb 又构成一级额外的 RC 延迟,串得越大上升沿越慢,所以它的取值同样被时序卡死。这三者(Rp、RS、Cb)是一个互相牵制的三角,任何一个动了另外两个都要重新算。
规范里那条“挂载数量只受 400pF 总线电容限制”的说法,本质上就是这条三角关系的另一种表述。不是地址不够用,也不是协议撑不住,而是电容一大,上升沿就爬不回来。有人把它当成一种理论上的说法,实际按最大电容去排线翻车的不在少数。
2.3 从 100k 到 3.4M:速率提升到底改了哪些电气参数
规范里三代速率的分层很能说明问题。标准模式 100kbit/s 是最初的定义;快速模式把位速率提高四倍到 400kbit/s,代价是输出级要做斜率控制、输入端要加滤波,用受控的边沿变化速率去压住电磁干扰;高速模式再往上跳到 3.4Mbit/s,光靠无源上拉已经不行了,必须换成有源上拉、更小的上拉阻值、以及在高速期间断开的分离上拉结构。
这里有一段容易被忽略的演进:早期快速模式对输出级有 0.6V/6mA 这类固定电平要求,后来的版本把这条删掉了,改成输入电平跟随总线电源电压决定。原因是电源电压一路往下降,2V 甚至更低的系统越来越多,固定电平的噪声容限在那个电压下根本没法保证。把判决门限定义成 VDD 的比例而不是绝对值,才让同一套协议能横跨 2V 到 5V 的电源域。这也是为什么现在的 I2C 器件数据手册里,VIH/VIL 往往写成 0.7VDD / 0.3VDD 这样的相对值。
高速模式还有一个特殊之处:它不是全程以 3.4Mbit/s 跑的。总线上大多数时间仍是标准或快速模式的报文,只有在一次传输进入高速段之后,才临时切成高速时序,结束时再切回低速。承担这个切换职责的部件在规范里被称为电桥,它负责在重复起始条件之后识别高速主机码,然后调整总线的上拉和滤波配置。这个设计让高速器件和慢速器件可以挂在同一条总线上共存,而不是逼着整条总线升级。
3. 起始、停止与应答:一次传输的帧结构是怎么被“边沿”定义的
3.1 数据有效性规则:为什么 SDA 只能在 SCL 低电平期间翻动
规范对位传输只有一句话:SDA 上的数据必须在 SCL 为高电平期间保持稳定,SDA 的状态只允许在 SCL 为低电平时改变。这条规则的直接效果是把“数据”和“控制”区分到不同的时间窗口里——高电平窗口属于数据采样,低电平窗口属于数据切换。
这个约束并不是为了节省带宽,而是为了让两个设备在不同时钟源下能对齐。I2C 是同步串行总线,不是异步的,接收方不需要独立的波特率发生器,只要跟着 SCL 采样即可。把数据切换强制放进低电平窗口,等于给了接收方半个时钟周期的建立时间,这是纯靠协议换来的时序余量,比任何“双方约定好跑多快”的软约定都可靠。
顺带一个副产品:既然数据只可能在 SCL 低电平期间变化,那么“SCL 高电平期间 SDA 发生跳变”就成了一种永远不会出现在正常数据流里的组合。I2C 正好把它拿来当帧边界,也就是下一节的起始和停止条件。用禁止态编码特殊含义,是通信协议里很经典的手法,I2C 的巧妙之处在于这个禁止态是物理规则自然带来的,不需要额外占用一个字节或一根线。
3.2 起始与停止条件:两根线谁先动的顺序暗号
起始条件的定义是:SCL 保持高电平时,SDA 由高变低。停止条件反过来:SCL 保持高电平时,SDA 由低变高。两个动作都发生在 SCL 的高电平窗口里,所以它们与普通数据位的区别不在于电平,而在于变化的时刻。
真正的关键在于顺序。要让 SDA 在 SCL 高电平期间下降,必须先让 SCL 变高、再动 SDA;要发停止条件,必须先动 SDA 让它升上去、再让 SCL 跟随。谁来先动、谁后动,构成了可被硬件检测器识别的签名。带 I2C 接口的器件内部有一个状态机专门盯着这两个组合,一旦命中就重置位计数器,所以帧同步不需要任何额外的同步头。
/* 软件模拟 I2C 的起始条件:顺序错了就是普通数据位 */ static void i2c_start(void) { SDA_HIGH(); /* 先确保两条线都是高,总线处于空闲 */ SCL_HIGH(); delay_half_bit(); /* 满足起始条件的建立时间 tHD;STA */ SDA_LOW(); /* SCL 为高时 SDA 下降 => 起始条件 */ delay_half_bit(); SCL_LOW(); /* 钳住时钟,准备发第一位 */ } static void i2c_stop(void) { SDA_LOW(); /* 先把数据线拉到确定状态 */ SCL_LOW(); delay_half_bit(); SCL_HIGH(); delay_half_bit(); SDA_HIGH(); /* SCL 为高时 SDA 上升 => 停止条件 */ delay_half_bit(); /* 满足总线空闲时间 tBUF 后才能发起下一次传输 */ }这段代码里每个 delay 对应一个时序参数:起始条件之前要求 SCL 高电平持续的最短时间,停止条件之后要求总线空闲的最短时间。参数名字在规范里是 tHD;STA 和 tBUF,数值随模式不同而不同。写完这两位之后的第一个动作必须是把 SCL 拉低,否则下一位数据的切换就会落在高电平窗口里,被从机误判成起始或停止。
重复起始条件 Sr 是同一套动作的变体:不发停止条件,直接再发一次起始。区别在于总线状态——发过停止条件之后总线进入空闲,允许其他主机抢;发重复起始时总线一直保持忙,当前主机牢牢握着控制权。读写复合操作里那个“先写地址再读数据”的转向,就是靠 Sr 完成的,中间那个停止条件一旦发出去,别的等待中的主机就可能插进来。
3.3 字节与应答:第 9 个时钟脉冲在传递什么
每一帧数据的单位是字节,8 位,高位先行,后面必须跟一个应答位,所以实际在总线上看到的是 9 个时钟脉冲。第 9 个脉冲期间,发送方释放 SDA,接收方如果收下了这个字节就把线拉低,如果没收下就让它保持高。
这个机制承担的功能比“确认收到”多得多。地址字节之后的应答表示“这个地址我这里有人”;数据字节之后的应答表示“缓冲区还装得下,继续发”;读操作里主机在最后一个字节回一个非应答,是告诉从机“够了,别再往总线上推了”,从机收到非应答才释放数据线,让主机去发停止条件。也就是说应答位既是流控信号,也是角色切换信号。
从机来不及处理时可以拉低 SCL,把主机拖住。这是时钟拉伸,后面还会展开。注意这里的能力边界:从机可以拖时间,但不能拒绝一个字节后继续拖下去,规范给的语义是——不能接收就回非应答,让主机去发停止或重复起始,而不是无限期地挂着时钟。我见过一些实现把拉伸当流控用,一拉就是几毫秒,遇到主机侧有超时保护就直接判总线错误。
还有一种情况值得单独提一句:如果报文以广播呼叫地址或者 CBUS 兼容地址开头,协议允许不产生应答。这类报文的语义是“发给所有人”,本来就不该有某一个器件出头确认。
3.4 7 位与 10 位寻址:地址空间怎么从 112 扩到 1024
7 位寻址的第一个字节结构是:7 位地址加 1 位读写方向位。剩下 7 位理论上有 128 个组合,但其中一部分被保留:全 0 开头的那一段用于广播呼叫,0000 001 那段留给 CBUS 兼容器件,1111 0xx 那段是 10 位寻址的前缀。扣掉这些,实际可自由分配的通常认为是 112 个。
地址空间不够用是后来加 10 位寻址的直接动因。10 位寻址用了两个字节:第一个字节的高 5 位固定为 11110,接着 2 位是地址的高两位,最低位还是读写方向;第二个字节是剩下的 8 位地址。这样组合出 1024 个地址。代价是每次寻址多花一个字节的时间,而且总线上的器件必须能区分“这是 10 位寻址的头”还是“这是一个普通地址”——靠的就是那 5 个固定位,这又是保留编码的用法。
有个细节经常被忽略:广播呼叫地址在 10 位寻址体系里没有对应的扩展形式,10 位地址空间里的全 0 是被占用的。另外规范里提到的起始字节(起始条件后紧跟 0000 0001)只适用于 7 位地址空间,它的用途是让那些靠软件轮询检测起始条件的微控制器能快速同步,因为起始字节的位模式在 SCL 上产生的是确定的脉冲序列,采样起来比解析任意地址更容易。这些都是地址编码层面的“预留位换功能”的设计,和前面 SDA 边沿换帧边界是同一个思路。
4. 仲裁与时钟同步:多主机同时开口时,凭什么不丢数据
4.1 时钟同步:低电平由最慢的器件决定,高电平由最快的决定
多主机总线最直接的问题是时钟源不唯一。规范的做法不是选一个主时钟,而是让每个主机都发自己的时钟,然后靠线与把它们合成一个。合成规则可以精确地表述为:SCL 的低电平周期等于所有参与主机中低电平最长的那个,高电平周期等于高电平最短的那个。
推导过程不复杂。某个主机把 SCL 放开让它变高,但如果另一个主机的低电平还没走完,它仍然在拉低,SCL 就保持低——所以先结束低电平的器件被迫进入等待。等所有器件都数完自己的低电平,线才真正变高,此时所有器件同时开始数高电平;谁的高电平最短,谁先数完就先拉低,线重新变低,其他器件的高电平计数被打断。结果是每个主机的时钟都被“压”到同一个节拍上,而且这个节拍是各自时钟里最保守的那个组合。
这个机制提供的是时间对齐,不是数据裁决。同步完成之后,多个主机仍然可能在同一时刻开始发不同的内容,那就需要下一节的仲裁。
4.2 逐位仲裁:谁先发出 1 谁就出局
仲裁发生在 SDA 线上,规则一句话:主机每发一位就同时读回总线电平,如果读回的和自己发的对不上,说明有别的主机在发更低的电平,自己输了,立刻关掉数据输出级退出。
为什么这个规则能保证不丢数据,关键在于“赢的那一方发的内容和总线上的内容完全一致”。仲裁是逐位进行的,输的那一位上,赢家发的是 0,总线呈现 0,输家发的是 1 但读回 0——从总线的角度看,从来没有出现过两个不同的值,只有一个最终胜出的值被所有器件观察到。输家退出时也不会在总线上留下任何痕迹,因为它本来就只是“没有拉低”。
仲裁可以持续很多位。第一阶段比的是地址,如果两个主机恰好寻址同一个从机,仲裁会继续比数据位;如果是主机接收方,还会比到应答位。这意味着两个主机可以同时对着同一个从机写,直到某一位数据不同才分出胜负,胜出的那个继续,输掉的那个立刻转去当从机或者等待下一次总线空闲。
/* 仲裁的判定逻辑,抽象成一位一位的形式 */ bool i2c_send_bit(bool bit) { if (bit) { SDA_RELEASE(); /* 发 1 = 松手,由上拉拉高 */ } else { SDA_DRIVE_LOW(); /* 发 0 = 主动拉低 */ } SCL_HIGH(); bool actual = SDA_READ(); /* 关键:读回总线真实电平 */ SCL_LOW(); if (bit && !actual) { /* 我发 1 却读到 0:有别人在发 0,我输了 */ SDA_RELEASE(); /* 立即释放,绝不反向驱动 */ return false; /* 上层据此放弃本次传输 */ } return true; }这段伪代码里最容易出事的是“输了之后”那一步。正确的做法是彻底释放 SDA,不参与后续任何一位的驱动;错误做法是继续发完当前字节再判断,那样会在总线上留下冲突。另一个隐藏前提是这一步必须用开漏输出,推挽输出在读回之前就已经把线强行拉高了,“读回”这个动作根本读不到真实的总线状态。
仲裁和上一节的时钟同步经常被混为一谈。区别在于:同步解决的是“大家节拍不一致”,仲裁解决的是“大家内容不一致”。前者在 SCL 上做,后者在 SDA 上做,两者同时发生,互不干扰。
4.3 时钟拉伸与握手:从机怎样让主机等一等
从机没有独立的时钟线可用,它想拖时间只能拉低 SCL。这个动作在物理上和主机拉低 SCL 完全一样,因为线与不区分是谁拉低的。主机在发完一位之后会释放 SCL 并试图把它拉高,如果从机还拉着,主机就会看到 SCL 迟迟不上升,于是停在原地等待。
规范里把这个机制进一步用成了握手:主机可以额外插入一个时钟脉冲,只是为了给从机一个“请回应”的窗口,从机在这个窗口里拉低就是“我收到了”,不拉就是“我还没准备好”。用时钟线做握手而不是用数据线,好处是不占数据带宽,也不破坏字节的 9 位结构。
有一点边界必须说清楚:拉伸只允许发生在字节边界或者特定的位之后,不能在第 8 位和第 9 位之间随意插入,否则接收方的位计数器会错位。另外主机侧通常要设一个超时,防止从机异常时把自己永久挂死。这个超时值没有统一标准,实践中常见的做法是按最慢从机的最大处理时间乘个安全系数,超了就复位总线。
4.4 混合速度总线与电桥:Hs 模式怎么和慢器件共存
高速模式引入了一个新问题:3.4Mbit/s 的时序要求(小上拉、有源上拉、更短的上升时间)和标准/快速模式的要求(大上拉、较低的功耗)是互斥的,没法用一套电气配置同时满足。规范给出的解法是在时间上分时复用总线的电气特性,由电桥负责切换。
整个过程大致是这样的:总线平时按 F/S 模式运行,上拉较强、速率较低。当某个高速主机要发起高速传输时,先发一个普通的起始条件加一个特殊的主机码(00001xxx),电桥上有个检测器专门认这个码,认出来之后它就切换到高速配置——断开普通上拉、接上有源上拉、把滤波器和斜率控制调过来。从这一刻起总线上的传输按 Hs 时序跑。传输结束时发一个停止条件,电桥再切回去。
这个设计的取舍很明显:用一次额外的识别码和一次电气切换,换取“同一条总线上高速器件和慢速器件可以混插”。如果不这么做,就得要求整条总线的所有器件都支持高速,或者物理上分成两条总线再加一座桥——前者成本高,后者等于把问题推到板级。混合速度系统的电桥时序要求里,最苛刻的一条是切换必须发生在起始条件之后的确定窗口内,早了会漏掉高速报文,晚了会把高速时序当成低速解析,两边都会翻车。
同一章里还有个相关但更常见的需求:逻辑电平不同的器件挂在同一条总线上。规范给的方案是双向电平转换器——一个 N 沟道 MOS 管,源极接低压侧、漏极接高压侧、栅极接低压侧电源,两侧各有上拉。低压侧拉低时,MOS 管的体二极管先把高压侧拽下来,随后沟道导通把低电平坐实;高压侧拉低时,MOS 管栅源电压为负不导通,但体二极管方向正好挡住,于是靠高压侧自己的上拉和低压侧的驱动配合完成。这个电路的精髓在于它不需要方向控制信号,靠 MOS 管的对称性自动判断谁在拉低。
5. 排查:I2C 调不通时,现象背后的五种真实原因
5.1 SDA 一直趴低:从机在传输中途被复位了
现象:上电后或者某次异常之后,SCL 还能动,SDA 却始终是低电平,主机发什么都是非应答,示波器上看不到任何上升沿。
原因:从机正在输出某一位数据 0 的时候被复位或者掉电重启,它的位计数器被清零,但输出级还停在拉低状态。主机这边走完了自己的流程、发了停止条件,从机根本没参与,于是它继续拉着 SDA 不放。此时总线已经没法产生起始条件了——起始条件要求 SDA 在 SCL 高时下降,而它本来就是低的。
解决:补时钟。主机把 SCL 当普通 GPIO,手动发最多 9 个时钟脉冲,让从机把残留的那一位(以及可能的应答位)走完,然后手动产生一个停止条件。
/* 总线恢复:从机拉住 SDA 不放时的标准手法 */ static void i2c_bus_recover(void) { SDA_INPUT(); /* 释放 SDA,改成输入 */ SCL_OUTPUT(); for (int i = 0; i < 9 && SDA_READ() == 0; i++) { SCL_LOW(); delay_us(5); SCL_HIGH(); delay_us(5); /* 每个脉冲让从机推进一位 */ } /* 手动构造停止条件:SCL 高时 SDA 上升 */ SCL_LOW(); delay_us(5); SDA_OUTPUT(); SDA_LOW(); delay_us(5); SCL_HIGH(); delay_us(5); SDA_HIGH(); delay_us(5); }九个脉冲的依据是:一个字节 8 位加一个应答位,最多九位就能把从机从任何一个中间状态推到字节边界。循环条件里加了SDA_READ() == 0,一旦 SDA 被放开就提前退出。这段代码要放在初始化里常规调用,而不是等出问题再手动跑。
5.2 地址对了却一直非应答:上拉太大或速率太快
现象:逻辑分析仪上地址字节和预期完全一致,第 9 个时钟期间 SDA 没有被人拉低,主机收到非应答。
原因往往不是地址写错,而是从机根本没认出来。上拉电阻偏大加上总线电容偏大,上升沿爬到 VIH 的时间超过了位周期的一半,从机在第 8 位采样到的可能是 0 而不是 1。另一种情况是主机跑得比从机支持的速率快,从机还没锁存完上一位,下一位就来了。
解决:先用示波器量上升时间,从 0.3VDD 到 0.7VDD 的这段是不是超过了规范给出的最大值;超了就减小上拉电阻或者降低速率。同时确认从机支持的速率等级——把标准模式的器件挂在 400k 的总线上是常见的踩坑,某些器件在数据手册里明确写了最高 100k。
5.3 单个器件正常,挂满一板就偶发错误
现象:实验室里一对一通信稳如磐石,装到整机上跑一段时间就开始出现随机的一位错、偶发的非应答,重启后又能好一阵。
原因:总线电容超标。每挂一个器件、每加一段排线都在往那 400pF 的预算里加,电容一大,上升沿变缓,留给从机采样的高电平窗口被压缩。这个错误是概率性的,和环境温度、电源电压、器件批次都有关系,所以表现为“偶发”。
解决:把所有器件的输入电容和走线电容估一遍,按上一章的公式重算上拉电阻;能缩短走线就缩短,能减少挂载就减少;实在降不下来就加 I2C 缓冲器把总线分段,每段独立计算电容预算。这种做法本质上是把一条大电容总线拆成几条小电容总线,代价是缓冲器引入的传播延迟。
5.4 两个主机同时工作时数据错乱:输出级不是开漏
现象:单主机跑得好好的,一旦第二个主机上线,就出现总线被拉死在某个中间电平、器件发热、或者数据整体错位。
原因:有人在软件模拟 I2C 时把引脚配成了推挽。两个推挽输出一个发高一个发低,等于电源和地之间短路,此时读回的值不再是总线的真实状态,仲裁逻辑完全失效——输的那一方根本察觉不到自己输了,因为它驱动能力足够强,把线强行拉到了自己想要的电平。
解决:把所有 I2C 引脚的输出模式改成开漏,外部加上拉。带硬件 I2C 外设的芯片通常会自动把引脚配成开漏,但复用功能配置错了也会变成推挽,这一点在多主机系统里必须逐项确认。同时检查有没有器件漏了开漏配置——总线上只要有一个推挽,整个仲裁机制就作废了。
5.5 3.3V 器件和 5V 器件直连:上拉接哪一边都不对
现象:两边的器件单独测试都正常,接在一起之后低压侧的器件工作异常,或者高压侧读到的电平始终不对。
原因:直接连线的做法通常是把上拉接到其中一侧的电源,于是另一侧的器件就要承受超出自己电源域的电压。I2C 的引脚虽然多是开漏、耐压通常也够,但输入高电平的门限是按各自 VDD 的比例定义的,5V 侧输出的高电平对 3.3V 器件来说是远超 VDD 的过压;反过来 3.3V 侧的高电平对 5V 器件可能刚好卡在门限边缘。
解决:用双向电平转换器,也就是上一节说的那个 N 沟道 MOS 管加两侧上拉的电路。它的好处是不需要方向控制,两侧各自上拉到自己的电源,任一时刻谁拉低都能正确传递,空闲时两侧各自保持自己的高电平。成本是一个 MOS 管加两个电阻,比任何带方向控制的方案都简单。如果两侧电源域差别不大、器件又都标称耐受,也可以赌一把直连,但那是靠器件余量撑着,不是靠设计。
6. 设计取舍:为什么 I2C 至今没被更快的总线取代
把这几层拆完,I2C 的设计取舍其实很清楚:它用带宽换线数,用电气约束换协议能力。两根线能挂上百个器件,代价是速率被 RC 充电时间卡住;多主机不需要中心仲裁器,代价是每个器件都必须遵守开漏规则,任何一个破坏规则整个机制就失效;地址空间可扩展,代价是 10 位寻址要多花一个字节。
这三笔交易在当年是划算的,现在也还是。板级管理、传感器读取、EEPROM 访问这类场景,数据量小、器件多、走线短,没有哪种总线能在这个成本点上做得更好。真正的边界出现在两个地方:一是速率往上顶到兆级之后,无源上拉撑不住,必须引入有源上拉和电桥,复杂度和成本开始逼近并行总线;二是总线电容一旦超标,所有时序余量同时消失,这时候再怎么调软件都没用,只能改硬件拓扑。
我一般的判断顺序是反过来的:先算电容,再定上拉,最后才调速率。很多人习惯从软件那头找问题,先降速、再加延时、再换驱动,折腾一圈发现是板上多挂了两个器件。从那以后我每次画完 I2C 相关的原理图,都会强制走一遍这个顺序——把所有挂载器件的输入电容加起来,加上走线估算,对着 400pF 的预算核一遍,再按算出来的 Cb 反推上拉阻值的上下限,最后才去选速率等级。这套流程花不了十分钟,但省下来的调试时间要以天计。希望帮到你。
本文还有配套的精品资源,点击获取