STM32F4内部Flash模拟EEPROM:磨损均衡与掉电保护的双缓冲区实现细节
1. 为什么需要双缓冲区?
STM32F4的Flash具有擦除次数限制(典型10K次),且写入前必须擦除(按扇区)。直接单缓冲区存储时,每次更新需擦除整个扇区,频繁写入会快速耗尽寿命。更重要的是,若在擦除或写入过程中掉电,数据可能丢失或损坏。双缓冲区方案通过交替使用两个扇区,实现磨损均衡,并利用备份机制保证掉电安全。
2. 核心原理
2.1 磨损均衡
- 将Flash划分为两个等大小的扇区(如Sector 11和Sector 12,各16KB)。
- 每次写入时,选择当前活跃扇区(Active Sector),写入新数据并标记为最新。
- 当活跃扇区写满或达到阈值时,切换至另一扇区,并将旧扇区擦除。这样两个扇区轮流使用,磨损均匀。
2.2 掉电保护
- 每个扇区头部存储状态标志(如有效、待擦除、擦除完成)。
- 写入流程:先写备份区(非活跃扇区),再更新活跃区标志,最后擦除旧数据。
- 掉电时,系统重启后通过状态标志判断哪个扇区有效,并恢复数据。
3. 实现步骤
3.1 内存布局定义
#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 初始化与恢复
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 写入操作(带磨损均衡)
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 读取操作
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. 完整代码示例(简化版)
// 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的场景。开发者可根据实际需求调整缓冲区大小和策略,以平衡寿命、速度和复杂度。