搞嵌入式的几乎没人躲得开I2C。两根线,一根SCL,一根SDA,看起来比UART还简单,但真要把开漏物理层、上拉电阻、时序图、多主仲裁这些点串起来讲透,我发现能讲明白的人真不多。很多工程师的状态是:能调通EEPROM,看得懂时序图,但一旦遇到GT911触摸屏通信失败、总线卡死、两个主机互相打架,就只能靠试。我花了一周时间,把这两根线从物理层到协议层重新过了一遍,示波器和逻辑分析仪都用上了,今天把整套东西整理出来。不管你是做单片机、Linux驱动还是FPGA,这份笔记应该都能帮你把I2C彻底吃透,至少能少走很多弯路。
1. 为什么I2C要用开漏这么“反直觉”的设计
1.1 开漏的本质:谁都能拉低,但谁都别想硬拉高
先从最底层看。I2C的SDA和SCL都是双向线,而且必须用开漏(Open Drain)或集电极开路(OC)输出。为什么不能用推挽?推挽输出高电平时由内部PMOS直接接到电源,低电平时由NMOS拉到地。如果两个设备都接在一条总线上,一个输出高、一个输出低,就等于一个PMOS和一个NMOS串联在电源和地之间单独给它们供电。电流会直接从电源经PMOS、总线、NMOS流到地,轻则逻辑错误,重则烧毁引脚、炸掉整个板子的电源。
开漏输出则完全不一样。内部只有一个N沟道MOS管,一端接GND、另一端接外部引脚,它只能主动把引脚拉低。要输出高电平,只能靠外部上拉电阻把引脚拉上去。于是总线上的规则变成了通信领域常说的“线与”关系:只要有一个设备拉低,总线就是低;所有设备都释放,总线才能被上拉电阻拉回高。这个机制的巧妙之处在于,主机和从机可以在任意时刻动态切换角色,不需要像SPI那样用片选信号去切总线所有权,天然就适合一主多从甚至多主场景。
很多MCU的GPIO外设里除了开漏和推挽之外,还有“复用开漏”的说法。所谓复用开漏,就是把引脚的输出控制权交给外设(比如I2C外设),但输出级仍然是开漏结构。在配置I2C引脚时,如果误选了推挽模式,通常短期内也能跑,因为芯片用推挽拉高时总线上确实也是高电平,但多设备并联之后就会变成“一个设备硬拉高、另一个设备硬拉低”的互相伤害模式。所以硬件上,I2C引脚老老实实配置为开漏,再配合适的上拉电阻,才是正确的姿势。
1.2 推挽和开漏的结构差异,远不止一个MOS管
推挽输出内部有上管和下管,上管负责把输出拉到VCC,下管负责拉到GND,输出阻抗低,边沿非常陡,适合高速信号,但多设备并联时容易打架。开漏输出只有下拉管,高电平完全依赖外部上拉电阻,边沿相对较缓,优点是所有设备可以安全地并联,因为没有任何设备能主动推高总线。
用这样一张表可以看得很清楚:
| 输出结构 | 上管 | 下管 | 高电平来源 | 多设备并联 | 上升沿速度 |
|---|---|---|---|---|---|
| 推挽 | 有 | 有 | 内部PMOS直接驱动 | 禁止 | 很快 |
| 开漏 | 无 | 有 | 外部上拉电阻 | 可以 | 受RC限制 |
| 开集 | 无 | 有(三极管) | 外部上拉电阻 | 可以 | 受RC限制 |
I2C选择开漏还有一个额外的好处:电平转换变得特别简单。总线上挂的器件可以是1.8V、2.5V、3.3V甚至5V,只要把上拉电阻接到对应的电源,总线高电平就自然和那个电源一致。所以一个3.3V主控可以很轻松地和一个5V传感器挂在同一根总线上,只要两者的VIH和VIL满足要求就行。如果不用开漏,想做到这种跨电压域的通信,还得额外加电平转换芯片。
2. 上拉电阻和速率这笔账,得从RC里算出来
2.1 总线电容决定了你能跑多快
开漏输出导致信号上升沿没法由芯片主动驱动,只能靠电阻给总线电容充电。总线上挂的每个器件都会贡献引脚电容,PCB走线也有寄生电容,加起来就是等效负载电容Cb。上拉电阻Rp和Cb一起决定上升时间tr,这是I2C信号完整性的核心。
I2C规范对每个模式的最大上升时间有明确限制:
| 模式 | 最高SCL频率 | 最小低电平时间 | 最大上升时间 |
|---|---|---|---|
| 标准模式 | 100 kHz | 4.7 μs | 1000 ns |
| 快速模式 | 400 kHz | 1.3 μs | 300 ns |
| 快速增强模式 | 1 MHz | 0.5 μs | 120 ns |
| 高速模式 | 3.4 MHz | 0.16 μs | 120/160 ns |
工程上可以用一阶RC近似估算上升时间:tr ≈ 0.85 × Rp × Cb。假设总线电容估算为100pF,想跑400kHz,tr必须小于300ns,那么Rp大约需要小于3.5kΩ。所以很多开发板上400kHz模式用2.2kΩ甚至1kΩ上拉,不是拍脑袋定的,是按电容和时序算出来的。
热搜词里出现的“100k i2c信号规格”其实是个误区。I2C的上拉电阻不可能用100k,除非总线频率低得离谱或者只挂一两个器件。100k和100pF算下来上升时间大约8.5μs,已经超过100kHz标准模式的上限,波形会变成慢吞吞的圆弧,靠近器件阈值的地方抖动明显,很容易出现随机错误。如果在原理图上看到100k上拉,多半是把“100kHz频率”和“100kΩ电阻”搞混了。
2.2 上拉电阻也不能无限调小
上拉电阻取小了,上升沿会变快,但低电平时的灌电流会变大。总线被某个设备拉低时,高电平电源通过上拉电阻直接灌进器件的下拉MOS管,阻值越小,电流越大。很多I2C器件手册会规定引脚最大灌电流或者最大VOL对应电流。以3.3V系统为例,1kΩ上拉在低电平时流约3.3mA,一般能接受;4.7kΩ流约0.7mA,功耗小但上升慢。如果用到200Ω,低电平电流高达16.5mA,很多器件的VOL会被抬高,甚至超出VIL规格,导致逻辑错误。
我实际调过一块板子,总线上挂了三个传感器,只用一个4.7k上拉,100kHz下没问题,一上400kHz就随机丢数据。拿示波器一看,上升沿已经快700ns了,超过300ns要求。换成1k之后,所有通信恢复正常。这个案例也说明:不要迷信开发板原理图上的某个固定值,要根据总线电容和模式去调整。
2.3 借助开漏实现双向电平转换
很多I2C电平转换模块内部就是两个N沟道MOS管交叉接成的“双向模拟开关”。低电压侧和高电压侧各有一个上拉电阻,MOS管跨在两侧之间,任一方向拉低都能让另一侧跟着变低,且一侧释放时另一侧的上拉电阻会把总线拉回高电平。这种电路不需要方向控制信号,天然支持双向。如果没有分立MOS管,也可以直接用PCA9306、TCA9406这类专用芯片。简单场景下,靠开漏的线与特性,把1.8V器件和3.3V器件挂在同一个总线上也可能工作,但前提是高电平超过器件的VIH阈值并且噪声裕量足够。最稳的方案还是加电平转换器件,尤其信号速率超过400kHz时。
3. 时序、数据帧与逻辑分析仪
3.1 起始、停止、ACK与NACK:先看懂骨架
I2C的时序核心可以浓缩成两句话:SCL高电平时,SDA从高到低变化是起始(START),从低到高变化是停止(STOP);正常传数据时,SDA电平只能在SCL低电平时改变,SCL高电平时必须保持稳定。起始条件之后,主机发送第一个字节,高7位是从机地址,最低位是读写标志:0表示写,1表示读。
每个字节传输完成后,接收方需要在第9个时钟周期把SDA拉低,表示“收到了”,这就是ACK。如果接收方不应答,SDA会保持高电平,主机收到NACK。注意:NACK不一定是错误。主机做读操作时,读到最后一个字节后会主动不回ACK,直接发停止,这是告诉从机“不要再发了”的标准做法,逻辑分析仪上看到最后一个字节后的NACK是正常的。
从机地址可以扩展到10位,主机通过保留的起始字节来识别,但从机地址冲突仍然存在。I2C不像SPI有片选信号,完全靠地址区分设备。如果总线上有两个同地址器件,要么改器件的地址引脚,要么用TCA9548A这样的多路复用器做通道隔离。
3.2 时序图里那些参数到底是什么
数据手册里经常出现的tHD;STA、tSU;DAT、tSU;STO等符号看起来吓人,意思其实很直接。tHD;STA是起始条件之后SCL低电平必须保持的最小时间,从机需要这段时间来完成内部状态切换。tSU;DAT是数据在SCL高电平之前必须提前稳定的时间,确保主机或从机采样时数据已经稳定。tSU;STO是停止条件前SDA从低到高和SCL高电平之间的建立时间。这些参数是协议可靠性的底线,只要满足,通信基本不会出问题。
软件模拟I2C最大的坑是GPIO翻转速度太快或太慢。太慢会拖垮时钟周期,太快可能破坏建立时间。硬件I2C外设会自动插入间隔,所以多数人用硬件I2C不会去关心这些细节。但如果用GPIO模拟,必须在每个时钟周期里严格检查SCL电平,尤其遇到支持时钟拉伸的从机时,主机必须“见缝插针”:释放SCL后先等待从机释放SCL,再继续翻转。很多网上随手抄的“模拟I2C”代码没有这一步,接了GT911之类的触摸屏就会失败。
3.3 用逻辑分析仪抓波形:一上来就改代码是大忌
排查I2C问题时,我通常不会立刻改代码,而是先抓一帧波形。逻辑分析仪的采样率至少设为SCL频率的10倍以上,建议24MHz以上。把SDA接到通道0,SCL接到通道1,触发方式选I2C的START或者直接用下降沿触发,然后抓一段完整通信。解码后的波形里能看到地址字节、数据、ACK/NACK,非常直观。
如果遇到GT911触摸屏通信失败,先抓波形看几个细节:是否有START?地址字节是否和器件实际地址一致?从机是否回了ACK?还是SDA在SCL高电平时不稳定?很多时候你会发现主机把7位地址写成了0xBA这种8位形式,或者器件在复位拉低期间就被访问,自然无应答。正确的处理方式是参照数据手册控制GT911的INT和RESET引脚时序,上电稳定后延时几十毫秒,再用i2cdetect扫描实际地址,而不是反复试改地址。
3.4 “自由数据模式”等特殊用法
有些传感器支持自由数据模式(Free Data Mode),本质上是把I2C当作纯位流通道,不受标准地址帧限制。这种模式只在特定芯片里出现。遇到时不要觉得协议错了,底层仍然是开漏和时序约束。我更建议能用硬件I2C就用硬件I2C,自由数据模式虽然灵活,但软件实时性差,中断一多,SCL就会出现毛刺,位流就废了。
4. 多主仲裁:两根线为什么不会互相打架
4.1 线与机制下的逐位仲裁过程
多主仲裁是I2C最容易被忽略但最惊艳的机制,基础仍然是开漏。假设主机A和主机B同时在总线上发起START并开始发送地址。A想发0x41(二进制01000001),B想发0x42(二进制01000010)。从START之后第一位开始,两个主机各自在SCL高电平期间控制SDA。由于线与效应,如果一个主机释放总线(想发1),而另一个主机拉低(想发0),总线实际就是低电平。想发1但读到0的主机立刻知道自己仲裁失败,停止驱动SDA,然后进入等待状态。获胜的主机继续完成传输,整个过程没有额外仲裁帧,也没有数据流量浪费。
只要两个主机的地址或者数据存在任何一个bit的差异,仲裁就必然在那个bit上分出胜负。但如果两个主机想发送的内容完全相同,仲裁会一直持续到最后一个bit,最后双方都认为自己成功了。这种情况在硬件层面无解,必须在系统设计时避免:要么给不同主机分配不同的地址,要么在应用层约定优先级。多主总线的地址规划不是随便排的,要专门留出仲裁空间。
4.2 时钟同步:多个SCL是如何“拧成一股绳”的
多主机场景下,每个主机都会产生自己的SCL,但总线上的SCL永远是和逻辑。总线的低电平时间等于所有主机中低电平时间最长的那个,高电平时间则要等所有主机都释放后才开始,因此总线的实际时钟周期会被“最慢”的主机拉长。这叫作多主时钟同步。好处是慢速主机的时序不会被快速主机冲掉,坏处是两个不同速率的MCU直接裸接做多主时,总线实际SCL频率会低于其中任意一个。
要注意区分多主时钟同步和从机时钟拉伸。时钟同步发生在多个主机之间,而时钟拉伸是单个从机在收到主机时钟后,主动拉低SCL来请求主机暂停,比如从机还需要时间准备数据。对于不支持时钟拉伸的I2C控制器,从机一拉低SCL,主机就会误判为总线故障。所以选型时要确认控制器是否支持时钟拉伸,不支持的话就把I2C频率降下来,或者换一颗支持拉伸的MCU。
4.3 仲裁失败后的恢复与数据一致性
仲裁失败的主机不能立刻重发,否则两个主机可能又撞在同一时刻。更合理的做法是等待当前传输结束,也就是总线回到空闲状态,再结合随机的短退避重试。这种思路和CSMA/CD很相似,只不过I2C在物理层就已经把冲突解决了,协议层不需要再检测冲突。
真实产品里多主用得并不多,主要因为复杂度远高于收益。两个主机同时访问同一个从机时的互斥、缓存数据一致性、仲裁失败后的重试策略,都要做大量验证。很多情况下,所谓的“从机主动更新主机寄存器”这个需求,正确的做法也不是让从机主动发起I2C传输,因为大多数从机根本没有总线主控能力。通常是在从机上拉一个中断脚,主机检测到中断后再主动去读取。热搜里那个“i2c从机主动更新主机寄存器”的问题,十有八九可以用这个方案解决。
5. 实操落地:EEPROM、Linux与I2C扩展
5.1 EEPROM读写流程:以AT24C02为例
AT24C02是2Kbit也就是256字节的EEPROM,地址引脚A0/A1/A2决定从机地址,全部接地时7位地址是0x50。写单字节的完整流程是:起始条件、发送0xA0(0x50地址加写位)、等待ACK、发送内部字节地址、等待ACK、发送数据、等待ACK、停止条件。
这里最容易踩的坑是EEPROM收到STOP后进入内部写周期,一般要几毫秒,期间不响应任何命令。正确的等待方式是ACK轮询:重复发送地址加写位,如果器件回ACK说明写完了,如果NACK就继续等。如果不等直接读,大概率读到旧数据或者无应答。页写时,AT24C02每页8字节,超过页边界地址会回卷到页开头,把超出的数据覆盖到错误位置。写数据前要判断是否跨页,跨页就拆成多条写事务。Verilog实现时,状态机通常要分IDLE、START、SEND_ADDR、WAIT_ACK、SEND_DATA、STOP、POLL_WRITE等状态。状态转移只允许在SCL低电平期间发生,避免SDA在SCL高电平时变化。下面是一个常见状态定义:
localparam IDLE = 3'b000, START = 3'b001, SEND_ADDR = 3'b010, WAIT_ACK = 3'b011, SEND_DATA = 3'b100, ACK_DATA = 3'b101, STOP = 3'b110;实际模块里还要做分频时钟,比如系统时钟50MHz,I2C时钟400kHz,分频值为125。每个bit用一个计数窗口,sda输出在scl低电平期间切换,数据采样在scl高电平期间完成。调试时最好加一个发送完成中断,不要在中断里死等ACK,否则一个慢速从机就能拖垮整条总线。
5.2 Linux下通过I2C设备节点调试
Linux里I2C控制器通常对应 /dev/i2c-N。用 i2cdetect -l 可以列出总线,用 i2cdetect -y 1 可以扫描1号总线上的从机地址。用户态读写可以用 ioctl 的 I2C_RDWR 接口,构造 i2c_msg 数组:
struct i2c_msg msgs[2]; uint8_t reg = 0x00; uint8_t buf[1] = {reg}; msgs[0].addr = 0x50; msgs[0].flags = 0; // 写 msgs[0].len = 1; msgs[0].buf = buf; msgs[1].addr = 0x50; msgs[1].flags = I2C_M_RD; // 读 msgs[1].len = 1; msgs[1].buf = buf; struct i2c_rdwr_ioctl_data rdwr = {0}; rdwr.msgs = msgs; rdwr.nmsgs = 2; ioctl(fd, I2C_RDWR, &rdwr);注意读寄存器之前先发送寄存器地址作为第一个msg,再紧跟一个读msg,Linux i2c-dev驱动默认产生重复起始条件。大多数器件支持,但个别老器件不支持重复起始,需要拆成两个独立事务,中间加停止再起始。如果遇到网卡PHY想要用I2C配置,但MAC没有MDIO接口,或者MDIO不可用,这种场景其实不是标准的PHY驱动能直接处理的。常见做法是写一个自定义phy_driver,在read/write回调里调用i2c_transfer,而不是走标准MDIO。设备树里也要把PHY节点的compatible和reg配置成与I2C地址匹配,同时把mdio相关属性去掉。
5.3 多路复用器:把一条总线扩展成八条
当总线上设备太多或者地址冲突严重时,最简单的扩展是加TCA9548A这种多路复用器。它通过I2C从机地址0x70接收控制字节,每个bit对应一个通道,比如写入0x01就只选中通道0,写入0x02就只选中通道1。通道选中后,后面访问的设备必须是这个通道后面的设备。好处是每一路的电容负载被隔离,还能把同地址的设备分到不同通道。
使用多路复用器有几个容易忽略的点:切换通道后要留出稳定时间,不能立刻访问后面的设备;不能同时选通两个电平不一致的通道,否则总线电平会被拖乱;扫描地址时要把复用器本身和它后面的设备分开,否则会误判。I2C转GPIO的PCF8574、I2C转UART、I2C RTC这些器件,底层协议都是一样的,真正掌握了开漏物理层和时序,这些扩展芯片用起来都很快。
6. 一周踩坑实录:把这些毛病提前排掉
6.1 排查顺序:先电平,再波形,最后协议
I2C问题九成出在物理层。先把SCL和SDA的静态电平量一遍:用万用表测电压,正常空闲状态两根线都应该是上拉高电平。如果某根线是低,说明有设备拉死了总线,或者上拉电阻没焊、虚焊。然后接逻辑分析仪抓波形,看上升沿是否太缓,地址是否正确,ACK是否存在。最后才去怀疑协议栈和驱动。
| 现象 | 首先排查项 | 建议手段 |
|---|---|---|
| SCL或SDA始终为低 | 总线被拉死、上拉缺失、某芯片损坏 | 分段断开可疑器件,量电平 |
| 主机一直无ACK | 地址错误、电源时序不对、从机处于写周期 | i2cdetect扫描地址,检查上电顺序 |
| 数据随机错乱 | 速率超过规格、总线电容过大、时序参数不足 | 降速、减上拉电阻、抓波形对照参数 |
| 多主总线偶尔冲突 | 同地址设备、仲裁失败后立即重发 | 规划地址、增加随机退避 |
| GT911触摸通信失败 | INT/RESET时序、地址配置不正确 | 正确复位、扫描地址、抓START段波形 |
6.2 硬件层面的几个“低频玄机”
我踩过一个很典型的坑:I2C只接一个传感器,上拉电阻用了10k,SCL频率400kHz,结果通信100次会挂两三次。示波器一看,上升沿将近700ns,超了300ns的规格。换成1k上拉之后,问题立刻消失。另一个项目里,某个传感器只要上电就把SDA拉低,导致整条总线瘫痪。后来查手册发现它上电默认状态下SDA方向是输出低。解决办法是给它单独供电,用MOS管控制电源,软件初始化完成后再上电,问题才解决。
硬件设计上还有一个细节:EEPROM的地址引脚和写保护引脚不能悬空。AT24C02的A0/A1/A2如果悬空,有的芯片内部有下拉,有的没有,扫描出来的地址会飘。写保护引脚最好按需求接高或接低,不要让它悬空。I2C总线的上拉电阻位置也有讲究:只允许在总线的最远端或者主控端放一组上拉,不能每颗器件都放一组,否则并联电阻太小,上升沿会很短但灌电流过大。
6.3 软件层面的常见问题
GPIO误配成推挽是最常见的软件错误。虽然有时能工作,但推挽输出会让多个器件互相灌电流,长时间工作可能损坏引脚。用GPIO模拟I2C时,一定要显式开启开漏模式。很多MCU的硬件I2C外设会自动选择开漏,但如果你自己用GPIO截图改位,就得很仔细了。
在中断里直接做I2C读写也不建议,尤其是软件模拟I2C时。高优先级中断一打断,SCL周期就被拉长,时序可能超限。如果只有一个线程/任务可以用I2C,就加一个互斥锁和队列,不要在多个上下文里同时抢总线。读操作发完寄存器地址后忘记发重复起始条件也是非常典型的错误,这时候从机不切到读模式,主机收到的一直是写阶段的数据。读最后一个字节时忘记发NACK也会出问题,从机会继续发送多余字节,逻辑分析仪上会看到怪异的连续ACK。
Windows设备管理器报“HID over I2C设备找不到足够资源(代码12)”的场景,我在笔记本触摸板上也遇到过。这多数不是I2C协议本身的错误,而是ACPI资源里IRQ或者GPIO中断被占用,或者驱动版本与触摸屏固件不匹配。先在BIOS里确认I2C控制器和HID设备启用,再清理冲突驱动,如果还不行就更新触摸屏固件。底层通信其实仍然是标准I2C,但系统层面的资源分配会直接影响设备枚举。
6.4 一周学习路线建议
如果你想在一周内把I2C彻底吃透,我建议按这个节奏来:第一天空闲时间专门看开漏、上拉、推挽对比,把物理层搞明白;第二天把时序图画到能默写,掌握START、STOP、ACK、数据位这些要素;第三天用GPIO模拟写一个最简单的读函数,不用硬件I2C模块,这一步能强迫你理解时序;第四天实现AT24C02读写,验证页写和ACK轮询;第五天研究多主仲裁,有条件的用两块开发板,故意让两个主机同时发,看逻辑分析仪上的仲裁失败波形;第六天接触Linux I2C子系统和i2c-tools,把设备树配置弄清楚;第七天总结这一周的踩坑,整理成自己的排查清单。
我个人在实际操作中的体会是,I2C的大多数“玄学”都源于开漏物理层。只要波形足够干净,时序参数在规格内,ACK、NACK、仲裁都是水到渠成的事情。真遇到说不清的问题,先插上逻辑分析仪抓一帧波形再开口,比在论坛里猜原因要快得多。这两根线值得花一周时间,也值得刨根问底,因为懂了一层之后,后面所有带I2C外设的项目都会变得轻松起来。