ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

I2C总线从开漏上拉到多主机仲裁与总线恢复

I2C总线从开漏上拉到多主机仲裁与总线恢复

简介:《I2C总线协议详解(中文)》是一份面向嵌入式开发初学者、硬件工程师及电子类专业学生的PDF技术文档,围绕I2C双向二线制串行总线的通信原理与规范展开,可用于入门学习、协议查阅和接口调试参考。全文从版本演进切入,梳理1.0至2.1规范在速率、地址空间和通信机制上的变化,并说明总线对设计人员与厂商在布线、成本、可靠性和模块化方面的益处;随后讲解SDA/SCL信号线、主从设备、标准/快速/高速模式、数据有效性、起始与停止条件、字节格式与应答、仲裁与时钟同步,以及7位和10位寻址等核心内容,目录层次较清晰,便于按主题检索。资源包共1个PDF文件,大小约872KB,轻量易存,适合在电脑或移动端随时翻阅。已有842人学习下载,可作为理解I2C时序、地址格式和总线通信流程的案头资料。

1. I2C 的答案藏在一根被上拉的线上:两根线凭什么撑起一整套总线

多数人对 I2C 的印象是"两根线、简单、慢"。但把时序图和电气图叠在一起看,会遇到一个反直觉的事实:I2C 真正的复杂度不在协议层,而在电气层。多主机仲裁、时钟同步、应答、时钟拉伸这四件看起来毫不相干的事,最后都由同一条规则解释——挂在总线上的任何器件,都只能把线往下拉,没有资格把线往上推。高电平是上拉电阻给的,不是器件给的。理解到这一层,很多玄学问题会自动消失:为什么换个上拉电阻通信就好了,为什么主机复位后总线会锁死,为什么慢速从机能拖住整个系统。I2C 是少见的、协议语义直接长在电路特性上的总线,读懂那根电阻,比背时序表有用得多。

2. 开漏输出与线与:上拉电阻为什么决定了协议的全部上限

2.1 谁有资格"拉高":开漏级的电流路径与线与的数学

先看一条物理事实。I2C 器件引脚内部接的是一个开漏(Open-Drain)或开集电极的管子,它的漏极只连到 SDA 或 SCL,源极接地。这个管子导通时把线拉到地,关断时引脚处于高阻态,什么都不做。线要回到高电平,只能靠外部那只上拉电阻 Rp 通过电源把总线电容充上去。所以总线上任意时刻的电平,等于"所有器件输出状态的逻辑与":只要有一个器件在拉低,线就是低的;只有所有器件都松手,线才会被 Rp 慢慢抬到 VDD。

这条"线与"关系是整个协议的物理前提,它带来两个直接后果。第一,任何器件都不能强行输出高电平去顶另一个输出低电平的器件,否则就是两个推挽输出对打,大电流烧管子的经典事故;开漏天然避开了电气冲突。第二,正因为高电平是"没人拉"的被动结果,总线从低到高的速度完全由 Rp 和总线电容 Cb 决定,规范里给出的经验式是 tr ≈ 0.8473 × Rp × Cb,这个 0.8473 是把上升沿定义在 0.3VDD 到 0.7VDD 之间推出来的系数。

于是选型变成一道双向夹逼的题:Rp 太大,上升沿爬不上去,采样点还没到 VIH 就被判成低电平;Rp 太小,器件拉低时灌进管子的电流超标,VOL 抬起来同样会被判错。这两条约束正好给出窗口。

def rp_window(vdd, vol_max, iol, cb, tr_max): """返回上拉电阻的可用区间 [rp_min, rp_max],单位欧姆""" # 下限:器件把线拉低时,灌入电流不能超过 IOL,且 VOL 不能超过 vol_max rp_min = (vdd - vol_max) / iol # 上限:上升沿必须在 tr_max 内从 0.3VDD 爬到 0.7VDD rp_max = tr_max / (0.8473 * cb) return rp_min, rp_max # 3.3V 系统,VOL <= 0.4V,灌电流能力 3mA # 总线电容 200pF,快速模式要求上升时间 <= 300ns print(rp_window(3.3, 0.4, 0.003, 200e-12, 300e-9)) # 大约输出 (966.7, 1770.5),中间取 1.5k 比较稳妥

