1. 项目概述:STM32与EEPROM的单字节读写操作
在嵌入式系统开发中,EEPROM(电可擦可编程只读存储器)因其非易失性存储特性被广泛用于保存配置参数、校准数据等关键信息。本项目基于STM32微控制器,通过I2C总线实现对EEPROM的单字节读写操作,这是嵌入式硬件开发中的基础但至关重要的技能点。
作为day18内容的延续,本实验重点解决两个核心问题:
- 如何通过STM32的硬件I2C外设实现精确的时序控制
- 如何处理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 完整写入流程
- 发送起始条件(START)
- 发送设备地址 + 写方向(0xA0)
- 发送内存地址(1字节)
- 发送数据字节
- 发送停止条件(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 复合读取时序
- 发送START + 设备地址(写)
- 发送内存地址
- 发送重复START
- 发送设备地址(读)
- 接收数据(NACK结束)
- 发送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 典型故障排查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 无法检测到ACK | 1. 设备地址错误 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 性能优化建议
- 使用DMA传输实现批量操作
- 采用页写入模式(AT24C02页大小为8字节)
- 合理规划存储结构减少擦写次数
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时钟时务必仔细计算,确保符合器件规格要求。