一、什么是时钟拉伸

I2C 是开漏总线,SCL 由主机驱动,但从机可以在任意时刻把 SCL 拉低,迫使主机等待——这就是时钟拉伸(Clock Stretching)。

  • 主机释放 SCL 后,靠上拉电阻把线拉高;
  • 从机若还没准备好数据,就把 SCL 拉低;
  • 主机检测到 SCL 仍为低,就进入等待,直到从机释放。

协议上这是合法的,但工程中它经常是“总线挂死”的元凶。

二、寄存器级原理

以 STM32F1 的 I2C 外设为例,关键寄存器:

  • I2C_CR1:SWRST 软复位、PE 使能、ACK 应答;
  • I2C_CR2:FREQ 外设时钟频率;
  • I2C_SR1:SB 起始位、ADDR 地址匹配、BTF 字节传输完成、AF 应答失败;
  • I2C_SR2:BUSY 总线忙、MSL 主从模式;
  • I2C_CCR:CCR 分频、FS 快速模式、DUTY 占空比。

主机发送时,硬件在 SCL 高电平期间采样 SDA。若从机把 SCL 拉低,硬件会等待 SCL 实际变高后才继续。问题在于:如果从机永远不释放 SCL,BUSY 位会一直为 1,任何新的 START 都无法发出。

三、挂死现象与定位思路

典型现象:

  • 调用 HAL_I2C_Master_Transmit 后卡死或超时;
  • I2C_SR2 的 BUSY 恒为 1;
  • 用示波器看 SCL 一直被拉低,SDA 状态不定。

定位分三步:

  1. 确认是时钟拉伸还是总线锁死:读 GPIO 输入电平,看 SCL/SDA 实际状态;
  2. 确认谁在拉低:断开从机或换从机,若 SCL 恢复高,则是从机 hold;
  3. 寄存器级确认:读 SR2.BUSY、SR1,判断外设是否卡在等待。

四、配置步骤

  1. 将 SCL/SDA 配置为开漏输出,并使能内部上拉(或外部 4.7k 上拉);
  2. 初始化 I2C,设置 FREQ、CCR、TRISE;
  3. 在通信前后加入超时与总线状态检查;
  4. 加入总线恢复函数:模拟 9 个 SCL 脉冲。

五、完整代码示例

#include "stm32f1xx.h"

#define I2C_TIMEOUT  100000

/* 读取 SCL/SDA 实际电平,用于判断是否被从机拉低 */
static uint8_t i2c_read_pins(void)
{
    uint8_t scl = (GPIOB->IDR & GPIO_IDR_ID6) ? 1 : 0;
    uint8_t sda = (GPIOB->IDR & GPIO_IDR_ID7) ? 1 : 0;
    return (scl << 1) | sda;
}

/* 总线恢复:主机手动发 9 个 SCL 脉冲,再发 STOP */
void i2c_bus_recover(void)
{
    /* 1. 关闭 I2C 外设,释放引脚 */
    I2C1->CR1 &= ~I2C_CR1_PE;

    /* 2. 配置 PB6/PB7 为开漏输出 */
    GPIOB->CRL &= ~(0xFF << 24);
    GPIOB->CRL |=  (0x77 << 24);   /* 通用开漏输出 50MHz */

    /* 3. 确保 SDA 为高,SCL 发 9 个脉冲 */
    GPIOB->BSRR = GPIO_BSRR_BS7;
    for (int i = 0; i < 9; i++) {
        GPIOB->BRR  = GPIO_BRR_BR6;   /* SCL 低 */
        for (volatile int d = 0; d < 50; d++);
        GPIOB->BSRR = GPIO_BSRR_BS6;  /* SCL 高 */
        for (volatile int d = 0; d < 50; d++);
    }

    /* 4. 产生 STOP:SCL 高时 SDA 由低变高 */
    GPIOB->BRR  = GPIO_BRR_BR7;
    for (volatile int d = 0; d < 50; d++);
    GPIOB->BSRR = GPIO_BSRR_BS6;
    for (volatile int d = 0; d < 50; d++);
    GPIOB->BSRR = GPIO_BSRR_BS7;
    for (volatile int d = 0; d < 50; d++);

    /* 5. 重新初始化 I2C */
    I2C1->CR1 |= I2C_CR1_SWRST;
    I2C1->CR1 &= ~I2C_CR1_SWRST;
    I2C1->CR1 |= I2C_CR1_PE;
}

/* 带超时的等待,返回 0 成功,-1 超时 */
int i2c_wait_flag(volatile uint32_t *reg, uint32_t flag, uint32_t timeout)
{
    while (!(*reg & flag)) {
        if (--timeout == 0) return -1;
    }
    return 0;
}

/* 发送一字节,检测 BUSY 与超时 */
int i2c_write_byte(uint8_t addr, uint8_t data)
{
    if (I2C1->SR2 & I2C_SR2_BUSY) {
        /* 总线忙,先尝试恢复 */
        i2c_bus_recover();
    }

    I2C1->CR1 |= I2C_CR1_START;
    if (i2c_wait_flag(&I2C1->SR1, I2C_SR1_SB, I2C_TIMEOUT)) return -1;

    I2C1->DR = addr << 1;
    if (i2c_wait_flag(&I2C1->SR1, I2C_SR1_ADDR, I2C_TIMEOUT)) return -1;
    (void)I2C1->SR2;   /* 清 ADDR */

    I2C1->DR = data;
    if (i2c_wait_flag(&I2C1->SR1, I2C_SR1_BTF, I2C_TIMEOUT)) return -1;

    I2C1->CR1 |= I2C_CR1_STOP;
    return 0;
}

六、注意事项

  • 不要依赖 HAL 的无限等待:HAL_I2C_Master_Transmit 默认 HAL_MAX_DELAY,从机 hold 时会死等;务必传入有限超时。
  • 恢复前先关外设:PE=0 或 SWRST,否则外设仍会驱动 SCL。
  • 9 个脉冲不是随便定的:从机可能停在任意 bit,9 个 SCL 足以让它把剩余位发完并释放 SDA。
  • 上拉电阻要合适:4.7k 是常见值,太小增加功耗,太大上升沿变缓,在快速模式下可能误判。
  • 区分时钟拉伸与总线锁死:拉伸是协议行为,锁死是异常;前者应优化从机处理时间,后者才需要恢复。
  • 多主场景:仲裁丢失也会让 BUSY 置位,恢复逻辑要能区分。

七、小结

时钟拉伸本身不是 bug,但从机异常或主机未处理超时,就会演变成总线挂死。定位的关键是:先看 GPIO 实际电平,再读 SR2.BUSY,最后用 9 脉冲恢复。把超时、状态检查、恢复函数写进驱动,才能让 I2C 在复杂电磁环境中稳定运行。