这段代码里vdd和vol_max来自电源与器件手册,iol是器件在 VOL 下能吸收的电流,cb是整条总线的等效电容(引脚电容加走线电容),tr_max由工作模式决定:标准模式 1000ns,快速模式 300ns。算出来的窗口往往比想象中窄,200pF 下只剩不到两倍余量,这就是为什么"从别的板子上扒一个 4.7k 过来"经常翻车——老板子器件少、走线短,Cb 只有几十 pF,4.7k 勉强够用;换到器件多、排线长的板子上,同一只电阻就爬不上去了。

2.2 一条线与逻辑被复用了三次:仲裁、同步、应答

顺着线与继续往下推,会发现协议设计者把这一个电气特性榨得干干净净。第一种复用是应答:第 9 个时钟周期里,发送方必须释放 SDA,接收方把 SDA 拉低表示 ACK。发送方自己去读这条线,读到低就是"收到了",读到高就是 NACK。整个过程不需要任何额外的应答信号线,因为"谁在拉低"这件事本来就是线与允许的操作。

第二种复用是多主机仲裁。两个主机同时发数据,一个想发 1(松手,让上拉决定),一个想发 0(拉低),线与的结果是 0。想发 1 的那个主机回读 SDA,发现读到的和自己发出的不一致,立刻知道自己输了,关闭输出级退出竞争。这里没有任何"冲突检测包"或"重传退避"的机制,仲裁是逐位实时完成的,而且败方退出的瞬间,胜方的数据完全没有被打断过——因为败方松手时它本来也没在拉低。

第三种复用是时钟同步。多主机的 SCL 也是线与的,所以低电平周期由所有主机里最长的那个决定,高电平周期由最短的那个决定。同步不是靠软件协商时钟频率,而是靠电容充电这个物理过程自然对齐。这三种机制合起来,几乎就是 I2C 协议里"高级功能"的全部,而它们的成本是一根电阻。

提示:仲裁和时钟同步都依赖"器件能回读自己刚写出去的电平"。如果某个驱动把 SDA 配成推挽输出,或者 GPIO 模拟时序里只写不读,仲裁机制就形同虚设,多主机环境下的数据损坏会以极低的概率随机出现,非常难查。

2.3 400pF 不是协议规定,是上升时间反推出来的器件数量上限

规范里说"挂到总线上的器件数量只受总线最大电容 400pF 限制",这句话常被当成一条硬性指标背下来。它的真实含义其实是上一条公式的反解:在标准模式 1000ns 的上升沿预算里,如果 Rp 取一个合理值(比如 4.7k 到 10k 之间),Cb 一旦超过 400pF,tr 就超预算了。换句话说,400pF 是"这个协议还能跑"的上限,不是"这个协议允许你挂这么多器件"的许可。

按每颗器件引脚 10pF 左右估算,400pF 在纸面上能挂三四十个器件,实际板子上挂超过十个就该开始算账:PCB 走线每厘米有一两个 pF 量级的电容,排线、连接器、ESD 器件都要计入。Cb 一涨,能用的 Rp 窗口就往下压,最后压到器件灌电流能力都撑不住。这也是为什么高速模式必须引入电流源上拉和串阻——电阻上拉在几百 pF 电容面前已经无解了,第 6 章会展开。

3. 位传输:SDA 为什么必须在 SCL 高电平期间"装死"

3.1 数据的有效性:采样点、建立时间与误触发的边界

I2C 有一条看起来很霸道的规则:SDA 上的数据只允许在 SCL 低电平期间改变,SCL 为高电平期间必须保持稳定。这不是为了规整好看,而是被"两个用途抢同一根线"逼出来的。SDA 同时承担数据线和控制信号线两个角色——起始条件和停止条件都是靠 SCL 高电平期间 SDA 的跳变来定义的。如果允许数据在高电平期间变化,接收方就无法区分"这是一个数据位"还是"这是一个起始条件"。

再往细看,接收方并不是在整个高电平期间连续采样,而是在高电平的某个窗口里取值,标准模式要求数据在 SCL 上升沿之前至少稳定 tSU;DAT(250ns,快速模式 100ns),而保持时间在规范里甚至可以给到 0。也就是说,真正苛刻的是"提前量":数据必须在时钟爬上去之前就已经就位,时钟落下来之后马上就可以改。这解释了为什么主频低的 MCU 用 GPIO 模拟 I2C 反而更容易稳——它把时序拉长了,慢得安全;而跑在几百兆的 MCU 上写软件延时,一不留神就把 SCL 拉高的动作排在了 SDA 变化之前,边沿挨得太近,示波器上看是两条几乎重合的竖线。

