STM32驱动AT24CXX系列EEPROM:从零构建跨平台IIC通用驱动
1. 为什么需要通用EEPROM驱动在嵌入式开发中AT24CXX系列EEPROM就像一个小型U盘可以断电保存数据。我做过一个智能家居项目需要保存200组温湿度数据当时选了AT24C32后来客户要求增加功能换成了AT24C256。如果每次换芯片都要重写驱动那真是噩梦。这就是为什么我们需要一个通吃全系列的驱动方案。AT24CXX家族从01到512型号主要区别在三个方面容量差异01只有128字节而512有64KB地址宽度小容量用单字节地址大容量需要双字节页写限制不同型号单次写入字节数不同常见4-64字节实测发现市面上90%的STM32项目用的都是软件模拟IIC原因很简单硬件IIC的时序问题太多特别是STM32F1系列。我用CubeMX生成的硬件IIC驱动遇到AT24C02频繁应答失败最后改用软件模拟才稳定。2. 软件IIC的底层实现技巧2.1 GPIO配置的跨平台设计先看这段被我优化过的初始化代码// iic_port.h #define IIC_SCL_PORT GPIOB #define IIC_SCL_PIN GPIO_Pin_6 #define IIC_SDA_PORT GPIOB #define IIC_SDA_PIN GPIO_Pin_7 void IIC_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStruct.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStruct.GPIO_Pin IIC_SCL_PIN; GPIO_Init(IIC_SCL_PORT, GPIO_InitStruct); GPIO_InitStruct.GPIO_Pin IIC_SDA_PIN; GPIO_Init(IIC_SDA_PORT, GPIO_InitStruct); IIC_SDA_HIGH(); IIC_SCL_HIGH(); }关键点用宏定义隔离硬件依赖移植到GD32时只需改头文件推挽输出模式比开漏输出更稳定实测波形更干净初始状态拉高是IIC总线的基本要求2.2 精准时序控制实战IIC最头疼的就是时序问题。我的经验值是标准模式(100kHz)每个时钟周期不少于10μs快速模式(400kHz)周期不少于2.5μs用SysTick实现微秒延时void IIC_Delay(uint16_t us) { uint32_t ticks us * (SystemCoreClock / 1000000) / 8; SysTick-LOAD ticks - 1; SysTick-VAL 0; SysTick-CTRL SysTick_CTRL_ENABLE_Msk; while(!(SysTick-CTRL SysTick_CTRL_COUNTFLAG_Msk)); SysTick-CTRL 0; }避坑指南启动信号保持时间≥4.7μs实测STM32F103需要5μs以上数据建立时间≥250ns快速模式要求更严格停止信号后要加1μs延时再操作总线3. EEPROM驱动分层设计3.1 地址自动适配方案AT24C01-16用8位地址AT24C32以上用16位地址。我的解决方案是// eeprom.h #define EEPROM_PAGE_SIZE (32) // 根据型号修改 #define EEPROM_SIZE (4096) // 单位:字节 #if (EEPROM_SIZE 2048) #define ADDR_BYTES 1 #else #define ADDR_BYTES 2 #endif读写函数里这样处理void EEPROM_Write(uint16_t addr, uint8_t data) { IIC_Start(); IIC_SendByte(0xA0 | ((addr (ADDR_BYTES*8-8)) 0x0E)); IIC_WaitAck(); if(ADDR_BYTES 1) { IIC_SendByte(addr 8); IIC_WaitAck(); } IIC_SendByte(addr 0xFF); IIC_WaitAck(); IIC_SendByte(data); IIC_WaitAck(); IIC_Stop(); Delay_ms(5); // 必须的写入等待 }3.2 页写优化策略AT24C32的页写缓冲区是32字节超过会回卷。我封装了个安全写入函数uint8_t EEPROM_Write_Page(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t remain EEPROM_PAGE_SIZE - (addr % EEPROM_PAGE_SIZE); uint16_t write_len (len remain) ? remain : len; IIC_Start(); // 发送地址... for(uint16_t i0; iwrite_len; i) { IIC_SendByte(buf[i]); if(IIC_WaitAck()) { IIC_Stop(); return 1; // 错误 } } IIC_Stop(); Delay_ms(5); return (len write_len) ? EEPROM_Write_Page(addrwrite_len, bufwrite_len, len-write_len) : 0; }4. 驱动测试与性能优化4.1 自动化测试框架我习惯用这套测试流程边界测试写满首尾地址跨页测试故意跨越页边界写入压力测试连续写入1000次检查耐久性速度测试统计写入1KB数据耗时测试代码示例void EEPROM_Test(void) { uint8_t pattern[256]; for(int i0; i256; i) pattern[i] i; // 全片写入测试 uint32_t start HAL_GetTick(); EEPROM_Write_Page(0, pattern, 256); uint32_t cost HAL_GetTick() - start; // 验证数据 uint8_t read_back[256]; EEPROM_Read(0, read_back, 256); if(memcmp(pattern, read_back, 256) 0) { printf(Test PASS! Cost %lums\n, cost); } else { printf(Test FAIL!\n); } }4.2 性能提升技巧批量写入优化// 每次写入尽可能填满一页 void EEPROM_Write_Bulk(uint16_t addr, uint8_t *data, uint32_t len) { while(len 0) { uint16_t chunk (len EEPROM_PAGE_SIZE) ? EEPROM_PAGE_SIZE : len; EEPROM_Write_Page(addr, data, chunk); addr chunk; data chunk; len - chunk; } }读写缓存策略#define CACHE_SIZE 64 typedef struct { uint16_t addr; uint8_t data[CACHE_SIZE]; bool dirty; } EEPROM_Cache; void EEPROM_Cache_Write(uint16_t addr, uint8_t val) { static EEPROM_Cache cache; if(cache.dirty (addr ! cache.addr 1 || (addr / CACHE_SIZE) ! (cache.addr / CACHE_SIZE))) { EEPROM_Write_Bulk(cache.addr, cache.data, CACHE_SIZE); cache.dirty false; } if(!cache.dirty) { cache.addr addr ~(CACHE_SIZE-1); EEPROM_Read(cache.addr, cache.data, CACHE_SIZE); } cache.data[addr % CACHE_SIZE] val; cache.dirty true; }5. 移植到其他平台的实战最近把驱动移植到GD32E230发现三个关键点时钟配置差异// GD32需要先解锁GPIO时钟 rcu_periph_clock_enable(RCU_GPIOB); gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_6 | GPIO_PIN_7);延时调整 GD32的主频更高需要重新校准延时void IIC_Delay(uint16_t us) { uint32_t start DWT-CYCCNT; uint32_t cycles us * (SystemCoreClock / 1000000); while((DWT-CYCCNT - start) cycles); }IO操作语法// GD32的IO操作函数不同 #define IIC_SCL_HIGH() gpio_bit_set(GPIOB, GPIO_PIN_6) #define IIC_SCL_LOW() gpio_bit_reset(GPIOB, GPIO_PIN_6)6. 常见问题解决方案问题1写入后立即读取数据错误原因EEPROM写入需要5ms周期解决所有写操作后加Delay_ms(5)问题2AT24C256偶尔写入失败对策void EEPROM_Retry_Write(uint16_t addr, uint8_t data) { uint8_t retry 3; while(retry--) { EEPROM_Write_Byte(addr, data); if(EEPROM_Read_Byte(addr) data) break; Delay_ms(10); } }问题3IIC总线被锁死应急恢复void IIC_Recover(void) { SDA_OUT(); for(int i0; i9; i) { IIC_SCL_LOW(); Delay_us(5); IIC_SCL_HIGH(); Delay_us(5); } IIC_Start(); }在智能电表项目里我们发现长时间运行后IIC总线挂死概率约0.1%加入看门狗和恢复函数后问题彻底解决。驱动代码最终通过10万次连续写入测试稳定性达到工业级要求。