# STM32F4内部Flash模拟EEPROM:磨损均衡与掉电保护的状态机设计 ## 1. 引言 在嵌入式产品中,参数存储(如校准值、配置信息)常依赖EEPROM。但STM32F4系列没有内置EEPROM,外部扩展会增加成本。利用内部Flash模拟EEPROM是一种经济方案,但面临两大挑战:**擦写寿命有限**(典型10K次)和**掉电数据完整性**。本文提出一种基于状态机的设计,将Flash划分为多个存储区,通过磨损均衡延长寿命,并通过状态标记实现掉电恢复。 ## 2. 原理分析 ### 2.1 Flash特性 - 写入前必须擦除(按扇区,通常16KB或64KB)。 - 擦除操作耗时(约1-2ms),且期间掉电可能导致数据损坏。 - 写入(编程)可以按16位或32位进行,但只能将1变0,不能0变1。 ### 2.2 磨损均衡策略 将Flash划分为多个逻辑槽(Slot),每个槽存储一份数据副本。写入时轮询使用不同槽,避免同一位置反复擦写。典型结构: - **槽头**:包含状态标记(如0xFFFF表示空,0xAAAA表示有效,0x5555表示待擦除)。 - **数据区**:存储实际数据(如4字节对齐)。 ### 2.3 掉电保护机制 通过状态机管理每个槽的状态,确保任何时刻至少有一个有效副本。写入过程分为两步: 1. 将新数据写入备用槽,并标记为“待提交”。 2. 将旧槽标记为“废弃”,然后擦除旧槽。 若掉电发生在步骤1后,恢复时扫描所有槽,选择状态为“有效”的槽作为最新数据;若步骤2中掉电,则“待提交”槽成为有效数据。 ## 3. 状态机设计 定义四种状态: - **EMPTY** (0xFFFFFFFF):槽未使用,可写入。 - **VALID** (0xAAAAAAAA):槽包含有效数据。 - **COMMIT** (0x55555555):槽数据已写入,但尚未确认(用于掉电恢复)。 - **ERASED** (0x00000000):槽已擦除,等待重新使用。 状态转换图: ``` EMPTY -> (写入数据) -> COMMIT -> (确认) -> VALID VALID -> (擦除) -> ERASED -> (重新初始化) -> EMPTY ``` ## 4. 配置步骤 ### 4.1 硬件及库准备 - 使用STM32F4系列(如STM32F407VG)。 - 启用HAL库的Flash模块。 - 定义Flash扇区地址(例如使用扇区11,地址0x080E0000,大小16KB)。 ### 4.2 存储布局 ```c #define FLASH_SECTOR_START 0x080E0000 #define SLOT_SIZE 128 // 每个槽大小(字节),包含头部和数据 #define NUM_SLOTS 8 // 槽数量,总占用1KB #define DATA_SIZE 64 // 实际数据长度(字节) ``` ### 4.3 状态机实现 #### 初始化函数 ```c void EEPROM_Init(void) { uint32_t addr = FLASH_SECTOR_START; uint32_t status; // 扫描所有槽,找到状态为VALID的槽作为当前有效槽 for (int i = 0; i < NUM_SLOTS; i++) { status = *(__IO uint32_t*)addr; if (status == VALID) { current_slot = i; break; } addr += SLOT_SIZE; } if (current_slot == -1) { // 无有效槽,则擦除整个扇区并初始化第一个槽 Flash_EraseSector(); current_slot = 0; Flash_WriteHeader(current_slot, EMPTY); } } ``` #### 写入函数(带磨损均衡) ```c int EEPROM_Write(uint8_t *data, uint16_t len) { // 1. 寻找下一个可用槽(当前槽+1取模) int next_slot = (current_slot + 1) % NUM_SLOTS; // 2. 擦除下一个槽(如果非空) Flash_EraseSlot(next_slot); // 3. 写入数据并标记为COMMIT Flash_WriteData(next_slot, data, len); Flash_WriteHeader(next_slot, COMMIT); // 4. 将旧槽标记为ERASED(擦除操作) Flash_EraseSlot(current_slot); // 实际擦除整个扇区,但这里简化 // 5. 将新槽标记为VALID Flash_WriteHeader(next_slot, VALID); current_slot = next_slot; return 0; } ``` #### 掉电恢复函数 ```c void EEPROM_Recover(void) { uint32_t addr = FLASH_SECTOR_START; uint32_t status; int valid_slot = -1; int commit_slot = -1; for (int i = 0; i < NUM_SLOTS; i++) { status = *(__IO uint32_t*)addr; if (status == VALID) valid_slot = i; else if (status == COMMIT) commit_slot = i; addr += SLOT_SIZE; } if (commit_slot != -1) { // 有未完成的写入,将COMMIT槽提升为VALID,并擦除其他槽 Flash_WriteHeader(commit_slot, VALID); // 擦除其他所有槽(除commit_slot) for (int i = 0; i < NUM_SLOTS; i++) { if (i != commit_slot) Flash_EraseSlot(i); } current_slot = commit_slot; } else if (valid_slot != -1) { current_slot = valid_slot; } else { // 无有效数据,初始化 Flash_EraseSector(); current_slot = 0; Flash_WriteHeader(current_slot, EMPTY); } } ``` ## 5. 完整代码示例(简化) ```c // flash_emul.h #ifndef FLASH_EMUL_H #define FLASH_EMUL_H #include "stm32f4xx_hal.h" #define FLASH_SECTOR_START 0x080E0000 #define SLOT_SIZE 128 #define NUM_SLOTS 8 #define DATA_SIZE 64 #define EMPTY 0xFFFFFFFF #define VALID 0xAAAAAAAA #define COMMIT 0x55555555 #define ERASED 0x00000000 void EEPROM_Init(void); int EEPROM_Write(uint8_t *data, uint16_t len); int EEPROM_Read(uint8_t *data, uint16_t len); void EEPROM_Recover(void); #endif ``` ```c // flash_emul.c #include "flash_emul.h" static int current_slot = -1; static void Flash_EraseSector(void) { FLASH_EraseInitTypeDef erase; uint32_t page_error = 0; erase.TypeErase = FLASH_TYPEERASE_SECTORS; erase.Sector = FLASH_SECTOR_11; // 根据实际调整 erase.NbSectors = 1; erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; HAL_FLASH_Unlock(); HAL_FLASHEx_Erase(&erase, &page_error); HAL_FLASH_Lock(); } static void Flash_WriteHeader(int slot, uint32_t status) { uint32_t addr = FLASH_SECTOR_START + slot * SLOT_SIZE; HAL_FLASH_Unlock(); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, status); HAL_FLASH_Lock(); } static void Flash_WriteData(int slot, uint8_t *data, uint16_t len) { uint32_t addr = FLASH_SECTOR_START + slot * SLOT_SIZE + 4; // 跳过头部 HAL_FLASH_Unlock(); for (uint16_t i = 0; i < len; i += 4) { uint32_t word = 0; for (int j = 0; j < 4 && (i+j) < len; j++) { word |= ((uint32_t)data[i+j]) << (8*j); } HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + i, word); } HAL_FLASH_Lock(); } static void Flash_EraseSlot(int slot) { // 简化:擦除整个扇区,实际可只擦除槽所在扇区,但这里为演示 Flash_EraseSector(); // 重新初始化所有槽为EMPTY(除了当前有效槽) for (int i = 0; i < NUM_SLOTS; i++) { if (i != slot) Flash_WriteHeader(i, EMPTY); } } void EEPROM_Init(void) { EEPROM_Recover(); } int EEPROM_Write(uint8_t *data, uint16_t len) { if (len > DATA_SIZE) return -1; int next_slot = (current_slot + 1) % NUM_SLOTS; // 擦除下一个槽(如果非空) Flash_EraseSlot(next_slot); // 写入数据并标记为COMMIT Flash_WriteData(next_slot, data, len); Flash_WriteHeader(next_slot, COMMIT); // 擦除旧槽(这里简化,实际只擦除旧槽所在扇区) Flash_EraseSlot(current_slot); // 将新槽标记为VALID Flash_WriteHeader(next_slot, VALID); current_slot = next_slot; return 0; } int EEPROM_Read(uint8_t *data, uint16_t len) { if (current_slot == -1) return -1; uint32_t addr = FLASH_SECTOR_START + current_slot * SLOT_SIZE + 4; for (uint16_t i = 0; i < len; i++) { data[i] = *(__IO uint8_t*)(addr + i); } return 0; } void EEPROM_Recover(void) { uint32_t addr = FLASH_SECTOR_START; uint32_t status; int valid_slot = -1, commit_slot = -1; for (int i = 0; i < NUM_SLOTS; i++) { status = *(__IO uint32_t*)addr; if (status == VALID) valid_slot = i; else if (status == COMMIT) commit_slot = i; addr += SLOT_SIZE; } if (commit_slot != -1) { Flash_WriteHeader(commit_slot, VALID); for (int i = 0; i < NUM_SLOTS; i++) { if (i != commit_slot) Flash_EraseSlot(i); } current_slot = commit_slot; } else if (valid_slot != -1) { current_slot = valid_slot; } else { Flash_EraseSector(); current_slot = 0; Flash_WriteHeader(current_slot, EMPTY); } } ``` ## 6. 注意事项 - **扇区擦除粒度**:STM32F4的擦除最小单位是扇区(16KB),因此磨损均衡的槽数量受限于扇区大小。若槽太小,擦除整个扇区会浪费寿命。建议槽大小至少为扇区的1/N,且N为槽数。 - **掉电时机**:在写入过程中,若掉电发生在擦除旧槽后、标记新槽为VALID前,恢复时COMMIT槽会被提升为VALID,但旧槽已被擦除,数据可能丢失。因此,实际设计应保证至少有一个槽始终有效,可增加冗余槽。 - **写保护**:操作Flash时需关闭中断,防止中断服务程序干扰。 - **性能**:Flash写入速度慢,频繁写入会阻塞系统,建议使用DMA或后台任务。 - **磨损均衡优化**:可记录每个槽的擦写次数,动态选择最少使用的槽,但会增加复杂度。 ## 7. 总结 通过状态机管理Flash槽,实现了磨损均衡和掉电保护,显著延长了Flash寿命并提高了数据可靠性。该设计适用于对成本敏感且写入频率不高的嵌入式应用。实际项目中可根据需求调整槽大小和状态机,例如增加CRC校验或使用双扇区备份。希望本文能为你提供有价值的参考。