这里还有个隐蔽的坑:起始/停止条件检测器通常是一个硬件状态机,它对 SDA 的任何跳变都敏感,包括毛刺。上拉电阻偏大、走线太长、附近有开关电源,都会在高电平期间给 SDA 送进去一个几十纳秒的尖峰,状态机把它当成 Start,整个通信就直接脱轨了。规范里强调"片上滤波器可以滤去毛刺",说的就是这个;没有滤波器的软件模拟实现,只能靠外部串联小电阻和严格布线来压制。

3.2 起始条件与重复起始:控制信号就是"违反数据规则"的那两个边沿

把规则说全:SCL 为高电平时,SDA 由高变低是起始条件 S,由低变高是停止条件 P。注意这两个动作恰好就是"数据有效性规则"禁止的动作——协议故意把违规边沿征用成了控制信号,这是 I2C 最节省引脚的一处设计。反过来推,只要总线上出现 S 或 P,说明时钟一定处于高电平,且这是有意为之的控制动作,不是数据。

起始条件之后总线进入"忙"状态,停止条件的某段时间之后总线才回到"空闲"。空闲态的定义也很物理:SDA 和 SCL 都被上拉拉高,没有任何器件在拉低。所以"总线忙不忙"不需要谁去广播,读一下两根线的电平就知道。

重复起始 Sr 值得单独说一句,因为它是很多驱动 bug 的来源。一次读操作通常是"写地址(告诉从机我要读哪个寄存器)→ 重复起始 → 读地址 → 读数据 → 停止",中间的 Sr 不产生 P,因此总线从头到尾没被释放过,其他主机没有插进来的机会。这既是原子性的保证,也意味着一旦在这中间某一步主机挂掉,总线会一直停在忙的状态,直到有人来收拾——第 5 章会讲怎么收拾。

3.3 字节格式与第 9 个时钟:ACK 到底是什么时候被谁驱动的

数据按字节传,每字节 8 位,高位在前(MSB first),后面紧跟第 9 个时钟作为应答位。这个"第 9 个时钟"是整段时序里唯一一次方向切换:前 8 个时钟由发送方驱动 SDA,第 9 个时钟发送方必须松手,由接收方驱动。

对写操作来说,主机是发送方,从机是接收方,从机在第 9 个时钟拉低 SDA 表示"这个字节我收下了"。对读操作来说方向反过来,从机发数据,主机接收,主机在第 9 个时钟拉低表示"我还要下一个字节",拉高(不拉低,也就是 NACK)表示"够了,别再发了"。所以 NACK 在 I2C 里有两个完全不同的语义:地址阶段收到 NACK,意思是"这个地址上没有器件";数据阶段由主机接收方发出 NACK,意思是"传输正常结束"。把这两个语义混在一起,就会出现"读最后一个字节时报警告"这种假故障。

还有一层:ACK 周期里接收方拉低 SDA 也是线与操作,其他器件此时都在松手,不会干扰。如果总线上某颗器件因为电源没起来、地址配错、或者内部状态机卡死而不拉低,主机读到的就是高,判为 NACK。所以 NACK 本身不是"错误码",它是一个中性的物理事实,需要结合上下文解释。

3.4 用 GPIO 复刻时序:位操作代码里藏着协议的骨架

把上面的规则翻成位操作,代码会短得出人意料。下面这段刻意省略了具体寄存器,只保留时序骨架,重点看每个动作之间的顺序关系。

