STM32 掉电保存数据:Flash 写入与 EEPROM 模拟
👁 7 阅读 · 2026-08-14 · STM32 存储
STM32 内置 Flash 不仅用于存储代码,也可在掉电时保存关键数据。本文从 STM32 Flash 硬件特性出发,讲解扇区擦除、编程操作及常见陷阱,并实现一套轻量级的 EEPROM 模拟方案,适用于无外部 EEPROM 的嵌入式系统。通过固件库/LL 库代码示例,帮助开发者安全、高效地管理掉电存储。
## 一、为什么用 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
```c
#include "stm32f4xx.h"
void Flash_Unlock(void) {
HAL_FLASH_Unlock();
// 或使用标准库:FLASH_Unlock();
}
```
### 2. 擦除扇区
```c
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. 写入数据
```c
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 读取和普通内存一样,直接通过指针访问:
```c
uint32_t value = *(volatile uint32_t *)address;
```
## 四、实现轻量级 EEPROM 模拟
### 设计思路
- 使用一个扇区(例如 16KB),分成 N 个 8 字节记录槽。
- 每个槽包含:**状态标记**(0xAA 表示已写入,0x00 表示已删除)+ **数据**(4 字节)+ **CRC**(可选)。
- 写入时从当前有效槽的下一个空槽开始写;读时扫描最后一个有效槽。
- 当空槽耗尽时,将最新数据搬移到新扇区并擦除旧扇区。
### 代码示例
```c
#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 校验等高级特性,为系统提供稳定可靠的掉电存储能力。