做嵌入式这几年,跟 I2C 打交道最多的场景,不是那种花里胡哨的多主机系统,而是“一个主控加一堆从机”的典型构型:MCU 下面挂着 EEPROM、温湿度传感器、OLED 屏、RTC、IO 扩展芯片,有的还会再挂几个舵机驱动板。两根线,SDA 和 SCL,把所有器件串在一起,主控挨个点名。只要能把这个一主多从玩明白,I2C 的 80% 的坑基本都能绕开。
这篇文章就从物理层、寻址、时序、仲裁到实际排查,完整梳理一遍一主多从总线上的门道。内容适合刚开始用 I2C 的新手,也适合那种“总线不知道为啥就死了”的老油条。
1. 两条线上挂几十个芯片:一主多从的物理基础
很多初学者第一次接触 I2C 都会有一个疑问:SPI 每条从机线都要单独拉一根片选,为什么 I2C 只需要两根线就能区分几十个设备?答案藏在它的物理层设计里。
1.1 开漏结构与“线与”逻辑:一对多通信的根基
I2C 的 SDA 和 SCL 都是开漏(open-drain)结构。所谓开漏,就是芯片内部的 MOS 管只有“拉低”和“释放”两种状态,它本身不能主动输出高电平。当设备想要让总线为低时,内部 MOS 管导通,把 SDA(或 SCL)拉低;当设备想释放总线时,MOS 管截止,引脚变成高阻态,总线电平由外部上拉电阻决定。
这带来一个非常重要的特性:只要总线上任何一个设备拉低,整条线就是低电平;只有当所有设备都释放,总线才会被上拉电阻拉回高电平。
这就是“线与”逻辑,也是 I2C 一主多从能够成立的物理前提。它意味着总线上天然没有“同时驱动冲突”的问题,任一时间点,谁想拉低谁就拉低,谁释放谁就释放,剩下的交给上拉电阻。相比之下,SPI 的 MOSI/MISO 是推挽输出,根本不能把多个输出直接并在一起,否则一个设备输出 1、另一个输出 0,就是实实在在的短路。
这个结构还带来一个现实结论:总线上设备再多,从电气角度看只是并联了多个“漏极开路”的引脚和电容负载而已。只要你的上拉电阻能在这个过程中把总线拉到合格的高电平,设备数量多少协议上并没有硬性限制。这也是为什么有些板子上挂 4、5 个 I2C 设备还能稳定跑 400kHz,而有些人只挂了两个传感器就死活不通——问题往往不在协议,而在物理层。
1.2 上拉电阻不是随便选的:数值计算与故障表现
既然开漏结构决定了总线高电平要靠外部电阻提供,那上拉电阻的取值就不是“找个 4.7k 怼上”这么简单。选太大,上升沿太慢,时序超标;选太小,灌入引脚的电流过大,器件可能扛不住。实际设计时,通常用上下限约束法来确定电阻范围。
下限由灌电流决定。I2C 规范定义了器件在拉低总线时能承受的最大灌电流 IOL,以及要求的最低低电平 VOL。在标准模式和快速模式下,多数器件 IOL = 3mA,VOL(max) = 0.4V,工作电压 VCC = 3.3V,那么上拉电阻最小值大约是:
Rp(min) = (VCC - VOL) / IOL = (3.3 - 0.4) / 0.003 ≈ 967Ω
所以取值不能低于 1kΩ,否则拉低时电流超标,从机可能无法把总线拉低到合格电平。
上限由总线电容和上升时间决定。I2C 对每个速率模式都有最大上升时间要求:标准模式 100kHz 是 1000ns,快速模式 400kHz 是 300ns,快速模式+(1MHz)是 120ns。上升时间 tr 与上拉电阻 Rp、总线总电容 Cb 的关系近似是:
tr = 0.8473 × Rp × Cb
假设 PCB 走线和器件引脚总共引入约 200pF 总线电容,标准模式下 Rp(max) = 1000ns / (0.8473 × 200pF) ≈ 5.9kΩ。这意味着如果总线电容再大一点,你用标准的 10kΩ 上拉电阻跑 100kHz,上升沿可能就已经超标了。
我自己的习惯是:板内短走线、设备不超过 5 个、跑 400kHz,用 2.2kΩ 到 4.7kΩ;跨排线、长线缆或者设备比较多时,先算一遍电容,再决定是否降到 1kΩ 并且把速率降回 100kHz。上拉电阻选错,示波器上看典型现象就是波形上升沿变成一个很缓的斜坡,数据位翻转跟不上时序,于是通信开始随机失败——这种问题排查起来非常费时间,不如设计阶段就算好。
2. 从机地址:一主多从的总线门牌号
物理层解决了“怎么连”的问题,接下来就是“怎么找到对方”。I2C 的一主多从寻址机制,可以说整个协议最精彩的部分都在这个地址字节里。
2.1 7 位地址空间与各种写法之间的换算
标准的 I2C 设备地址是 7 位,加上一个读写位后组成了传输时的第一个字节。7 位地址总共可以表示 128 个地址,但其中有一部分被协议保留:0x00 到 0x07 是一般呼叫、起始字节等保留地址,0x78 到 0x7B 是 10 位寻址模式的头字节区间,0x7C 到 0x7F 也是保留地址。真正能分配给普通从机芯片的,大约是 0x08 到 0x77 这个范围。
在实际工程里,地址写法一直是个容易踩坑的点。很多芯片手册会直接把地址写成一个 8 位字节,比如七位地址 0x50 的 AT24C02,手册上写的是写地址 0xA0、读地址 0xA1。这里的换算关系是:0x50 左移一位得到 0xA0,最低位再补上 R/W 位。用代码时有人写0xA0,有人写0x50,还有人写成0x50 << 1,关键是要弄清楚自己的驱动库里,传入的到底是 7 位地址还是 8 位地址。
如果你在代码里看到类似这样的表,对照着用就不会晕:
| 设备芯片 | 7 位地址 | 写地址字节(<<1 + 0) | 读地址字节(<<1 + 1) | 备注 |
|---|---|---|---|---|
| AT24C02 | 0x50~0x57 | 0xA0~0xAE | 0xA1~0xAF | 引脚 A2/A1/A0 决定低 3 位 |
| PCF8574 | 0x20~0x27 | 0x40~0x4E | 0x41~0x4F | A2/A1/A0 三脚配置 |
| PCF8574A | 0x38~0x3F | 0x70~0x7E | 0x71~0x7F | 和 PCF8574 地址区间互补 |
| SSD1306 OLED | 0x3C / 0x3D | 0x78 / 0x7A | 0x79 / 0x7B | 由 SA0 脚决定 |
| BMP280 | 0x76 / 0x77 | 0xEC / 0xEE | 0xED / 0xEF | SDO 脚决定 |
| MPU6050 | 0x68 / 0x69 | 0xD0 / 0xD2 | 0xD1 / 0xD3 | AD0 脚决定 |
这张表建议直接收藏。我在实际项目中见过太多人把 0x68 和 0xD0 混用,结果 HAL 库和寄存器地址写法双方错位,调了一晚上没找到问题。
2.2 地址冲突怎么破:引脚跳线、软件扫描与多路开关
一主多从最让人头疼的事,就是两个芯片的地址一样。比如 MPU6050 的默认地址是 0x68,而 DS3231 RTC 的默认地址也是 0x68。如果一块板子上恰好要用这两个芯片,又都只能靠一个 AD0/INT 引脚切换两个地址,那你最多只能同时挂两片相同型号。遇到这种问题,常见的解决方案有三个。
第一个方案是把地址引脚拉高拉低。大部分支持多地址的芯片,都留了 A0/A1/A2 或 AD0/SA0 之类的引脚,把它们接 VCC 或 GND,可以在一小段地址范围内腾挪。比如 AT24C02 的 A2/A1/A0 三个引脚,就能组合出 8 个地址,让同一颗 EEPROM 在一主多从总线上挂 8 片。
第二个方案是上电时做地址扫描。I2C 总线本身支持“广播式探测”:主设备可以依次对 1 到 127 的每个地址发送一个写请求,如果哪个从机应答了 ACK,就说明这个地址被占用。实际项目里,我会在系统初始化时跑一遍扫描函数,把应答地址打印出来,再和你预期的设备表对比。这样不只是为了确认地址,也能顺带检查有没有焊错元件、有没有虚焊。
第三个方案是使用 I2C 多路复用器,比如 PCA9546 或 TCA9548A。这类芯片相当于一个“总线路由器”,你可以在它的上游总线上选通某个下游通道,从而让多个相同地址的设备分时挂在不同通道上,互不干扰。它的代价是通信需要额外写一个通道选择寄存器,但对设备数量多、地址又不好调的场景,这是最实际的办法。
2.3 10 位地址模式:什么时候才用得上
7 位地址是主流,但 I2C 规范也定义了 10 位地址模式,能扩展到 1024 个地址。10 位寻址时,主设备发送的第一字节格式比较特殊:前五位是 11110,接着两位是 10 位地址的最高两位,最后一位是 R/W 位;第二字节才是 10 位地址的低 8 位。
这个模式和 7 位模式可以同时存在于同一条总线上,因为 0x78 到 0x7B 这些“10 位寻址头字节”的地址段,在 7 位模式下被保留,不会被普通从机占用。实际项目中,10 位地址的使用率其实很低,绝大多数芯片根本不支持它。除非你在做需要挂上百个设备的特殊系统,否则认准 7 位地址足够应对 99% 的嵌入式开发需求,不必在这个特性上花太多精力。
3. 一次完整通信的逐拍拆解:从起始条件到 ACK/NACK
物理层搞清楚、地址对上了,接下来就是主设备怎么把数据准确送到目标从机,再从目标从机拿回数据。这个流程的每一步都有严格时序约束,任何一个位错了,总线就会进入异常状态。
3.1 起始条件之后,第一字节永远是地址加读写位
I2C 通信以起始条件开始:SCL 保持高电平,SDA 由高变成低。这个下降沿定义了“总线要被使用了”。之后主设备开始送第一个字节——从机 7 位地址加读写位。比如你往 7 位地址 0x50 的 EEPROM 里写数据,第一字节就是 0xA0(写);如果是想读,第一字节就是 0xA1(读)。
在这里有个容易踩的坑:很多人以为 I2C 通信里“每次传输的第一字节都是设备地址”,但更准确的描述是“第一字节的高 7 位是地址,最低位是方向”。主设备并不是先发一个地址字节,再单独发一个读写位,而是把两者拼成一个字节一次发出去。这听起来像文字游戏,但当你用逻辑分析仪抓波形时,如果没理解这一点,很容易把 0xA0 当成一个奇怪的设备地址,而忽略了它只是 0x50 加写标志。
第一字节发完后,从机如果地址匹配,会在第 9 个时钟周期把 SDA 拉低,回一个 ACK;如果总线上没有这个地址,SDA 保持高电平,主设备收到 NACK,就能立刻知道这个地址的从机不在线上。
3.2 ACK/NACK 的语义:读数据时最后一个字节要发 NACK
ACK/NACK 是新手最容易稀里糊涂的部分,尤其是读操作。写数据时,主设备每发一个字节,从机就应该回一个 ACK,这个逻辑比较直观。但在读传输里,情况反过来了:从机发数据,主设备收数据。前面每个字节主设备都会回 ACK,告诉从机“我还想继续读”;当主设备读完最后一个需要的字节后,不能回 ACK,必须回一个 NACK,从机收到 NACK 就明白“哦,你不要了”,于是释放总线。
这个设计一开始会觉得别扭,但想明白就通了:如果主设备在读最后一个字节时也回 ACK,从机会误以为主设备还想继续读,它可能继续驱动 SDA 发送数据,整个读操作的结束状态就不干净了。实际工程里,因为最后一个字节回了 ACK 而导致的“读数据后总线多出一个额外字节”的 bug,我见过不止一次。
此外还有一个细节是“重复起始条件”。典型场景是读 EEPROM 的随机地址数据:你需要先写一个寄存器地址告诉芯片“我要从哪个地址开始读”,然后再切到读模式。这个过程如果中间发停止条件再重新启动,会让总线释放再占用,其他主机可能趁机插入执行命令。而使用重复起始条件(SCL 高电平时 SDA 拉低,不经过停止条件),整个读操作被封装成一个原子的、不可被其他主机拆散的传输序列。对于一主多从系统来说,这可能无伤大雅,但一旦后续你要在同一总线上引入多主,这种原子性就非常重要了。
3.3 两次实际传输的动作序列
用文字罗列往往不如直接看序列。一次向 EEPROM 的写传输,完整的动作序列是:
- 主设备发起始条件
- 主设备发地址字节 0xA0(7 位地址 0x50,写标志)
- EEPROM 回 ACK
- 主设备发寄存器地址,比如 0x10
- EEPROM 回 ACK
- 主设备发要写入的数据,比如 0x3C
- EEPROM 回 ACK
- 主设备发停止条件
一次随机读传输,则是:
- 主设备发起始条件
- 主设备发地址字节 0xA0(写标志)
- EEPROM 回 ACK
- 主设备发寄存器地址 0x10
- EEPROM 回 ACK
- 主设备发重复起始条件
- 主设备发地址字节 0xA1(读标志)
- EEPROM 回 ACK
- EEPROM 发送数据字节
- 主设备回 NACK(表示我已经读够了)
- 主设备发停止条件
我建议你拿着这个序列去对比逻辑分析仪的抓包,看几遍之后整个 I2C 协议就会在大脑里变得特别具象。很多单片机库已经把这一步封装好了,你调用HAL_I2C_Mem_Read或HAL_I2C_Mem_Write就行,但底层发生的事一定得能自己推演出来。
4. 一主多从总线上的时钟拉伸与仲裁:单主系统也别掉以轻心
一主多从听起来只有一个主机,不会发生冲突。但 I2C 协议里有两个机制,即便在单主系统中也可能触发,需要提前了解。
4.1 时钟拉伸:从机太慢时,它会按暂停键
I2C 的时钟信号 SCL 并不总是由主设备单方面控制,因为从机也可以把 SCL 拉低。协议上这叫时钟拉伸(clock stretching):当从机还没准备好接收下一字节数据时,它可以在 ACK 之后的某个时间点持续拉低 SCL,主设备看到 SCL 一直为低,就必须等待,直到从机释放 SCL,传输才能继续。
我最早遇到这个现象是在跑 BMP280 时:芯片上电初期没有就绪,你给它发读命令,它就像一个慢吞吞的店员,SCL 被它按得死死的不放手。当时我用的是纯 GPIO 模拟 I2C,驱动里没有处理“SCL 被从机拉低”的逻辑,导致读出来的数据全是 0xFF。后来才知道需要在等待循环里判断 SCL 电平。
这个坑的具体表现是:用硬件 I2C 外设时,很多 MCU 的 I2C 控制器会自动处理时钟拉伸,你基本感觉不到;但如果你是用 GPIO 模拟时序,一定要在发送每个位、每个字节前检查 SCL 是否真的被释放了。不校验的话,遇到慢速从机,总线会陷入死等,最后往往是看门狗复位才把系统救回来。
4.2 仲裁机制:就算你只有一个主设备,也可能触发
仲裁(arbitration)是 I2C 为多主机设计的能力:当两个主设备在同一时刻都想占用总线时,它们会在发送数据的过程中自动比较 SDA 电平。由于开漏结构,一个设备写 1、另一个设备写 0 时,总线上实际电平是 0。写 1 的设备经过比较发现自己发送的电平与总线上实际电平不一致,就知道自己输了,立即停止发送,等待总线空闲后重试。
听起来和多主机相关,单主系统应该不会用到。但实际中,我最少碰到过三次类似仲裁的场景:
第一次是某款 MCU 的 I2C 引脚和另一个外设的引脚复用了,引脚模式没配对,另一个外设趁机把 SDA 拉成了低电平,导致主设备以为总线一直忙。第二次是系统里有一个会休眠的从机芯片,它在休眠时 IO 状态不稳定,漏电把 SDA 拉低,主设备发起传输时当场“仲裁失败”。第三次是代码初始化时序有问题,两个线程同时调用同一个 I2C 驱动句柄,虽然没有第二台物理主机,但软件层面出现并发访问,驱动内部状态机错乱了。
所以不要以为“一主多从”就没有仲裁问题。实际项目里,如果 I2C 驱动在某次传输后一直报总线忙错误,你要去排查的往往不是协议本身,而是总线电平是不是被某个异常状态拉低了、引脚是否被别的外设占用、驱动是否具备互斥保护。
5. 实际项目里的一主多从硬件设计与排查经验
理论部分聊得差不多了,最后落回到工程实践。这一节是我在多个量产项目里沉淀下来的硬件设计清单和排查流程,能帮你少走很多弯路。
5.1 一主多从硬件设计清单
接线与拓扑
I2C 总线的走线建议采用“菊花链”或者“就近接入”的方式,尽量避免把每条从机分支拉得太长。板内布线时,SDA 和 SCL 尽量平行走近,减少环路面积。如果是跨板连接、线缆连接,线长超过 10cm 就要开始考虑降速。低速模式下,1 米以内的短排线通常还能工作,但不建议把 I2C 当成长距离总线来用,那是 RS485 的活。
电源域与电平转换
一主多从系统中经常出现 3.3V 主控和 5V 传感器混搭。记住 I2C 的开漏结构其实天然支持不同电压域(只要上拉电阻接到高电压域),但前提是器件引脚能容忍那个高电平。比如 3.3V 的 MCU 直接挂在 5V 上拉上,引脚如果不能容忍 5V 就会烧。这时需要加双向电平转换电路,最简单的方案是 BSS138 MOSFET 加两个上拉电阻,或者直接用 TXS0102 这类专用芯片。电平转换的方案稳定性比“赌引脚耐压”要靠谱得多,尤其量产的板子,一定要按耐压指标设计。
设备数量与总线电容
一个常见问题是“一条 I2C 总线上最多挂多少设备”。规范上并没有数量限制,真正的限制是总线电容。标准模式和快速模式建议总线电容不超过 400pF。一个典型芯片的引脚电容加走线电容大约 10pF 到 20pF,所以理论上可以挂二三十个设备,但这取决于 PCB 走线长度和上拉电阻。如果你要挂的设备超过 10 个且速率要求 400kHz,建议做一个总线电容估算,再算一遍上拉电阻。
5.2 一个亲测有效的总线卡死恢复流程
I2C 总线卡死,可以说是一主多从系统里最高频的故障。现象通常是主设备初始化后,调用读写函数一直返回忙或超时,示波器一抓发现 SDA 被拉死低电平,SCL 正常。
最先要做的不是重新上电,而是确认是不是某个从机把 SDA 按住了。恢复的办法是:对 SCL 连续输出 9 个脉冲。I2C 协议规定,设备识别到不完整的起始条件会释放总线,9 个脉冲能让状态机错乱的从机把 SDA 放掉。很多时候,发完 9 个时钟,总线就恢复了。
之后还要找出根因。从我碰到过的案例来看,常见原因有这么几类:
- 从机芯片后于主控上电,主控在从机还未就绪时就开始通信,从机内部死锁。
- 从机芯片的供电不稳或电源纹波过大,导致芯片逻辑异常。
- 软件发送过程中发生中断抢占,导致 SDA/SCL 时序被中断打断,遗留了一个半截的起始条件。
处理办法分别是:调整上下电时序,确保主控初始化 I2C 前从机已经稳定供电;给从机电源增加滤波电容;在驱动里加超时保护,并且在上电初始化阶段主动做一次 9 脉冲恢复。这个“初始化先发 9 个脉冲”的经验,我后来直接做进了标准驱动模板,凡是能用 I2C 的项目都先做一次,成本极低,收益明显。
5.3 用示波器和逻辑分析仪快速定位问题的排查步骤
排查 I2C 问题,建议按照“先看静态、再看波形、最后测数据”的顺序来。
第一步,断电状态用万用表测 SDA 和 SCL 对地电阻,排除短路。上电后测空闲状态电平,两条线都应该是接近 VCC 的高电平。如果某一条线是 0V 或者只有零点几伏,说明上拉电阻没接、或者有设备把它拉死了。测量时注意:如果数值在 1V 到 2V 之间半高不高,大概率是总线驱动能力不足或者上拉电阻选得过大。
第二步,用示波器或逻辑分析仪抓启动瞬间的波形。重点看这些位置:
- 起始条件是否标准(SCL 高电平时 SDA 下降沿)
- 第一字节地址值是否符合预期(比如 0xA0 对应写 0x50)
- ACK 位有没有被拉低(从机应答)
- 停止条件是否完整
如果总线上根本没有波形,先查主设备初始化是否完成、引脚功能是否选对、有没有被别的外设占用。如果起始条件有了,但地址后一直收不到 ACK,建议用扫描程序对所有地址发写请求,看哪些地址有应答,立即就能确认从机到底有没有挂在总线上、地址对不对。
第三步,针对间歇性故障,把捕获深度开大,抓长时间波形。间歇性问题九成和时序裕量有关,逻辑分析仪的协议解析功能会提示超时、NACK、数据错误等事件。抓几轮之后,基本能确认是上升沿太慢、还是某个从机偶尔不应答、还是总线上有周期性干扰。
5.4 一个关于速率的实际观点
最后说两句速率。I2C 的速率不是越高越好。在实际项目里,当总线长度超过 20cm、设备数量多于 4 个时,我一般直接把速率从 400kHz 降到 100kHz。降速带来的性能损耗几乎可以忽略,因为 I2C 本来就不是为高速传输设计的,传感器、EEPROM 这类设备的典型读写量都很小,100kHz 和 400kHz 的差别通常只有几十微秒。
反倒是为了“跑满 400kHz”而去调整上拉电阻、缩短走线、优化电容,会浪费大量调试时间,性价比很低。除非你的系统里有大量连续读写且对延迟敏感的设备,否则在稳定性和速率之间,我毫不犹豫选稳定性。
做 I2C 一主多从这些年,最大的体会是:这个总线协议看起来极简,但它的优雅和隐患都藏在线与逻辑、开漏结构、时钟同步这些细节里。你越是深入理解它的物理层和时序语义,越能在故障发生时快速定位问题。如果你正准备在手头项目里搭一条一主多从总线,建议先把 SDA/SCL 的波形、地址表、上拉电阻这几个基础点全部过一遍,再动手画板写驱动。这样后面大概率能少熬好几个夜晚。