引言
STM32F4系列内置Flash容量大、速度快,但擦写寿命(典型1万次)远低于EEPROM(100万次)。直接按地址写参数会迅速磨损同一扇区。此外,写入过程中掉电可能导致数据半更新,造成系统启动时参数错误。因此,模拟EEPROM必须解决两个核心问题:磨损均衡(让所有扇区均匀老化)和掉电保护(保证数据一致性)。
一、原理剖析
1. Flash硬件特性
- STM32F4的Flash以扇区为单位擦除(如1KB、2KB等),写入前必须先擦除,且只能将1写为0。
- 擦除操作耗时约数十毫秒,期间掉电会导致扇区数据全0或损坏。
- 每个扇区擦写次数有限,超出后可能产生坏块。
2. 磨损均衡设计
核心思想:将多个扇区组成一个逻辑存储区,每次写入时选择擦写次数最少的扇区,并记录当前活动扇区。常见策略有:
- 轮询法:按顺序循环使用扇区,简单但无法应对非均匀写入。
- 计数法:在每个扇区头部记录擦写次数,写入时选择次数最少者。
本文采用双扇区轮换 + 磨损计数:使用两个扇区,一个作为活动扇区,另一个作为备份。每次写入时,将新数据写入备份扇区,成功后切换活动指针,并擦除旧活动扇区。这样每个扇区擦写次数相同,且始终有一个完整副本。
3. 掉电保护策略
- 双备份:数据同时存于两个扇区,但只有活动扇区有效。写入时先写备份,再切换,保证任何时刻至少有一个完整数据。
- 校验和:每个记录包含CRC32校验,读取时校验失败则回退到另一扇区。
- 写入顺序:先写数据+校验,再写状态标记(如有效标志),最后切换。若在写状态前掉电,旧扇区仍有效。
二、具体实现方案
1. 存储布局
假设使用两个1KB扇区(如扇区11和12,地址0x080E0000和0x080E0400)。每个扇区结构如下:
#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. 核心函数实现
扇区擦除与写入
#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();
}
磨损均衡控制
// 读取两个扇区的头部,返回有效且写入次数较少的扇区地址
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;
}
}
写入数据(带掉电保护)
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<len; i+=4) {
uint32_t word = 0;
memcpy(&word, &new_hdr.data[i], 4);
flash_write_word(addr+16+i, word);
}
// 最后写magic,表示写入完成
flash_write_word(addr, MAGIC_NUM);
// 擦除旧活动扇区(此时备份已完整)
flash_erase_sector((active == ADDR_SECTOR_11) ? FLASH_SECTOR_11 : FLASH_SECTOR_12);
return 0;
}
读取数据(带校验和回退)
int eeprom_read(uint8_t *data, uint32_t len) {
uint32_t active = select_active_sector();
SectorHeader hdr;
memcpy(&hdr, (void*)active, sizeof(hdr));
if (hdr.magic != MAGIC_NUM || crc32(hdr.data, DATA_SIZE) != hdr.crc) {
// 活动扇区损坏,尝试备份
uint32_t backup = (active == ADDR_SECTOR_11) ? ADDR_SECTOR_12 : ADDR_SECTOR_11;
memcpy(&hdr, (void*)backup, sizeof(hdr));
if (hdr.magic != MAGIC_NUM || crc32(hdr.data, DATA_SIZE) != hdr.crc) {
return -1; // 两个都无效
}
}
memcpy(data, hdr.data, len);
return 0;
}
CRC32实现(简化版)
uint32_t crc32(const uint8_t *data, uint32_t len) {
uint32_t crc = 0xFFFFFFFF;
for (uint32_t i=0; i<len; i++) {
crc ^= data[i];
for (int j=0; j<8; j++) {
crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1));
}
}
return ~crc;
}
三、使用示例
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校验进一步增强了数据完整性。该方案适用于参数存储、配置保存等低频写入场景,是嵌入式开发中实用且可靠的技术。