## 一、EEPROM 的物理特性与挑战 EEPROM(Electrically Erasable Programmable Read-Only Memory)支持字节级擦写,但每次写入前需先擦除(通常按页或整片),且擦写次数有限(典型 10 万次)。在频繁保存数据的场景(如设备计数器、用户设置),若不加以管理,很快会耗尽寿命。此外,掉电瞬间可能发生写入中断,导致数据损坏或丢失。因此,设计需兼顾**数据完整性**与**磨损均衡**。 ## 二、掉电保存设计:从硬件到软件 ### 2.1 硬件掉电检测 - 使用 MCU 内置的 BOD(Brown-Out Detector)或外部电压监控芯片(如 TPS3839),在电源跌落至阈值时触发中断。 - 在中断服务函数中,立即将关键数据写入 EEPROM,并禁止其他中断,确保写入过程不被干扰。 - 注意:EEPROM 写入时间约 3-5ms,需保证掉电后电源能维持足够时间(通常通过大电容储能)。 ### 2.2 软件数据缓冲策略 - **双备份(Double Buffer)**:将数据存储在两个固定区域,写入时先写备份区,再写主区,读取时校验主区,若损坏则从备份区恢复。 - **校验和(Checksum)**:在数据末尾附加 CRC 或累加和,读取时校验,防止写入中断导致的数据不完整。 ## 三、磨损均衡算法详解 磨损均衡的核心是避免反复擦写同一地址,将写入操作分散到整个存储空间。常用方法有: ### 3.1 轮询写入(Round-Robin) 将存储区划分为 N 个槽位,每次写入使用下一个槽位,并记录当前槽位索引。读取时需扫描所有槽位,找到最新有效数据。 **优点**:实现简单,均衡效果好。 **缺点**:读取需遍历,速度较慢;需额外存储索引。 ### 3.2 日志结构(Log-Structured) 类似日志文件系统,每次写入追加到存储区末尾,当写满时进行垃圾回收(擦除旧数据)。 **优点**:写入效率高,适合频繁更新。 **缺点**:需要管理日志头,回收机制复杂。 ### 3.3 动态磨损均衡(结合索引表) 在 RAM 中维护一个逻辑地址到物理地址的映射表,每次写入选择擦写次数最少的物理页。适用于较大容量 EEPROM。 ## 四、完整代码示例(基于 STM32 HAL 库) 以下示例实现轮询写入 + 双备份 + 校验和,适用于保存 16 字节的用户数据。 ```c // eeprom_manage.h #ifndef EEPROM_MANAGE_H #define EEPROM_MANAGE_H #include "stdint.h" #include "stdbool.h" #define DATA_SIZE 16 // 数据长度(字节) #define SLOT_COUNT 8 // 槽位数量(需为偶数,便于双备份) #define SLOT_SIZE (DATA_SIZE + 2) // 数据 + 校验和(2字节) #define EEPROM_START_ADDR 0x08080000 // 假设外部 EEPROM 起始地址,内部需调整 typedef struct { uint8_t data[DATA_SIZE]; uint16_t checksum; } Slot_t; bool EEPROM_Save(uint8_t *data); bool EEPROM_Load(uint8_t *data); void EEPROM_Init(void); #endif ``` ```c // eeprom_manage.c #include "eeprom_manage.h" #include "i2c.h" // 假设使用 I2C 接口的外部 EEPROM static uint8_t current_slot = 0; // 当前写入槽位索引 // 计算校验和(简单累加) static uint16_t calc_checksum(uint8_t *data) { uint16_t sum = 0; for (int i = 0; i < DATA_SIZE; i++) { sum += data[i]; } return sum; } // 写入一个槽位 static void write_slot(uint8_t slot, uint8_t *data) { uint16_t addr = EEPROM_START_ADDR + slot * SLOT_SIZE; uint16_t checksum = calc_checksum(data); // 先写数据,再写校验和(分两次写,防止中断导致校验和不匹配) HAL_I2C_Mem_Write(&hi2c1, 0xA0, addr, 2, data, DATA_SIZE, 100); HAL_I2C_Mem_Write(&hi2c1, 0xA0, addr + DATA_SIZE, 2, (uint8_t*)&checksum, 2, 100); } // 读取并校验槽位,返回是否有效 static bool read_slot(uint8_t slot, uint8_t *data) { uint16_t addr = EEPROM_START_ADDR + slot * SLOT_SIZE; uint8_t buf[SLOT_SIZE]; HAL_I2C_Mem_Read(&hi2c1, 0xA0, addr, 2, buf, SLOT_SIZE, 100); uint16_t checksum = (buf[DATA_SIZE] << 8) | buf[DATA_SIZE+1]; if (calc_checksum(buf) == checksum) { memcpy(data, buf, DATA_SIZE); return true; } return false; } // 初始化:扫描所有槽位,找到最新有效数据(槽位索引最大且校验通过) void EEPROM_Init(void) { uint8_t temp[DATA_SIZE]; current_slot = 0; for (int i = SLOT_COUNT-1; i >= 0; i--) { if (read_slot(i, temp)) { current_slot = i; break; } } } // 保存数据:写入下一个槽位(循环),并更新当前索引 bool EEPROM_Save(uint8_t *data) { uint8_t next_slot = (current_slot + 1) % SLOT_COUNT; write_slot(next_slot, data); // 写入后立即验证 uint8_t verify[DATA_SIZE]; if (read_slot(next_slot, verify)) { if (memcmp(verify, data, DATA_SIZE) == 0) { current_slot = next_slot; return true; } } return false; // 写入失败,保持原槽位 } // 读取数据:从当前槽位读取,若失败则尝试前一个槽位(双备份) bool EEPROM_Load(uint8_t *data) { if (read_slot(current_slot, data)) { return true; } // 尝试前一个槽位(备份) uint8_t prev_slot = (current_slot == 0) ? (SLOT_COUNT-1) : (current_slot-1); if (read_slot(prev_slot, data)) { return true; } return false; } ``` **使用说明**: - 初始化时调用 `EEPROM_Init()`,上电后调用 `EEPROM_Load()` 读取数据。 - 需要保存时调用 `EEPROM_Save()`,内部自动轮询槽位。 - 掉电中断中调用 `EEPROM_Save()` 时,需确保 I2C 中断优先级最高,或使用轮询方式。 ## 五、注意事项与优化建议 - **写入时间**:EEPROM 写入较慢,频繁保存会阻塞主循环,建议采用“脏数据标记 + 定时批量保存”策略。 - **磨损均衡粒度**:槽位数量越多,均衡效果越好,但占用空间增大,需权衡。 - **掉电保护**:在写入过程中掉电,可能导致校验和不匹配,双备份可有效恢复。 - **外部 EEPROM vs 内部**:内部 EEPROM 擦写次数更少(通常 10 万次),外部如 AT24Cxx 可达 100 万次,选型时注意。 - **测试**:使用循环写入测试寿命,并模拟掉电(随机中断写入)验证数据完整性。 ## 六、总结 EEPROM 数据管理是嵌入式开发中的关键细节,通过掉电检测、双备份和磨损均衡,可以显著提升系统可靠性。本文提供的轮询写入算法简单高效,适用于大多数场景。对于更高频率的写入,可考虑日志结构或外部 FRAM(铁电存储器)替代。希望本文能帮助你在实际项目中设计出健壮的存储方案。