基于 STM32 的 I2C 死锁恢复:硬件复位与软件时钟延展的边界条件

1. 死锁的本质与触发场景

I2C 总线是开漏结构,任何设备拉低 SCL 或 SDA 都会阻塞通信。死锁通常表现为:

  • SCL 被从设备拉低:从设备在 ACK 阶段或内部处理时异常,导致时钟延展(Clock Stretching)超时。
  • SDA 被拉低:主设备在发送起始条件后,从设备未释放总线,或总线竞争导致数据线卡死。
  • 主设备自身异常:代码 bug 导致 I2C 外设状态机卡在中间状态,无法释放总线。

在 STM32 上,死锁恢复的核心是释放总线并复位外设状态,但不同场景需要不同策略。

2. 硬件复位:简单粗暴但需谨慎

2.1 原理

硬件复位通过 GPIO 控制 I2C 设备的复位引脚(若有),或直接切断电源,强制所有设备释放总线。对于 STM32 本身,可通过 __HAL_I2C_DISABLE() 禁用外设,再重新初始化。

2.2 边界条件

  • 从设备无复位引脚:无法单独复位,只能断电或等待其超时。
  • 总线电容大:复位后 SCL/SDA 可能仍保持低电平(寄生电容放电慢),需等待足够时间。
  • 多主设备:硬件复位可能影响其他主设备正在进行的传输,需确保总线空闲。

2.3 代码示例

void i2c_hard_reset(I2C_HandleTypeDef *hi2c) {
    // 禁用外设
    __HAL_I2C_DISABLE(hi2c);
    // 配置 GPIO 为开漏输出,并拉高,释放总线
    GPIO_InitTypeDef gpio = {0};
    gpio.Pin = I2C_SCL_PIN | I2C_SDA_PIN;
    gpio.Mode = GPIO_MODE_OUTPUT_OD;
    gpio.Pull = GPIO_PULLUP;
    gpio.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(I2C_GPIO_PORT, &gpio);
    HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN | I2C_SDA_PIN, GPIO_PIN_SET);
    HAL_Delay(10); // 等待总线释放
    // 重新初始化外设
    HAL_I2C_Init(hi2c);
}

注意:硬件复位后,所有从设备的状态机可能回到未知状态,需重新配置(如重新发送控制字)。

3. 软件时钟延展:精准但依赖从设备配合

3.1 原理

软件时钟延展(Software Clock Stretching)是主设备主动控制 SCL 时钟,通过发送 9 个时钟脉冲,让从设备释放 SDA。原理是:从设备在检测到 SCL 脉冲时,会推进内部状态机,最终释放 SDA。

3.2 边界条件

  • 从设备必须支持时钟延展:若从设备不支持,此方法无效。
  • 从设备卡死状态:若从设备内部死循环,时钟脉冲无法唤醒,需结合超时机制。
  • 总线冲突:若 SDA 被其他主设备拉低,软件时钟延展可能加剧冲突。

3.3 代码示例

void i2c_software_recover(I2C_HandleTypeDef *hi2c) {
    // 将 SCL 和 SDA 配置为开漏输出
    GPIO_InitTypeDef gpio = {0};
    gpio.Pin = I2C_SCL_PIN | I2C_SDA_PIN;
    gpio.Mode = GPIO_MODE_OUTPUT_OD;
    gpio.Pull = GPIO_PULLUP;
    gpio.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(I2C_GPIO_PORT, &gpio);

    // 产生 9 个时钟脉冲
    for (int i = 0; i < 9; i++) {
        HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_RESET);
        HAL_Delay(1); // 至少 5us,根据总线速率调整
        HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_SET);
        HAL_Delay(1);
        // 检查 SDA 是否释放
        if (HAL_GPIO_ReadPin(I2C_GPIO_PORT, I2C_SDA_PIN) == GPIO_PIN_SET) {
            break; // SDA 已释放,提前退出
        }
    }

    // 发送停止条件
    HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_SET);
    HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_SET);
    HAL_Delay(1);

    // 重新初始化外设
    HAL_I2C_Init(hi2c);
}

