一、I2C 协议简介
I2C(Inter-Integrated Circuit,芯片间总线)是飞利浦公司开发的串行通讯协议,仅需 2 根线 即可实现多设备间的全双工通讯,广泛应用于 MCU 与 EEPROM、传感器、触摸屏等外设的连接。
生活类比:I2C 总线就像小区的公共楼道。SCL 时钟线是楼道的公共时钟(统一大家的行动节奏),SDA 数据线是大家传递物品的通道。每个住户(外设)都有唯一的门牌号(设备地址),只有被叫到门牌号的住户才会开门接收/发送物品。楼道尽头有一个弹簧门(上拉电阻),没人用的时候自动关上(总线为高电平),有人用的时候才推开(拉低总线)。
1.1 物理层特性
I2C 的物理层定义了电气连接和硬件要求:
| 项目 | 说明 |
|---|---|
| 总线结构 | 仅需 2 根双向总线 :SCL (Serial Clock)串行时钟线 + SDA(Serial Data)串行数据线 |
| 多设备支持 | 一条总线上可挂载多个设备,最多支持 127 个 7 位地址的设备 |
| 上拉电阻 | SCL 和 SDA 线必须通过 4.7kΩ 电阻上拉到电源(通常 3.3V 或 5V) |
| 高阻态特性 | 所有 I2C 设备空闲时输出高阻态(相当于与总线断开),避免干扰其他设备通讯 |
| 传输速率 | 标准模式 100 kbit/s / 快速模式 400 kbit/s (最常用)/ 高速模式 3.4 Mbit/s(多数外设不支持) |
| 电容限制 | 总线总电容不能超过 400 pF,否则会导致信号失真 |
-
SCL (Serial Clock):串行时钟线,由主机产生,用于同步数据收发
-
SDA(Serial Data):串行数据线,用于传输地址和数据


1.2 协议层核心机制
协议层定义了数据传输的时序和规则,所有 I2C 设备必须遵守。
1.2.1 起始与停止信号
通讯的开始和结束由主机主动发起:
-
起始信号(S) :SCL 为高电平时,SDA 从高电平 → 低电平切换
-
停止信号(P) :SCL 为高电平时,SDA 从低电平 → 高电平切换
1.2.2 数据有效性
I2C 在 SCL 的每个时钟周期传输 1 位 数据:
-
SCL 高电平时 :SDA 数据必须稳定 ,此时表示的数据有效(高为
1,低为0) -
SCL 低电平时 :SDA 可以切换电平,为下一位数据做准备

1.2.3 地址与数据方向
I2C 通讯的第一个字节是 从机地址 + 读写位:
-
前 7 位 是从机的设备地址(每个设备唯一)
-
第 8 位 是读写方向位:
-
0:主机向从机写数据(主发送模式) -
1:主机从从机读数据(主接收模式)
-
注意 :地址本身是 7 位,加上读写位后构成一个 8 位字节。例如 AT24C02 的 7 位地址是
0x50,则写地址为0xA0(0x50 << 1),读地址为0xA1((0x50 << 1) | 1)。
1.2.4 应答机制
I2C 的每一个字节传输都需要接收方返回应答信号,确保数据传输正确:
-
发送方发送完 8 位数据后,释放 SDA 总线
-
在第 9 个时钟周期 ,接收方将 SDA 拉低 表示 应答(ACK),表示已成功接收数据
-
如果接收方不拉低 SDA(保持高电平),表示 非应答(NACK),表示接收失败或要求停止传输

