STM32通过I2C实现EEPROM单字节读写操作详解
2026/7/19 4:33:42 网站建设 项目流程

1. 项目概述:STM32与EEPROM的单字节读写操作

在嵌入式系统开发中,EEPROM(电可擦可编程只读存储器)因其非易失性存储特性被广泛用于保存配置参数、校准数据等关键信息。本项目基于STM32微控制器,通过I2C总线实现对EEPROM的单字节读写操作,这是嵌入式硬件开发中的基础但至关重要的技能点。

作为day18内容的延续,本实验重点解决两个核心问题:

  1. 如何通过STM32的硬件I2C外设实现精确的时序控制
  2. 如何处理EEPROM特有的写入等待周期

关键提示:AT24C02系列EEPROM的典型写入周期为5ms,这意味着在连续写入操作时必须插入适当的延迟,否则会导致数据丢失。

2. 硬件架构与电路设计

2.1 I2C总线物理连接

典型的STM32与EEPROM连接方案:

  • SCL:PB8(I2C1_SCL)
  • SDA:PB9(I2C1_SDA)
  • 上拉电阻:4.7kΩ(VCC=3.3V时)

电路设计要点:

  • 必须使用开漏输出模式(GPIO_OType_OD)
  • 总线电容应小于400pF(长距离传输时需要降低上拉电阻值)
  • 地址引脚A0-A2接地时,AT24C02的7位设备地址为0x50

2.2 硬件初始化代码实现

void I2C_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; // 使能时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); // 配置引脚复用功能 GPIO_PinAFConfig(GPIOB, GPIO_PinSource8, GPIO_AF_I2C1); GPIO_PinAFConfig(GPIOB, GPIO_PinSource9, GPIO_AF_I2C1); // 配置GPIO GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType = GPIO_OType_OD; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; GPIO_Init(GPIOB, &GPIO_InitStructure); }

3. I2C外设配置详解

3.1 时钟配置计算

对于400kHz快速模式:

  • 假设PCLK1=42MHz
  • 目标SCL周期T = 1/400000 = 2.5μs
  • CCR = THIGH/(1/PCLK1) = (2.5μs/3)/(1/42MHz) ≈ 35

配置代码示例:

void I2C_Mode_Config(void) { I2C_InitTypeDef I2C_InitStructure; I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStructure.I2C_OwnAddress1 = 0x0A; // 任意未占用地址 I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockSpeed = 400000; I2C_Init(I2C1, &I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); }

3.2 关键状态标志解析

STM32硬件I2C状态寄存器关键位:

  • SB:起始位已发送
  • ADDR:地址已发送
  • BTF:字节传输完成
  • RxNE:接收数据寄存器非空
  • TxE:发送数据寄存器空

4. 单字节写入实现

4.1 完整写入流程

  1. 发送起始条件(START)
  2. 发送设备地址 + 写方向(0xA0)
  3. 发送内存地址(1字节)
  4. 发送数据字节
  5. 发送停止条件(STOP)

代码实现:

uint8_t I2C_EE_ByteWrite(uint8_t data, uint8_t addr) { // 等待总线空闲 while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); // 发送START I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 发送设备地址(写) I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // 发送内存地址 I2C_SendData(I2C1, addr); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 发送数据 I2C_SendData(I2C1, data); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 发送STOP I2C_GenerateSTOP(I2C1, ENABLE); return 1; }

4.2 写入等待处理

EEPROM内部写入需要时间(典型5ms),必须检测ACK:

void I2C_EE_WaitStandbyState(void) { do { I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter); } while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_GenerateSTOP(I2C1, ENABLE); }

5. 单字节读取实现

5.1 复合读取时序

  1. 发送START + 设备地址(写)
  2. 发送内存地址
  3. 发送重复START
  4. 发送设备地址(读)
  5. 接收数据(NACK结束)
  6. 发送STOP

代码实现:

uint8_t I2C_EE_ByteRead(uint8_t addr) { uint8_t data; // 发送START + 设备地址(写) while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // 发送内存地址 I2C_SendData(I2C1, addr); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 重复START + 设备地址(读) I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, 0xA1, I2C_Direction_Receiver); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); // 接收数据(NACK结束) I2C_AcknowledgeConfig(I2C1, DISABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); data = I2C_ReceiveData(I2C1); // 发送STOP I2C_GenerateSTOP(I2C1, ENABLE); I2C_AcknowledgeConfig(I2C1, ENABLE); return data; }

6. 常见问题与调试技巧

6.1 典型故障排查表

现象可能原因解决方案
无法检测到ACK1. 设备地址错误
2. 上拉电阻过大
3. 总线电容过大
1. 确认设备地址
2. 减小上拉电阻
3. 缩短总线长度
数据校验错误1. 未等待写入完成
2. 电源噪声干扰
3. 时序不符合规格
1. 增加等待周期
2. 加强电源滤波
3. 检查时钟配置
随机通信失败1. 总线竞争
2. 中断干扰
3. 静电干扰
1. 增加总线仲裁
2. 调整中断优先级
3. 改进屏蔽

6.2 示波器调试要点

  • SCL/SDA信号上升时间应<1μs(3.3V系统)
  • START条件后SCL第一个上升沿前SDA必须稳定
  • 数据变化必须发生在SCL低电平期间

6.3 性能优化建议

  1. 使用DMA传输实现批量操作
  2. 采用页写入模式(AT24C02页大小为8字节)
  3. 合理规划存储结构减少擦写次数

7. 进阶应用:页写入与连续读取

7.1 页写入优化

AT24C02支持页写入(最大8字节):

void I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t addr, uint8_t len) { // ... 前导部分与单字节写入相同 ... // 连续发送多个字节 while(len--) { I2C_SendData(I2C1, *pBuffer++); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); } I2C_GenerateSTOP(I2C1, ENABLE); }

7.2 连续读取技巧

void I2C_EE_SequentialRead(uint8_t* pBuffer, uint8_t addr, uint16_t len) { // ... 前导部分与单字节读取相同 ... while(len > 1) { while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); *pBuffer++ = I2C_ReceiveData(I2C1); len--; } // 最后一个字节用NACK I2C_AcknowledgeConfig(I2C1, DISABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); *pBuffer = I2C_ReceiveData(I2C1); I2C_GenerateSTOP(I2C1, ENABLE); I2C_AcknowledgeConfig(I2C1, ENABLE); }

在实际项目中,我发现硬件I2C对时序要求极为严格。曾经遇到过一个案例:当系统时钟配置为168MHz时,I2C通信偶尔失败。最终发现是APB1分频设置导致I2C时钟超出规格,调整分频系数后问题解决。这提醒我们,在配置I2C时钟时务必仔细计算,确保符合器件规格要求。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询