I2C 通信详解:STM32 读写 EEPROM 与传感器
👁 13 阅读 · 2026-08-14 · STM32 I2C
I2C 是嵌入式系统中最常用的板级总线之一,仅需两根线即可挂载多个设备。本文从协议帧格式出发,剖析 I2C 时序与地址机制,然后以 STM32 硬件 I2C 外设为例,手把手演示如何初始化、读写 AT24C02 EEPROM 与 SHT30 温湿度传感器,并给出完整的工程级代码。同时分享多字节读写、时钟拉伸、总线死锁等常见坑的排查经验,帮助你避开新手最容易犯的错误。
## 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 外设。下次遇到传感器无响应,记得先查地址、再查波形、最后查时序——大概率是这三个问题之一了。