# STM32F4 片上Flash模拟EEPROM:磨损均衡与掉电保护实现 ## 为什么需要模拟EEPROM? STM32F4系列(如STM32F407)内部Flash容量大(512KB~1MB),但擦写寿命有限(典型10000次)。而EEPROM虽可字节擦写且寿命长(100万次),但多数F4型号未集成。因此,当需要频繁保存配置参数(如校准值、运行状态)时,直接使用Flash会导致寿命迅速耗尽。解决方案是:**用软件算法将Flash模拟成EEPROM,通过磨损均衡分散擦写,并利用掉电保护确保数据不丢失**。 ## 核心原理 ### 1. Flash特性与限制 - **最小擦除单位**:扇区(Sector),F4中通常16KB或64KB。 - **写入单位**:半字(16位)或字(32位),且只能将1写为0(编程),擦除后恢复为0xFF。 - **寿命**:每个扇区约10,000次擦写。 ### 2. 磨损均衡策略 将Flash划分为两个或多个逻辑区,每次写入新数据到下一个空闲位置,而不是原地更新。当整个区写满后,擦除旧区并切换。典型方案:**双区交替(Ping-Pong)**。 - 区A和区B各存一份最新数据副本。 - 写入时,先写区A的下一位置,若区A满则擦除区B并写入区B。 - 读取时,比较两区的版本号或时间戳,取最新。 ### 3. 掉电保护设计 掉电可能发生在写入过程中,导致数据半更新。解决: - **原子写入**:先写入数据到临时区,再更新标志位(如CRC校验值)。 - **双备份**:每个数据项存两份,读取时校验CRC,若主备份损坏则用备用。 - **状态机**:定义写入状态(如:空闲、写入中、完成),掉电后重启根据状态恢复。 ## 实现步骤 ### 步骤1:定义存储结构 ```c // 数据项结构(带CRC和状态) typedef struct { uint32_t magic; // 魔数,用于识别有效数据 uint16_t version; // 版本号,越大越新 uint16_t crc; // CRC16校验 uint8_t data[64]; // 实际数据(示例64字节) } EepromItem_t; // 存储区管理结构 #define SECTOR_SIZE (16 * 1024) // 假设扇区16KB #define ITEM_SIZE (sizeof(EepromItem_t)) // 约76字节 #define ITEMS_PER_SECTOR (SECTOR_SIZE / ITEM_SIZE) // 约215个 ``` ### 步骤2:Flash驱动函数 ```c #include "stm32f4xx_hal.h" // 擦除扇区 void Flash_EraseSector(uint32_t sector) { FLASH_EraseInitTypeDef erase = {0}; uint32_t pageError = 0; erase.TypeErase = FLASH_TYPEERASE_SECTORS; erase.Sector = sector; erase.NbSectors = 1; erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; HAL_FLASH_Unlock(); HAL_FLASHEx_Erase(&erase, &pageError); HAL_FLASH_Lock(); } // 写入半字(16位) void Flash_WriteHalfWord(uint32_t addr, uint16_t data) { HAL_FLASH_Unlock(); HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr, data); HAL_FLASH_Lock(); } // 写入整个数据项(逐半字写入) void Flash_WriteItem(uint32_t baseAddr, uint16_t index, EepromItem_t *item) { uint32_t addr = baseAddr + index * ITEM_SIZE; uint16_t *p = (uint16_t *)item; for (int i = 0; i < ITEM_SIZE / 2; i++) { Flash_WriteHalfWord(addr + i * 2, p[i]); } } ``` ### 步骤3:磨损均衡逻辑 ```c #define SECTOR_A 0 // 扇区编号(根据实际映射) #define SECTOR_B 1 // 查找最新有效项(返回所在扇区和索引) int FindLatestItem(uint32_t *sector, uint16_t *index) { uint32_t sectors[2] = {SECTOR_A, SECTOR_B}; uint16_t maxVersion = 0; int found = 0; for (int s = 0; s < 2; s++) { uint32_t base = GetSectorAddr(sectors[s]); for (int i = 0; i < ITEMS_PER_SECTOR; i++) { EepromItem_t item; ReadItem(base, i, &item); if (item.magic == MAGIC_NUM && item.crc == CalcCRC(&item)) { if (item.version > maxVersion) { maxVersion = item.version; *sector = sectors[s]; *index = i; found = 1; } } } } return found; } // 写入新数据(自动选择位置) void Eeprom_Write(uint8_t *data, uint16_t len) { uint32_t curSector, baseAddr; uint16_t curIndex; // 找到当前最新项的位置 if (FindLatestItem(&curSector, &curIndex)) { // 若当前扇区还有空间,则写下一个位置 if (curIndex < ITEMS_PER_SECTOR - 1) { baseAddr = GetSectorAddr(curSector); EepromItem_t newItem; FillItem(&newItem, data, len); Flash_WriteItem(baseAddr, curIndex + 1, &newItem); } else { // 当前扇区已满,擦除另一个扇区并写入 uint32_t otherSector = (curSector == SECTOR_A) ? SECTOR_B : SECTOR_A; Flash_EraseSector(otherSector); baseAddr = GetSectorAddr(otherSector); EepromItem_t newItem; FillItem(&newItem, data, len); Flash_WriteItem(baseAddr, 0, &newItem); } } else { // 首次写入,擦除A扇区并写入 Flash_EraseSector(SECTOR_A); baseAddr = GetSectorAddr(SECTOR_A); EepromItem_t newItem; FillItem(&newItem, data, len); Flash_WriteItem(baseAddr, 0, &newItem); } } ``` ### 步骤4:掉电保护增强 - **写入前备份**:在写入新项前,先将旧项复制到临时扇区(若存在),但为简化,我们采用双副本策略:每个数据项在写入时,同时写入两个位置(如当前扇区和另一扇区),读取时取CRC有效且版本高的。 - **状态标志**:在数据项末尾添加一个状态字节(如0xAA表示完成,0x55表示写入中),掉电后重启时,若发现状态为0x55,则丢弃该项。 ```c // 增强的写入函数(带状态) void Eeprom_WriteSafe(uint8_t *data, uint16_t len) { // 先写主副本 EepromItem_t item; FillItem(&item, data, len); item.status = 0x55; // 写入中 uint32_t sector, base; uint16_t idx; // 选择写入位置(略,同前) Flash_WriteItem(base, idx, &item); // 写备用副本(另一扇区) // ... // 最后更新状态为完成 item.status = 0xAA; Flash_WriteItem(base, idx, &item); } ``` ## 完整代码示例(简化版) ```c // 初始化:检查并恢复 void Eeprom_Init(void) { uint32_t sector; uint16_t idx; if (!FindLatestItem(§or, &idx)) { // 无有效数据,格式化A扇区 Flash_EraseSector(SECTOR_A); EepromItem_t initItem; FillItem(&initItem, (uint8_t*)"default", 8); Flash_WriteItem(GetSectorAddr(SECTOR_A), 0, &initItem); } } // 读取最新数据 int Eeprom_Read(uint8_t *buf, uint16_t len) { uint32_t sector; uint16_t idx; if (FindLatestItem(§or, &idx)) { EepromItem_t item; ReadItem(GetSectorAddr(sector), idx, &item); if (item.status == 0xAA && item.crc == CalcCRC(&item)) { memcpy(buf, item.data, len); return 0; } } return -1; } ``` ## 注意事项 - **扇区大小**:根据实际芯片调整,F4的扇区大小可能不同(如F407的扇区0为16KB,扇区1-3为16KB,扇区4-7为128KB)。 - **写入对齐**:Flash写入必须半字对齐,且地址不能跨扇区。 - **擦除时间**:擦除一个扇区约需1~2秒(取决于芯片),期间不能掉电,否则数据丢失。建议在关键操作前关闭中断。 - **磨损均衡效率**:双区方案只能将寿命延长2倍,若需更长,可多区轮换(如4区)。 - **CRC计算**:可使用硬件CRC单元(如STM32F4的CRC模块)加速。 ## 总结 通过双区交替和状态机设计,我们成功用STM32F4的片上Flash模拟了EEPROM,实现了磨损均衡和掉电保护。该方法在工业设备中广泛使用,既节省了外部EEPROM成本,又保证了数据可靠性。开发者可根据实际需求调整数据大小、扇区数量和均衡策略,以达到最佳性能。