# 引言 STM32F4系列内置Flash容量大、速度快,但擦写寿命(典型1万次)远低于EEPROM(100万次)。直接按地址写参数会迅速磨损同一扇区。此外,写入过程中掉电可能导致数据半更新,造成系统启动时参数错误。因此,模拟EEPROM必须解决两个核心问题:**磨损均衡**(让所有扇区均匀老化)和**掉电保护**(保证数据一致性)。 # 一、原理剖析 ## 1. Flash硬件特性 - STM32F4的Flash以**扇区**为单位擦除(如1KB、2KB等),写入前必须先擦除,且只能将1写为0。 - 擦除操作耗时约数十毫秒,期间掉电会导致扇区数据全0或损坏。 - 每个扇区擦写次数有限,超出后可能产生坏块。 ## 2. 磨损均衡设计 **核心思想**:将多个扇区组成一个逻辑存储区,每次写入时选择擦写次数最少的扇区,并记录当前活动扇区。常见策略有: - **轮询法**:按顺序循环使用扇区,简单但无法应对非均匀写入。 - **计数法**:在每个扇区头部记录擦写次数,写入时选择次数最少者。 本文采用**双扇区轮换 + 磨损计数**:使用两个扇区,一个作为活动扇区,另一个作为备份。每次写入时,将新数据写入备份扇区,成功后切换活动指针,并擦除旧活动扇区。这样每个扇区擦写次数相同,且始终有一个完整副本。 ## 3. 掉电保护策略 - **双备份**:数据同时存于两个扇区,但只有活动扇区有效。写入时先写备份,再切换,保证任何时刻至少有一个完整数据。 - **校验和**:每个记录包含CRC32校验,读取时校验失败则回退到另一扇区。 - **写入顺序**:先写数据+校验,再写状态标记(如有效标志),最后切换。若在写状态前掉电,旧扇区仍有效。 # 二、具体实现方案 ## 1. 存储布局 假设使用两个1KB扇区(如扇区11和12,地址0x080E0000和0x080E0400)。每个扇区结构如下: ```c #define SECTOR_SIZE 1024 #define DATA_SIZE 64 // 实际数据长度 typedef struct { uint32_t magic; // 魔数,用于识别有效扇区 uint32_t write_cnt; // 写入次数(磨损计数) uint32_t crc; // 数据CRC32 uint8_t data[DATA_SIZE]; } SectorHeader; ``` 扇区头部固定占用16字节,数据区64字节,剩余空间填充0xFF。 ## 2. 核心函数实现 ### 扇区擦除与写入 ```c #include "stm32f4xx_hal.h" #define FLASH_SECTOR_11 11 #define FLASH_SECTOR_12 12 #define ADDR_SECTOR_11 0x080E0000 #define ADDR_SECTOR_12 0x080E0400 // 擦除指定扇区 static void flash_erase_sector(uint8_t sector) { FLASH_EraseInitTypeDef erase = {0}; uint32_t page_error = 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, &page_error); HAL_FLASH_Lock(); } // 写入数据到指定地址(按32位字写) static void flash_write_word(uint32_t addr, uint32_t data) { HAL_FLASH_Unlock(); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, data); HAL_FLASH_Lock(); } ``` ### 磨损均衡控制 ```c // 读取两个扇区的头部,返回有效且写入次数较少的扇区地址 static uint32_t select_active_sector(void) { SectorHeader h1, h2; memcpy(&h1, (void*)ADDR_SECTOR_11, sizeof(h1)); memcpy(&h2, (void*)ADDR_SECTOR_12, sizeof(h2)); int valid1 = (h1.magic == MAGIC_NUM && crc_check(&h1)); int valid2 = (h2.magic == MAGIC_NUM && crc_check(&h2)); if (valid1 && valid2) { // 选择写入次数少的扇区作为备份,另一个作为活动 return (h1.write_cnt <= h2.write_cnt) ? ADDR_SECTOR_11 : ADDR_SECTOR_12; } else if (valid1) { return ADDR_SECTOR_11; } else if (valid2) { return ADDR_SECTOR_12; } else { // 两个都无效,初始化扇区11 flash_erase_sector(FLASH_SECTOR_11); return ADDR_SECTOR_11; } } ``` ### 写入数据(带掉电保护) ```c int eeprom_write(uint8_t *data, uint32_t len) { if (len > DATA_SIZE) return -1; uint32_t active = select_active_sector(); uint32_t backup = (active == ADDR_SECTOR_11) ? ADDR_SECTOR_12 : ADDR_SECTOR_11; // 构造新头部 SectorHeader new_hdr; memcpy(&new_hdr, (void*)active, sizeof(new_hdr)); new_hdr.write_cnt++; new_hdr.crc = crc32(data, len); memcpy(new_hdr.data, data, len); new_hdr.magic = MAGIC_NUM; // 擦除备份扇区 flash_erase_sector((backup == ADDR_SECTOR_11) ? FLASH_SECTOR_11 : FLASH_SECTOR_12); // 写入备份扇区(先写头部,再写数据,最后写magic) uint32_t addr = backup; flash_write_word(addr, new_hdr.write_cnt); flash_write_word(addr+4, new_hdr.crc); for (int i=0; i> 1) ^ (0xEDB88320 & -(crc & 1)); } } return ~crc; } ``` # 三、使用示例 ```c int main(void) { HAL_Init(); // ... 系统时钟初始化 uint8_t params[64] = {0}; // 读取参数 if (eeprom_read(params, 64) != 0) { // 首次使用,初始化默认值 memset(params, 0, 64); eeprom_write(params, 64); } // 修改参数并保存 params[0] = 42; eeprom_write(params, 64); while(1) {} } ``` # 四、注意事项 - **扇区大小**:STM32F4不同型号扇区大小不同(如1KB、2KB、128KB),需根据实际调整地址和擦除函数。 - **写入粒度**:Flash编程必须按字(32位)或半字,且地址对齐。 - **擦除时间**:擦除期间掉电会损坏扇区,但双备份机制可保证数据不丢。 - **磨损计数溢出**:write_cnt为32位,足够使用,但需处理溢出(如达到0xFFFFFFFF时重置所有扇区)。 - **性能**:每次写入需擦除一个扇区,耗时约20ms,不适合高频写入。 - **中断保护**:写入过程中应关闭中断,防止打断导致状态不一致。 # 五、总结 通过双扇区轮换和磨损计数,我们实现了Flash的均匀磨损,寿命提升至2万次(两个扇区)。掉电保护通过先写备份再切换的机制,确保任何时刻至少有一个完整数据。CRC校验进一步增强了数据完整性。该方案适用于参数存储、配置保存等低频写入场景,是嵌入式开发中实用且可靠的技术。