# STM32F4内部Flash模拟EEPROM:双缓冲区实现磨损均衡与掉电保护 ## 1. 为什么需要双缓冲区? STM32F4内部Flash具有擦写次数限制(通常10,000次),且写入前必须擦除(按扇区)。直接模拟EEPROM会导致频繁擦写同一扇区,迅速耗尽寿命。此外,写入过程中掉电可能造成数据损坏。双缓冲区方案通过两个扇区交替使用,分散擦写压力,并利用状态标志实现掉电恢复,兼顾寿命与可靠性。 ## 2. 核心原理 ### 2.1 磨损均衡 - 使用两个扇区(如Sector 11和Sector 12,各128KB),每次写入选择“活跃”扇区。 - 活跃扇区写满后,将最新数据复制到另一扇区,擦除旧扇区,并切换活跃标志。 - 通过交替擦写,每个扇区的擦除次数减半,寿命延长至2倍。 ### 2.2 掉电保护 - 每个扇区头部存储状态字(如0xAAAAAAAA表示有效,0x55555555表示待擦除)。 - 写入过程分两步:先写数据到临时区,再更新状态字。若掉电,通过状态字判断数据完整性。 - 采用“备份-切换”策略:新数据先写入非活跃扇区,成功后更新活跃标志,避免旧数据丢失。 ## 3. 配置步骤 ### 3.1 硬件与软件准备 - 使用STM32F407,内部Flash起始地址0x08000000,扇区大小128KB(注意不同型号扇区大小可能不同)。 - 启用Flash接口时钟:`__HAL_RCC_FLASH_CLK_ENABLE()`。 - 定义扇区基址和状态字宏。 ### 3.2 扇区划分 ```c #define SECTOR_ACTIVE_1 0x080E0000 // 扇区11(假设从0x080E0000开始) #define SECTOR_ACTIVE_2 0x08100000 // 扇区12 #define STATUS_VALID 0xAAAAAAAA #define STATUS_ERASED 0xFFFFFFFF #define STATUS_PENDING 0x55555555 ``` ### 3.3 初始化与状态检测 ```c uint8_t Flash_EEPROM_Init(void) { uint32_t status1 = *(volatile uint32_t*)SECTOR_ACTIVE_1; uint32_t status2 = *(volatile uint32_t*)SECTOR_ACTIVE_2; if (status1 == STATUS_VALID && status2 != STATUS_VALID) { active_sector = 1; } else if (status2 == STATUS_VALID && status1 != STATUS_VALID) { active_sector = 2; } else if (status1 == STATUS_VALID && status2 == STATUS_VALID) { // 异常情况,选择状态较新的扇区(可比较写入计数) active_sector = (read_counter(1) > read_counter(2)) ? 1 : 2; } else { // 首次使用,擦除两个扇区并初始化 Flash_EraseSector(SECTOR_ACTIVE_1); Flash_EraseSector(SECTOR_ACTIVE_2); Flash_WriteWord(SECTOR_ACTIVE_1, STATUS_VALID); active_sector = 1; } return 0; } ``` ## 4. 写入与读取实现 ### 4.1 写入流程(带磨损均衡) ```c uint8_t Flash_EEPROM_Write(uint16_t addr, uint32_t data) { uint32_t target_sector = (active_sector == 1) ? SECTOR_ACTIVE_1 : SECTOR_ACTIVE_2; uint32_t backup_sector = (active_sector == 1) ? SECTOR_ACTIVE_2 : SECTOR_ACTIVE_1; // 1. 在备份扇区写入新数据(先擦除) Flash_EraseSector(backup_sector); Flash_WriteWord(backup_sector, STATUS_PENDING); // 标记写入中 Flash_WriteWord(backup_sector + 4 + addr*4, data); // 2. 更新备份扇区状态为有效 Flash_WriteWord(backup_sector, STATUS_VALID); // 3. 将旧扇区标记为待擦除(可选,用于掉电恢复) Flash_WriteWord(target_sector, STATUS_PENDING); // 4. 切换活跃扇区 active_sector = (active_sector == 1) ? 2 : 1; // 5. 擦除旧扇区(延迟擦除可提高效率,但此处直接擦除) Flash_EraseSector(target_sector); return 0; } ``` ### 4.2 读取流程 ```c uint32_t Flash_EEPROM_Read(uint16_t addr) { uint32_t sector = (active_sector == 1) ? SECTOR_ACTIVE_1 : SECTOR_ACTIVE_2; return *(volatile uint32_t*)(sector + 4 + addr*4); } ``` ### 4.3 掉电恢复处理 在初始化时,若检测到状态为`STATUS_PENDING`的扇区,说明上次写入未完成,需根据另一扇区状态恢复: - 若备份扇区有效,则直接切换活跃扇区到备份扇区,擦除损坏扇区。 - 若两个扇区均无效,则重新初始化。 ## 5. 完整代码示例(简化版) ```c // flash_eeprom.h #ifndef FLASH_EEPROM_H #define FLASH_EEPROM_H #include "stm32f4xx_hal.h" #define SECTOR1_BASE 0x080E0000 #define SECTOR2_BASE 0x08100000 #define STATUS_OFFSET 0 #define DATA_OFFSET 4 #define STATUS_VALID 0xAAAAAAAA #define STATUS_PENDING 0x55555555 void Flash_EEPROM_Init(void); uint32_t Flash_EEPROM_Read(uint16_t addr); void Flash_EEPROM_Write(uint16_t addr, uint32_t data); #endif // flash_eeprom.c #include "flash_eeprom.h" static uint8_t active_sector; static void Flash_EraseSector(uint32_t sector) { FLASH_EraseInitTypeDef erase = {0}; uint32_t page_error = 0; erase.TypeErase = FLASH_TYPEERASE_SECTORS; erase.Sector = (sector == SECTOR1_BASE) ? FLASH_SECTOR_11 : FLASH_SECTOR_12; erase.NbSectors = 1; erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; HAL_FLASH_Unlock(); HAL_FLASHEx_Erase(&erase, &page_error); HAL_FLASH_Lock(); } static void Flash_WriteWord(uint32_t addr, uint32_t data) { HAL_FLASH_Unlock(); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, data); HAL_FLASH_Lock(); } void Flash_EEPROM_Init(void) { uint32_t s1 = *(volatile uint32_t*)SECTOR1_BASE; uint32_t s2 = *(volatile uint32_t*)SECTOR2_BASE; if (s1 == STATUS_VALID && s2 != STATUS_VALID) active_sector = 1; else if (s2 == STATUS_VALID && s1 != STATUS_VALID) active_sector = 2; else if (s1 == STATUS_VALID && s2 == STATUS_VALID) { // 选择写入计数较大的(此处简化,可比较数据区末尾的计数器) active_sector = 1; // 实际需更复杂判断 } else { Flash_EraseSector(SECTOR1_BASE); Flash_EraseSector(SECTOR2_BASE); Flash_WriteWord(SECTOR1_BASE, STATUS_VALID); active_sector = 1; } } void Flash_EEPROM_Write(uint16_t addr, uint32_t data) { uint32_t target, backup; if (active_sector == 1) { target = SECTOR1_BASE; backup = SECTOR2_BASE; } else { target = SECTOR2_BASE; backup = SECTOR1_BASE; } // 写入备份扇区 Flash_EraseSector(backup); Flash_WriteWord(backup + STATUS_OFFSET, STATUS_PENDING); Flash_WriteWord(backup + DATA_OFFSET + addr*4, data); Flash_WriteWord(backup + STATUS_OFFSET, STATUS_VALID); // 切换活跃扇区 active_sector = (active_sector == 1) ? 2 : 1; // 擦除旧扇区 Flash_EraseSector(target); } uint32_t Flash_EEPROM_Read(uint16_t addr) { uint32_t sector = (active_sector == 1) ? SECTOR1_BASE : SECTOR2_BASE; return *(volatile uint32_t*)(sector + DATA_OFFSET + addr*4); } ``` ## 6. 注意事项 - **扇区大小**:不同STM32F4型号扇区大小不同(如F407为128KB,F411为128KB但扇区数不同),需查阅参考手册。 - **擦除时间**:Flash擦除耗时较长(约1-2秒),写入操作可能阻塞,建议在RTOS中或使用中断处理。 - **磨损均衡优化**:可增加写入计数或使用环形缓冲区,进一步均衡磨损。 - **掉电保护**:状态字写入顺序至关重要,确保先写数据后更新状态,避免中间状态。 - **地址对齐**:Flash写入必须按字(4字节)对齐,且地址不能跨扇区。 - **中断安全**:写入过程中应关闭中断或使用临界区,防止并发访问。 - **寿命估算**:若每秒写1次,10K次寿命约2.7小时,双缓冲区可延长至5.4小时,需根据应用评估。 ## 7. 总结 双缓冲区方案通过交替使用两个扇区,实现了磨损均衡,并通过状态机管理确保了掉电时的数据一致性。本文提供的代码框架可直接应用于STM32F4项目,但实际生产环境需根据具体需求增强错误处理和性能优化。掌握此技术,可有效利用内部Flash资源,降低硬件成本。