1. 项目概述STM32与EEPROM的单字节读写操作在嵌入式系统开发中EEPROM电可擦可编程只读存储器因其非易失性存储特性被广泛用于保存配置参数、校准数据等关键信息。本项目基于STM32微控制器通过I2C总线实现对EEPROM的单字节读写操作这是嵌入式硬件开发中的基础但至关重要的技能点。作为day18内容的延续本实验重点解决两个核心问题如何通过STM32的硬件I2C外设实现精确的时序控制如何处理EEPROM特有的写入等待周期关键提示AT24C02系列EEPROM的典型写入周期为5ms这意味着在连续写入操作时必须插入适当的延迟否则会导致数据丢失。2. 硬件架构与电路设计2.1 I2C总线物理连接典型的STM32与EEPROM连接方案SCLPB8I2C1_SCLSDAPB9I2C1_SDA上拉电阻4.7kΩVCC3.3V时电路设计要点必须使用开漏输出模式GPIO_OType_OD总线电容应小于400pF长距离传输时需要降低上拉电阻值地址引脚A0-A2接地时AT24C02的7位设备地址为0x502.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快速模式假设PCLK142MHz目标SCL周期T 1/400000 2.5μsCCR 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必须检测ACKvoid 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 典型故障排查表现象可能原因解决方案无法检测到ACK1. 设备地址错误2. 上拉电阻过大3. 总线电容过大1. 确认设备地址2. 减小上拉电阻3. 缩短总线长度数据校验错误1. 未等待写入完成2. 电源噪声干扰3. 时序不符合规格1. 增加等待周期2. 加强电源滤波3. 检查时钟配置随机通信失败1. 总线竞争2. 中断干扰3. 静电干扰1. 增加总线仲裁2. 调整中断优先级3. 改进屏蔽6.2 示波器调试要点SCL/SDA信号上升时间应1μs3.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时钟时务必仔细计算确保符合器件规格要求。