一、什么是时钟拉伸
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 状态不定。
定位分三步:
- 确认是时钟拉伸还是总线锁死:读 GPIO 输入电平,看 SCL/SDA 实际状态;
- 确认谁在拉低:断开从机或换从机,若 SCL 恢复高,则是从机 hold;
-
寄存器级确认:读
SR2.BUSY、SR1,判断外设是否卡在等待。
四、配置步骤
- 将 SCL/SDA 配置为开漏输出,并使能内部上拉(或外部 4.7k 上拉);
- 初始化 I2C,设置
FREQ、CCR、TRISE; - 在通信前后加入超时与总线状态检查;
- 加入总线恢复函数:模拟 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 在复杂电磁环境中稳定运行。