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,则写地址为 0xA00x50 << 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:116: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 = 1BTF = 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.cbsp_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 数据手册

相关推荐
通信小呆呆36 分钟前
当算法有了“五感”:多模态数据融合如何向人体感官协同学习?
人工智能·学习·算法·机器学习·机器人
H__Rick1 小时前
自动对焦学习-3
人工智能·学习·计算机视觉
✎ ﹏梦醒͜ღ҉繁华落℘1 小时前
单片机基础知识---stm32单片机的优先级
stm32·单片机·mongodb
Daisy Lee1 小时前
量化学习-第1章-什么是量化金融
学习·金融·datawhale
Alsn862 小时前
等待学习-学习目录:Docker 容器安全攻防
学习·安全·docker
YM52e2 小时前
买菜计算器小应用 - HarmonyOS ArkUI 开发实战-PC版本
学习·华为·harmonyos·鸿蒙·鸿蒙系统
小雨下雨的雨2 小时前
HarmonyOS ArkUI训练营入门-组件掌握系列-Animation 动画效果实现-PC版本
学习·华为·harmonyos·鸿蒙
闪闪发亮的小星星3 小时前
高斯光以及高斯光公式解释
笔记
cqbzcsq3 小时前
CellFlow虚拟细胞论文阅读
论文阅读·人工智能·笔记·学习·生物信息
牛根生同志4 小时前
SPI数据收发的时候 TXE与RXNE标志位置位的时机
stm32·spi·transfer