引言
STM32F4系列内置Flash具有1MB容量,但每个扇区擦写寿命仅约1万次(典型值),而EEPROM可达百万次。若直接模拟,频繁写入将迅速耗尽寿命。同时,写入过程中掉电可能导致数据半更新,引发系统故障。因此,设计磨损均衡(Wear Leveling)和掉电保护(Power-fail Protection)是嵌入式存储的关键。
一、原理剖析
1. Flash与EEPROM差异
- 擦除粒度:Flash按扇区擦除(如F4的128KB扇区),EEPROM按字节。
- 写入限制:Flash只能将1写为0,需先擦除(全1)。
- 寿命:扇区擦写次数有限,磨损均衡需分散写入。
2. 磨损均衡机制
- 思想:将逻辑地址映射到多个物理扇区,轮流使用。
- 实现:维护一个写入计数器,每次写入切换扇区,擦除最旧扇区。
- 效果:将擦写次数平均化,延长Flash寿命。
3. 掉电保护策略
- 风险:写入过程中掉电,数据可能部分写入或扇区状态不一致。
- 方案:采用双备份(Double Backup)或日志式(Log-based)存储。
- 核心:写入前先备份旧数据,写入完成后更新标志,掉电后恢复时检测标志决定回滚或提交。
二、系统设计
本设计使用两个扇区(Sector0和Sector1,各16KB)模拟一个EEPROM区域,存储结构如下:
// 存储记录头(位于每个扇区起始)
typedef struct {
uint32_t magic; // 魔数,0xA5A5A5A5表示有效
uint32_t write_cnt; // 写入次数,用于磨损均衡
uint32_t crc; // 数据CRC校验
uint8_t data[512]; // 用户数据区
} FlashRecord;
-
扇区状态:每个扇区有
ACTIVE(当前写入)和STANDBY(备用)状态。 -
写入流程:
- 读取当前ACTIVE扇区的记录。
- 修改数据,更新write_cnt+1,计算CRC。
- 写入STANDBY扇区(先擦除)。
- 写入完成后,将STANDBY设为ACTIVE(通过更新扇区头标志)。
- 擦除旧ACTIVE扇区,作为新STANDBY。
-
掉电恢复:上电时扫描两个扇区,检查magic和CRC。若只有一个有效,则使用之;若两个都有效,则选择write_cnt较大者(最新)。若一个损坏,则从另一个恢复。
三、代码实现
1. 底层Flash操作函数
#include "stm32f4xx.h"
#define FLASH_SECTOR0 0
#define FLASH_SECTOR1 1
#define FLASH_ADDR0 0x08000000 // 假设从起始地址开始
#define FLASH_ADDR1 0x08004000 // 16KB偏移
// 擦除扇区
void Flash_EraseSector(uint8_t sector) {
FLASH_Unlock();
while (FLASH_GetStatus() != FLASH_BUSY);
FLASH_EraseSector(sector, VoltageRange_3);
FLASH_Lock();
}
// 写入数据(半字为单位)
void Flash_WriteData(uint32_t addr, uint32_t *data, uint16_t len) {
FLASH_Unlock();
for (uint16_t i = 0; i < len; i++) {
while (FLASH_GetStatus() != FLASH_BUSY);
FLASH_ProgramWord(addr + i*4, data[i]);
}
FLASH_Lock();
}
// 读取数据(直接指针访问)
void Flash_ReadData(uint32_t addr, uint32_t *data, uint16_t len) {
for (uint16_t i = 0; i < len; i++) {
data[i] = *(volatile uint32_t *)(addr + i*4);
}
}
2. 磨损均衡与写入逻辑
// 定义记录结构(需对齐)
#define RECORD_SIZE (sizeof(FlashRecord) + 3) & ~3 // 4字节对齐
// 查找有效扇区
uint8_t Flash_FindActiveSector(void) {
FlashRecord rec0, rec1;
Flash_ReadData(FLASH_ADDR0, (uint32_t*)&rec0, sizeof(rec0)/4);
Flash_ReadData(FLASH_ADDR1, (uint32_t*)&rec1, sizeof(rec1)/4);
uint8_t valid0 = (rec0.magic == 0xA5A5A5A5) && (CRC32(rec0.data) == rec0.crc);
uint8_t valid1 = (rec1.magic == 0xA5A5A5A5) && (CRC32(rec1.data) == rec1.crc);
if (valid0 && valid1) {
return (rec0.write_cnt >= rec1.write_cnt) ? 0 : 1; // 取最新
} else if (valid0) return 0;
else if (valid1) return 1;
else return 0; // 默认0,首次使用
}
// 写入数据(带磨损均衡)
void Flash_WriteData_WithWearLeveling(uint8_t *newData, uint16_t len) {
uint8_t activeSector = Flash_FindActiveSector();
uint8_t standbySector = (activeSector == 0) ? 1 : 0;
uint32_t standbyAddr = (standbySector == 0) ? FLASH_ADDR0 : FLASH_ADDR1;
// 读取旧记录(若存在)
FlashRecord oldRec;
Flash_ReadData((activeSector==0)?FLASH_ADDR0:FLASH_ADDR1, (uint32_t*)&oldRec, sizeof(oldRec)/4);
// 构建新记录
FlashRecord newRec;
newRec.magic = 0xA5A5A5A5;
newRec.write_cnt = (oldRec.magic==0xA5A5A5A5) ? oldRec.write_cnt + 1 : 1;
memcpy(newRec.data, newData, len);
newRec.crc = CRC32(newData, len);
// 擦除备用扇区
Flash_EraseSector(standbySector);
// 写入新记录(先写数据,最后写magic和cnt,确保完整性)
// 注意:为简化,这里一次性写入,实际可分段
Flash_WriteData(standbyAddr, (uint32_t*)&newRec, sizeof(newRec)/4);
// 更新状态:备用扇区成为ACTIVE,旧扇区可擦除(下次备用)
// 此处通过擦除旧扇区实现(但需确保新扇区写入成功)
Flash_EraseSector(activeSector);
}
// 读取数据
void Flash_ReadData_WithWearLeveling(uint8_t *data, uint16_t len) {
uint8_t activeSector = Flash_FindActiveSector();
FlashRecord rec;
Flash_ReadData((activeSector==0)?FLASH_ADDR0:FLASH_ADDR1, (uint32_t*)&rec, sizeof(rec)/4);
memcpy(data, rec.data, len);
}
3. 掉电恢复函数
void Flash_PowerFailRecovery(void) {
FlashRecord rec0, rec1;
Flash_ReadData(FLASH_ADDR0, (uint32_t*)&rec0, sizeof(rec0)/4);
Flash_ReadData(FLASH_ADDR1, (uint32_t*)&rec1, sizeof(rec1)/4);
uint8_t valid0 = (rec0.magic == 0xA5A5A5A5) && (CRC32(rec0.data) == rec0.crc);
uint8_t valid1 = (rec1.magic == 0xA5A5A5A5) && (CRC32(rec1.data) == rec1.crc);
if (!valid0 && !valid1) {
// 两个都无效,初始化扇区0
Flash_EraseSector(0);
FlashRecord initRec = {0xA5A5A5A5, 1, 0, {0}};
initRec.crc = CRC32(initRec.data, sizeof(initRec.data));
Flash_WriteData(FLASH_ADDR0, (uint32_t*)&initRec, sizeof(initRec)/4);
} else if (valid0 && !valid1) {
// 扇区1损坏,从0复制到1(可选)
Flash_EraseSector(1);
Flash_WriteData(FLASH_ADDR1, (uint32_t*)&rec0, sizeof(rec0)/4);
} else if (!valid0 && valid1) {
// 扇区0损坏,从1复制到0
Flash_EraseSector(0);
Flash_WriteData(FLASH_ADDR0, (uint32_t*)&rec1, sizeof(rec1)/4);
}
// 若都有效,无需处理
}
四、配置步骤
-
开启Flash访问:在
stm32f4xx_hal_conf.h中启用HAL_FLASH_MODULE_ENABLED。 - 定义存储区域:在链接脚本中预留两个扇区(如从0x08000000开始,但注意避开代码区)。
-
初始化:系统启动时调用
Flash_PowerFailRecovery()进行恢复。 -
读写调用:使用
Flash_WriteData_WithWearLeveling()和Flash_ReadData_WithWearLeveling()。 - 优化:根据数据大小调整记录结构,可增加多页存储。
五、注意事项
- 写入顺序:先写数据区,最后写magic和write_cnt,避免半写状态被误认为有效。
- CRC校验:建议使用硬件CRC单元(如STM32F4内置CRC)加速。
- 中断保护:写入过程中应关闭中断或使用临界区,防止干扰。
- 磨损均衡粒度:若数据频繁小量更新,可增加扇区数量或使用日志式。
- 测试:通过反复上下电和写入测试验证可靠性。
结语
通过上述策略,STM32F4的Flash模拟EEPROM可显著延长寿命并保证数据安全。实际项目中可根据需求调整扇区大小和数量,甚至结合RTOS实现异步写入。嵌入式开发中,细节决定成败,希望本文能助你构建更稳健的存储系统。