## 1. I2C 协议核心概念 I2C(Inter-Integrated Circuit)由 Philips 设计,是一种**半双工**、**同步**、**多主机**总线。物理层只有两根线: - **SCL**:串行时钟线,由主机产生 - **SDA**:串行数据线,双向传输,开漏输出 每根线必须接上拉电阻(典型 4.7kΩ),这也是调试时最容易被忽略的点。 ### 1.1 帧格式与地址 一次完整传输包含: ```text START | 7-bit 设备地址 | R/W | ACK | 数据... | ACK/NACK | STOP ``` - **START**:SCL 高电平时,SDA 产生下降沿 - **STOP**:SCL 高电平时,SDA 产生上升沿 - **设备地址**:7 位,通常硬件引脚(A0/A1/A2)决定低几位 - **R/W**:0 表示写,1 表示读 - **ACK**:接收方在第 9 个时钟拉低 SDA 表示应答 以 AT24C02 为例,其地址固定为 `0b1010`,加上 A2/A1/A0 引脚电平,再拼上 R/W 位,得到: ```c #define AT24C02_ADDR_W 0xA0 // 写地址 #define AT24C02_ADDR_R 0xA1 // 读地址 ``` ## 2. STM32 硬件 I2C 外设与初始化 STM32 的 I2C 外设功能强大,但寄存器操作复杂。推荐使用 **STM32 HAL 库**,既有性能保障又易于维护。 ### 2.1 初始化步骤 1. 开启 GPIO 时钟和 I2C 时钟 2. 配置 SCL/SDA 引脚为开漏复用模式 3. 设置 I2C 时序参数(时钟速度、上升时间等) 4. 使能 I2C 外设 HAL 库中,一切封装为 `I2C_HandleTypeDef`: ```c I2C_HandleTypeDef hi2c1; void MX_I2C1_Init(void) { hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 100000; // 100kHz 标准模式 hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; // 主机模式不需要 hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode = I2D_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c1) != HAL_OK) { Error_Handler(); } } ``` **注意**:部分 STM32 型号(如 F1 系列)的硬件 I2C 有已知 bug,如果通信不稳定,可考虑用 GPIO 模拟 I2C。后文代码基于硬件 I2C,但思路同样适用。 ## 3. 读写 EEPROM(AT24C02) AT24C02 容量 256 字节,按页(8 字节)组织,写入时需要先发送设备地址,再发送内部字节地址。 ### 3.1 写单个字节 ```c HAL_StatusTypeDef EEPROM_WriteByte(uint8_t addr, uint8_t data) { uint8_t buf[2] = { addr, data }; return HAL_I2C_Master_Transmit(&hi2c1, AT24C02_ADDR_W, buf, 2, 100); } ``` ### 3.2 读单个字节 读操作需要**先写后读**——先发设备地址+字节地址,然后重新发送起始条件,再发读地址: ```c HAL_StatusTypeDef EEPROM_ReadByte(uint8_t addr, uint8_t *data) { // 先写字节地址 if (HAL_I2C_Master_Transmit(&hi2c1, AT24C02_ADDR_W, &addr, 1, 100) != HAL_OK) return HAL_ERROR; // 然后读数据 return HAL_I2C_Master_Receive(&hi2c1, AT24C02_ADDR_R, data, 1, 100); } ``` **关键**:AT24C02 执行写操作后会进入内部编程周期(约 5ms),此时不响应任何请求,所以写完要延时或轮询 ACK。 ### 3.3 页写入与连续读 连续读写时,HAL 提供了便捷函数: ```c // 连续写 16 字节到地址 0x00 HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_W, 0x00, I2C_MEMADD_SIZE_8BIT, txBuffer, 16, 200); // 连续读 16 字节从地址 0x00 HAL_I2C_Mem_Read(&hi2c1, AT24C02_ADDR_R, 0x00, I2C_MEMADD_SIZE_8BIT, rxBuffer, 16, 200); ``` **注意页边界**:跨页写入会回卷覆盖,必须拆分为两次写操作。 ## 4. 读写传感器(SHT30 温湿度) SHT30 是 I2C 接口的温湿度传感器,地址固定 `0x44`(写 0x88,读 0x89)。先发送命令,再等待测量完成,最后读取 6 个字节(温度 2 字节 + CRC + 湿度 2 字节 + CRC)。 ```c #define SHT30_ADDR_W 0x88 #define SHT30_ADDR_R 0x89 HAL_StatusTypeDef SHT30_ReadData(float *temp, float *humi) { uint8_t cmd[2] = {0x2C, 0x06}; // 高重复性测量命令 uint8_t buf[6]; // 发送测量命令 if (HAL_I2C_Master_Transmit(&hi2c1, SHT30_ADDR_W, cmd, 2, 100) != HAL_OK) return HAL_ERROR; HAL_Delay(20); // 等待测量完成(典型 15ms) // 读取 6 字节数据 if (HAL_I2C_Master_Receive(&hi2c1, SHT30_ADDR_R, buf, 6, 100) != HAL_OK) return HAL_ERROR; // 解析温度:14 位有效数据,公式 = -45 + 175 * raw / 65535 uint16_t rawT = (buf[0] << 8) | buf[1]; *temp = -45.0f + 175.0f * rawT / 65535.0f; // 解析湿度:公式 = 100 * raw / 65535 uint16_t rawH = (buf[3] << 8) | buf[4]; *humi = 100.0f * rawH / 65535.0f; // CRC 校验由 buf[2] 和 buf[5] 提供,如需严格校验请实现 CRC8 算法 return HAL_OK; } ``` ## 5. 常见问题与排查技巧 ### 5.1 总线死锁(SDA 被拉低) 设备在异常时可能持续拉低 SDA,导致总线永久阻塞。解决思路: - 在初始化前强制切换 SDA 为 GPIO 输出,发送 9 个时钟脉冲,释放总线 - 或使用硬件复位引脚复位传感器 ### 5.2 上拉电阻阻值不合适 - 100kHz 标准模式:4.7kΩ - 10kΩ - 400kHz 快速模式:1kΩ - 4.7kΩ - 过大的电阻会导致上升沿变慢,波形畸变 ### 5.3 地址错误 使用逻辑分析仪抓波形,确认发送的地址字节与设备地址完全匹配。注意 7 位地址与 8 位地址(含 R/W)的换算。 ### 5.4 时序问题 - 某些传感器需要延长 START 到 CLK 的时间,可适当降低 ClockSpeed - 对于睡眠唤醒型设备,写命令后必须延时 ## 6. 总结 I2C 的优雅在于极简的物理连接,但代价是协议状态机的复杂性。使用 STM32 HAL 库能大幅降低开发门槛,但底层时序仍需开发者心中有数。核心要点: - 正确配置上拉电阻 - 理解设备地址与 R/W 位的组合 - 注意 EEPROM 的页边界与写周期延时 - 用示波器或逻辑分析仪验证时序 掌握了这些,你就能轻松驾驭大多数 I2C 外设。下次遇到传感器无响应,记得先查地址、再查波形、最后查时序——大概率是这三个问题之一了。