注意:时钟脉冲间隔需大于从设备的最小 SCL 高/低电平时间,通常 5us 足够。

4. 边界条件对比与选择策略

| 场景 | 硬件复位 | 软件时钟延展 | |------|----------|--------------| | 从设备有复位引脚 | 首选 | 可用 | | 从设备无复位引脚 | 不可用 | 首选 | | 总线电容大 | 需等待 | 需调整脉冲宽度 | | 多主设备 | 风险高 | 风险低 | | 从设备不支持时钟延展 | 可用 | 不可用 | | 死锁原因不明 | 保守 | 激进 |

推荐策略:先尝试软件时钟延展(轻量、快速),若失败再升级到硬件复位。同时,在应用层增加超时重试机制,避免无限阻塞。

5. 完整工程示例

以下是一个基于 STM32 HAL 库的 I2C 死锁检测与恢复模块:

// i2c_recover.h
#ifndef I2C_RECOVER_H
#define I2C_RECOVER_H

#include "stm32f1xx_hal.h"

#define I2C_RECOVER_TIMEOUT 100  // 超时时间 ms

typedef enum {
    I2C_RECOVER_OK,
    I2C_RECOVER_TIMEOUT,
    I2C_RECOVER_FAIL
} I2C_RecoverStatus;

I2C_RecoverStatus i2c_check_and_recover(I2C_HandleTypeDef *hi2c);

#endif
// i2c_recover.c
#include "i2c_recover.h"

static I2C_RecoverStatus i2c_software_recover(I2C_HandleTypeDef *hi2c) {
    // 实现见上文
    return I2C_RECOVER_OK;
}

static I2C_RecoverStatus i2c_hard_recover(I2C_HandleTypeDef *hi2c) {
    // 实现见上文
    return I2C_RECOVER_OK;
}

I2C_RecoverStatus i2c_check_and_recover(I2C_HandleTypeDef *hi2c) {
    // 检查总线是否空闲(SCL 和 SDA 均为高)
    if (HAL_GPIO_ReadPin(I2C_GPIO_PORT, I2C_SCL_PIN) == GPIO_PIN_SET &&
        HAL_GPIO_ReadPin(I2C_GPIO_PORT, I2C_SDA_PIN) == GPIO_PIN_SET) {
        return I2C_RECOVER_OK;
    }

    // 先尝试软件恢复
    if (i2c_software_recover(hi2c) == I2C_RECOVER_OK) {
        return I2C_RECOVER_OK;
    }

    // 软件恢复失败,尝试硬件复位
    return i2c_hard_recover(hi2c);
}

在应用层调用:

// 每次 I2C 传输前检查总线状态
if (i2c_check_and_recover(&hi2c1) != I2C_RECOVER_OK) {
    // 记录错误,重启设备或进入安全模式
}
HAL_I2C_Master_Transmit(&hi2c1, dev_addr, data, len, timeout);

6. 注意事项

  • 时序要求:软件时钟延展的脉冲宽度需根据总线速率调整,100kHz 下最小高电平 4us,400kHz 下 0.6us,建议留余量。
  • 中断冲突:恢复过程中应关闭 I2C 中断,避免状态机干扰。
  • 多主设备:在尝试恢复前,需确认总线空闲,否则可能破坏其他主设备的传输。
  • 从设备状态:恢复后,从设备可能处于未知状态,建议重新初始化所有从设备(如发送配置命令)。
  • 调试技巧:使用逻辑分析仪观察恢复过程中的 SCL/SDA 波形,验证时序是否符合预期。

7. 总结

I2C 死锁恢复没有银弹,硬件复位和软件时钟延展各有边界。硬件复位适合从设备有复位引脚且总线电容小的场景,而软件时钟延展则更通用,但依赖从设备支持。实际工程中,应结合超时检测、状态机重试和两种恢复策略,构建分层恢复机制,确保系统在异常后能快速自愈。