STM32软件I2C驱动AT24C02 EEPROM实战指南
1. 项目概述在嵌入式开发中I2C总线因其简单的两线制SDA和SCL和高效的多主从通信机制成为连接各类传感器、存储器和外设的常用接口。STM32F103作为经典的Cortex-M3内核微控制器虽然内置硬件I2C外设但在实际项目中我们常会遇到硬件资源冲突或引脚分配受限的情况。这时通过GPIO模拟I2C时序的软件I2C方案就显示出独特的优势。以AT24C02这款2Kbit的EEPROM为例其典型工作电压2.5-5.5V支持100kHz标准模式和400kHz快速模式通信速率采用页写入机制每页8字节。通过软件模拟实现对其读写不仅能灵活选择任意GPIO引脚还能深入理解I2C协议的底层时序。本文将完整展示从GPIO配置、时序模拟到AT24C02读写的全流程实现。2. 硬件设计与接口定义2.1 引脚分配策略在STM32F103RCT6开发板上我们选择PC11和PC12作为模拟I2C的引脚主要基于三点考虑这两个引脚在开发板上未被其他关键外设占用它们属于同一GPIO端口便于寄存器级操作优化物理位置相邻布线方便具体连接方式PC12 - AT24C02 SCLPC11 - AT24C02 SDA需接4.7kΩ上拉电阻至3.3V注意虽然软件I2C不限制引脚选择但建议优先考虑具有中断能力和开漏输出的GPIO以便未来功能扩展。2.2 GPIO模式配置对于软件I2CGPIO需要动态切换输入输出模式// 输出模式配置 #define SDA_OUT() GPIO_Init(PC11, GPIO_Mode_Out_PP, GPIO_Speed_50MHz) #define SCL_OUT() GPIO_Init(PC12, GPIO_Mode_Out_PP, GPIO_Speed_50MHz) // 输入模式配置 void SDA_IN(void) { GPIO_Init(PC11, GPIO_Mode_IPD, GPIO_Speed_50MHz); // 下拉输入 }关键参数说明推挽输出Out_PP确保高低电平驱动能力50MHz速度满足标准模式100kHz时序要求下拉输入IPD避免SDA线浮空时产生误触发3. I2C时序模拟实现3.1 基础信号生成3.1.1 起始信号时序起始信号定义为SCL高电平时SDA从高到低的跳变void I2C_Start(void) { SDA_OUT(); GPIO_SetBits(PC11); // SDA1 Delay_us(5); GPIO_SetBits(PC12); // SCL1 Delay_us(5); GPIO_ResetBits(PC11); // SDA0 Delay_us(5); GPIO_ResetBits(PC12); // SCL0 }时序要点保持SCL高电平时改变SDA状态每个状态变化后插入5μs延时100kHz时钟周期为10μs最终SCL保持低电平为数据传输做准备3.1.2 停止信号时序停止信号是SCL高电平时SDA从低到高的跳变void I2C_Stop(void) { SDA_OUT(); GPIO_ResetBits(PC11); // SDA0 Delay_us(5); GPIO_SetBits(PC12); // SCL1 Delay_us(5); GPIO_SetBits(PC11); // SDA1 Delay_us(5); }3.2 数据传输机制3.2.1 字节发送流程每个字节传输包含8个时钟脉冲void I2C_WriteByte(u8 byte) { u8 i; SDA_OUT(); GPIO_ResetBits(PC12); // SCL0 for(i0; i8; i) { (byte 0x80) ? GPIO_SetBits(PC11) : GPIO_ResetBits(PC11); byte 1; Delay_us(2); // 数据建立时间 GPIO_SetBits(PC12); // SCL1 Delay_us(5); // 高电平保持 GPIO_ResetBits(PC12); // SCL0 Delay_us(3); // 低电平保持 } }关键细节数据在SCL上升沿前至少2μs稳定满足tSU:DATSCL高电平保持5μs半周期采用MSB优先的传输顺序3.2.2 字节接收流程u8 I2C_ReadByte(void) { u8 i, byte 0; SDA_IN(); for(i0; i8; i) { byte 1; GPIO_SetBits(PC12); // SCL1 Delay_us(5); if(GPIO_ReadInputDataBit(PC11)) byte | 0x01; GPIO_ResetBits(PC12); // SCL0 Delay_us(5); } return byte; }3.3 应答机制实现3.3.1 从机应答检测u8 I2C_WaitAck(void) { u16 timeout 5000; // 5ms超时 SDA_IN(); GPIO_ResetBits(PC12); // SCL0 Delay_us(5); GPIO_SetBits(PC12); // SCL1 while(GPIO_ReadInputDataBit(PC11)) { if(--timeout 0) { I2C_Stop(); return 1; // 超时错误 } Delay_us(1); } GPIO_ResetBits(PC12); // SCL0 return 0; // 正常应答 }3.3.2 主机应答生成void I2C_Ack(u8 ack) { SDA_OUT(); GPIO_ResetBits(PC12); // SCL0 ack ? GPIO_ResetBits(PC11) : GPIO_SetBits(PC11); Delay_us(5); GPIO_SetBits(PC12); // SCL1 Delay_us(5); GPIO_ResetBits(PC12); // SCL0 }4. AT24C02驱动实现4.1 器件寻址机制AT24C02的7位设备地址为0b1010(A2)(A1)(A0)其中A2/A1/A0由硬件引脚决定。对于单独使用的芯片通常将地址引脚接地得到写地址0xA0读地址0xA14.2 单字节写入操作void AT24C02_WriteByte(u8 addr, u8 data) { I2C_Start(); I2C_WriteByte(0xA0); // 器件地址写 I2C_WaitAck(); I2C_WriteByte(addr); // 内存地址 I2C_WaitAck(); I2C_WriteByte(data); // 写入数据 I2C_WaitAck(); I2C_Stop(); Delay_ms(10); // 等待写入完成(tWR) }重要注意事项每次写入后必须等待5-10ms的编程周期地址自动递增特性仅在页写入时有效超过页边界(每8字节)会回绕到页首4.3 随机读取操作u8 AT24C02_ReadByte(u8 addr) { u8 data; I2C_Start(); I2C_WriteByte(0xA0); // 器件地址写 I2C_WaitAck(); I2C_WriteByte(addr); // 内存地址 I2C_WaitAck(); I2C_Start(); I2C_WriteByte(0xA1); // 器件地址读 I2C_WaitAck(); data I2C_ReadByte(); I2C_Ack(1); // NACK结束读取 I2C_Stop(); return data; }4.4 页写入优化利用AT24C02的16字节页缓冲特性void AT24C02_WritePage(u8 addr, u8 *buf, u8 len) { if(len 16) len 16; // 页大小限制 if((addr 0xF0) ! ((addrlen-1) 0xF0)) { len 16 - (addr % 16); // 防止跨页 } I2C_Start(); I2C_WriteByte(0xA0); I2C_WaitAck(); I2C_WriteByte(addr); I2C_WaitAck(); while(len--) { I2C_WriteByte(*buf); I2C_WaitAck(); } I2C_Stop(); Delay_ms(10); }5. 调试技巧与性能优化5.1 逻辑分析仪调试推荐使用Saleae Logic或PulseView配合以下触发设置触发条件SCL高电平时SDA下降沿起始信号采样率至少4MHz满足100kHz信号细节解码协议I2C地址设为0xA0典型问题诊断无应答检查上拉电阻、供电电压数据错误调整时序延时参数波形畸变缩短信号线长度增加上拉强度5.2 时序参数优化通过调整延时函数参数提升可靠性// 标准模式(100kHz)推荐参数 #define I2C_DELAY_SCL_HIGH 5 // μs #define I2C_DELAY_SCL_LOW 5 // μs #define I2C_DELAY_DATA_SETUP 2 // μs // 快速模式(400kHz)参数 // #define I2C_DELAY_SCL_HIGH 1 // #define I2C_DELAY_SCL_LOW 1 // #define I2C_DELAY_DATA_SETUP 05.3 错误处理机制增强鲁棒性的措施添加超时检测#define I2C_TIMEOUT 10000 // 10ms u8 I2C_WaitAck_Enhanced(void) { u32 timeout I2C_TIMEOUT; // ...原有代码... while(Read_SDA timeout--) Delay_us(1); if(timeout 0) return ERROR_TIMEOUT; }总线状态恢复void I2C_Recover(void) { SDA_OUT(); for(int i0; i9; i) { // 发送9个时钟脉冲 GPIO_SetBits(PC12); Delay_us(5); GPIO_ResetBits(PC12); Delay_us(5); } I2C_Stop(); }6. 实际应用案例6.1 系统参数存储存储设备校准参数typedef struct { float temp_offset; u32 serial_num; u8 config_flags; } SystemParams; void SaveParams(SystemParams *p) { AT24C02_WritePage(0, (u8*)p, sizeof(SystemParams)); } void LoadParams(SystemParams *p) { u8 *ptr (u8*)p; for(int i0; isizeof(SystemParams); i) { *ptr AT24C02_ReadByte(i); } }6.2 多器件总线管理当总线上挂载多个I2C设备时#define EEPROM_ADDR 0xA0 #define SENSOR_ADDR 0xB8 u8 ProbeDevice(u8 addr) { I2C_Start(); I2C_WriteByte(addr); u8 ack I2C_WaitAck(); I2C_Stop(); return ack 0; // 返回1表示设备存在 } void ScanBus(void) { for(u8 addr0x08; addr0x78; addr1) { if(ProbeDevice(addr)) { printf(Device found at 0x%X\n, addr); } } }7. 进阶话题7.1 中断驱动实现通过GPIO中断优化CPU占用// 在GPIO初始化中添加 GPIO_EXTILineConfig(PC11); EXTI_InitStructure.EXTI_Line EXTI_Line11; EXTI_InitStructure.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Rising_Falling; EXTI_Init(EXTI_InitStructure); void EXTI15_10_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line11)) { // 处理SDA边沿事件 EXTI_ClearITPendingBit(EXTI_Line11); } }7.2 与RTOS集成在FreeRTOS中的线程安全实现SemaphoreHandle_t i2c_mutex; void I2C_Task(void *pv) { i2c_mutex xSemaphoreCreateMutex(); while(1) { if(xSemaphoreTake(i2c_mutex, portMAX_DELAY)) { AT24C02_WriteByte(0, counter); xSemaphoreGive(i2c_mutex); } vTaskDelay(pdMS_TO_TICKS(1000)); } }7.3 功耗优化技巧空闲时释放总线void I2C_Idle(void) { SDA_OUT(); GPIO_SetBits(PC11); // SDA1 GPIO_SetBits(PC12); // SCL1 GPIO_Init(PC11, GPIO_Mode_AIN); // 模拟输入降低功耗 GPIO_Init(PC12, GPIO_Mode_AIN); }降低通信速率void I2C_SetLowSpeed(void) { i2c_delay 50; // 20kHz }