《STM32 I2C 总线挂死排查:时钟拉伸、从机拉低 SDA 与总线恢复的工程方法》
一、问题现象:总线“死”在低电平上
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 不怕慢,怕的是主机与从机对“现在轮到谁说话”理解不一致;时序对齐了,总线自然就稳了。
- 点赞
- 收藏
- 关注作者
评论(0)