/* SDA/SCL 均配置为开漏输出,读引脚前先写 1 释放总线 */ #define SDA_H() gpio_write(SDA_PIN, 1) #define SDA_L() gpio_write(SDA_PIN, 0) #define SCL_H() gpio_write(SCL_PIN, 1) #define SCL_L() gpio_write(SCL_PIN, 0) #define SDA_READ() gpio_read(SDA_PIN) static void i2c_start(void) { SDA_H(); SCL_H(); delay_ns(TSU_DAT); /* 空闲:两线都被上拉拉高 */ SDA_L(); /* SCL 高期间 SDA 下降 = Start */ delay_ns(THD_STA); SCL_L(); /* 拉低时钟,进入数据传输阶段 */ } static int i2c_write_byte(uint8_t b) { for (int i = 7; i >= 0; i--) { /* MSB first */ if (b & (1u << i)) SDA_H(); else SDA_L(); delay_ns(TSU_DAT); /* 数据必须先于时钟就位 */ SCL_H(); delay_ns(THIGH); /* 从机在此窗口内采样 */ SCL_L(); delay_ns(TLOW); /* 低电平期间才允许改 SDA */ } SDA_H(); /* 第 9 个时钟:发送方必须释放 SDA */ delay_ns(TSU_DAT); SCL_H(); delay_ns(THIGH); int ack = (SDA_READ() == 0); /* 接收方拉低 = ACK */ SCL_L(); return ack; } static void i2c_stop(void) { SDA_L(); SCL_H(); delay_ns(TSU_DAT); /* 先让 SCL 到高 */ SDA_H(); /* SCL 高期间 SDA 上升 = Stop */ delay_ns(TBUF); }

i2c_write_byte里最能说明问题的是三处顺序:写 SDA 在拉高 SCL 之前(满足 tSU;DAT)、拉低 SCL 在改 SDA 之前(满足数据有效性)、第 9 个时钟前把 SDA 置高(释放总线交给接收方)。TSU_DAT这类常量不是随便填的,标准模式对应的量级是几百纳秒,快速模式是一百纳秒上下,具体值查对应模式的时序表;THIGH、TLOW决定实际速率,占空比不必 50%,但低电平时间不能短于规范最小值。这段代码跑在几十兆的 MCU 上时,delay_ns很容易被编译器优化掉或者被中断打断,一旦被打断,就会在总线上留下一个超长的低电平——从机不会报错,只会安静地等,于是表现为偶发的通信卡顿。

4. 仲裁与时钟同步:多个主机同时说话为什么数据不会被撕碎

4.1 时钟同步:低电平由最慢的定,高电平由最快的定

多主机总线的第一道难题是时钟:每个主机都有自己的 SCL 发生器,频率不可能严格一致,如果各发各的,总线上会出现两路时钟叠加的乱码。I2C 的解法完全依赖线与:SCL 上只要有一个主机在拉低,它就是低;所有主机都松手,它才被上拉变高。

由此得到两条结论。低电平周期由"低电平持续时间最长"的主机决定——短的那个数完自己的低电平时间想拉高,但线还是低的(别人还拉着),它只能进入等待状态,等线真的变高再开始数高电平。高电平周期由"高电平持续时间最短"的主机决定——它先数完,先把线拉低,其他主机的高电平计数被迫中断。最终总线上的时钟是所有人时钟的"最慢低电平 + 最快高电平"的合成。

这个机制看起来只是解决了频率不一致,实际作用更大:它保证了所有主机在同一个位边界上对齐,只有这样逐位仲裁才有意义。如果两个主机的时钟相位错开半个位,它们在 SDA 上比较的根本不是同一位数据,仲裁结果就毫无意义。

4.2 逐位仲裁:发 1 读出 0 的那一刻,主机就降级成了从机

仲裁的规则一句话能说完:主机每发一位,就回读 SDA 一次;如果自己发的是 1(松手)而读回是 0,说明有别的主机在拉低,自己输了,立刻停止驱动 SDA,转为从机接收状态。赢家甚至不知道自己赢过——它只是继续发,从头到尾没被打断。

这套机制能成立,前提是"赢家的数据就是总线的数据"。因为线与的结果永远等于所有竞争者中"最低"的那个值(有 0 则 0),而输的那个正是因为发了 1 才输的,所以总线上呈现的始终是赢家的报文,不需要重传,也没有任何位被破坏。这是 I2C 相比某些"冲突后随机退避重发"的总线在效率上的关键差异:仲裁的代价只是败方要重新排队,报文本身零损失。

