STM32F4内部Flash模拟EEPROM:磨损均衡与掉电保护的实战策略
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式系统中,当需要非易失性存储但成本或空间受限时,使用STM32F4内部Flash模拟EEPROM是一种常见方案。然而,Flash的擦写寿命有限(通常10K次),且掉电可能导致数据损坏。本文深入探讨磨损均衡与掉电保护的具体实现策略,包括扇区管理、动态/静态均衡算法、双备份与校验机制,并提供基于STM32F4的完整代码示例,帮助开发者构建可靠、耐用的数据存储方案。
# 引言
在嵌入式产品中,EEPROM常用于保存配置参数、校准数据等。但独立EEPROM芯片增加成本和PCB面积,而STM32F4系列内部Flash容量大、速度快,通过模拟EEPROM可节省资源。然而,Flash的擦写寿命(典型10K次)远低于EEPROM(1M次),且写入前必须擦除整个扇区(通常16KB),掉电时可能造成数据丢失或损坏。因此,实现磨损均衡和掉电保护是模拟EEPROM的核心挑战。
# 一、Flash模拟EEPROM的基本原理
STM32F4的Flash以扇区为单位组织,每个扇区大小不等(如16KB、64KB等)。写入数据时,Flash只能将1变为0(编程),擦除则将所有位恢复为1。因此,模拟EEPROM需采用“写后擦”策略:
- 将数据写入扇区中的空闲位置(追加写)。
- 当扇区写满后,擦除整个扇区并重新写入有效数据。
这种策略减少了擦除次数,但若频繁写入同一地址,仍会快速耗尽该扇区寿命。磨损均衡旨在将擦写均匀分布到多个扇区,延长整体寿命。
# 二、磨损均衡策略
## 2.1 扇区管理结构
通常使用2个或更多扇区作为存储池。每个扇区划分为多个固定大小的记录槽(如32字节),每个记录包含:
- 数据区:用户数据。
- 状态区:标记记录状态(如有效、无效、擦除)。
- 序号区:单调递增的序号,用于区分新旧数据。
以2个扇区为例,结构如下:
```c
#define SECTOR_SIZE (16 * 1024) // 假设扇区大小16KB
#define RECORD_SIZE 32 // 每个记录32字节
#define RECORDS_PER_SECTOR (SECTOR_SIZE / RECORD_SIZE)
typedef struct {
uint8_t data[24]; // 用户数据,24字节
uint32_t seq; // 序号,4字节
uint8_t status; // 状态:0xFF有效,0x00无效,0xAA擦除标记
} Record_t;
```
## 2.2 动态磨损均衡
每次写入时,在当前活动扇区中寻找空闲记录槽(状态为0xFF且seq为0xFFFFFFFF)。若找到,则写入新数据并更新序号。若扇区已满,则切换到另一个扇区:
1. 擦除备用扇区。
2. 将当前扇区中所有有效记录(状态为0xFF)复制到备用扇区,并保持序号不变。
3. 擦除当前扇区,将其设为备用。
这样,每次切换扇区时,两个扇区的擦写次数交替增加,实现均衡。但若写入频率极不均匀(如只写一个地址),动态均衡仍可能导致某个扇区提前耗尽。
## 2.3 静态磨损均衡
静态均衡将不常修改的数据(如出厂参数)定期迁移到擦写次数较少的扇区。实现方法:
- 维护每个扇区的擦写计数(存储在Flash中)。
- 当某个扇区擦写次数超过阈值(如平均次数+10),触发迁移:将该扇区中的有效数据复制到最冷扇区,并擦除原扇区。
但静态均衡增加复杂度,对于大多数应用,动态均衡已足够。若需极致寿命,可结合静态均衡。
# 三、掉电保护策略
掉电可能发生在写入过程中,导致记录不完整或状态不一致。保护措施包括:
## 3.1 双备份与校验
- 每个记录写入两次:先写主记录,再写备份记录(位于不同扇区或不同偏移)。
- 读取时,先读主记录,校验CRC;若失败,则读备份。
- 写入时,先写备份,再写主记录,确保至少一个有效。
## 3.2 状态机与原子操作
- 写入记录时,先擦除目标槽(置为0xFF),然后写入数据,最后更新状态为有效。
- 若掉电发生在写数据期间,状态仍为0xFF,读取时视为无效,可回滚。
- 使用Flash的编程操作(半字或字)确保写入原子性,避免部分写入。
## 3.3 掉电检测与恢复
- 利用STM32的PVD(可编程电压检测)中断,检测电压跌落时,禁止新写入并完成当前操作。
- 上电后,扫描所有记录,找出序号最大的有效记录作为最新数据,并清理无效记录。
# 四、完整代码示例
以下代码实现基于2个扇区的动态磨损均衡和简单掉电保护(使用CRC校验)。
```c
#include "stm32f4xx.h"
#include
// 配置:使用扇区11和12(地址0x080E0000和0x080E4000,假设容量足够)
#define FLASH_SECTOR_ACTIVE 11
#define FLASH_SECTOR_BACKUP 12
#define ACTIVE_ADDR 0x080E0000
#define BACKUP_ADDR 0x080E4000
#define RECORD_SIZE 32
#define RECORDS_PER_SECTOR (16 * 1024 / RECORD_SIZE)
typedef struct {
uint8_t data[24];
uint32_t seq;
uint8_t status; // 0xFF有效,0x00无效
} Record_t;
// 简单CRC8校验
uint8_t crc8(uint8_t *data, uint16_t len) {
uint8_t crc = 0;
while (len--) {
crc ^= *data++;
for (int i = 0; i < 8; i++) {
crc = (crc & 0x80) ? (crc << 1) ^ 0x07 : (crc << 1);
}
}
return crc;
}
// 擦除扇区
void flash_erase_sector(uint32_t sector) {
FLASH_Unlock();
FLASH_EraseSector(sector, VoltageRange_3);
FLASH_Lock();
}
// 写入一条记录(带CRC)
void flash_write_record(uint32_t addr, Record_t *rec) {
uint8_t buf[RECORD_SIZE];
memcpy(buf, rec->data, 24);
memcpy(buf + 24, &rec->seq, 4);
buf[28] = rec->status;
buf[29] = crc8(buf, 29);
buf[30] = 0xFF; // 保留
buf[31] = 0xFF;
FLASH_Unlock();
for (int i = 0; i < RECORD_SIZE; i += 2) {
FLASH_ProgramHalfWord(addr + i, *(uint16_t*)(buf + i));
}
FLASH_Lock();
}
// 读取记录并校验
int flash_read_record(uint32_t addr, Record_t *rec) {
uint8_t buf[RECORD_SIZE];
memcpy(buf, (uint8_t*)addr, RECORD_SIZE);
if (buf[28] != 0xFF) return 0; // 无效
if (crc8(buf, 29) != buf[29]) return 0;
memcpy(rec->data, buf, 24);
memcpy(&rec->seq, buf + 24, 4);
rec->status = 0xFF;
return 1;
}
// 查找最新有效记录(遍历两个扇区)
uint32_t find_latest(Record_t *rec) {
uint32_t max_seq = 0;
uint32_t found = 0;
Record_t tmp;
for (int i = 0; i < RECORDS_PER_SECTOR; i++) {
if (flash_read_record(ACTIVE_ADDR + i * RECORD_SIZE, &tmp) && tmp.seq > max_seq) {
max_seq = tmp.seq;
memcpy(rec, &tmp, sizeof(Record_t));
found = 1;
}
if (flash_read_record(BACKUP_ADDR + i * RECORD_SIZE, &tmp) && tmp.seq > max_seq) {
max_seq = tmp.seq;
memcpy(rec, &tmp, sizeof(Record_t));
found = 1;
}
}
return found;
}
// 写入数据(磨损均衡+掉电保护)
void eeprom_write(uint8_t *data, uint16_t len) {
Record_t rec;
uint32_t seq = 0;
// 获取当前最大序号
Record_t latest;
if (find_latest(&latest)) {
seq = latest.seq + 1;
}
memcpy(rec.data, data, len);
rec.seq = seq;
rec.status = 0xFF;
// 尝试写入活动扇区
for (int i = 0; i < RECORDS_PER_SECTOR; i++) {
uint32_t addr = ACTIVE_ADDR + i * RECORD_SIZE;
if (*(uint8_t*)addr == 0xFF && *(uint32_t*)(addr + 24) == 0xFFFFFFFF) {
flash_write_record(addr, &rec);
return;
}
}
// 活动扇区已满,切换扇区
flash_erase_sector(FLASH_SECTOR_BACKUP);
// 复制有效记录到备份扇区
for (int i = 0; i < RECORDS_PER_SECTOR; i++) {
Record_t tmp;
if (flash_read_record(ACTIVE_ADDR + i * RECORD_SIZE, &tmp)) {
flash_write_record(BACKUP_ADDR + i * RECORD_SIZE, &tmp);
}
}
// 写入新记录
flash_write_record(BACKUP_ADDR, &rec);
// 擦除活动扇区,并交换角色
flash_erase_sector(FLASH_SECTOR_ACTIVE);
// 注意:实际中需交换地址,这里简化,使用宏定义切换
}
// 读取数据
void eeprom_read(uint8_t *data, uint16_t len) {
Record_t rec;
if (find_latest(&rec)) {
memcpy(data, rec.data, len);
}
}
```
# 五、注意事项
- **扇区大小**:STM32F4不同型号扇区大小不同,需根据实际调整宏定义。
- **擦写时间**:擦除一个16KB扇区约需1秒,写入半字约50us,需考虑实时性。
- **磨损均衡粒度**:记录槽越小,均衡效果越好,但管理开销增大。
- **掉电保护**:建议使用PVD中断,在电压跌落时禁止写入,并确保当前操作完成。
- **测试**:进行掉电测试时,需模拟不同阶段的掉电,确保恢复后数据一致。
# 结语
通过合理的扇区管理、磨损均衡和掉电保护机制,STM32F4内部Flash可以可靠地模拟EEPROM,满足大多数嵌入式应用需求。本文提供的策略和代码可作为基础,开发者可根据具体需求扩展,如增加静态均衡、多扇区管理等。掌握这些技术,能显著提升产品的数据存储可靠性。