STM32F4内部Flash模拟EEPROM:磨损均衡与掉电保护的实战方案
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式系统中,当需要存储少量参数且不想外挂EEPROM时,利用STM32F4内部Flash模拟EEPROM是常见做法。但Flash的擦写寿命有限(约1万次),且掉电时可能损坏数据。本文深入剖析磨损均衡与掉电保护的核心原理,给出基于扇区管理、双备份和校验恢复的具体实现方案,并附完整C代码,帮助开发者构建高可靠的参数存储系统。
# 引言
STM32F4系列内置Flash容量大、速度快,但擦写寿命(典型1万次)远低于EEPROM(100万次)。直接按地址写参数会迅速磨损同一扇区。此外,写入过程中掉电可能导致数据半更新,造成系统启动时参数错误。因此,模拟EEPROM必须解决两个核心问题:**磨损均衡**(让所有扇区均匀老化)和**掉电保护**(保证数据一致性)。
# 一、原理剖析
## 1. Flash硬件特性
- STM32F4的Flash以**扇区**为单位擦除(如1KB、2KB等),写入前必须先擦除,且只能将1写为0。
- 擦除操作耗时约数十毫秒,期间掉电会导致扇区数据全0或损坏。
- 每个扇区擦写次数有限,超出后可能产生坏块。
## 2. 磨损均衡设计
**核心思想**:将多个扇区组成一个逻辑存储区,每次写入时选择擦写次数最少的扇区,并记录当前活动扇区。常见策略有:
- **轮询法**:按顺序循环使用扇区,简单但无法应对非均匀写入。
- **计数法**:在每个扇区头部记录擦写次数,写入时选择次数最少者。
本文采用**双扇区轮换 + 磨损计数**:使用两个扇区,一个作为活动扇区,另一个作为备份。每次写入时,将新数据写入备份扇区,成功后切换活动指针,并擦除旧活动扇区。这样每个扇区擦写次数相同,且始终有一个完整副本。
## 3. 掉电保护策略
- **双备份**:数据同时存于两个扇区,但只有活动扇区有效。写入时先写备份,再切换,保证任何时刻至少有一个完整数据。
- **校验和**:每个记录包含CRC32校验,读取时校验失败则回退到另一扇区。
- **写入顺序**:先写数据+校验,再写状态标记(如有效标志),最后切换。若在写状态前掉电,旧扇区仍有效。
# 二、具体实现方案
## 1. 存储布局
假设使用两个1KB扇区(如扇区11和12,地址0x080E0000和0x080E0400)。每个扇区结构如下:
```c
#define SECTOR_SIZE 1024
#define DATA_SIZE 64 // 实际数据长度
typedef struct {
uint32_t magic; // 魔数,用于识别有效扇区
uint32_t write_cnt; // 写入次数(磨损计数)
uint32_t crc; // 数据CRC32
uint8_t data[DATA_SIZE];
} SectorHeader;
```
扇区头部固定占用16字节,数据区64字节,剩余空间填充0xFF。
## 2. 核心函数实现
### 扇区擦除与写入
```c
#include "stm32f4xx_hal.h"
#define FLASH_SECTOR_11 11
#define FLASH_SECTOR_12 12
#define ADDR_SECTOR_11 0x080E0000
#define ADDR_SECTOR_12 0x080E0400
// 擦除指定扇区
static void flash_erase_sector(uint8_t sector) {
FLASH_EraseInitTypeDef erase = {0};
uint32_t page_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, &page_error);
HAL_FLASH_Lock();
}
// 写入数据到指定地址(按32位字写)
static void flash_write_word(uint32_t addr, uint32_t data) {
HAL_FLASH_Unlock();
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, data);
HAL_FLASH_Lock();
}
```
### 磨损均衡控制
```c
// 读取两个扇区的头部,返回有效且写入次数较少的扇区地址
static uint32_t select_active_sector(void) {
SectorHeader h1, h2;
memcpy(&h1, (void*)ADDR_SECTOR_11, sizeof(h1));
memcpy(&h2, (void*)ADDR_SECTOR_12, sizeof(h2));
int valid1 = (h1.magic == MAGIC_NUM && crc_check(&h1));
int valid2 = (h2.magic == MAGIC_NUM && crc_check(&h2));
if (valid1 && valid2) {
// 选择写入次数少的扇区作为备份,另一个作为活动
return (h1.write_cnt <= h2.write_cnt) ? ADDR_SECTOR_11 : ADDR_SECTOR_12;
} else if (valid1) {
return ADDR_SECTOR_11;
} else if (valid2) {
return ADDR_SECTOR_12;
} else {
// 两个都无效,初始化扇区11
flash_erase_sector(FLASH_SECTOR_11);
return ADDR_SECTOR_11;
}
}
```
### 写入数据(带掉电保护)
```c
int eeprom_write(uint8_t *data, uint32_t len) {
if (len > DATA_SIZE) return -1;
uint32_t active = select_active_sector();
uint32_t backup = (active == ADDR_SECTOR_11) ? ADDR_SECTOR_12 : ADDR_SECTOR_11;
// 构造新头部
SectorHeader new_hdr;
memcpy(&new_hdr, (void*)active, sizeof(new_hdr));
new_hdr.write_cnt++;
new_hdr.crc = crc32(data, len);
memcpy(new_hdr.data, data, len);
new_hdr.magic = MAGIC_NUM;
// 擦除备份扇区
flash_erase_sector((backup == ADDR_SECTOR_11) ? FLASH_SECTOR_11 : FLASH_SECTOR_12);
// 写入备份扇区(先写头部,再写数据,最后写magic)
uint32_t addr = backup;
flash_write_word(addr, new_hdr.write_cnt);
flash_write_word(addr+4, new_hdr.crc);
for (int i=0; i> 1) ^ (0xEDB88320 & -(crc & 1));
}
}
return ~crc;
}
```
# 三、使用示例
```c
int main(void) {
HAL_Init();
// ... 系统时钟初始化
uint8_t params[64] = {0};
// 读取参数
if (eeprom_read(params, 64) != 0) {
// 首次使用,初始化默认值
memset(params, 0, 64);
eeprom_write(params, 64);
}
// 修改参数并保存
params[0] = 42;
eeprom_write(params, 64);
while(1) {}
}
```
# 四、注意事项
- **扇区大小**:STM32F4不同型号扇区大小不同(如1KB、2KB、128KB),需根据实际调整地址和擦除函数。
- **写入粒度**:Flash编程必须按字(32位)或半字,且地址对齐。
- **擦除时间**:擦除期间掉电会损坏扇区,但双备份机制可保证数据不丢。
- **磨损计数溢出**:write_cnt为32位,足够使用,但需处理溢出(如达到0xFFFFFFFF时重置所有扇区)。
- **性能**:每次写入需擦除一个扇区,耗时约20ms,不适合高频写入。
- **中断保护**:写入过程中应关闭中断,防止打断导致状态不一致。
# 五、总结
通过双扇区轮换和磨损计数,我们实现了Flash的均匀磨损,寿命提升至2万次(两个扇区)。掉电保护通过先写备份再切换的机制,确保任何时刻至少有一个完整数据。CRC校验进一步增强了数据完整性。该方案适用于参数存储、配置保存等低频写入场景,是嵌入式开发中实用且可靠的技术。