1. 从一次"设备死活不上线"说起:I2C调试到底难在哪
搞嵌入式的人,几乎都经历过这样的场景:硬件同事说板子焊好了,驱动同事说代码写完了,结果上电一跑,i2cdetect扫出来一片空白,或者地址是出来了,但读回来的数据全是0xFF。然后两边开始互相甩锅——硬件说软件时序不对,软件说硬件上拉没焊。这种扯皮我见得太多了,而 I2C 恰恰是最容易引发这类扯皮的 bus 之一。
为什么是 I2C?因为它是一个多主多从、开漏输出、靠上拉电阻拉高、没有硬件流控、没有标准错误反馈机制的总线。SPI 你接错了顶多读出来是乱的,UART 你波特率不对顶多是乱码,但 I2C 一旦出问题,表现往往是"完全没反应"或者"卡死在等待 ACK",你连它到底走到哪一步了都不知道。更麻烦的是,I2C 的电气特性决定了它对上拉阻值、总线电容、走线长度、电源时序都极其敏感,而这些因素在原理图上往往看不出来,只有上电用示波器才能暴露。
这篇内容我想聊的不是"I2C 协议是什么"——那个网上讲烂了。我想聊的是当你面对一个不工作的 I2C 设备时,脑子里应该有一套什么样的排查顺序。这套思路我在 Linux 平台(设备树 + i2c-tools + 内核驱动)和裸机 MCU 平台(STM32、CH32 这类)上都反复用过,核心逻辑是通用的:先确认物理层,再确认协议层,最后才怀疑驱动和代码。顺序错了,你会在软件里绕一整天,结果发现是上拉电阻焊成了 10k 而总线电容又大。
关键词里出现了i2c-tools、设备树、i2c读写eeprom、ssd1306 i2c控制命令、i2c时序这些,说明关注这个主题的人覆盖了从裸机到 Linux 驱动的完整谱系。所以我会把两条线的调试方法都讲清楚,并且重点放在**"怎么一步步缩小问题范围"**这个方法论上,而不是罗列寄存器。
2. 物理层先过关:上拉、电平和总线电容这三件事
2.1 上拉电阻不是"随便焊一个4.7k"
I2C 的 SDA 和 SCL 都是开漏结构,也就是说器件只能把线拉低,拉高完全靠上拉电阻。这就带来一个经典的两难:
- 上拉电阻太大(比如 10k、20k),上升沿会变缓。因为总线有电容(走线电容 + 器件引脚电容 + 上拉本身的寄生),RC 时间常数变大,波形上升沿变成"圆角"。在 100kHz 标准模式下可能还能凑合,一旦上到 400kHz 快速模式,上升时间超过规格(标准模式 1000ns,快速模式 300ns),从机就可能采样错误。
- 上拉电阻太小(比如 1k),上升是快了,但器件拉低时的灌电流变大。I2C 规定单根线灌电流一般不超过 3mA,3.3V 系统下 1k 上拉就是 3.3mA,已经到边缘了,多几个器件并联就更危险,长期可能损伤 IO。
所以 4.7k 之所以成为"默认值",是因为它在 3.3V、100kHz、总线电容不大的场景下刚好平衡。但它不是万能值。我一般的经验判断是:
| 场景 | 建议上拉 | 理由 |
|---|---|---|
| 3.3V / 100kHz / 短线(<10cm) | 4.7k | 通用默认,稳妥 |
| 3.3V / 400kHz / 短线 | 2.2k ~ 3.3k | 加快上升沿 |
| 3.3V / 长线或多器件(电容大) | 1.5k ~ 2.2k | 对抗大电容,但要算灌电流 |
| 5V 系统 | 4.7k ~ 10k | 5V 下同样阻值灌电流更大,可适当加大 |
| 混合电压(3.3V 主 + 5V 从) | 需电平转换 | 不能直接上拉,会倒灌 |
提示:如果你不确定总线电容,最土但最有效的办法是拿示波器看上升沿。把探头挂在 SCL 上,触发一个下降沿,看它从 0.3VDD 升到 0.7VDD 用了多久。超过规格就减小上拉,波形"方"了但灌电流没超标就对了。
2.2 用万用表做的三个"上电前检查"
在写任何代码之前,我一定会做这三件事,能省掉后面 80% 的扯皮:
- 断电测通断:SDA、SCL 到主控引脚是否导通,有没有虚焊。特别是 QFN、BGA 封装的器件,肉眼根本看不出来。
- 上电测静态电平:不通信时,SDA 和 SCL 都应该是高电平(被上拉拉高)。如果测出来是低电平,说明要么有器件把线拉死了(器件损坏或地址冲突),要么上拉没焊,要么主控引脚配置成了推挽输出低。
- 测上拉是否真的存在:断电,万用表电阻档测 SDA 对 VCC 的阻值,应该接近上拉阻值。如果测出来是无穷大,恭喜你,上拉漏焊了——这是新手最常见的坑。
我见过一个案例:板子上 SDA 和 SCL 的上拉电阻位置画反了,焊的时候也跟着反了,结果 SCL 上拉到了 SDA 的网络,两条线互相干扰,i2cdetect扫出来一堆莫名其妙的地址。这种问题你盯着代码看一辈子也看不出来。
2.3 总线电容:被忽视的"隐形杀手"
I2C 规范规定总线电容上限是400pF。这个数字听起来很大,但实际很容易超:
- 每米普通排线大约 50~100pF
- 每个器件的 IO 引脚约 5~10pF
- PCB 走线每厘米约 1~2pF
如果你挂了 8 个器件,光引脚电容就 40~80pF,再加 30cm 排线 15~30pF,再加上走线,很容易逼近 200pF。这时候如果还用 4.7k 上拉,上升沿就会明显变缓。解决办法要么减小上拉,要么用I2C 缓冲器/中继器(比如 PCA9515 这类)把总线分段,每段电容独立计算。
3. 协议层排查:i2c-tools 是 Linux 下最好用的"听诊器"
3.1 i2cdetect 扫不到设备,先别急着改驱动
在 Linux 平台上,i2c-tools是我排查 I2C 的第一工具。基本流程:
# 列出所有 I2C 总线 i2cdetect -l # 扫描某条总线上的设备(-y 跳过交互确认,-r 用读方式探测) i2cdetect -y -r 1i2cdetect -l会列出系统里注册的所有 I2C adapter,比如i2c-0、i2c-1。这里有个关键点:你要先确认你的设备挂在哪条总线上。在设备树平台(比如瑞芯微 RK3568、各种 ARM SoC)上,总线编号和硬件控制器是对应的,但编号顺序不一定和原理图上的 I2C1、I2C2 一致。我一般会去/sys/bus/i2c/devices/下面看,或者直接dmesg | grep i2c看内核启动时注册了哪些控制器。
如果i2cdetect扫出来全是--,说明总线上没有任何设备响应。这时候按这个顺序排查:
- 确认总线选对了:换几条总线都扫一遍,别死磕一条。
- 确认设备供电了:很多传感器有独立的 VDD,主控上电不代表传感器上电。用万用表量一下传感器的 VCC 引脚。
- 确认设备没有处于复位状态:有些器件有 RESET 引脚,低电平复位,如果这个脚悬空或者被拉低,器件根本不工作。
- 确认地址:有些器件的 I2C 地址由地址引脚决定(比如 AT24C02 的 A0/A1/A2),地址引脚接错,扫描的地址范围就不对。
i2cdetect默认扫 0x03~0x77,如果你的设备地址在这个范围外(比如某些器件的 0x78),需要手动指定范围。
3.2 扫到了地址但读写失败:区分"假地址"和"真设备"
有时候i2cdetect会扫出一个地址,但那个地址根本不是你的设备。这种情况通常是:
- 地址冲突:两个器件地址一样,或者某个器件的地址引脚配置成了和另一个器件相同的值。
- 总线被拉低产生的假象:如果 SDA 被某个器件持续拉低,
i2cdetect可能误判。 - 上拉问题导致的误读:上升沿太缓,采样点落在不确定区域。
验证方法很简单:用i2cget读一个已知寄存器,看返回值是否合理。
# 读地址 0x50 的寄存器 0x00(以 AT24C02 EEPROM 为例) i2cget -y 1 0x50 0x00 # 写一个字节再读回来验证 i2cset -y 1 0x50 0x00 0xAB i2cget -y 1 0x50 0x00如果写进去读出来不一样,或者读出来永远是0xFF(总线空闲时的高电平)或0x00,那基本可以确定通信没真正建立。EEPROM 是特别好的测试对象,因为它读写逻辑简单、地址固定、不怕写坏(只要别写太频繁),非常适合用来验证一条 I2C 总线是否健康。
3.3 用 i2c-tools 的-f和-a参数处理特殊情况
有些场景下i2cdetect会报 "Resource temporarily unavailable",这是因为内核里已经有驱动占用了这个地址。这时候可以用-f强制扫描,或者用-a指定地址范围。但要注意,强制扫描可能会和正在工作的驱动冲突,生产环境慎用。
另外,i2ctransfer是比i2cget/i2cset更灵活的工具,可以一次传输多个字节,适合调试那些需要"先写寄存器地址再读数据"的器件:
# 先写寄存器地址 0x00,再读 4 个字节 i2ctransfer -y 1 w1@0x50 0x00 r4这个命令的语义是:向地址 0x50 写 1 个字节(0x00),然后读 4 个字节。对于很多传感器(比如带寄存器的加速度计、温湿度传感器),这种"写地址+读数据"的组合是标准操作,i2ctransfer一条命令就能搞定,比i2cget方便得多。
4. 设备树里的 I2C:那些让你设备"注册不上"的细节
4.1 设备树节点写对了,驱动才可能 probe
在 Linux 嵌入式平台(RK3568、i.MX、全志、瑞芯微这些),I2C 设备是通过设备树描述的。一个典型的 I2C 设备节点长这样:
&i2c1 { status = "okay"; clock-frequency = <100000>; eeprom@50 { compatible = "atmel,24c02"; reg = <0x50>; pagesize = <8>; }; };这里有几个极其容易出错的点:
status必须是"okay":很多 SoC 的 I2C 控制器在默认 dtsi 里是"disabled",你不在板级 dts 里改成"okay",总线根本不会注册,i2cdetect -l里都看不到这条总线。reg是 7 位地址:写的是0x50,不是0xA0。有些老驱动或者文档里用 8 位地址(左移一位),设备树里必须用 7 位。这个坑我踩过,写成了0xA0,驱动死活 probe 不上。compatible要和驱动匹配:这个字符串必须和驱动里的of_device_id表完全一致,大小写、逗号位置都不能错。写错了内核不会报"找不到驱动",而是静默地不 probe,非常隐蔽。clock-frequency:不写的话用控制器默认值,但有些控制器默认是 100kHz,你的器件支持 400kHz 却跑在 100kHz,性能上不去。反过来,如果器件只支持 100kHz 而你写了 400kHz,通信会不稳定。
4.2 用/sys和dmesg确认设备树是否生效
设备树改完、重新编译 dtb、重启之后,怎么确认生效了?
# 看这条总线上注册了哪些设备 ls /sys/bus/i2c/devices/ # 看某个设备的详细信息 cat /sys/bus/i2c/devices/1-0050/name # 看内核日志里 I2C 相关的信息 dmesg | grep -i i2c dmesg | grep -i "1-0050"如果/sys/bus/i2c/devices/下面有1-0050这样的目录,说明设备树节点被正确解析了(1是总线号,0050是地址)。如果没有,说明设备树没生效,或者节点写错了位置。
我遇到过一个很典型的问题:设备树节点写在了&i2c1下面,但硬件实际接的是i2c3。结果就是设备树里有个"幽灵设备",实际总线上什么都没有。这种问题只能靠对照原理图确认总线编号来避免。原理图上标的 I2C1,不一定对应软件里的i2c1,中间可能经过 pinmux 重映射。
4.3 pinmux 冲突:I2C 引脚被别的功能占了
这是设备树调试里最隐蔽的坑之一。SoC 的引脚往往是复用的,一个物理引脚可以当 GPIO、UART、I2C、PWM 用。如果你的设备树里,某个引脚被配置成了 GPIO,而它实际要当 I2C 的 SCL 用,那 I2C 控制器虽然注册了,但引脚没接到控制器上,波形根本出不来。
排查方法:查 SoC 的 pinmux 配置,确认 I2C 对应的引脚被正确设置为 I2C 功能。在设备树里通常体现为pinctrl节点:
&i2c1 { pinctrl-names = "default"; pinctrl-0 = <&i2c1_xfer>; status = "okay"; };如果pinctrl-0引用的节点不对,或者这个节点被别的外设也引用了(资源冲突),I2C 就可能工作不正常。这种问题在dmesg里有时会有 "pin already requested" 之类的报错,有时则完全静默,只能靠仔细核对。
5. 裸机 MCU 侧:时序、ACK 和那些"卡死"的瞬间
5.1 硬件 I2C 还是软件模拟 I2C
在 STM32、CH32 这类 MCU 上,第一个决策就是:用硬件 I2C 外设,还是 GPIO 模拟?
- 硬件 I2C:省 CPU、时序精确、支持中断和 DMA。但很多 MCU 的硬件 I2C 有历史遗留 bug(尤其是早期 STM32 的 I2C 外设,卡死、总线锁死是出了名的),而且调试起来不透明,出问题只能看寄存器。
- 软件模拟 I2C:用两个 GPIO 手动翻转,时序完全可控,出问题可以用示波器逐位看。缺点是占 CPU,高速下(400kHz 以上)可能跟不上。
我的经验是:调试阶段优先用软件模拟,因为可控性高,能快速定位是时序问题还是器件问题。等通信稳定了,如果对性能有要求,再换成硬件 I2C。很多量产项目最后用的其实是软件模拟,因为稳定、可移植、不挑芯片。
5.2 软件模拟 I2C 的四个关键时序点
软件模拟 I2C 看起来简单,但有几个时序细节必须严格遵守,否则就会出现"偶尔能通、偶尔不通"的玄学问题:
- 起始条件:SCL 高时,SDA 从高变低。这个顺序不能反,反了就是从机眼里的停止条件。
- 数据建立时间:SDA 变化必须在 SCL 低电平期间完成,SCL 拉高后 SDA 要保持稳定。很多新手在 SCL 高的时候改 SDA,导致从机采样到错误数据。
- ACK 采样:主机发完 8 位数据后释放 SDA,在第 9 个时钟周期采样 SDA 是否为低。如果读到高,说明从机没应答(NACK),要么地址错,要么器件没准备好。
- 停止条件:SCL 高时,SDA 从低变高。
一个常见的坑是延时不够。软件模拟时,SCL 高低电平的持续时间要满足器件要求。100kHz 下,一个周期 10us,高低各 5us。如果你用delay_us(1),实际可能因为函数调用开销变成 2~3us,导致频率偏高。稳妥的做法是用示波器实测 SCL 频率,调整延时。
5.3 总线锁死:SDA 被从机拉低怎么办
I2C 最恶心的问题之一是总线锁死:某个从机在传输过程中复位或异常,把 SDA 持续拉低,导致主机无法产生起始条件,整条总线瘫痪。
标准恢复方法是手动发送 9 个时钟脉冲:把 SCL 配置成 GPIO,手动翻转 9 次,让从机把剩余的位发完,然后发一个停止条件。代码逻辑大致是:
// 伪代码:I2C 总线恢复 void i2c_bus_recover(void) { // 1. 把 SCL 和 SDA 都配置为 GPIO 开漏输出 // 2. 确保 SDA 释放(输出高) // 3. 翻转 SCL 9 次 for (int i = 0; i < 9; i++) { scl_low(); delay_us(5); scl_high(); delay_us(5); } // 4. 发送停止条件:SCL 高时 SDA 从低变高 sda_low(); delay_us(5); scl_high(); delay_us(5); sda_high(); delay_us(5); // 5. 重新初始化 I2C 外设 }这个恢复流程我在多个项目里都用过,成功率很高。但要注意,恢复之后要重新初始化 I2C 控制器,否则硬件状态机可能还停在错误状态。
6. 典型器件实战:EEPROM 和 OLED 的调试差异
6.1 AT24C02 EEPROM:验证总线健康的最佳试金石
EEPROM 是 I2C 调试的"Hello World",因为它的协议最简单:写地址、写数据、等 5ms(写周期)、读回来。用i2c-tools验证:
# 写 0xAB 到地址 0x00 i2cset -y 1 0x50 0x00 0xAB # 等待写周期完成(EEPROM 写入需要时间) sleep 0.01 # 读回来 i2cget -y 1 0x50 0x00 # 应该返回 0xab如果写进去读出来不对,常见原因:
- 写周期没等够:AT24C02 的写周期典型 5ms,最大可能到 10ms。如果你写完立刻读,器件还在内部写入,不会应答,读出来就是错的。稳妥做法是写完
sleep 10ms再读。 - 页边界问题:AT24C02 每页 8 字节,跨页写会回卷到页首。如果你一次写超过 8 字节且跨越页边界,数据会写错位置。这是 EEPROM 的经典坑。
- 地址引脚:A0/A1/A2 决定设备地址的低 3 位,接错地址就变了。
6.2 SSD1306 OLED:命令和数据要分清
0.9 寸、1.3 寸的 OLED 模块很多用 SSD1306 驱动,I2C 接口。它的坑和 EEPROM 完全不同:
- 控制字节:SSD1306 的 I2C 传输第一个字节是控制字节,
0x00表示后面是命令,0x40表示后面是数据。如果你把命令当数据发,屏幕不会有任何反应,但总线是通的(有 ACK),所以i2cdetect能扫到,i2cget也能读到东西,但屏幕就是不亮。这种"通信正常但功能不对"的问题,只能靠看数据手册解决。 - 初始化序列:SSD1306 上电后必须发送一长串初始化命令(设置时钟、复用率、对比度、扫描方向、电荷泵等),少一条都可能不显示。而且电荷泵命令(
0x8D, 0x14)特别关键,不发的话屏幕供电不足,全黑。 - 兼容性问题:关键词里提到"0.9寸oled对i2c兼容问题",这个确实存在。不同批次的 0.9 寸模块,有的用 SSD1306,有的用 SH1106。SH1106 的显存是 132x64,比 SSD1306 的 128x64 宽,如果按 SSD1306 的寻址方式写 SH1106,图像会偏移 2 列。判断方法:看模块背面丝印,或者试着发 SH1106 的初始化序列看是否正常。
调试 OLED 时,我一般先用i2ctransfer手动发几条命令,确认屏幕有反应(比如发显示开命令0xAF),再上完整驱动。这样能把"总线问题"和"初始化序列问题"分开。
7. 一套可复用的 I2C 排查决策树
把上面所有内容浓缩成一套排查顺序,遇到 I2C 不工作时按这个走:
| 步骤 | 检查项 | 工具 | 通过标准 |
|---|---|---|---|
| 1 | 供电是否正常 | 万用表 | 器件 VCC 达到规格 |
| 2 | 上拉是否存在 | 万用表(断电测阻) | SDA/SCL 对 VCC 有阻值 |
| 3 | 静态电平是否为高 | 万用表/示波器 | 空闲时 SDA/SCL 都是高 |
| 4 | 波形是否干净 | 示波器 | 上升沿满足规格,无毛刺 |
| 5 | 总线是否注册 | i2cdetect -l | 能看到对应 adapter |
| 6 | 设备是否响应 | i2cdetect -y -r N | 扫出预期地址 |
| 7 | 读写是否正常 | i2cget/i2cset/i2ctransfer | 写入能读回 |
| 8 | 设备树是否生效 | ls /sys/bus/i2c/devices/ | 有对应设备目录 |
| 9 | 驱动是否 probe | dmesg | 无报错,有 probe 成功日志 |
| 10 | 功能是否正常 | 实际应用 | 数据正确、功能符合预期 |
这个顺序的核心逻辑是从物理到协议再到软件,每一步都建立在前一步通过的基础上。很多人一上来就改驱动,结果绕了一大圈发现是上拉没焊。按这个顺序走,能最快定位问题层级。
8. 几个我踩过的坑和私藏技巧
坑一:示波器探头电容影响波形。用普通无源探头(约 10pF)测 I2C 时,探头本身的电容会加重总线负载,本来勉强能通的波形,一挂探头就不通了。这时候要么用低电容探头,要么把上拉临时减小再测。我遇到过测的时候正常、拔了探头就不正常的情况,就是探头电容在"帮忙"。
坑二:多个主控共享总线时的仲裁问题。如果一条 I2C 总线上有两个主控(比如 SoC 和 MCU 都能控制),一定要确认它们不会同时发起传输。I2C 有仲裁机制,但前提是双方都正确实现了。如果一方是软件模拟且没做仲裁检测,就可能出现两个主控同时拉低、数据错乱的情况。这种场景建议加一个硬件互斥信号,或者干脆分成两条总线。
坑三:热插拔导致的总线锁死。有些设备支持热插拔(比如某些传感器模块),拔插瞬间如果正好在传输中,从机可能把 SDA 拉死。所以支持热插拔的 I2C 设备,驱动里最好加上总线恢复逻辑,检测到连续 NACK 就触发 9 时钟恢复。
私藏技巧:用逻辑分析仪抓完整传输。示波器只能看波形,逻辑分析仪能直接解码出地址、数据、ACK/NACK,一眼就能看出是地址错了还是数据错了。几百块的入门逻辑分析仪配合开源软件,调试 I2C 的效率比示波器高一个数量级。我现在的习惯是:示波器看电气特性(上升沿、电平),逻辑分析仪看协议内容(地址、数据、ACK),两者配合,基本没有定位不了的问题。
私藏技巧:给 I2C 总线留测试点。画 PCB 时,在 SDA、SCL、GND 上各留一个测试点(哪怕是 1mm 的焊盘),调试时能直接夹探头或焊飞线。这个成本几乎为零,但能省下大量"找不到地方下探头"的时间。很多新手板子画得很紧凑,结果调试时连个下探头的地方都没有,只能飞线,飞线又引入干扰,恶性循环。
私藏技巧:软件模拟 I2C 加超时保护。软件模拟时,等待 ACK 的循环一定要加超时,否则从机没响应时程序会死循环卡死。我一般设一个计数器,超过一定次数就返回错误,并触发总线恢复。这个习惯能避免"程序跑飞"这类难查的问题。
I2C 这个总线,协议本身半小时就能学明白,但真正把它调稳,靠的是对电气特性、时序细节和排查方法的理解。上面这些内容,每一条背后都是实际项目里踩出来的。下次再遇到 I2C 设备不上线,别急着改代码,先按第 7 节那张表走一遍,大概率能少走很多弯路。