仲裁不只发生在地址阶段。两个主机寻址同一个从机时,地址位全部相同,仲裁会一路延续到数据位;如果一个是读一个是写,R/W 位不同,仲裁在第一个字节的最低位就分出胜负。甚至 ACK 位也参与仲裁:主机发送器和主机接收器在应答时钟上的行为不同,谁的表现和总线不一致谁退出。这意味着"仲裁窗口"覆盖了从起始条件到停止条件的整段传输,直到总线上只剩一个主机为止。

一个实践中的推论:软件模拟的 I2C 主机如果只做写、不做回读,就等于主动放弃了仲裁能力。在单主机系统里这没问题,但只要板子上存在第二个可能发起传输的主机(比如另一颗 MCU、或者带主控能力的调试接口),就必须老老实实每一位都读回来比对,否则数据损坏会以"平时都对、偶尔错一个字节"的形式出现,用逻辑分析仪抓十次也未必复现。

4.3 时钟拉伸:从机唯一能"踩刹车"的手段

从机在 I2C 里天然是被动的——它不能主动发起传输,只能等主机来问。但有一件事它能做:把 SCL 拉住不放。从机在第 9 个 ACK 之后继续保持 SCL 低电平,主机想拉高也拉不高(线与),只能干等;从机处理完内部事务(比如 EEPROM 的写周期、ADC 的转换、中断服务)再松手,主机才继续往下一个字节走。

这是 I2C 里唯一的流控手段,也是它能在低速器件和高速主机之间共存的原因:不需要协商速率,慢的器件自己决定什么时候让时钟前进。代价是主机必须尊重这个信号——很多硬件 I2C 外设有超时机制,如果从机拉太久,主机可能判超时并复位总线,把一次本来正常的慢操作变成错误。

拉伸还有个容易忽略的细节:它发生在字节边界之后、ACK 位之后,也就是说第 9 个时钟的低电平期间就可能开始。所以主机在发完一个字节后不能假设"下一个时钟一定是我的",必须先确认 SCL 真的回到了高电平,再开始下一位。手写的位操作代码里如果少了这个检查,遇到会拉伸的从机(比如某些带内部复位的传感器)就会在某个字节之后整段错位。

4.4 代码:一次完整的写事务与总线恢复

把前三节的机制拼起来,一次典型的寄存器写长这样:Start → 从机地址+W → ACK → 寄存器地址 → ACK → 数据 → ACK → Stop。每一步都要检查 ACK,NACK 时立刻 Stop 释放总线,而不是继续往下发——继续发只会让从机状态机更乱。

/* 返回 0 表示成功,负数表示在第几步失败,便于定位 */ static int i2c_write_reg(uint8_t addr7, uint8_t reg, uint8_t val) { i2c_start(); if (!i2c_write_byte((addr7 << 1) | 0)) { i2c_stop(); return -1; } if (!i2c_write_byte(reg)) { i2c_stop(); return -2; } if (!i2c_write_byte(val)) { i2c_stop(); return -3; } i2c_stop(); return 0; } /* 总线锁死恢复:主机在传输中途复位,从机可能还占着 SDA 等第 9 个时钟 */ static int i2c_bus_recover(void) { if (SDA_READ() == 1) return 0; /* 线是自由的,不需要恢复 */ for (int i = 0; i < 9; i++) { /* 补最多 9 个时钟把它送过 ACK */ SCL_L(); delay_us(5); SCL_H(); delay_us(5); if (SDA_READ() == 1) break; /* 从机松手了 */ } i2c_stop(); /* 再补一个 Stop 复位所有状态机 */ return (SDA_READ() == 1) ? 0 : -1; /* 还低说明是硬件问题 */ }

i2c_write_reg里那个addr7 << 1是关键,7 位地址左移一位后最低位留给读写方向,这是第 5 章要展开的地址格式问题。i2c_bus_recover则是实战中必备的兜底:主机侧发生看门狗复位时,从机可能正好在输出一个 0 位或者等待 ACK,它不知道主机已经重启,会一直拉着 SDA。新启动的主机如果直接发 Start,会因为 SDA 已经是低的而发不出下降沿,表现为"总线彻底没反应"。补 9 个时钟脉冲就是替从机把它没走完的那个字节走完,让它吐出 ACK 松手,再用一个 Stop 收尾。

5. 排查:I2C 通信失败时,那几条踩过的坑

5.1 现象:波形上升沿像馒头,ACK 位总是读成高;原因:上拉电阻照抄了别人的板子

