STM32F4 片上Flash模拟EEPROM:磨损均衡与掉电保护实战指南
👁 2 阅读 · 2026-08-27 · 嵌入式
在嵌入式系统中,EEPROM常用于存储参数,但STM32F4系列大多无独立EEPROM。本文深入讲解如何利用片上Flash模拟EEPROM,重点解决擦写寿命(磨损均衡)和掉电数据完整性两大核心问题。通过原理剖析、双区备份策略、磨损均衡算法及完整代码示例,帮助开发者构建可靠的非易失性存储方案,适用于参数频繁更新的工业控制、物联网节点等场景。
# STM32F4 片上Flash模拟EEPROM:磨损均衡与掉电保护实现
## 为什么需要模拟EEPROM?
STM32F4系列(如STM32F407)内部Flash容量大(512KB~1MB),但擦写寿命有限(典型10000次)。而EEPROM虽可字节擦写且寿命长(100万次),但多数F4型号未集成。因此,当需要频繁保存配置参数(如校准值、运行状态)时,直接使用Flash会导致寿命迅速耗尽。解决方案是:**用软件算法将Flash模拟成EEPROM,通过磨损均衡分散擦写,并利用掉电保护确保数据不丢失**。
## 核心原理
### 1. Flash特性与限制
- **最小擦除单位**:扇区(Sector),F4中通常16KB或64KB。
- **写入单位**:半字(16位)或字(32位),且只能将1写为0(编程),擦除后恢复为0xFF。
- **寿命**:每个扇区约10,000次擦写。
### 2. 磨损均衡策略
将Flash划分为两个或多个逻辑区,每次写入新数据到下一个空闲位置,而不是原地更新。当整个区写满后,擦除旧区并切换。典型方案:**双区交替(Ping-Pong)**。
- 区A和区B各存一份最新数据副本。
- 写入时,先写区A的下一位置,若区A满则擦除区B并写入区B。
- 读取时,比较两区的版本号或时间戳,取最新。
### 3. 掉电保护设计
掉电可能发生在写入过程中,导致数据半更新。解决:
- **原子写入**:先写入数据到临时区,再更新标志位(如CRC校验值)。
- **双备份**:每个数据项存两份,读取时校验CRC,若主备份损坏则用备用。
- **状态机**:定义写入状态(如:空闲、写入中、完成),掉电后重启根据状态恢复。
## 实现步骤
### 步骤1:定义存储结构
```c
// 数据项结构(带CRC和状态)
typedef struct {
uint32_t magic; // 魔数,用于识别有效数据
uint16_t version; // 版本号,越大越新
uint16_t crc; // CRC16校验
uint8_t data[64]; // 实际数据(示例64字节)
} EepromItem_t;
// 存储区管理结构
#define SECTOR_SIZE (16 * 1024) // 假设扇区16KB
#define ITEM_SIZE (sizeof(EepromItem_t)) // 约76字节
#define ITEMS_PER_SECTOR (SECTOR_SIZE / ITEM_SIZE) // 约215个
```
### 步骤2:Flash驱动函数
```c
#include "stm32f4xx_hal.h"
// 擦除扇区
void Flash_EraseSector(uint32_t sector) {
FLASH_EraseInitTypeDef erase = {0};
uint32_t pageError = 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, &pageError);
HAL_FLASH_Lock();
}
// 写入半字(16位)
void Flash_WriteHalfWord(uint32_t addr, uint16_t data) {
HAL_FLASH_Unlock();
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr, data);
HAL_FLASH_Lock();
}
// 写入整个数据项(逐半字写入)
void Flash_WriteItem(uint32_t baseAddr, uint16_t index, EepromItem_t *item) {
uint32_t addr = baseAddr + index * ITEM_SIZE;
uint16_t *p = (uint16_t *)item;
for (int i = 0; i < ITEM_SIZE / 2; i++) {
Flash_WriteHalfWord(addr + i * 2, p[i]);
}
}
```
### 步骤3:磨损均衡逻辑
```c
#define SECTOR_A 0 // 扇区编号(根据实际映射)
#define SECTOR_B 1
// 查找最新有效项(返回所在扇区和索引)
int FindLatestItem(uint32_t *sector, uint16_t *index) {
uint32_t sectors[2] = {SECTOR_A, SECTOR_B};
uint16_t maxVersion = 0;
int found = 0;
for (int s = 0; s < 2; s++) {
uint32_t base = GetSectorAddr(sectors[s]);
for (int i = 0; i < ITEMS_PER_SECTOR; i++) {
EepromItem_t item;
ReadItem(base, i, &item);
if (item.magic == MAGIC_NUM && item.crc == CalcCRC(&item)) {
if (item.version > maxVersion) {
maxVersion = item.version;
*sector = sectors[s];
*index = i;
found = 1;
}
}
}
}
return found;
}
// 写入新数据(自动选择位置)
void Eeprom_Write(uint8_t *data, uint16_t len) {
uint32_t curSector, baseAddr;
uint16_t curIndex;
// 找到当前最新项的位置
if (FindLatestItem(&curSector, &curIndex)) {
// 若当前扇区还有空间,则写下一个位置
if (curIndex < ITEMS_PER_SECTOR - 1) {
baseAddr = GetSectorAddr(curSector);
EepromItem_t newItem;
FillItem(&newItem, data, len);
Flash_WriteItem(baseAddr, curIndex + 1, &newItem);
} else {
// 当前扇区已满,擦除另一个扇区并写入
uint32_t otherSector = (curSector == SECTOR_A) ? SECTOR_B : SECTOR_A;
Flash_EraseSector(otherSector);
baseAddr = GetSectorAddr(otherSector);
EepromItem_t newItem;
FillItem(&newItem, data, len);
Flash_WriteItem(baseAddr, 0, &newItem);
}
} else {
// 首次写入,擦除A扇区并写入
Flash_EraseSector(SECTOR_A);
baseAddr = GetSectorAddr(SECTOR_A);
EepromItem_t newItem;
FillItem(&newItem, data, len);
Flash_WriteItem(baseAddr, 0, &newItem);
}
}
```
### 步骤4:掉电保护增强
- **写入前备份**:在写入新项前,先将旧项复制到临时扇区(若存在),但为简化,我们采用双副本策略:每个数据项在写入时,同时写入两个位置(如当前扇区和另一扇区),读取时取CRC有效且版本高的。
- **状态标志**:在数据项末尾添加一个状态字节(如0xAA表示完成,0x55表示写入中),掉电后重启时,若发现状态为0x55,则丢弃该项。
```c
// 增强的写入函数(带状态)
void Eeprom_WriteSafe(uint8_t *data, uint16_t len) {
// 先写主副本
EepromItem_t item;
FillItem(&item, data, len);
item.status = 0x55; // 写入中
uint32_t sector, base;
uint16_t idx;
// 选择写入位置(略,同前)
Flash_WriteItem(base, idx, &item);
// 写备用副本(另一扇区)
// ...
// 最后更新状态为完成
item.status = 0xAA;
Flash_WriteItem(base, idx, &item);
}
```
## 完整代码示例(简化版)
```c
// 初始化:检查并恢复
void Eeprom_Init(void) {
uint32_t sector;
uint16_t idx;
if (!FindLatestItem(§or, &idx)) {
// 无有效数据,格式化A扇区
Flash_EraseSector(SECTOR_A);
EepromItem_t initItem;
FillItem(&initItem, (uint8_t*)"default", 8);
Flash_WriteItem(GetSectorAddr(SECTOR_A), 0, &initItem);
}
}
// 读取最新数据
int Eeprom_Read(uint8_t *buf, uint16_t len) {
uint32_t sector;
uint16_t idx;
if (FindLatestItem(§or, &idx)) {
EepromItem_t item;
ReadItem(GetSectorAddr(sector), idx, &item);
if (item.status == 0xAA && item.crc == CalcCRC(&item)) {
memcpy(buf, item.data, len);
return 0;
}
}
return -1;
}
```
## 注意事项
- **扇区大小**:根据实际芯片调整,F4的扇区大小可能不同(如F407的扇区0为16KB,扇区1-3为16KB,扇区4-7为128KB)。
- **写入对齐**:Flash写入必须半字对齐,且地址不能跨扇区。
- **擦除时间**:擦除一个扇区约需1~2秒(取决于芯片),期间不能掉电,否则数据丢失。建议在关键操作前关闭中断。
- **磨损均衡效率**:双区方案只能将寿命延长2倍,若需更长,可多区轮换(如4区)。
- **CRC计算**:可使用硬件CRC单元(如STM32F4的CRC模块)加速。
## 总结
通过双区交替和状态机设计,我们成功用STM32F4的片上Flash模拟了EEPROM,实现了磨损均衡和掉电保护。该方法在工业设备中广泛使用,既节省了外部EEPROM成本,又保证了数据可靠性。开发者可根据实际需求调整数据大小、扇区数量和均衡策略,以达到最佳性能。