STM32 I2C 通信 HAL 库踩坑记录

问题

HAL 库的 I2C 用起来感觉比 SPI 麻烦------有时候卡死在 HAL_I2C_Master_Transmit,有时候第一次通信正常第二次就超时。

下面是最常见的几个坑和对应的解决办法。

坑一:总线锁死(SCL 被拉低)

I2C 通信中断时(比如刚发完地址突然复位),从机可能把 SCL 或 SDA 锁死在低电平,后续所有 I2C 操作都超时。

现象: 第一次调用 HAL_I2C_Master_Transmit 就超时返回。

解决------复位 I2C 总线:

复制代码
void I2C_ResetBus(I2C_HandleTypeDef *hi2c)
{
    GPIO_InitTypeDef gpio = {0};
​
    // 将 SCL 和 SDA 临时设成普通 GPIO 输出
    gpio.Mode = GPIO_MODE_OUTPUT_OD;
    gpio.Pull = GPIO_PULLUP;
    gpio.Speed = GPIO_SPEED_FREQ_MEDIUM;
​
    // SCL 引脚(查你的原理图,这里是 PB6)
    gpio.Pin = GPIO_PIN_6;
    HAL_GPIO_Init(GPIOB, &gpio);
​
    // SDA 引脚(PB7)
    gpio.Pin = GPIO_PIN_7;
    HAL_GPIO_Init(GPIOB, &gpio);
​
    // 产生 9 个时钟脉冲,让从机释放总线
    for (int i = 0; i < 9; i++)
    {
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET);  // SCL Low
        HAL_Delay(1);
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);    // SCL High
        HAL_Delay(1);
    }
​
    // 产生 STOP 条件:SDA 在 SCL 高时上升沿
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);
    HAL_Delay(1);
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);  // SDA Low
    HAL_Delay(1);
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);    // SDA High → STOP
​
    // 重新初始化 I2C 外设
    HAL_I2C_DeInit(hi2c);
    HAL_I2C_Init(hi2c);
}

坑二:未使用超时参数

复制代码
// ❌ 超时设太大,卡死在这里 10 秒
HAL_I2C_Master_Transmit(&hi2c1, addr, data, len, 10000);
​
// ✅ 设一个合理的超时,超时就返回错误
if (HAL_I2C_Master_Transmit(&hi2c1, addr, data, len, 100) != HAL_OK)
{
    printf("I2C Error!\r\n");
    I2C_ResetBus(&hi2c1);  // 自动恢复
}

坑三:地址左移

I2C 的 7 位地址和 HAL 库传的地址关系:

复制代码
// 假设器件地址是 0x3C(7 位)
// HAL 库的 addr 参数要左移 1 位,即 0x78
// 但 CubeMX 生成的 MX_I2C1_Init 用的是 HAL_I2C_Init 结构体中的 AddressingMode
// 大部分示例直接传 0x78 而不是 0x3C
​
// 推荐:直接传左移后的地址
#define DEV_ADDR  (0x3C << 1)  // = 0x78
HAL_I2C_Master_Transmit(&hi2c1, DEV_ADDR, data, len, 100);

坑四:I2C 时序配置不对

I2C 时钟 = APB1 时钟(42MHz)/ 分频系数。

重要:F407 使用的是旧版 I2C 外设,寄存器叫 CCR 和 TRISE (不是 Timing,Timing 是 F0/F3/F7/L0/L4 等新版 I2C 外设的寄存器名)。最可靠的方式是让 HAL 库的 I2C_Init() 自动计算,而不是手动改寄存器。

复制代码
I2C_InitTypeDef I2C_InitStruct;
I2C_InitStruct.ClockSpeed = 400000;  // 快速模式 400kHz
I2C_InitStruct.DutyCycle = I2C_DUTYCYCLE_2;  // DUTY=0 或 DUTY=1
// ... 其他参数 ...
HAL_I2C_Init(&hi2c1);  // HAL 自动计算 CCR/TRISE

手动计算公式(仅供理解):

  • CCR(快速模式 DUTY=0):CCR = 42MHz / (3 × 400kHz) ≈ 35

  • CCR(快速模式 DUTY=1):CCR = 42MHz / (25 × 400kHz) ≈ 4

  • TRISE:TRISE = (最大上升时间 / T_PCLK1) + 1,100kHz 模式 TRISE≈43,400kHz 模式 TRISE≈13

CubeMX 中选择 Standard Mode / Fast Mode 它会自动算。

坑五:没有上拉电阻

I2C 的 SCL 和 SDA 是开漏输出,必须外部接上拉电阻(4.7kΩ 典型)。

如果你的模块是单独买的,通常自带。但如果用杜邦线直连芯片引脚,就可能忘记加上拉。

现象:有时能通信有时不能,波形上升沿很缓。

测:用示波器看 SCL/SDA 的上升沿------如果 > 1μs,说明上拉电阻太大或没有。

复制代码
// 紧急补救:STM32 内部有上拉电阻(约 40kΩ)
// 在 CubeMX GPIO 设置中把 SCL/SDA 的 Pull 改成 Pull-up
// 但 40kΩ 只适合低速短距离,标准还是用外部 4.7kΩ

总结

一句话解决
总线锁死 软件复位 I2C 总线
超时卡死 设短超时 + 错误处理
地址不对 7 位地址左移 1 位
时序不对 用 CubeMX 自动计算
波形不好 外加 4.7kΩ 上拉电阻
相关推荐
恒锐丰科技林技术员2 小时前
EG2181D 半桥栅极驱动芯片详解:高压 MOS/IGBT 驱动实用器件
经验分享·嵌入式硬件·硬件工程
国科安芯3 小时前
面向卫星测控应答机的抗辐射MCU信号采集与定时控制研究
单片机·嵌入式硬件·mcu·架构·卫星·商业航天·抗辐射
CC城子3 小时前
EtherNet/IP I/O 丢包排查(linux系统)
linux·单片机·tcp/ip·ethernetip
新晨单片机设计3 小时前
S010A-基于STM32单片机水温控制系统(数码管显示)【Proteus仿真+Keil程序+原理图】
stm32·单片机·嵌入式硬件
事界见闻4 小时前
高性能电机控制MCU选型对比:精度、功耗与实时性如何兼得?
单片机·嵌入式硬件
Brilliantwxx4 小时前
【STM32】 初认识USART串口
linux·stm32·单片机·嵌入式硬件·架构
分享资料5 小时前
用小电容消除高频信号的原因
嵌入式硬件·硬件工程
沐欣工作室_lvyiyi5 小时前
基于STM32的人脸识别门禁系统的设计与实现(论文+源码)
stm32·单片机·嵌入式硬件
zcmodeltech9 小时前
智慧矿山沙盘模型多场景控制系统设计——基于STM32与Modbus RTU的智能开采、无人矿卡、数字孪生全场景联动方案
数据库·stm32·单片机·嵌入式硬件·物联网
qq_402995759 小时前
Secure Boot Part 3 —— 信任根和信任链
arm开发·stm32·信任链