二、STM32 的 I2C 外设架构
STM32F103 内置了硬件 I2C 外设,可自动生成符合协议的时序,无需 CPU 手动控制每个电平,大大减轻了 CPU 负担。
2.1 软件模拟 vs 硬件 I2C
| 方式 | 实现 | 优点 | 缺点 |
|---|---|---|---|
| 软件模拟 I2C | 用普通 GPIO 引脚,通过 CPU 直接控制电平变化来模拟 I2C 时序 | 引脚灵活,不受硬件限制,兼容性好 | 占用大量 CPU 资源,速度慢 |
| 硬件 I2C | 由 STM32 内部的 I2C 外设自动处理时序 | 速度快,CPU 占用率低 | 引脚固定(可通过重映射修改),部分早期 STM32 芯片存在硬件 bug |
2.2 I2C 外设架构
STM32 的 I2C 外设由 4 个核心部分组成:
┌─────────────────────────────────────────────┐
│ 整体控制逻辑 │
│ CR1/CR2 控制寄存器 SR1/SR2 状态寄存器 │
└─────────┬─────────────────────┬─────────────┘
│ │
┌─────────────────┐ ┌─────▼─────┐ ┌───────▼───────┐
│ 时钟控制逻辑 │ │ 数据控制逻辑│ │ 中断/DMA控制 │
│ CCR 寄存器 │ │ 移位寄存器 │ └───────────────┘
└─────────┬─────┘ │ 数据寄存器DR│
│ │ 地址寄存器OAR│
│ └─────┬───────┘
│ │
▼ ▼
SCL SDA
2.2.1 通讯引脚
STM32F103 有 2 个 I2C 外设,引脚映射如下:
| 外设 | 默认引脚 | 重映射引脚 |
|---|---|---|
| I2C1 | SCL: PB6 SDA: PB7 | SCL: PB8 SDA: PB9 |
| I2C2 | SCL: PB10 SDA: PB11 | 无 |
重点 :I2C 引脚必须配置为 复用开漏输出(AF_OD) 模式。这是因为 I2C 需要线与特性 ,如果配置为推挽输出,当两个设备同时输出高低电平时会发生短路。
2.2.2 时钟控制逻辑
SCL 时钟由 I2C 外设根据 **时钟控制寄存器(CCR)** 自动生成:
-
支持标准模式(100 kHz)和快速模式(400 kHz)
-
快速模式下可配置 SCL 占空比:
Tlow/Thigh = 2:1或16:9 -
时钟计算公式(PCLK1 = 36 MHz,STM32F103 默认 APB1 时钟):
-
标准模式:
SCL频率 = PCLK1 / (2 × CCR) -
快速模式(2:1 占空比):
SCL频率 = PCLK1 / (3 × CCR)
-
示例:配置 400 kHz 快速模式
SCL 周期 = 1 / 400000 = 2.5 μs
Thigh ≈ 2.5 μs / 3 ≈ 0.833 μs
CCR = Thigh / (1 / 36000000) ≈ 30
因此向 CCR 寄存器写入 30即可得到约 400 kHz 的 SCL 时钟。
2.2.3 数据控制逻辑
| 部件 | 作用 |
|---|---|
| 数据移位寄存器 | 将并行数据转换为串行数据通过 SDA 发送,或将 SDA 接收的串行数据转换为并行数据 |
| **数据寄存器(DR)** | 存储待发送或已接收的字节数据 |
| **地址寄存器(OAR1/OAR2)** | 存储 STM32 作为从机时的自身地址,支持双地址模式 |
2.2.4 整体控制逻辑
| 寄存器 | 作用 |
|---|---|
| **控制寄存器(CR1/CR2)** | 配置 I2C 的工作模式、使能外设、产生起始/停止信号等 |
| **状态寄存器(SR1/SR2)** | 反映 I2C 的当前工作状态,如起始信号已发送、地址已匹配、数据已发送等 |
2.3 I2C 主模式通讯事件
STM32 的 I2C 外设会在通讯的不同阶段产生对应的事件,软件通过检测这些事件来推进通讯流程:
| 事件 | 标志位 | 含义 |
|---|---|---|
| EV5 | SB = 1 |
起始信号已发送 |
| EV6 | ADDR = 1 |
从机地址已发送并收到应答 |
| EV8 | TxE = 1 |
发送缓冲区为空,可写入下一个数据 |
| EV8_2 | TxE = 1且 BTF = 1 |
最后一个数据已发送完成 |
| EV7 | RxNE = 1 |
接收缓冲区非空,可读取数据 |
三、I2C 标准库初始化详解
STM32 标准库提供了 I2C_InitTypeDef结构体用于配置 I2C 外设:
typedef struct {
uint32_t I2C_ClockSpeed; /* SCL 时钟频率,最大 400000 */
uint16_t I2C_Mode; /* 工作模式:I2C 模式或 SMBus 模式 */
uint16_t I2C_DutyCycle; /* 快速模式下的占空比 */
uint16_t I2C_OwnAddress1; /* STM32 作为从机时的自身地址 */
uint16_t I2C_Ack; /* 使能或禁止应答 */
uint16_t I2C_AcknowledgedAddress; /* 地址长度:7 位或 10 位 */
} I2C_InitTypeDef;
各成员详细说明:
| 成员 | 说明 |
|---|---|
I2C_ClockSpeed |
设置 SCL 时钟频率,通常设为 400000(400 kHz)或 100000(100 kHz) |
I2C_Mode |
一般设为 I2C_Mode_I2C(标准 I2C 模式) |
I2C_DutyCycle |
快速模式下的占空比,一般设为 I2C_DutyCycle_2(2:1) |
I2C_OwnAddress1 |
STM32 作为从机时的地址;主机模式下也需要设置(只要不与总线上其他设备冲突即可) |
I2C_Ack |
必须设为 I2C_Ack_Enable(使能应答),否则无法正常通讯 |
I2C_AcknowledgedAddress |
一般设为 I2C_AcknowledgedAddress_7bit(7 位地址模式) |
四、AT24C02 EEPROM 读写实验
AT24C02 是一款 **2 Kbit(256 字节)** 的串行 EEPROM,掉电后数据不丢失,采用 I2C 接口与 MCU 通讯。
4.1 硬件设计
AT24C02 的引脚连接如下:
| AT24C02 引脚 | 连接目标 | 说明 |
|---|---|---|
| VCC | 3.3 V 电源 | --- |
| GND | 地 | --- |
| SCL | STM32 的 **I2C1_SCL(PB6)** | I2C 时钟线 |
| SDA | STM32 的 **I2C1_SDA(PB7)** | I2C 数据线 |
| A0 / A1 / A2 | 全部接地 | 7 位设备地址 = 0x50 |
| WP | 接地 | 关闭写保护 |
4.2 软件设计
将 I2C 和 EEPROM 相关代码封装在 bsp_i2c_ee.c和 bsp_i2c_ee.h文件中。
4.2.1 头文件定义(bsp_i2c_ee.h)
#ifndef __BSP_I2C_EE_H
#define __BSP_I2C_EE_H
#include "stm32f10x.h"
/*==================== I2C 外设定义 ====================*/
#define EEPROM_I2Cx I2C1
#define EEPROM_I2C_CLK RCC_APB1Periph_I2C1
#define EEPROM_I2C_GPIO_CLK RCC_APB2Periph_GPIOB
/* I2C 引脚定义 */
#define EEPROM_I2C_SCL_PIN GPIO_Pin_6
#define EEPROM_I2C_SCL_PORT GPIOB
#define EEPROM_I2C_SDA_PIN GPIO_Pin_7
#define EEPROM_I2C_SDA_PORT GPIOB
/*==================== EEPROM 参数定义 ====================*/
#define EEPROM_ADDRESS 0xA0 /* 写地址(7 位地址 0x50 左移 1 位) */
#define I2C_Speed 400000 /* SCL 时钟频率 400 kHz */
#define I2Cx_OWN_ADDRESS7 0x0A /* STM32 自身地址(从机模式备用) */
#define I2C_PageSize 8 /* AT24C02 每页 8 字节 */
/*==================== 超时时间定义 ====================*/
#define I2CT_FLAG_TIMEOUT ((uint32_t)0x1000)
#define I2CT_LONG_TIMEOUT ((uint32_t)(10 * I2CT_FLAG_TIMEOUT))
/*==================== 函数声明 ====================*/
void I2C_EE_Init(void);
uint32_t I2C_EE_ByteWrite(uint8_t* pBuffer, uint8_t WriteAddr);
uint32_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint8_t NumByteToWrite);
void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint16_t NumByteToWrite);
uint8_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr, uint16_t NumByteToRead);
void I2C_EE_WaitEepromStandbyState(void);
#endif /* __BSP_I2C_EE_H */
4.2.2 GPIO 和 I2C 初始化(bsp_i2c_ee.c)
#include "bsp_i2c_ee.h"
#include "usart.h"
#include <stdio.h>
static __IO uint32_t I2CTimeout = I2CT_LONG_TIMEOUT;
/**
* @brief I2C GPIO 初始化
* @param 无
* @retval 无
*/
static void I2C_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能 I2C 和 GPIO 时钟 */
RCC_APB1PeriphClockCmd(EEPROM_I2C_CLK, ENABLE);
RCC_APB2PeriphClockCmd(EEPROM_I2C_GPIO_CLK, ENABLE);
/* 配置 SCL 和 SDA 为复用开漏输出 */
GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN | EEPROM_I2C_SDA_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; /* 复用开漏输出 ------ 关键! */
GPIO_Init(EEPROM_I2C_SCL_PORT, &GPIO_InitStructure);
}
/**
* @brief I2C 模式配置
* @param 无
* @retval 无
*/
static void I2C_Mode_Config(void)
{
I2C_InitTypeDef I2C_InitStructure;
/* I2C 配置 */
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 = I2Cx_OWN_ADDRESS7;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;
/* 初始化 I2C */
I2C_Init(EEPROM_I2Cx, &I2C_InitStructure);
/* 使能 I2C */
I2C_Cmd(EEPROM_I2Cx, ENABLE);
}
/**
* @brief I2C EEPROM 初始化
* @param 无
* @retval 无
*/
void I2C_EE_Init(void)
{
I2C_GPIO_Config();
I2C_Mode_Config();
}
/**
* @brief 超时处理函数
* @param errorCode: 错误代码
* @retval 0: 失败
*/
static uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode)
{
printf("I2C 等待超时! 错误代码: %d\r\n", errorCode);
return 0;
}
C 语言解释 :
__IO是标准库定义的宏,等价于volatile,告诉编译器这个变量可能被硬件修改,不要优化掉。如果不加volatile,编译器可能会认为I2CTimeout在循环中没有被修改,从而优化掉整个超时判断逻辑,导致程序死循环。
4.2.3 字节写入函数
/**
* @brief 向 EEPROM 写入一个字节
* @param pBuffer: 待写入数据的指针
* @param WriteAddr: EEPROM 内部写入地址 (0 ~ 255)
* @retval 1: 成功, 0: 失败
*/
uint32_t I2C_EE_ByteWrite(uint8_t* pBuffer, uint8_t WriteAddr)
{
/* ---------- 1. 产生起始信号 ---------- */
I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
/* 等待 EV5 事件:起始信号已发送 */
I2CTimeout = I2CT_FLAG_TIMEOUT;
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);
}
/* ---------- 2. 发送 EEPROM 写地址 ---------- */
I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Transmitter);
/* 等待 EV6 事件:地址已发送并收到应答 */
I2CTimeout = I2CT_FLAG_TIMEOUT;
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);
}
/* ---------- 3. 发送 EEPROM 内部写入地址 ---------- */
I2C_SendData(EEPROM_I2Cx, WriteAddr);
/* 等待 EV8 事件:数据已发送 */
I2CTimeout = I2CT_FLAG_TIMEOUT;
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);
}
/* ---------- 4. 发送待写入的数据 ---------- */
I2C_SendData(EEPROM_I2Cx, *pBuffer);
/* 等待 EV8_2 事件:最后一个数据已发送完成 */
I2CTimeout = I2CT_FLAG_TIMEOUT;
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
}
/* ---------- 5. 产生停止信号 ---------- */
I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
return 1;
}
4.2.4 等待 EEPROM 内部擦写完成(空闲检测)
EEPROM 写入数据后需要一定时间(最大 5 ms )进行内部擦写操作,此时不会响应主机的任何请求。通过不断发送写地址并检测应答来判断 EEPROM 是否空闲:
/**
* @brief 等待 EEPROM 内部擦写完成(轮询 ACK 法)
* @param 无
* @retval 无
*/
void I2C_EE_WaitEepromStandbyState(void)
{
vu16 SR1_Tmp = 0;
do
{
/* 产生起始信号 */
I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
/* 读取 SR1 寄存器(用来清除起始条件后的状态) */
SR1_Tmp = I2C_ReadRegister(EEPROM_I2Cx, I2C_Register_SR1);
/* 发送 EEPROM 写地址(看 EEPROM 是否 ACK) */
I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Transmitter);
}
/* 等待 ADDR 位被置 1(即 EEPROM 应答了) */
while (!(I2C_ReadRegister(EEPROM_I2Cx, I2C_Register_SR1) & 0x0002));
/* 清除 AF 标志 */
I2C_ClearFlag(EEPROM_I2Cx, I2C_FLAG_AF);
/* 产生停止信号 */
I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
}
4.2.5 页写入函数
AT24C02 支持页写入 ,一次最多可写入 8 字节(一页),比单字节写入快很多:
/**
* @brief 向 EEPROM 写入一页数据(最多 8 字节)
* @param pBuffer: 待写入数据的指针
* @param WriteAddr: EEPROM 内部起始写入地址
* @param NumByteToWrite: 写入字节数 (≤ 8)
* @retval 1: 成功, 0: 失败
*/
uint32_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint8_t NumByteToWrite)
{
I2CTimeout = I2CT_LONG_TIMEOUT;
/* 等待总线空闲 */
while (I2C_GetFlagStatus(EEPROM_I2Cx, I2C_FLAG_BUSY))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(4);
}
/* --- 起始信号 --- */
I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(5);
}
/* --- 发送写地址 --- */
I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(6);
}
/* --- 发送 EEPROM 内部起始地址 --- */
I2C_SendData(EEPROM_I2Cx, WriteAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(7);
}
/* --- 循环写入数据 --- */
while (NumByteToWrite--)
{
I2C_SendData(EEPROM_I2Cx, *pBuffer);
pBuffer++;
I2CTimeout = I2CT_FLAG_TIMEOUT;
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(8);
}
}
/* --- 停止信号 --- */
I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
return 1;
}
4.2.6 多字节写入函数(跨页安全拆分)
对于超过一页的数据,需要将其分成多个页进行写入,并处理起始地址不对齐的情况:
/**
* @brief 向 EEPROM 写入任意长度的数据(跨页安全拆分)
* @param pBuffer: 待写入数据的指针
* @param WriteAddr: EEPROM 内部起始写入地址
* @param NumByteToWrite: 写入字节数
* @retval 无
*/
void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint16_t NumByteToWrite)
{
uint8_t NumOfPage = 0;
uint8_t NumOfSingle = 0;
uint8_t Addr = 0;
uint8_t count = 0;
uint8_t temp = 0;
/* 计算起始地址在页内的偏移 */
Addr = WriteAddr % I2C_PageSize;
/* 当前页还能写入多少字节 */
count = I2C_PageSize - Addr;
/* 整页数 */
NumOfPage = NumByteToWrite / I2C_PageSize;
/* 剩余不满一页的字节数 */
NumOfSingle = NumByteToWrite % I2C_PageSize;
/* ======== Case 1:起始地址正好对齐到页边界 ======== */
if (Addr == 0)
{
/* 数据不足一页 */
if (NumOfPage == 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
/* 数据 ≥ 一页 */
else
{
while (NumOfPage--)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
I2C_EE_WaitEepromStandbyState();
WriteAddr += I2C_PageSize;
pBuffer += I2C_PageSize;
}
/* 尾巴 */
if (NumOfSingle != 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
}
}
/* ======== Case 2:起始地址未对齐到页边界 ======== */
else
{
/* 连一页剩余空间都填不满 */
if (NumOfPage == 0)
{
if (NumOfSingle > count)
{
/* 先写满当前页 */
temp = NumOfSingle - count;
I2C_EE_PageWrite(pBuffer, WriteAddr, count);
I2C_EE_WaitEepromStandbyState();
WriteAddr += count;
pBuffer += count;
/* 再写剩下的 */
I2C_EE_PageWrite(pBuffer, WriteAddr, temp);
I2C_EE_WaitEepromStandbyState();
}
else
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
I2C_EE_WaitEepromStandbyState();
}
}
/* 数据超过当前页剩余空间 */
else
{
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / I2C_PageSize;
NumOfSingle = NumByteToWrite % I2C_PageSize;
/* 写满第一页(不对齐部分) */
I2C_EE_PageWrite(pBuffer, WriteAddr, count);
I2C_EE_WaitEepromStandbyState();
WriteAddr += count;
pBuffer += count;
/* 写所有整页 */
while (NumOfPage--)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
I2C_EE_WaitEepromStandbyState();
WriteAddr += I2C_PageSize;
pBuffer += I2C_PageSize;
}
/* 写尾巴 */
if (NumOfSingle != 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
}
}
}
4.2.7 数据读取函数
读取数据需要先写模式发内部地址 ,再发**重复起始(Repeated START)**切到读模式:
/**
* @brief 从 EEPROM 读取任意长度的数据
* @param pBuffer: 存储读取数据的缓冲区指针
* @param ReadAddr: EEPROM 内部起始读取地址
* @param NumByteToRead: 读取字节数
* @retval 1: 成功, 0: 失败
*/
uint8_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr, uint16_t NumByteToRead)
{
I2CTimeout = I2CT_LONG_TIMEOUT;
/* 等待总线空闲 */
while (I2C_GetFlagStatus(EEPROM_I2Cx, I2C_FLAG_BUSY))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(9);
}
/* ========== Phase 1:假写 ------ 发送内部地址 ========== */
I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10);
}
/* 发送写地址(目的是让 EEPROM 锁存内部地址指针) */
I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(11);
}
/* 发送要读取的内部地址 */
I2C_SendData(EEPROM_I2Cx, ReadAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(12);
}
/* ========== Phase 2:重复起始 → 切到读模式 ========== */
I2C_GenerateSTART(EEPROM_I2Cx, ENABLE); /* Repeated START */
I2CTimeout = I2CT_FLAG_TIMEOUT;
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(13);
}
/* 发送读地址 */
I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Receiver);
I2CTimeout = I2CT_FLAG_TIMEOUT;
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(14);
}
/* ========== Phase 3:循环读取 ========== */
while (NumByteToRead)
{
/* 最后一个字节:发 NACK + STOP */
if (NumByteToRead == 1)
{
I2C_AcknowledgeConfig(EEPROM_I2Cx, DISABLE); /* 禁止应答 → 发 NACK */
I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE); /* 产生停止信号 */
}
/* 等待 EV7:数据已收到 */
I2CTimeout = I2CT_LONG_TIMEOUT;
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_RECEIVED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(15);
}
*pBuffer = I2C_ReceiveData(EEPROM_I2Cx);
pBuffer++;
NumByteToRead--;
}
/* 重新使能应答,方便下一次通讯 */
I2C_AcknowledgeConfig(EEPROM_I2Cx, ENABLE);
return 1;
}
4.2.8 主函数测试(main.c)
#include "stm32f10x.h"
#include "bsp_led.h"
#include "bsp_usart.h"
#include "bsp_i2c_ee.h"
#include <stdio.h>
/* 读写缓冲区 */
uint8_t I2c_Buf_Write[256];
uint8_t I2c_Buf_Read[256];
/**
* @brief I2C EEPROM 读写测试
* @param 无
* @retval 1: 成功, 0: 失败
*/
uint8_t I2C_Test(void)
{
uint16_t i;
printf("写入的数据:\r\n");
/* 填充写入缓冲区 */
for (i = 0; i < 256; i++)
{
I2c_Buf_Write[i] = i;
printf("0x%02X ", I2c_Buf_Write[i]);
if (i % 16 == 15) printf("\r\n");
}
/* 写入数据到 EEPROM */
I2C_EE_BufferWrite(I2c_Buf_Write, 0, 256);
printf("\r\n写入完成\r\n");
printf("读出的数据:\r\n");
/* 从 EEPROM 读取数据 */
I2C_EE_BufferRead(I2c_Buf_Read, 0, 256);
/* 校验数据 */
for (i = 0; i < 256; i++)
{
if (I2c_Buf_Read[i] != I2c_Buf_Write[i])
{
printf("0x%02X ", I2c_Buf_Read[i]);
printf("\r\n错误: 写入与读出的数据不一致!\r\n");
return 0;
}
printf("0x%02X ", I2c_Buf_Read[i]);
if (i % 16 == 15) printf("\r\n");
}
printf("\r\nI2C AT24C02 读写测试成功!\r\n");
return 1;
}
int main(void)
{
/* 初始化 LED */
LED_GPIO_Config();
LED_BLUE; /* 亮蓝灯表示程序开始运行 */
/* 初始化串口 */
USART_Config();
printf("\r\n欢迎使用秉火 STM32F103 开发板\r\n");
printf("\r\n这是一个 I2C AT24C02 读写测试例程\r\n");
/* 初始化 I2C EEPROM */
I2C_EE_Init();
/* 进行读写测试 */
if (I2C_Test() == 1)
{
LED_GREEN; /* 亮绿灯表示测试成功 */
}
else
{
LED_RED; /* 亮红灯表示测试失败 */
}
while (1)
{
}
}
五、避坑指南
| # | 坑 | 后果 | 解决 |
|---|---|---|---|
| 1 | GPIO 模式配错 ------ 没用 AF_OD(复用开漏)而用了推挽 |
总线短路,无法通讯 | SCL/SDA 必须配成 GPIO_Mode_AF_OD |
| 2 | 忘记开时钟 ------ 只开了 GPIO 没开 I2C 外设时钟(或反过来) | 外设不工作,卡在等事件 | 确认 RCC_APB1PeriphClockCmd(I2Cx)和 RCC_APB2PeriphClockCmd(GPIOx)都调用了 |
| 3 | 没有超时处理 ------ while(等事件)无限等 |
硬件异常时程序死循环 | 每个等待循环加 I2CTimeout--判断 |
| 4 | 写入后不等空闲 ------ 写完立刻继续操作 EEPROM | EEPROM 还在内部擦写,不 ACK,后续全部失败 | 每次页写后调 I2C_EE_WaitEepromStandbyState()等它就绪 |
| 5 | 页写入越界 ------ 一次写超过 8 字节 | 地址回卷,覆盖页首数据 | BufferWrite必须按 8 字节分页,处理好不对齐起始 |
| 6 | 读最后一字节没发 NACK ------ 最后一字节仍发 ACK | 从机以为还要继续发,多读一字节导致帧错位 | 倒数第 1 字节前 AcknowledgeConfig(DISABLE)+ GenerateSTOP() |
| 7 | 总线锁死 ------ 异常退出导致 SDA 被拉低 | 以后所有通讯直接失败 | 初始化时手动给 SCL 模拟 9 个时钟脉冲把从机"拍出来"释放 SDA |
六、参考出处
-
《零死角玩转 STM32F103 - 指南者》第 24 章 I2C - 读写 EEPROM
-
《STM32F10xxx 参考手册》第 24 章 I2C 接口
-
AT24C02 数据手册


