STM32F4内部Flash模拟EEPROM:磨损均衡与掉电保护的双缓冲区实现细节
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式系统中,当需要非易失性存储但成本或体积受限时,使用STM32F4内部Flash模拟EEPROM是一种常见方案。然而,Flash的擦写寿命有限(通常10K次),且掉电可能导致数据损坏。本文深入探讨双缓冲区机制,结合磨损均衡与掉电保护,给出完整的实现细节、代码示例及注意事项,帮助开发者构建可靠的数据存储方案。
# 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的场景。开发者可根据实际需求调整缓冲区大小和策略,以平衡寿命、速度和复杂度。