一、为什么用 Flash 模拟 EEPROM
很多 STM32 型号没有真正的 EEPROM,但提供了大容量 Flash。Flash 的写入以“页”或“扇区”为单位,且只能将 1 写为 0,擦除后恢复为 0xFF。模拟 EEPROM 的核心思路是:将一个扇区划分为多个记录槽,每次写入只修改一个槽,避免频繁擦写。
二、STM32 Flash 硬件特性
- 擦除最小单位:不同型号差异大。F1 系列为 1KB/2KB 页,F4 系列为 16KB 扇区,L4 系列为 2KB 页。
- 编程最小单位:通常为 16 位或 32 位(半字/字)。
- 写寿命:典型 10K 次擦写,但通过模拟 EEPROM 可分散磨损。
- 操作电压:编程/擦除时要求 VDD 稳定,禁止在低电压下操作。
三、直接写入 Flash 的步骤(以 STM32F4 为例)
1. 解锁 Flash
#include "stm32f4xx.h"
void Flash_Unlock(void) {
HAL_FLASH_Unlock();
// 或使用标准库:FLASH_Unlock();
}
2. 擦除扇区
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_FLASHEx_Erase(&erase, &pageError);
}
注意:擦除后整个扇区变为 0xFF,不可恢复。
3. 写入数据
void Flash_WriteData(uint32_t address, uint32_t *data, uint32_t len) {
for (uint32_t i = 0; i < len; i++) {
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address + i*4, data[i]);
}
}
4. 读取数据
Flash 读取和普通内存一样,直接通过指针访问:
uint32_t value = *(volatile uint32_t *)address;
四、实现轻量级 EEPROM 模拟
设计思路
- 使用一个扇区(例如 16KB),分成 N 个 8 字节记录槽。
- 每个槽包含:状态标记(0xAA 表示已写入,0x00 表示已删除)+ 数据(4 字节)+ CRC(可选)。
- 写入时从当前有效槽的下一个空槽开始写;读时扫描最后一个有效槽。
- 当空槽耗尽时,将最新数据搬移到新扇区并擦除旧扇区。
代码示例
#define EEPROM_SECTOR FLASH_SECTOR_5 // 例如 0x08020000
#define EEPROM_SECTOR_SIZE 0x4000 // 16KB
#define SLOT_SIZE 8 // 8字节/槽
#define SLOT_COUNT (EEPROM_SECTOR_SIZE / SLOT_SIZE)
#define STATUS_VALID 0xAA
#define STATUS_ERASED 0xFF
uint32_t eeprom_previous_slot = 0;
uint32_t EEPROM_FindLastSlot(void) {
uint32_t address = EEPROM_SECTOR_BASE;
uint32_t last = 0;
for (uint32_t i = 0; i < SLOT_COUNT; i++) {
uint8_t status = *(volatile uint8_t *)(address + i * SLOT_SIZE);
if (status == STATUS_VALID) {
last = i;
} else if (status == STATUS_ERASED) {
return last; // 遇到空槽则返回
}
}
return last;
}
uint32_t EEPROM_Read(uint32_t address) {
uint32_t slot = EEPROM_FindLastSlot();
uint32_t data_addr = EEPROM_SECTOR_BASE + slot * SLOT_SIZE + 1; // 跳过状态字节
return *(volatile uint32_t *)data_addr;
}
void EEPROM_Write(uint32_t address, uint32_t value) {
uint32_t slot = EEPROM_FindLastSlot();
// 如果已写满,则搬移+擦除(简化处理:直接擦除整个扇区,并写入第一槽)
if (slot >= SLOT_COUNT - 1) {
Flash_EraseSector(EEPROM_SECTOR);
slot = 0;
} else {
slot++;
}
uint32_t write_addr = EEPROM_SECTOR_BASE + slot * SLOT_SIZE;
uint8_t status = STATUS_VALID;
// 先写状态字节,再写数据,确保掉电一致性
HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, write_addr, status);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, write_addr + 1, value);
}
实际工程需加锁保护、CRC 校验、多扇区冗余等,上述代码仅为教学演示。
五、注意事项
-
写操作中禁止中断:若在 Flash 写入时发生中断,尤其是中断里也有 Flash 操作,会导致硬件错误。建议
__disable_irq()/__enable_irq()保护,或使用 Flash 操作完成中断。 - 地址对齐:STM32 写 32 位数据时,地址必须 4 字节对齐,否则触发 Fault。
- 掉电时序:断电时电压跌落很快,必须通过检测到掉电后再写入。常见做法是使用 RV 调节器/电源监控 + VBAT 后备寄存器标记,或在掉电中断中立即写入关键数据。
- 磨损均衡:1KB 页的型号(如 F103)可拆分为多个小页轮流使用,避免同一页频繁擦除。
- 读改写风险:写数据前必须确保该区域已擦除,否则写入值可能不正确(只能从 1 改 0)。
- 使用 HAL/LL 库:标准库、HAL、LL 各有优势,但 HAL 的 Flash 驱动更安全,已处理等待周期和电压范围。
六、实际应用建议
- 只在关键数据变化时写入(如系统参数、计数值),不要每秒都写。
- 结合 BKP 后备寄存器,保存无需频繁擦写的实时时钟、唤醒标志。
- 若数据量小但要求高可靠,可考虑外部 I2C EEPROM(如 AT24C02),但成本稍高。
- 务必测试多次掉电场景,并用示波器观察电源跌落时序,确保写入区间电压足够。
七、总结
STM32 内部 Flash 在正确管理下完全可以替代 EEPROM。开发者需理解扇区擦除、写入对齐和掉电保护机制,通过“槽位”+“状态标记”实现磨损均衡和断点续写。本文提供的框架可扩展到多扇区备份、CRC 校验等高级特性,为系统提供稳定可靠的掉电存储能力。