《STM32 I2C 总线挂死排查:时钟拉伸、从机拉低 SDA 与总线恢复的工程方法》

举报
yd_223794387 发表于 2026/11/01 06:17:23 2026/11/01
【摘要】 一、问题现象:总线“死”在低电平上I2C 出问题时最典型的现场是:SCL 或 SDA 被拉低后不再释放,HAL_I2C_Master_Transmit 一直返回 HAL_BUSY 或 HAL_TIMEOUT,复位从机芯片也不一定恢复。初学者常把原因归结为“从机坏了”或“上拉电阻不对”,直接换芯片、换电阻,结果同一块板子隔几天又复现。真正的原因大多在时序与状态机层面:某一次传输在中途被打断,从...

一、问题现象:总线“死”在低电平上

I2C 出问题时最典型的现场是:SCL 或 SDA 被拉低后不再释放,HAL_I2C_Master_Transmit 一直返回 HAL_BUSY 或 HAL_TIMEOUT,复位从机芯片也不一定恢复。初学者常把原因归结为“从机坏了”或“上拉电阻不对”,直接换芯片、换电阻,结果同一块板子隔几天又复现。真正的原因大多在时序与状态机层面:某一次传输在中途被打断,从机停在等待时钟或等待应答的状态里,一直拉着 SDA 不放;主机以为总线空闲,再次发起起始条件时,从机根本听不懂,于是总线看起来“挂死”。

理解这一点要先回到 I2C 的基本约定:SDA 和 SCL 都是开漏输出,平时靠上拉电阻维持高电平;起始条件是 SCL 为高时 SDA 由高变低,停止条件是 SCL 为高时 SDA 由低变高。任何一方在不该拉低的时候拉低总线,另一方都无法靠“强行拉高”纠正,只能等对方释放。调试时先用示波器或逻辑分析仪抓一帧完整波形,看挂死发生在哪个边沿,比反复换件有效得多。

二、时钟拉伸:合法行为,别当成故障

很多从机(尤其是带内部 EEPROM 写周期、传感器做 ADC 转换的器件)会在 SCL 为低期间拉低 SCL,这叫时钟拉伸(clock stretching),是协议允许的。主机必须等 SCL 真正回到高电平再继续,而不是按自己的节拍硬发下一个边沿。STM32 的硬件 I2C 外设本身支持时钟拉伸,但如果代码里用软件延时模拟时序,或者在中断里关了过长时间,就可能在从机还没释放 SCL 时误判为超时,进而复位外设、重发数据,把一次合法等待变成一次总线冲突。

工程上的做法是:超时阈值按从机手册里最长的拉伸时间留足余量,例如 EEPROM 页写的最长 5ms、传感器转换最长 10ms,超时值至少取两倍;超时后不要立刻复位整个外设,先读一次总线状态,确认 SCL 是否已被释放。把“拉伸”误判为“挂死”而频繁复位,反而会让从机状态机错乱。

三、从机拉低 SDA:九个时钟脉冲恢复法

真正挂死时常见的是 SDA 被从机拉低、SCL 为高。此时主机发不出起始条件,因为 SDA 已经是低电平。标准恢复方法是:主机把 SCL 当作普通 GPIO,手动输出 9 个时钟脉冲。每个脉冲都给从机一次移位机会,让它把正在发送的字节发完;第 9 个脉冲后从机一般会释放 SDA,随后主机再发一次停止条件,总线回到空闲。恢复代码的核心是先把 I2C 外设去初始化、把两个引脚切回 GPIO 开漏模式,恢复完成后再重新初始化外设,绝不能在外设仍占用引脚时直接翻转 GPIO。

代码:

void I2C_BusRecover(GPIO_TypeDef *port, uint16_t scl, uint16_t sda)
{
    GPIO_InitTypeDef cfg = {0};
    cfg.Mode = GPIO_MODE_OUTPUT_OD;
    cfg.Pull = GPIO_PULLUP;
    cfg.Speed = GPIO_SPEED_FREQ_HIGH;
    cfg.Pin = scl | sda;
    HAL_GPIO_Init(port, &cfg);
    HAL_GPIO_WritePin(port, sda, GPIO_PIN_SET);
    for (int i = 0; i < 9; i++) {
        HAL_GPIO_WritePin(port, scl, GPIO_PIN_RESET);
        HAL_Delay(1);
        HAL_GPIO_WritePin(port, scl, GPIO_PIN_SET);
        HAL_Delay(1);
        if (HAL_GPIO_ReadPin(port, sda) == GPIO_PIN_SET) break;
    }
    HAL_GPIO_WritePin(port, scl, GPIO_PIN_SET);
    HAL_GPIO_WritePin(port, sda, GPIO_PIN_RESET);
    HAL_Delay(1);
    HAL_GPIO_WritePin(port, sda, GPIO_PIN_SET);
}

需要注意:9 个脉冲不是魔法数字,它对应一个字节加应答位的最长移位次数;如果从机内部状态更深(例如正在处理多字节连续读),可能需要重复一次恢复流程再判断。恢复后第一帧建议发一次带 NACK 的读或重新发从机地址加写方向,先确认从机回到可寻址状态,再继续正常业务数据,避免恢复成功后立刻发大块数据又把从机顶回异常状态。

四、上拉电阻与速率:先算再选

挂死的另一个常见诱因是上拉电阻选得不合理。电阻太小,低电平灌电流大、功耗高,从机拉低时边沿虽快但发热与干扰也大;电阻太大,上升沿被总线电容拖慢,高速模式下 SCL 还没升到高电平阈值主机就采样,读到的全是错数据。估算公式是 Rp(max) 约等于 tr 除以 (0.8473 乘以 Cb),其中 tr 是允许的最长上升时间,Cb 是总线总电容(含走线、引脚与从机输入电容)。400kHz 快速模式下 tr 一般取 300ns,若总电容按 200pF 估算,上拉上限约 1.8kΩ;实际选 2.2kΩ 到 4.7kΩ 之间再用示波器验证上升沿,是比“照抄例程 4.7kΩ”更可靠的做法。板上有多个从机、走线长时,总电容要把每个器件的输入电容都加进去,不能只算一个。

五、软件层面的三条防线

第一,发送前先判忙:调 HAL_I2C_IsDeviceReady 探测从机地址,确认应答后再发数据;探测失败先走恢复流程,不要直接重发同一帧。第二,中断与 DMA 配合时,传输完成回调里再释放总线资源,禁止在中断服务函数里做长延时或打印,避免打断正在进行的拉伸等待。第三,记录“挂死现场”:超时时把当时的 SCL/SDA 电平、已发字节数、从机地址记进日志,下次复现时直接比对,而不是每次都从头猜。很多间歇性挂死最后查下来都是同一类原因:某个从机在特定温度或电源波动下转换时间变长,超时阈值刚好卡在临界点上。

六、排查顺序总结

遇到 I2C 挂死,按这个顺序走最省时间:先看波形确认是 SCL 被拉低还是 SDA 被拉低;SCL 被拉低先等拉伸、放宽超时;SDA 被拉低用 9 脉冲恢复法;恢复后用地址探测确认从机可寻址;最后核对上拉电阻与上升沿,确认速率与总线电容匹配。把恢复流程做成开机自检的一部分——上电后先读一次总线电平,异常就先恢复再初始化业务——能把很多“偶发挂死”挡在产品出厂之前。I2C 不怕慢,怕的是主机与从机对“现在轮到谁说话”理解不一致;时序对齐了,总线自然就稳了。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。