ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

I2C总线Busy状态卡死排查与恢复实战:从原理到预防方案

2026/8/29 13:52:52 拓冰建站 浏览量
I2C总线Busy状态卡死排查与恢复实战:从原理到预防方案 搞嵌入式这几年I2C 应该是我打交道最多、也最容易“上头”的总线协议之一。前两天刚解决了一个很有代表性的问题I2C 接口进入 Busy 状态后无法退出i2cdetect扫不到任何设备系统里依赖这条总线的触摸屏、EEPROM 全部失联。这个问题表面上看只是“总线卡死”真正排查起来却涉及主控状态机、从设备拉死、上下拉电阻、驱动超时处理等多个层面一路踩坑下来收获不少。这篇把从现象到原理、从定位到恢复、再到长期预防的完整过程写出来供遇到类似问题的朋友参考也当给自己留一份复盘笔记。我这次遇到问题的平台是一片基于 ARM 主控的 Linux 板卡I2C2 总线上挂了电容触摸屏和一颗 EEPROM。系统跑压力测试跑到第三天突然在日志里看到大量i2c transfer timeout之后整条总线像“死”了一样任何读写都直接超时。用i2cdetect -y 2扫描返回的是空列表连本来固定地址 0x50 的 EEPROM 都认不出来了。当时第一反应是“从设备挂了”但反复重启应用、重载驱动都没用直到手动复位了整个 I2C 控制器才恢复。这类问题在 Linux 嵌入式开发里并不少见尤其是总线长时间工作、从设备异常复位、电源波动之后。难的不是恢复而是搞清楚它为什么卡死、以及如何在产品里自动规避。下面按我的排查顺序逐步讲。1. 先搞明白I2C 总线的 Busy 状态到底是怎么出现的1.1 聊聊 I2C 总线的正常时序和状态判定I2C 总线虽然只有 SCL 和 SDA 两根线但状态判定逻辑比很多人想象的复杂。总线空闲时SCL 和 SDA 都被外部上拉电阻拉高主控控制器的状态寄存器里会标记当前“总线空闲”。一旦主机发出起始信号SDA 从高拉低此时 SCL 为高总线就进入 Busy 状态。正常通信结束后主机发出停止信号SDA 从低拉高SCL 为高总线才重新回到空闲状态。关键点在于控制器判断总线是否 Busy并不是靠“逻辑上通信是否结束”而是靠持续采样 SDA/SCL 的电平状态。我用的这颗 SoC 的 I2C 状态寄存器里有一个Busy位硬件根据当前总线状态实时更新。只要在某一个采样窗口里看到总线不满足空闲条件它就会把Busy位置起来。所以你会遇到一种很诡异的场景明明应用层已经不发起任何传输了但寄存器里Busy位始终是 1。原因可能是主控状态机没收到“停止条件”也可能是外部电平真的一直不满足空闲条件。这两种情况对应完全不同的排查方向后文会展开。1.2 从设备拉死总线最常见也最隐蔽的“真凶”从设备异常导致总线卡死是我见过最多的情形。典型表现是SDA 被某个从设备一直拉低SCL 倒是正常但主控一看 SDA 不是高电平就认为总线忙停止一切后续传输。为什么从设备会把 SDA 抱住不放常见原因有几种从设备电源电压跌落内部逻辑处于“半死不死”状态输出驱动管脚正好卡在低电平。从设备的 I2C 状态机跑飞比如主控在通信过程中突然复位导致从设备没收到预期的停止条件它就一直等待后续数据把 SDA 锁住。从设备的中断处理或内部 EEPROM 擦写被异常打断对外表现为“忙”状态。上拉电阻阻值选得过大加上总线电容较大SDA 被从设备释放后上升沿慢到主控采样时还没达到高电平阈值误判为一直拉低。我那块板子上触摸屏的 INT 引脚和 SDA 之间的干扰问题也出现过后来才发现是触摸屏内部固件在上电未完成时收到读命令直接导致后续输出异常。这类问题最麻烦的地方在于单纯靠软件重启主控不一定能解决因为根因在外设身上。1.3 控制器自身状态机和时钟的坑另一类是主控 I2C 控制器自己出了问题。常见原因包括传输过程中发生了总线错误比如仲裁丢失、收到意外的 ACK/NACK但控制器状态机没有正确跳回 IDLE导致Busy一直不清零。时钟未开启或时钟频率配置异常。某些 SoC 的 I2C 外设时钟来自可配置的 PLL 分频如果驱动在运行时被错误调整了时钟树控制器内部状态机可能无法产生完整的停止条件。DMA/FIFO 路径异常数据没发完但控制器已经尝试停止总线上出现残缺的时序。主控软件复位了从设备但从设备复位期间又把 SDA 拉低恰好这个状态被控制器采样到。这种情况下SDA/SCL 用示波器看可能都是正常的“高电平空闲”但寄存器里的Busy位就是纹丝不动。遇到这种核心思路是把控制器的状态机强制拉回 IDLE或者做一次完整的软复位。2. 排查 Busy 状态不能退出的实操流程2.1 准备一套趁手的排查工具排查 I2C 卡死工具比经验更重要很多时候一根线的问题用眼睛看不出来必须靠工具量化。我常用的几样东西示波器或逻辑分析仪至少 100MHz 带宽用来抓 SCL/SDA 电平。逻辑分析仪更轻便适合看长时间时序示波器适合看上升沿质量和毛刺。万用表量 SDA/SCL 对地电压快速判断是否被拉低以及上拉电压是否正常。i2c-toolsi2cdetect、i2cget、i2cset用于与总线交互和枚举设备。devmem2/devmem直接读写物理寄存器特别适合查看主控 I2C 控制器的状态位。内核调试节点部分 SoC 的 I2C 驱动注册了 debugfs 接口可以直接 dump 控制器寄存器。如果手头暂时没有示波器可以先用万用表量 SDA 对地电压。SDA 如果是 0V基本可以断定为外部拉死如果是高电平则问题可能出在主控状态机。这个判断在做完之前不要急着改任何驱动代码。2.2 拿到现场第一手证据我先用dmesg看内核日志确认超时报错发生在哪条总线。然后执行i2cdetect -l i2cdetect -y 2i2cdetect -l列出系统里所有 I2C adapter确认问题总线号i2cdetect -y 2扫描总线 2 上的设备。正常情况下列表里应该能看到 0x50EEPROM和触摸屏的地址但故障时全部消失。接着读控制器状态寄存器。以某颗常见 ARM SoC 为例I2C 控制器基地址是 0x21A0000状态寄存器偏移 0x08我用 devmem 直接读devmem2 0x21A0008看到返回值的 Bit0 为 1说明控制器认为总线忙。这时候赶紧配合万用表量 SDA 电压发现 SDA 对地只有 0.1V几乎被钳位到地。基本可以断定总线上有器件把 SDA 拉住了。2.3 用示波器抓关键波形用示波器同时夹 SDA 和 SCL触发方式选下降沿等一个总线事件。正常的空闲状态应该两条线都是高电平。可当我抓到的波形里SCL 是正常的高电平方波但 SDA 一直是一条直线在地附近这就说明不是主控在主动拉低而是有从设备或外部因素把 SDA 钳死了。另一个值得注意的细节如果 SDA 只是被“弱拉低”电压不是标准的 0V而是比如 0.8V、1.2V 这种中间电平就要怀疑是某个从设备的管脚驱动能力不足输出级半开半闭。这种情况下总线电压不高不低主控采样可能造成“读到低电平”和“读到高电平”随机变化非常难查。我遇到过一次是某颗触控芯片在低功耗模式下的 IO 状态异常导致的换掉那颗芯片后问题消失。2.4 查看 I2C 控制器寄存器不同 SoC 的寄存器布局差异很大但思路一致找到 I2C 控制器的基地址读状态寄存器和控制寄存器。通常关注这几类位Busy位当前总线忙/空闲状态。TX/RX状态位控制器是否正在发送/接收中。FIFO状态位发送/接收 FIFO 是否为空。中断标志位是否有未处理的总线错误中断。比如某常见主控的 I2C 状态寄存器偏移 0x08Bit0 是 BusyBit1 是总线错误标志。devmem2读出来如果是0x3说明不仅 Busy还有错误标志没清。这时候可以写相应寄存器把错误标志清掉再尝试复位控制器。对驱动开发者来说更直接的方式是看内核里i2c-imx.c、i2c-omap.c、i2c-s3c2410.c这类源码找到对应的readl/writel操作。我在现场一般先用devmem2拿到寄存器值再对照datasheet或驱动源码分析效率最高。3. 把 Busy 状态拽回来的几种恢复手段3.1 首选软件复位 I2C 控制器如果是主控状态机问题最常见恢复手段就是软复位控制器。我用的 SoC 的 I2C 控制器有一个软复位位在控制寄存器偏移 0x00 的某个 bit写 1 触发复位写完后整个控制器回到默认配置。# 假设 i2c 控制器基地址为 0x21A0000控制寄存器偏移 0x00bit7 为软复位位 devmem2 0x21A0000 32 0x80 usleep 1000 devmem2 0x21A0000 32 0x00这里有个坑有些 SoC 的软复位不是“写 1 然后自动清零”而是需要写两遍或者复位后还要重新配置时钟分频和中断使能。所以复位完以后先不要急着扫描设备先确认Busy位是否归零。如果归零再用i2cdetect扫描大概率就能看到设备回来了。3.2 经典招数9 个时钟脉冲复位从设备如果软复位控制器没用或者 SDA 被外部器件拉死就得把外设“唤醒”了。业界最经典的做法是主控用 GPIO 模拟 I2C在 SCL 上连续翻转 9 个时钟脉冲让所有从设备状态机复位从而释放 SDA。原理很简单I2C 协议规定如果从设备收不到预期的停止条件它会在第 9 个时钟上升沿释放 SDA。所以人为给 9 个脉冲等于把所有从设备的内部状态机“踢”回 IDLE 状态。我在 Linux 下用 GPIO 模拟过这个过程。先确认控制 SCL 和 SDA 的两个 GPIO 号然后写一个小脚本#!/bin/bash # SCLgpio88, SDAgpio89假设默认都是高电平 SCL88 SDA89 gpioset $SCL1 gpioset $SDA1 for i in $(seq 1 9); do gpioset $SCL0 usleep 500 gpioset $SCL1 usleep 500 done # 释放 SDA总线空闲 gpioset $SDA1执行完以后用万用表量 SDA电压如果恢复到上拉高电平就说明从设备被成功复位。再回到 I2C 控制器这边把控制器的Busy位清零总线基本就活了。这个操作有一个注意点GPIO 模拟的 9 个脉冲频率不要太快尤其是总线电容大的时候SCL 必须维持足够低电平时间让从设备采样到有效沿。我在现场用 500us 的半周期虽然慢但对大多数器件都够用。有些老工程师喜欢用逻辑分析仪触发确认 9 个脉冲的完整性这个方法虽然粗暴但实际非常好用。3.3 硬件复位和断电重启当 9 个脉冲也救不回来时就得考虑从设备彻底断电重新上电。这招对电源问题引起的锁定特别有效。但要注意断电不只是电源脚如果从设备有独立复位脚要确保复位脚也拉低再释放否则某些芯片上电后仍处于内部复位状态I2C 依旧异常。我的板子上触摸屏有一颗 VDD 电源脚接在一个 GPIO 控制的 LDO 后面。操作路径是先解除 I2C 控制器对总线的占用必要时关掉控制器时钟。拉低触摸屏的供电 GPIO等待 100ms。拉高供电 GPIO等待 200ms 让内部上电完成。再次检查 SDA 是否恢复正常高电平。这里要特别提醒如果整条总线上还挂着别的器件断电前最好把总线事务停掉避免控制器在总线上产生无效时序。另外给从设备断电期间SDA/SCL 如果有外部上拉这两个脚会保持高电平主控没有风险但如果存在多个电源域要确认断电期间没有倒灌电流的问题。3.4 更彻底的做法在驱动里加自动恢复手动恢复只能救急产品化场景里肯定不能靠人拿命令行去敲。我后来在驱动里加了两个机制第一个是在每次 transfer 返回超时后检查控制器状态寄存器的Busy位如果发现Busy并且 SDA 异常拉低就触发 GPIO 9 脉冲恢复流程然后重新初始化控制器。第二个是在驱动 probe 阶段增加一个“总线自检”函数确保上电时如果总线被外部器件拉死能主动复位总线再继续后续的触摸屏初始化。C 语言的伪代码大致长这样static void i2c_bus_recover(struct i2c_adapter *adap) { struct i2c_bus_recovery_info *rinfo adap-bus_recovery_info; int ret; /* 先尝试控制器软复位 */ i2c_controller_soft_reset(adap); /* 再尝试 SCL 9 脉冲恢复外部器件 */ if (rinfo rinfo-pinctrl_recovery) pinctrl_select_state(rinfo-pinctrl, rinfo-pinctrl_recovery); ret i2c_generic_scl_recovery(adap); if (ret) dev_err(adap-dev.parent, bus recovery failed\n); }内核里其实已经有i2c_generic_scl_recovery这套现成的机制它做的事情就是切换 pinmux 把 SCL 变成普通 GPIO然后按「9 个时钟脉冲」的方式尝试恢复。如果你的 SoC 支持 pinmux 切换直接在设备树里配pinctrl-1为“gpio recovery mode”就行比我手动 gpioset 更优雅。因为 Linux 内核官方 I2C 框架本身就支持 recovery 机制i2c_imx、i2c_s3c2410这些驱动都有对应的实现平时可以用CONFIG_I2C_GPIO配合i2c-gpio做软恢复。关键还是要把“总线卡死”当成可预期事件来设计而不是等它发生了再救火。4. 一个完整案例复盘触摸屏挂死导致整条总线瘫痪4.1 现场现象回到我最开始说的压力测试场景。I2C2 总线上挂着两个设备一个电容触摸屏一个 EEPROM。压力测试脚本每隔几秒会读一次 EEPROM同时触摸屏在正常使用。第三天晚上日志开始刷i2c2: transfer timeout紧接着就发现触控无反应EEPROM 读写也失败了。我第一时间用i2cdetect -y 2扫总线返回的时候是空列表。量 SDA直接被拉低到 0V 左右。此时 SCL 是正常的空闲高电平。这个组合基本确认是“从设备拉死 SDA”。比较有意思的是查看dmesg的时候发现在transfer timeout出现之前的几秒触摸屏驱动正好报了一堆reset信息好像是触摸屏的固件异常触发了软复位。这个线索很关键说明在触摸屏复位过程中可能发生了 I2C 总线上的不完整时序。4.2 定位过程我先把触摸屏从总线上断开软件层不注册它但物理上还连着再看总线状态结果 SDA 不是 0V 了但Busy位还是 1。这说明主控自身的状态机已经被“弄脏”了需要同时复位控制器和外设才能彻底恢复。于是我做了一个组合操作通过 devmem 对 I2C 控制器软复位。用 GPIO 模拟 9 个脉冲复位所有从设备。重新初始化控制器设置时钟频率为 100kHz。用i2cdetect -y 2扫描两个设备都回来了。问题恢复到这一步只是把现象解决了一半。真正让我头疼的是根因触摸屏为什么会在正常工作状态下突然拉死总线最后反复看示波器抓到的波形才发现触摸屏的 INT 引脚和 SDA 之间存在一个非常窄的毛刺恰好出现在触摸屏固件进行内部校准的瞬间。这颗触摸屏的 INT 引脚在固件异常时会被拉低而 SDA 线上的干扰会让主控误判为起始条件进而产生残缺时序最终导致触摸屏的状态机锁死。4.3 修复和规避确认根因后我做了三件事修改触摸屏驱动在初始化时增加 200ms 的上电稳定等待并且严格按厂商手册的时序要求发送命令避免在固件未就绪时通信。给触摸屏的 INT 加了一个 100nF 滤波电容减小毛刺传播到 SDA 的概率。在驱动里注册内核的 bus recovery 回调这样即使偶发锁定系统也能自动恢复不用人工干预。这套组合方案上线以后压力测试又跑了两个礼拜没有再复现。过程中我也意识到一个问题I2C 总线的可靠性不是靠某一次“修复”就一劳永逸的它更像是一个系统性工程涉及硬件设计、驱动初始化、异常恢复三者协同。5. 长期预防让 I2C 总线不容易卡死5.1 设备树里的超时参数和时钟配置在设备树里I2C 控制器有几个参数直接影响总线稳定性建议拿到新板子先确认一遍i2c2 { clock-frequency 100000; pinctrl-names default, recovery; pinctrl-0 pinctrl_i2c2; pinctrl-1 pinctrl_i2c2_gpio; status okay; eeprom50 { compatible atmel,24c02; reg 0x50; pagesize 8; }; touchscreen38 { compatible vendor,ts; reg 0x38; reset-gpios gpio1 8 GPIO_ACTIVE_LOW; }; };clock-frequency我建议优先从 100kHz 起步。很多开发板默认配 400kHz但总线电容、从设备时序裕量不足时400kHz 非常容易出偶发问题。降低到 100kHz 后上升沿要求放宽误采样概率大幅下降。pinctrl-1配成 GPIO 模式是为了让内核在超时后使用上面说的i2c_generic_scl_recovery机制。5.2 驱动层面的看门狗机制我在多个项目里验证过给 I2C 传输设置超时和重试非常有必要。Linux 内核的 I2C 框架本身有adapter-timeout参数默认通常是 1 秒但如果你的从设备响应很慢可以适当调大。自定义驱动里我习惯在每次读写时记一个时间戳超过阈值就自动进入恢复流程。很多真实场景是“偶发超时”不是“永久卡死”。如果驱动能自动重试几次往往不需要大动作就能恢复。比如 EEPROM 写入时会 busy驱动里做一个 3 次重试、间隔 5ms 的机制就能避免因为芯片内部擦写时间超出预期而误报失败。重试逻辑要放在业务代码里而不是死在 I2C 核心层。5.3 硬件设计上的建议硬件层面的优化更重要软件只是兜底。几个经验上拉电阻阻值3.3V 电压下1kΩ 到 4.7kΩ 之间要根据总线电容调整。总线电容大就选小一点的电阻但电阻太小会增加功耗。我一般先用 4.7kΩ 起步示波器看上升沿如果太缓就换 2.2kΩ。总线电容不要超过 400pF这是标准限制。如果挂载设备多、走线长适当拆分总线或用 I2C 缓冲器。从设备复位脚尽量把每个从设备的复位脚引到主控 GPIO便于软件恢复时独立复位。实在不能单独控制至少要保证整组供电可控。电源域如果从设备由 PMIC 的 LDO 供电要确保 sleep/active 切换时 I2C 不会在电源未稳定的情况下发起传输。这类问题在量产阶段尤其容易爆发因为开发阶段往往只有一两块板子总线电容和器件批次差异不足以暴露问题量产几百台后上拉电阻、板厂阻抗、芯片电气特性稍有差异卡死概率会明显上升。所以如果手头产品量级比较大建议在研发阶段就把恢复机制和硬件裕量都做足。6. 常见问题与排查技巧实录6.1 问题排查速查表我自己把它整理成一张表遇到 I2C 异常时逐行对照效率非常高现象可能原因快速排查解决方案SDA 恒低SCL 正常某从设备拉死 SDA万用表量 SDA逐个断开从设备GPIO 9 脉冲复位断电重启SDA/SCL 都是高电平但 Busy 位为 1主控状态机卡死读总线状态寄存器软复位 I2C 控制器偶发 timeout频率和温度相关上拉电阻太大/总线电容超标示波器看上升沿减小上拉电阻或降速触摸屏/传感器正常工作时突然卡死从设备固件异常查看驱动日志是否先报 reset修时序、加滤波、加恢复回调休眠唤醒后 Busy 不退出电源域复位时序不对测试唤醒后电平调整上下电时序Windows 下“AMD I2C Controller”惊叹号驱动安装失败或总线枚举异常查看设备管理器状态更新驱动/检查 BIOS 设置i2cdetect 扫到乱码地址总线上存在毛刺或地址冲突示波器抓 START/STOP 条件检查上拉、滤波、地址配置6.2 几个额外的避坑建议有些坑只有在现场踩过才会记得住。第一个是复位控制器之前先停掉可能正在进行的 DMA 传输否则 DMA 描述符可能悬挂在一个半完成的传输上回来之后还会再发一次。第二个是别在主控软复位之后立刻扫描总线给控制器 10ms 左右的时间稳定内部状态否则会读到错误的总线状态。第三个是恢复流程里一定要打印日志至少要明确记录“已触发总线恢复”这条信息否则后续查问题时会漏掉关键线索。关于i2cdetect扫描时遇到地址重复的问题也顺带说一句。如果总线上有两个设备地址一样扫描结果会出现随机性甚至导致总线冲突。标准做法是先把其中一个设备的地址引脚改掉再集成到同一总线上。如果你在设计阶段评估设备数量时发现地址不够用可以考虑用类似 PCA9548 的 I2C 多路器切分总线而不是强行挂在一条线上。另一个容易被忽略的细节是从设备的中断引脚。很多 I2C 触控、传感器芯片的 INT 引脚和 SDA 距离很近如果 INT 上毛刺较多会在 SDA 上耦合出干扰导致主控错误识别起始条件或停止条件。建议在硬件设计时给 INT 加 RC 滤波软件上尽量把 INT 配置成低有效并加上拉减少误触发。还有一个我个人的偏好给每条 I2C 总线在驱动加载时做一次“总线自检”。实现方式很简单在 probe 阶段向总线上的每个地址发一个 0 字节读能收到 ACK 就说明设备在位。如果自检失败宁可把该总线标记为不可用也不要继续初始化后面的外设否则容易在系统启动阶段就把总线弄脏后续更难排查。最后说说我在实际处理这类问题时的习惯。踩过几次坑之后我的固定流程变成出现 timeout 先看 dmesg确认是哪条总线然后用万用表量 SDA/SCL 电压有条件就上示波器抓波形再读控制器寄存器确认Busy位最后按“外部拉死”和“主控状态机卡死”两个方向分头处理。整个过程看起来步骤很多熟练以后十分钟内基本能定位到根因层面。I2C 的 Busy 状态问题说到底是“电平异常”和“状态机异常”的组合问题只要思路清晰工具到位大多数情况都能在几小时内解决。希望这篇复盘能帮你少走一些我走过的弯路。