STM32F103 学习笔记-24-I2C-读写EEPROM(第1节)-I2C物理层介绍

一、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 的每一个字节传输都需要接收方返回应答信号,确保数据传输正确:

  1. 发送方发送完 8 位数据后,释放 SDA 总线

  2. 在第 9 个时钟周期 ,接收方将 SDA 拉低 ​ 表示 应答(ACK),表示已成功接收数据

  3. 如果接收方不拉低 ​ 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

六、参考出处

  1. 《零死角玩转 STM32F103 - 指南者》第 24 章 I2C - 读写 EEPROM

  2. 《STM32F10xxx 参考手册》第 24 章 I2C 接口

  3. AT24C02 数据手册

相关推荐
一隅论数智5 天前
给AI一张“业务概念地图“:本体如何从哲学走向企业智能
大数据·人工智能·经验分享·笔记·学习·学习方法·政务
XiHongShi20165 天前
STM32F407 RTC定时器例程,建议保存
stm32·单片机·学习
爱吃苹果的日记本5 天前
离散数学第六课
学习·离散数学
优信电子5 天前
AIT1001 非接触式红外测温模块 STM32 驱动开发实战:从引脚到代码全解析
stm32·嵌入式·温度传感器·红外测温·非接触式测温·ait1001
AI职业加油站5 天前
AI智能体应用工程师证书:政策红利下的职业新风口
大数据·运维·人工智能·学习·职场发展
深圳老胡5 天前
STM32F407 控制 L6470 步进电机驱动 —— 控制过程简介
笔记·stm32·单片机·嵌入式硬件·代码规范
旖旎夜光5 天前
力控面试题 01.01: 判定字符是否唯一(位运算) —— 题解
c++·学习·算法·leetcode·力控
奇思妙想聪明勤奋的小羊5 天前
DeepAgents第5章:子Agent 与上下文隔离—让 Agent学会委派
人工智能·python·学习·语言模型
wuyk5555 天前
《WiFi 嵌入式物联网开发全套实战》| 第 16 章 ESP32 AP+STA 双模共存原理与工程坑点
网络·stm32·物联网