现象很好认:逻辑分析仪上 SCK 方方正正,SDA 的上升沿却是个圆润的斜坡,读 ACK 的那一位常常被判成 NACK,但重试几次又能通。原因几乎总是上拉电阻值偏大,配上一块电容不小的板子,上升时间 tr 超过了当前模式的预算,接收方的采样点落在斜坡上,采到 0 还是 1 取决于温度、批次和运气。开发板上能跑,是因为人家只挂了两颗器件、走线三厘米;换成八颗器件加一段排线,Cb 翻了几倍,同一只电阻就不够用了。

解决方式是回到公式算:先把 Cb 估出来(器件数 × 约 10pF 加上走线),再用 tr ≤ 0.8473 × Rp × Cb 反推 Rp 上限,同时用 (VDD − VOL)/IOL 校核下限,取窗口中间值。改完电阻仍然上不去,就说明电容实在太大,要么加 I2C 缓冲器把总线分段,要么降速运行。这里最忌讳的做法是"再加一只电阻并联"——两只电阻并联确实降低了阻值,但位置不对的话,只会让某一段的上升沿变好、另一段维持原状,问题从"全都不稳"变成"局部不稳",更难查。

5.2 现象:总线彻底没反应,SDA 长期被拉低,主机发不出起始条件;原因:从机在等一个永远不会来的时钟

这个坑在带看门狗的产品上出现频率极高。主机正在读一个从机的数据,读到一半因为某个异常复位了,从机还停在自己的输出位上,等着主机给它第 9 个时钟。重新初始化后主机去发 Start,SDA 已经是低电平,从高到低的下降沿根本产生不了,硬件外设直接报总线错误,或者更糟——外设认为自己发出了 Start,从机也这么认为,双方对着一个错位的时序往下跑。

定位方法很简单:不通信时用万用表量 SDA 对地电压,如果是零点几伏而不是接近 VDD,基本可以确认被某颗器件拉死。解决办法就是上一节那段恢复代码,用 GPIO 把 SCL 当普通输出,手动打 9 个时钟加一个 Stop,让从机把这个字节走完。真正值得做的是把恢复代码放进初始化流程:每次上电先检查 SDA 是否为高,不高于就先恢复再初始化外设,这条习惯能省掉很多"偶发不开机"的现场返修。

5.3 现象:写操作一切正常,读操作第一个字节就 NACK;原因:7 位地址和 8 位地址混用

这个错误几乎每个写 I2C 驱动的人都犯过一次。器件的规格书里给的地址是 7 位,比如写作 0x50;而某些驱动接口、或者从别人代码里抄来的宏,用的是把读写位算进去的 8 位地址,写作 0xA0 / 0xA1。这两套写法只要有一处混用,就出现了"写能通、读不通"或者完全相反的现象,因为写地址左移后刚好撞上了另一个器件的地址,或者撞上了保留地址段。

判断方法很直接:把总线上实际发出的第一个字节抓出来,和规格书里的 7 位地址左移一位对比。规范的做法是在驱动层统一按 7 位地址存储,在拼第一个字节时左移并或上读写位,接口对外只暴露 7 位地址。另外留个心眼,有些老器件的"地址"实际上是"地址 + 固定读写位",规格书里给的就是 8 位形式,遇到这种要在驱动注释里写清楚,别让下一个人再踩一次。

5.4 现象:单主机系统好好的,接上第二颗能发起传输的器件后偶发数据错乱;原因:软件模拟没有做回读仲裁

单主机的时候,位操作代码只写不读完全没问题,因为没人跟它抢。一旦总线上出现第二个主机——另一颗 MCU、一个可做主控的调试接口、甚至某个上电瞬间会乱发信号的器件——没有回读的模拟实现就等于放弃了仲裁能力,两个主机同时驱动 SDA,总线上出现的是它们的逻辑与,双方都以为自己发的是对的,报文被悄悄改写,谁都不会报错。

解决方式是给模拟实现的每一位加上回读比对:发 1 之后读回 0 就说明输了,立即停止驱动、转为接收,并把自己标记为仲裁失败,交由上层决定是否重试。同时要保证 SDA 的引脚配置真的是开漏输出而不是推挽——推挽输出遇到对方拉低就是短路,轻则波形畸变,重则烧引脚。这条经验的价值在于:它把一类"看起来像随机故障"的问题,变成了一个有明确检测点的状态机分支。

