STM32F4 片上 Flash 模拟 EEPROM:磨损均衡与掉电保护实战指南
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式系统中,EEPROM 常用于存储配置参数,但 STM32F4 系列没有片上 EEPROM,通常依赖外部芯片或 Flash 模拟。本文深入讲解如何利用 STM32F4 的片上 Flash 实现高效、可靠的 EEPROM 模拟,重点剖析磨损均衡算法与掉电保护策略,提供完整代码示例,帮助开发者解决 Flash 擦写寿命有限和掉电数据丢失的痛点。
# 引言
STM32F4 系列微控制器(如 STM32F407)内部集成了大容量 Flash,但缺少真正的 EEPROM。Flash 的擦写寿命(通常 10,000 次)远低于 EEPROM(100,000 次以上),且必须以扇区为单位擦除。直接使用 Flash 存储频繁更新的参数会导致寿命迅速耗尽。因此,需要设计一种软件层,模拟 EEPROM 的按字节读写功能,同时通过磨损均衡(Wear Leveling)延长 Flash 寿命,并通过掉电保护确保数据一致性。
# 原理讲解
## Flash 硬件特性
- **扇区擦除**:STM32F4 的 Flash 扇区大小不等(如 16KB、64KB),擦除操作会将整个扇区置为 0xFF。
- **写入限制**:只能将 1 写为 0,不能反向操作。写入最小单位通常为 16 位(半字)或 32 位(字)。
- **寿命限制**:每个扇区擦写次数有限,超过后可能损坏。
## 磨损均衡算法
磨损均衡的核心思想是**将写入操作分散到多个扇区**,避免固定扇区被频繁擦写。常见方法有:
- **日志结构(Log-structured)**:将数据以记录形式追加写入当前活动扇区,当扇区写满时,擦除下一个扇区并继续。
- **双扇区轮换**:使用两个扇区,交替写入,每次写入前先检查当前扇区剩余空间,若不足则切换到另一个扇区并擦除旧扇区。
本文采用**多扇区日志式**方案,每个扇区内部维护记录头,包含有效标志、数据长度和 CRC 校验。
## 掉电保护策略
掉电可能发生在写入过程中,导致数据半写或记录不完整。保护策略包括:
- **原子写入**:先写入数据到临时位置,再更新有效标志。
- **CRC 校验**:每条记录附带 CRC,读取时校验,无效记录视为垃圾。
- **启动扫描**:上电时扫描所有扇区,找到最新有效记录,并清理无效记录。
# 配置步骤
## 1. 硬件资源
- 使用 STM32F407 开发板,Flash 扇区 11(地址 0x080E0000,大小 16KB)和扇区 12(0x080E4000,16KB)作为模拟 EEPROM 区域。
- 注意:避免使用存放程序代码的扇区。
## 2. 软件设计
定义数据结构:
```c
#define EEPROM_SECTOR_SIZE (16 * 1024)
#define EEPROM_SECTOR_COUNT 2
#define EEPROM_START_ADDR 0x080E0000
// 记录头(8字节)
typedef struct {
uint16_t crc; // CRC16 校验
uint16_t len; // 数据长度(字节)
uint32_t valid; // 有效标志(0xA5A5A5A5 表示有效)
} RecordHeader;
```
## 3. 核心函数实现
### 3.1 擦除扇区
```c
void eeprom_erase_sector(uint32_t addr) {
FLASH_EraseInitTypeDef erase;
uint32_t page_error = 0;
erase.TypeErase = FLASH_TYPEERASE_SECTORS;
erase.Sector = get_sector_num(addr);
erase.NbSectors = 1;
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
HAL_FLASH_Unlock();
HAL_FLASHEx_Erase(&erase, &page_error);
HAL_FLASH_Lock();
}
```
### 3.2 写入记录
```c
int eeprom_write(uint32_t key, uint8_t *data, uint16_t len) {
// 1. 查找当前活动扇区(最后一个有效记录所在扇区)
uint32_t active_sector = find_active_sector();
uint32_t write_addr = find_free_addr(active_sector);
// 2. 若剩余空间不足,切换到下一个扇区并擦除
if (write_addr + sizeof(RecordHeader) + len > active_sector + EEPROM_SECTOR_SIZE) {
uint32_t next_sector = get_next_sector(active_sector);
eeprom_erase_sector(next_sector);
write_addr = next_sector;
}
// 3. 写入数据(先写数据,再写头部)
HAL_FLASH_Unlock();
// 写入数据内容(按半字)
for (uint16_t i = 0; i < len; i += 2) {
uint16_t half = (i+1 < len) ? (data[i] | (data[i+1] << 8)) : data[i];
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, write_addr + sizeof(RecordHeader) + i, half);
}
// 写入头部(先写 crc 和 len,最后写 valid)
RecordHeader hdr;
hdr.crc = crc16(data, len);
hdr.len = len;
hdr.valid = 0xA5A5A5A5;
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, write_addr, *(uint32_t*)&hdr);
HAL_FLASH_Lock();
return 0;
}
```
### 3.3 读取最新记录
```c
int eeprom_read(uint32_t key, uint8_t *buf, uint16_t *len) {
// 从最后一个扇区向前扫描,找到匹配 key 的最新有效记录
for (int s = EEPROM_SECTOR_COUNT-1; s >= 0; s--) {
uint32_t sector_addr = EEPROM_START_ADDR + s * EEPROM_SECTOR_SIZE;
uint32_t addr = sector_addr;
while (addr < sector_addr + EEPROM_SECTOR_SIZE) {
RecordHeader *hdr = (RecordHeader*)addr;
if (hdr->valid == 0xA5A5A5A5) {
// 校验 CRC
if (crc16((uint8_t*)(addr + sizeof(RecordHeader)), hdr->len) == hdr->crc) {
memcpy(buf, (uint8_t*)(addr + sizeof(RecordHeader)), hdr->len);
*len = hdr->len;
return 0;
}
}
// 跳到下一条记录(若头部无效,则结束)
if (hdr->valid != 0xA5A5A5A5 && hdr->valid != 0xFFFFFFFF) break;
addr += sizeof(RecordHeader) + hdr->len;
}
}
return -1; // 未找到
}
```
### 3.4 启动扫描与垃圾回收
```c
void eeprom_init(void) {
// 扫描所有扇区,如果发现有效记录,则记录最新位置;否则初始化第一个扇区
// 若某个扇区无效记录过多,可执行压缩:将有效记录复制到新扇区,然后擦除旧扇区
}
```
# 完整代码示例
以下为简化版完整代码(省略 CRC 实现和部分辅助函数):
```c
// eeprom_emul.h
#ifndef __EEPROM_EMUL_H
#define __EEPROM_EMUL_H
#include "stm32f4xx_hal.h"
#define EEPROM_OK 0
#define EEPROM_ERROR -1
int eeprom_write(uint32_t key, uint8_t *data, uint16_t len);
int eeprom_read(uint32_t key, uint8_t *buf, uint16_t *len);
void eeprom_init(void);
#endif
```
```c
// eeprom_emul.c
#include "eeprom_emul.h"
// ... 实现上述函数
```
# 注意事项
- **扇区选择**:务必确认所选扇区未被程序代码占用,否则会覆盖程序。
- **写入对齐**:Flash 写入必须按半字或字对齐,数据长度最好为偶数。
- **掉电处理**:在写入过程中掉电,可能导致头部 valid 未写入,此时该记录视为无效,但数据可能残留,不影响正确性。
- **磨损均衡效果**:本算法将写入分散到多个扇区,但若数据量小且频繁,仍可能集中在某个扇区,可增加扇区数量或采用更复杂的均衡策略。
- **性能考虑**:每次写入需扫描扇区,耗时较长,可缓存当前写入位置以加速。
# 总结
通过日志式磨损均衡和 CRC 校验,STM32F4 的片上 Flash 可以可靠地模拟 EEPROM,满足大多数嵌入式应用需求。本文提供的代码框架可直接移植到实际项目中,开发者可根据需求调整扇区大小和数量。掉电保护策略确保了数据一致性,是工业级应用的关键。希望本文能帮助你更好地利用 STM32F4 的 Flash 资源。