# STM32F4内部Flash模拟EEPROM:磨损均衡与掉电保护的双缓冲区实现细节 ## 1. 为什么需要双缓冲区? STM32F4的Flash具有擦除次数限制(典型10K次),且写入前必须擦除(按扇区)。直接单缓冲区存储时,每次更新需擦除整个扇区,频繁写入会快速耗尽寿命。更重要的是,若在擦除或写入过程中掉电,数据可能丢失或损坏。双缓冲区方案通过交替使用两个扇区,实现磨损均衡,并利用备份机制保证掉电安全。 ## 2. 核心原理 ### 2.1 磨损均衡 - 将Flash划分为两个等大小的扇区(如Sector 11和Sector 12,各16KB)。 - 每次写入时,选择当前活跃扇区(Active Sector),写入新数据并标记为最新。 - 当活跃扇区写满或达到阈值时,切换至另一扇区,并将旧扇区擦除。这样两个扇区轮流使用,磨损均匀。 ### 2.2 掉电保护 - 每个扇区头部存储状态标志(如有效、待擦除、擦除完成)。 - 写入流程:先写备份区(非活跃扇区),再更新活跃区标志,最后擦除旧数据。 - 掉电时,系统重启后通过状态标志判断哪个扇区有效,并恢复数据。 ## 3. 实现步骤 ### 3.1 内存布局定义 ```c #define FLASH_SECTOR_ACTIVE 11 #define FLASH_SECTOR_BACKUP 12 #define FLASH_SECTOR_SIZE 16 * 1024 // 16KB #define DATA_SIZE 512 // 用户数据长度(字节) typedef struct { uint32_t magic; // 魔数,用于校验 uint32_t status; // 状态:0xFFFFFFFF有效,0x00000000待擦除 uint8_t data[DATA_SIZE]; uint32_t crc32; // 数据CRC校验 } FlashPage_t; ``` ### 3.2 初始化与恢复 ```c void Flash_Init(void) { FlashPage_t page; uint32_t active_sector = FLASH_SECTOR_ACTIVE; // 读取两个扇区头部,判断有效状态 Flash_Read(FLASH_SECTOR_ACTIVE, &page); if (page.magic != MAGIC_NUM || page.status != 0xFFFFFFFF) { // 活跃扇区无效,尝试备份扇区 Flash_Read(FLASH_SECTOR_BACKUP, &page); if (page.magic == MAGIC_NUM && page.status == 0xFFFFFFFF) { active_sector = FLASH_SECTOR_BACKUP; } else { // 两个都无效,格式化(擦除两个扇区,写初始数据) Flash_Erase(FLASH_SECTOR_ACTIVE); Flash_Erase(FLASH_SECTOR_BACKUP); Flash_WritePage(FLASH_SECTOR_ACTIVE, &default_data); active_sector = FLASH_SECTOR_ACTIVE; } } g_active_sector = active_sector; } ``` ### 3.3 写入操作(带磨损均衡) ```c void Flash_Write(uint8_t *data, uint32_t len) { FlashPage_t new_page; uint32_t backup_sector = (g_active_sector == FLASH_SECTOR_ACTIVE) ? FLASH_SECTOR_BACKUP : FLASH_SECTOR_ACTIVE; // 1. 构造新页数据 new_page.magic = MAGIC_NUM; new_page.status = 0xFFFFFFFF; memcpy(new_page.data, data, len); new_page.crc32 = CRC32_Compute(data, len); // 2. 擦除备份扇区(若未擦除) Flash_Erase(backup_sector); // 3. 写入备份扇区(先写备份,保证掉电时旧数据仍在) Flash_WritePage(backup_sector, &new_page); // 4. 更新活跃扇区状态为待擦除(标记旧数据无效) Flash_UpdateStatus(g_active_sector, 0x00000000); // 5. 切换活跃扇区 g_active_sector = backup_sector; // 6. 擦除旧活跃扇区(延迟擦除,减少磨损) Flash_Erase(g_active_sector == FLASH_SECTOR_ACTIVE ? FLASH_SECTOR_BACKUP : FLASH_SECTOR_ACTIVE); } ``` ### 3.4 读取操作 ```c void Flash_Read(uint8_t *data, uint32_t len) { FlashPage_t page; Flash_ReadPage(g_active_sector, &page); if (page.magic == MAGIC_NUM && page.status == 0xFFFFFFFF && CRC32_Check(page.data, len, page.crc32)) { memcpy(data, page.data, len); } else { // 数据损坏,返回默认值或错误 memset(data, 0xFF, len); } } ``` ## 4. 完整代码示例(简化版) ```c // flash_ee.h #define MAGIC_NUM 0xA5A5A5A5 #define FLASH_SECTOR_ACTIVE 11 #define FLASH_SECTOR_BACKUP 12 #define DATA_SIZE 512 typedef struct { uint32_t magic; uint32_t status; uint8_t data[DATA_SIZE]; uint32_t crc32; } FlashPage_t; void Flash_Init(void); void Flash_Write(uint8_t *data, uint32_t len); void Flash_Read(uint8_t *data, uint32_t len); // flash_ee.c #include "flash_ee.h" #include "stm32f4xx_hal.h" static uint32_t g_active_sector; // 底层Flash操作(使用HAL库) static void Flash_Erase(uint32_t sector) { FLASH_EraseInitTypeDef erase = {0}; uint32_t 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, &error); HAL_FLASH_Lock(); } static void Flash_WritePage(uint32_t sector, FlashPage_t *page) { uint32_t addr = FLASH_BASE + sector * FLASH_SECTOR_SIZE; HAL_FLASH_Unlock(); // 写入头部和用户数据(注意对齐) HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, page->magic); addr += 4; HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, page->status); addr += 4; for (int i = 0; i < DATA_SIZE; i += 4) { uint32_t word = *(uint32_t*)&page->data[i]; HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + i, word); } HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + DATA_SIZE, page->crc32); HAL_FLASH_Lock(); } static void Flash_ReadPage(uint32_t sector, FlashPage_t *page) { uint32_t addr = FLASH_BASE + sector * FLASH_SECTOR_SIZE; memcpy(page, (void*)addr, sizeof(FlashPage_t)); } static void Flash_UpdateStatus(uint32_t sector, uint32_t status) { uint32_t addr = FLASH_BASE + sector * FLASH_SECTOR_SIZE + 4; HAL_FLASH_Unlock(); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, status); HAL_FLASH_Lock(); } // 初始化、写入、读取函数实现见上文 ``` ## 5. 注意事项 - **扇区大小匹配**:确保所选扇区大小足够存放FlashPage_t结构体,且地址对齐(4字节)。 - **擦除时间**:Flash擦除耗时较长(约1-2秒),写入期间应避免中断或任务切换,必要时关中断。 - **磨损均衡粒度**:每次写入都擦除备份扇区,实际寿命仍受限于擦除次数。可增加多页缓冲或使用日志式存储进一步优化。 - **掉电检测**:若系统有掉电检测(如BOD),可在掉电瞬间完成关键写入,提高可靠性。 - **CRC校验**:使用硬件CRC或软件CRC,确保数据完整性。 - **Flash编程电压**:确保供电稳定,编程时电压波动会导致写入失败。 ## 6. 总结 双缓冲区方案通过交替使用两个Flash扇区,实现了磨损均衡,并通过先写备份再切换的策略,保证了掉电时数据不丢失。该实现简单可靠,适用于大多数需要模拟EEPROM的场景。开发者可根据实际需求调整缓冲区大小和策略,以平衡寿命、速度和复杂度。