5.5 现象:上电瞬间从机偶尔收到一个假起始条件,第一条命令就失败;原因:浮空引脚上的毛刺

上电时序是最容易被忽略的一段。电源还没稳、上拉电阻还没把线拉到 VDD 的时候,SDA 和 SCL 处于浮空状态,任何一点耦合都会在线上产生跳变。从机的起始条件检测器一上电就开始工作,它不管电源稳不稳,看到 SCL 高、SDA 下降就认为传输开始了,于是进入接收状态,等着后面永远不会来的时钟。

对策有三层:硬件上给 SDA/SCL 串小阻值电阻并在靠近器件处加小电容抑制高频毛刺;时序上保证主机在从机电源稳定之后才开始通信,必要时主机先把两根线拉低一小段时间再释放,人为制造一个干净的 Stop 让所有从机状态机复位;软件上在初始化后的第一次通信前先发一个 Stop,哪怕总线上什么都没有。这三层里最有效的是第二层,因为它从物理上保证了"所有从机在通信开始前都处于空闲态"。

6. 从 100k 到 3.4M 再到混合电压:每一次提速都在付电气代价

把三种速率模式并排放,会发现它们的差别几乎全在电气层,协议层一个字没改。

模式速率上限上拉方式关键电气约束
标准模式100 kbit/s电阻上拉到 VDD总线电容 ≤ 400pF,上升时间 ≤ 1000ns
快速模式400 kbit/s电阻上拉 + 输出级斜率控制上升时间 ≤ 300ns,输出级加斜率控制与输入滤波改善 EMC
高速模式1.7 / 3.4 Mbit/s主机侧电流源上拉,SDA/SCL 串阻需要电桥隔离低速段电容,传输前用主机码切换

从标准到快速,代价是把上升时间的预算从 1000ns 压到 300ns,器件数量不变的前提下 Rp 必须成比例减小,灌电流随之上升,于是规范给输出级加了斜率控制——限制下降沿的陡度,把高频谐波压下去,顺带缓解了 EMI。从快速到高速,电阻上拉这条路彻底走到头:3.4Mbit/s 要求上升沿在几十纳秒量级,靠电阻给几百 pF 充电根本做不到,只能改成主机侧电流源上拉,用恒流把电容顶上去;同时在高速段和低速段之间放一个电桥,把低速段的 400pF 挡在外面,让高速传输只面对很小的局部电容。为了一次传输里能同时容纳两种速度,规范还设计了主机码:传输开始时用低速时序发一个特殊的主机码,所有从机看到它就切到高速模式,结束后再切回来。

这里的设计取舍非常值得琢磨。I2C 明明可以像别的总线那样"升级换代、不兼容旧器件",但它每一次提速都坚持向后兼容:快速模式器件可以在 100k 系统里用,高速模式器件可以和标准模式器件挂在同一条总线上,只是分处电桥两侧。代价是规范里堆了大量混合速度系统的时序要求和电桥时序约束,实现复杂度落在了主控和电桥上,而不是逼所有器件一起换。另一个取舍是电平转换:当总线上同时有 3.3V 和更低电压的器件时,不能简单用电阻分压,因为开漏输出是双向的,两个方向的电平都要转换,于是规范专门给了用一只 MOS 管加两只上拉构成的双向电平转换电路——利用 MOS 管的体二极管和栅极自偏置,让两侧各自拉到自己的 VDD,不需要方向控制信号。

验证这类系统有个很实用的办法:不要只看通信成功与否,要看边沿。把示波器探头挂在实际器件的引脚上(不是主控引脚),量最远端从机的上升时间和 VOL,这两个数才反映真实的裕量。通信"能通"和"有裕量"是两回事,前者是当前温度下的运气。

我自己的教训是早年在一条挂了七八颗器件的板子上直接沿用了开发板的 4.7k,实验室里跑了一周都没事,一到冬天整机在现场开始间歇性报错。从那以后我每画一版 I2C,都强制把 Cb 估一遍、把 Rp 窗口算一遍,再留一段总线恢复代码在初始化里,哪怕这颗芯片的手册写着"内置上拉"。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表