I2C 总线通常被认为是一个简单且可靠的接口,用来连接芯片,但实际上它也存在一些隐蔽的陷阱。其中最常见、也最令人头痛的问题就是 I2C 总线死锁。
本文将介绍 I2C 的基本原理,分析死锁是如何发生的,并分享在嵌入式设计中 预防和恢复 I2C 死锁的实用方法。
什么是 I2C? #
I2C(Inter-Integrated Circuit,集成电路间总线)是由 Philips(现为 NXP)在 40 多年前开发的两线串行通信协议。它广泛用于在微控制器和 SoC 上连接低速或中速的外设,例如 EEPROM、温度传感器和 ADC 等。
I2C 总线包含两条信号线:
- SCL(时钟线)
- SDA(数据线)
两条信号线均为 开漏(open-drain) 结构,并通过上拉电阻实现“线与”逻辑。这样总线上可以连接多个设备而不冲突。
下面是一个典型的 I2C 多字节读传输示例:
在开始通信之前,主设备必须确认总线空闲(SCL 和 SDA 都为高电平)。如果其中任意一条线被拉低,总线就被视为忙,新的通信无法发起。
I2C 死锁是如何发生的? #
虽然 I2C 看起来简单,但它可能进入一个 永久忙碌状态——也就是死锁,导致任何新的通信都无法进行。死锁的常见原因包括:
- 噪声或干扰
- 如果丢失或额外产生了时钟沿,可能导致从设备一直拉低 SDA。
- 上电时的毛刺
- 主设备 I/O 管脚在初始化之前可能产生异常跳变,从设备会误判信号。
- 软件崩溃或复位
- 在调试时若在传输过程中断点或重启软件,从设备可能一直认为通信未结束。
结果就是:
- 从设备认为通信还未完成,持续拉低 SDA。
- 主设备认为通信已经结束,不再发出时钟。
- 总线进入死锁状态。
如何预防 I2C 死锁? #
可靠的设计要从 预防 开始。以下方法有助于减少死锁发生:
-
硬件措施
- 使用更强的上拉电阻,加快 SCL/SDA 的上升沿。
- 确保 I2C 主设备 I/O 管脚在复位后默认为高电平。
-
软件措施
- 初始化时小心配置管脚,避免产生无效跳变。
- 在系统启动时主动执行一次 恢复时钟序列,清除可能的死锁。
如何检测并恢复 I2C 死锁? #
即使有预防措施,死锁仍然可能发生。健壮的系统必须具备 检测和恢复机制:
-
检测手段
- 任何等待 I2C 事件(如总线空闲、传输完成)都必须设定超时,而不是无限等待。
-
恢复策略
- 复位从设备(如果硬件支持)。
- 强制时钟脉冲:向 SCL 线发出至少 10 个脉冲,迫使从设备释放 SDA。
为什么是 10 个脉冲?
- 因为通常需要 9 个时钟来传输一个字节,外加 1 个 ACK 确认。
如果使用的微控制器有专用 I2C 控制器,可能需要临时将 SCL 引脚切换为 GPIO 输出,才能手动生成脉冲。
实例:软件恢复代码 #
下面的例子来自 NXP KL17 微控制器。由于其 I2C 控制器无法直接驱动时钟,因此需要将 SCL 引脚临时配置为 GPIO 来产生恢复脉冲:
#define I2C_RECOVER_NUM_CLOCKS 10U /* 恢复时钟数 */
#define I2C_RECOVER_CLOCK_FREQ 50000U /* 恢复频率 */
#define I2C_RECOVER_CLOCK_DELAY_US (1000000U / (2U * I2C_RECOVER_CLOCK_FREQ))
void i2cLockupRecover (void)
{
/* 将 SCL 配置为 GPIO */
PORT_SetPinMux(I2C_SCL_PORT, I2C_SCL_GPIO_PIN, kPORT_MuxAsGpio);
const gpio_pin_config_t pinConfig = {
.pinDirection = kGPIO_DigitalOutput,
.outputLogic = 1U,
};
GPIO_PinInit(I2C_SCL_GPIO_PORT, I2C_SCL_GPIO_PIN, &pinConfig);
/* 产生恢复时钟脉冲 */
for (unsigned int i = 0U; i < I2C_RECOVER_NUM_CLOCKS; ++i) {
delayUs(I2C_RECOVER_CLOCK_DELAY_US);
GPIO_PinWrite(I2C_SCL_GPIO_PORT, I2C_SCL_GPIO_PIN, 0U);
delayUs(I2C_RECOVER_CLOCK_DELAY_US);
GPIO_PinWrite(I2C_SCL_GPIO_PORT, I2C_SCL_GPIO_PIN, 1U);
}
/* 重新配置为 I2C SCL */
PORT_SetPinMux(I2C_SCL_PORT, I2C_SCL_GPIO_PIN, kPORT_MuxAlt4);
}
提示: 在系统启动时始终执行一次恢复时钟序列。这样不仅能避免上电时毛刺引起的死锁,还能大大简化调试时的重复复位操作。
下图展示了启动时软件产生的恢复时钟序列,紧接着是第一次有效的 I2C 通信:
总结 #
I2C 死锁是嵌入式系统中一个真实且常见的问题,但通过合理的 硬件设计 与 软件机制,它完全可以被 预防、检测并恢复。
关键措施包括:
- 硬件上使用强上拉和默认高电平的 I/O 管脚;
- 软件上注意初始化顺序,设置超时机制;
- 遇到死锁时通过复位从设备或强制时钟脉冲恢复总线。
核心结论: 在 I2C 系统设计中,始终要考虑死锁恢复机制。这样不仅能提高系统稳定性,还能在调试时省去许多不必要的麻烦。