STM32 I2C协议详解:从原理到实战
一句话总结I2C 是用两根线搞定一堆外设的同步半双工协议。搞懂起始/停止/应答3个信号你就能用STM32驱动市场上 90% 的传感器。本文融合了野火、正点原子、江科大等精品教程的核心思想并结合 HAL 库实战经验重写1. 为什么 STM32 项目离不开 I2C1.1 I2C 是什么I2C (Inter-Integrated Circuit) 是飞利浦在1982年发明的两根线SCL (Serial Clock):时钟线完全由主机掌控SDA (Serial Data):数据线双向特点同步、半双工、串行多主多从一条总线理论可挂112个7位地址设备只需要2根线 2个上拉电阻极度省 IO1.2 I2C vs SPI vs USART特性I2CSPIUSART线数2根 (SCLSDA)4根 (SCKMOSIMISOCS)2根 (TXRX)速度低速 100k / 400k / 3.4M高速 10M中低速双工半双工全双工全双工组网多主多从地址寻址一主多从片选寻址点对点典型外设OLED, MPU6050, AT24C02, BMP280, RTCFlash, 屏幕, 无线模块蓝牙, GPS, 串口屏结论传输传感器、小数据、用线少选 I2C传大文件、要速度选 SPI板间远距离选 USART。2. 硬件层为什么 SDA/SCL 必须接上拉电阻这是 90% 新手踩的第一坑。STM32 的 I2C 引脚是开漏输出 (Open-Drain)结构类似这样输出0N-MOS导通强行把总线拉低到 GND输出1N-MOS截止引脚处于高阻态靠外部上拉电阻拉到 VCC好处支持线与逻辑— 只要有一个设备拉低总线就是低。只有所有设备都释放总线才为高。这正是多机仲裁和时钟拉伸的基础。阻值如何选经验值标准模式(100kHz)用4.7kΩ快速模式(400kHz)用2.2kΩ。 理论公式Rmax Tr / (0.8473 * Cb)Cb是总线电容Tr是上升时间。阻值太小功耗大太大上升沿慢导致时序错误。3. 协议层6大核心时序信号软件模拟I2C的核心就是用GPIO翻出下面6种波形。3.1 空闲状态SCL 和 SDA 都为高电平。3.2 起始信号 S 和停止信号 P这是 I2C 的帧头帧尾定义非常严格在 SCL 为高电平时SDA的跳变。起始 SSCL 高时SDA 从高变低 (下降沿)停止 PSCL 高时SDA 从低变高 (上升沿)记忆技巧野火教程版把SCL看成水面SDA看成船。船头下沉是Start船尾翘起是Stop。3.3 数据有效性 8位数据帧SCL 低电平时允许 SDA 变化准备数据SCL 高电平时SDA 必须稳定接收方在此刻采样每次传8位MSB 高位在前3.4 应答信号 ACK / NACK每传完1个字节8 bit第9个时钟是应答位。发送方在第9个时钟前释放 SDA。接收方拉低 SDA ACK (0)告诉对方我收到了继续发释放 SDA NACK (1)告诉对方我不想收了 / 地址没找到3.5 时钟拉伸 Clock Stretching这是I2C唯一的“从机反制主机”的手段。从机如果处理不过来比如MPU6050做DMP解算可以在 ACK 前把SCL 强行拉低主机发现 SCL 没变高就会乖乖等待直到从机释放 SCL。STM32 硬件I2C自动处理拉伸软件模拟I2C你需要在拉高SCL后加一句检查SCL是否真的变高的代码否则可能丢数据。4. 数据帧格式真正读写是怎么玩的I2C的第一个字节永远是7位从机地址 1位读写位 R/W。0 写 (Master - Slave)1 读 (Master - Slave)HAL库大坑手册上写的0x3C是7位地址HAL库函数要求传8位地址所以代码里要写成0x3C 1 0x78。这就是为什么你抄 0x3C 通信永远失败。4.1 4种核心读写时序1) 指定地址写最常用写 OLED、写寄存器S | DevAddrW | ACK | RegAddr | ACK | Data | ACK | P2) 当前地址读不指定寄存器读当前位置指针S | DevAddrR | ACK | Data | NACK | P3) 指定地址读复合格式最重要读 MPU6050必用。先用一次写操作把寄存器指针指过去再用重复起始 Sr 读。S | DevAddrW | ACK | RegAddr | ACK | Sr | DevAddrR | ACK | Data | NACK | P // Sr Repeated Start中间不发 PSCL一直被主机占着防止总线被别的Master抢走4) 连续读写在指定地址后连续发送多个Data适用于读多个字节例如MPU6050一次读14字节的加速度温度陀螺仪5. STM32的两种实现硬件I2C vs 软件模拟I2C对比项软件模拟 (Bit-Banging)硬件I2C (Peripheral)引脚任意GPIO固定复用引脚 PB6/PB7 (I2C1), PB10/PB11 (I2C2)实现CPU手动翻转GPIOdelay微秒配置CR1/CR2/SR/DR寄存器外设自动产生时序优点代码简单无Bug移植强不受死锁影响省CPU可配合DMA/中断支持多主机、SMBus缺点占用CPU不能用DMA速率不精确STM32F1/F4的硬件I2C有著名Bug容易卡死在BUSY标志需要复杂的错误处理推荐入门、新手、稳定性要求高的项目 强烈推荐追求极致效率且有能力处理EV5/EV6等事件的中高级开发者老司机建议90%的国产开源项目正点原子、江科大用软件I2C驱动OLED/MPU6050不是他们不会硬件I2C而是软件I2C真的更稳。5.1 STM32 硬件I2C关键寄存器以F103为例I2C_CR1 PE位: 外设使能开关配置前必须关闭I2C_CR1 START/STOP位: 产生起始/停止I2C_CR1 ACK位: 接收后是否给ACKI2C_SR1 SB位 (EV5): 起始位已发送I2C_SR1 ADDR位 (EV6): 地址已发送/匹配I2C_SR1 TXE/RXNE: 发送/接收寄存器空/非空6. 实战一CubeMX HAL 硬件I2C驱动0.96寸OLED这是最快跑通 I2C 的方法。1. CubeMX配置Connectivity - I2C1 - Mode: I2CPB6 - SCL, PB7 - SDA自动配置为复用开漏输出Parameter Settings:Clock Speed:100000(先用100k跑通再提速到400k)Duty Cycle: 50%Addressing Mode: 7Bit2. 核心代码解析OLED本质是写从机只需要一个写入函数。OLED有命令和数据之分通过第二个参数区分0x00写命令0x40写数据。#define OLED_ADDR 0x78 // 0x3C 18位地址 extern I2C_HandleTypeDef hi2c1; // 写命令/数据移植自官方SSD1306 void OLED_Write(uint8_t reg, uint8_t data) { // HAL_I2C_Mem_Write 完美匹配 “指定地址写” 时序 // hi2c, DevAddress, MemAddress(Reg), MemAddSize, pData, Size, Timeout HAL_I2C_Mem_Write(hi2c1, OLED_ADDR, reg, I2C_MEMADD_SIZE_8BIT, data, 1, 100); } void OLED_WR_CMD(uint8_t cmd){ OLED_Write(0x00, cmd); } void OLED_WR_DATA(uint8_t dat){ OLED_Write(0x40, dat); }常用HAL库函数全家桶HAL_I2C_Master_Transmit(hi2c1, DevAddr, pData, Size, Timeout); // 纯写不带寄存器地址 HAL_I2C_Master_Receive(hi2c1, DevAddr, pData, Size, Timeout); // 纯读 HAL_I2C_Mem_Write(hi2c1, DevAddr, MemAddr, MemAddSize, pData, Size, Timeout); // 指定地址写 (强推) HAL_I2C_Mem_Read(hi2c1, DevAddr, MemAddr, MemAddSize, pData, Size, Timeout); // 指定地址读 (强推) HAL_I2C_IsDeviceReady(hi2c1, DevAddr, Trials, Timeout); // 扫总线神器查设备在不在7. 实战二HAL读取MPU6050 - 指定地址读的完美例子MPU6050地址AD0接地为0x68接VCC为0x69。HAL用0xD0 / 0xD2。#define MPU6050_ADDR 0xD0 uint8_t MPU6050_ReadReg(uint8_t reg) { uint8_t data; HAL_I2C_Mem_Read(hi2c1, MPU6050_ADDR, reg, I2C_MEMADD_SIZE_8BIT, data, 1, 100); return data; } // 一次性读14字节效率更高 void MPU6050_Read_All(int16_t* ax, int16_t* ay, int16_t* az, int16_t* gx, int16_t* gy, int16_t* gz) { uint8_t buf[14]; HAL_I2C_Mem_Read(hi2c1, MPU6050_ADDR, 0x3B, I2C_MEMADD_SIZE_8BIT, buf, 14, 100); *ax (buf[0]8)|buf[1]; *ay (buf[2]8)|buf[3]; *az (buf[4]8)|buf[5]; // ... buf[6][7]是温度 ... *gx (buf[8]8)|buf[9]; *gy (buf[10]8)|buf[11]; *gz (buf[12]8)|buf[13]; }8. 实战三软件模拟I2C终极模板江科大风格亲测可移植MyI2C.h- 任意GPIO都行#ifndef __MYI2C_H #define __MYI2C_H #define SDA_PIN GPIO_Pin_9 #define SCL_PIN GPIO_Pin_8 #define SDA_PORT GPIOB #define SCL_PORT GPIOB void MyI2C_Init(void); void MyI2C_Start(void); void MyI2C_Stop(void); uint8_t MyI2C_SendByte(uint8_t byte); uint8_t MyI2C_ReceiveByte(void); uint8_t MyI2C_ReceiveAck(void); void MyI2C_SendAck(uint8_t ack); #endifMyI2C.c- 核心是空出时序单元拼接void MyI2C_W_SDA(uint8_t bit){ GPIO_WriteBit(SDA_PORT, SDA_PIN, (BitAction)bit); Delay_us(2); } void MyI2C_W_SCL(uint8_t bit){ GPIO_WriteBit(SCL_PORT, SCL_PIN, (BitAction)bit); Delay_us(2); } uint8_t MyI2C_R_SDA(void){ uint8_t bit GPIO_ReadInputDataBit(SDA_PORT, SDA_PIN); Delay_us(2); return bit; } void MyI2C_Start(void){ MyI2C_W_SDA(1); MyI2C_W_SCL(1); // 保证兼容SrSCL低时先释放SDA MyI2C_W_SDA(0); MyI2C_W_SCL(0); } void MyI2C_Stop(void){ MyI2C_W_SDA(0); MyI2C_W_SCL(1); MyI2C_W_SDA(1); } uint8_t MyI2C_SendByte(uint8_t byte){ for(uint8_t i0;i8;i){ MyI2C_W_SDA((byte (0x80i)) ? 1:0); MyI2C_W_SCL(1); MyI2C_W_SCL(0); } return 0; } uint8_t MyI2C_ReceiveAck(void){ uint8_t ack; MyI2C_W_SDA(1); // 主机释放SDA MyI2C_W_SCL(1); ack MyI2C_R_SDA(); MyI2C_W_SCL(0); return ack; // 0ACK, 1NACK } // 指定地址写 void I2C_WriteReg(uint8_t devAddr, uint8_t regAddr, uint8_t data){ MyI2C_Start(); MyI2C_SendByte(devAddr); MyI2C_ReceiveAck(); MyI2C_SendByte(regAddr); MyI2C_ReceiveAck(); MyI2C_SendByte(data); MyI2C_ReceiveAck(); MyI2C_Stop(); } // 指定地址读 - 复合时序 uint8_t I2C_ReadReg(uint8_t devAddr, uint8_t regAddr){ uint8_t data; MyI2C_Start(); MyI2C_SendByte(devAddr); MyI2C_ReceiveAck(); MyI2C_SendByte(regAddr); MyI2C_ReceiveAck(); MyI2C_Start(); // 重复起始 MyI2C_SendByte(devAddr|0x01); MyI2C_ReceiveAck(); data MyI2C_ReceiveByte(); MyI2C_SendAck(1); // NACK结束 MyI2C_Stop(); return data; }9. 调试排坑指南I2C不通的99%原因没接上拉电阻用万用表量 SDA/SCL空闲必须是 3.3V不是0V。地址搞错0x3C和0x78双重宇宙。HAL库扫地址函数HAL_I2C_IsDeviceReady写个for循环0-127扫一遍串口打印能ACK的地址。GPIO模式错软件模拟必须是GPIO_Mode_Out_OD开漏输出不能是推挽。硬件I2C是复用开漏。F1硬件I2C卡死I2C_FLAG_BUSY一直置位。解决总线恢复函数手动产生9个时钟脉冲把从机刷出来。void I2C_Bus_Recover(void){ GPIO_InitTypeDef g; // 将SCL配置为GPIO输出SDA输入 // ... for(int i0;i9;i){ GPIO_ResetBits(GPIOB, SCL_PIN); Delay_us(5); GPIO_SetBits(GPIOB, SCL_PIN); Delay_us(5); } // 再产生一个STOP GPIO_ResetBits(GPIOB, SDA_PIN); Delay_us(5); GPIO_SetBits(GPIOB, SCL_PIN); Delay_us(5); GPIO_SetBits(GPIOB, SDA_PIN); Delay_us(5); }速度太快杜邦线面包板寄生电容大400k跑不通先降到100k或50k。从机没上电 / 电源不共地最低级的错误查一下。10. 总结与面试常考点I2C为什么要开漏上拉 - 实现线与支持多主支持时钟拉伸。I2C一帧多少位 - 9位8位数据1位ACK。100k的I2C一秒传多少字节 - 100k/9 ≈ 11kB/s理论值考虑地址和开销更低。如何处理I2C死锁 - 9时钟法 产生STOP。解释一下重复起始和起始的区别 - Sr中间总线不释放防止被多主机抢占。参考与推荐学习路线江科大《STM32入门教程》I2C章节 - 软件I2C讲得最清楚 江科大STM32学习笔记8—— I2C通信协议_stm32 i2c通信协议-CSDN博客野火《STM32模块例程介绍》I2C协议详解 - 原理解析最透彻正点原子AT24C02/ MPU6050例程 - 实战代码最全 7FastBit STM32 I2C Clock Stretching 专题 - 深度理解时钟拉伸 4