STM32F4内部Flash模拟EEPROM:磨损均衡与掉电保护的状态机设计
👁 2 阅读 · 2026-08-27 · 嵌入式
在嵌入式系统中,当需要非易失性存储但成本或空间受限时,使用STM32F4内部Flash模拟EEPROM是一种常见方案。然而,Flash的擦写寿命有限(通常10K次),且掉电时可能损坏数据。本文深入探讨如何通过状态机设计实现磨损均衡和掉电保护,确保数据可靠性和Flash寿命最大化。内容涵盖原理、状态定义、配置步骤及完整代码示例,适合有STM32开发经验的工程师。
# STM32F4内部Flash模拟EEPROM:磨损均衡与掉电保护的状态机设计
## 1. 引言
在嵌入式产品中,参数存储(如校准值、配置信息)常依赖EEPROM。但STM32F4系列没有内置EEPROM,外部扩展会增加成本。利用内部Flash模拟EEPROM是一种经济方案,但面临两大挑战:**擦写寿命有限**(典型10K次)和**掉电数据完整性**。本文提出一种基于状态机的设计,将Flash划分为多个存储区,通过磨损均衡延长寿命,并通过状态标记实现掉电恢复。
## 2. 原理分析
### 2.1 Flash特性
- 写入前必须擦除(按扇区,通常16KB或64KB)。
- 擦除操作耗时(约1-2ms),且期间掉电可能导致数据损坏。
- 写入(编程)可以按16位或32位进行,但只能将1变0,不能0变1。
### 2.2 磨损均衡策略
将Flash划分为多个逻辑槽(Slot),每个槽存储一份数据副本。写入时轮询使用不同槽,避免同一位置反复擦写。典型结构:
- **槽头**:包含状态标记(如0xFFFF表示空,0xAAAA表示有效,0x5555表示待擦除)。
- **数据区**:存储实际数据(如4字节对齐)。
### 2.3 掉电保护机制
通过状态机管理每个槽的状态,确保任何时刻至少有一个有效副本。写入过程分为两步:
1. 将新数据写入备用槽,并标记为“待提交”。
2. 将旧槽标记为“废弃”,然后擦除旧槽。
若掉电发生在步骤1后,恢复时扫描所有槽,选择状态为“有效”的槽作为最新数据;若步骤2中掉电,则“待提交”槽成为有效数据。
## 3. 状态机设计
定义四种状态:
- **EMPTY** (0xFFFFFFFF):槽未使用,可写入。
- **VALID** (0xAAAAAAAA):槽包含有效数据。
- **COMMIT** (0x55555555):槽数据已写入,但尚未确认(用于掉电恢复)。
- **ERASED** (0x00000000):槽已擦除,等待重新使用。
状态转换图:
```
EMPTY -> (写入数据) -> COMMIT -> (确认) -> VALID
VALID -> (擦除) -> ERASED -> (重新初始化) -> EMPTY
```
## 4. 配置步骤
### 4.1 硬件及库准备
- 使用STM32F4系列(如STM32F407VG)。
- 启用HAL库的Flash模块。
- 定义Flash扇区地址(例如使用扇区11,地址0x080E0000,大小16KB)。
### 4.2 存储布局
```c
#define FLASH_SECTOR_START 0x080E0000
#define SLOT_SIZE 128 // 每个槽大小(字节),包含头部和数据
#define NUM_SLOTS 8 // 槽数量,总占用1KB
#define DATA_SIZE 64 // 实际数据长度(字节)
```
### 4.3 状态机实现
#### 初始化函数
```c
void EEPROM_Init(void) {
uint32_t addr = FLASH_SECTOR_START;
uint32_t status;
// 扫描所有槽,找到状态为VALID的槽作为当前有效槽
for (int i = 0; i < NUM_SLOTS; i++) {
status = *(__IO uint32_t*)addr;
if (status == VALID) {
current_slot = i;
break;
}
addr += SLOT_SIZE;
}
if (current_slot == -1) {
// 无有效槽,则擦除整个扇区并初始化第一个槽
Flash_EraseSector();
current_slot = 0;
Flash_WriteHeader(current_slot, EMPTY);
}
}
```
#### 写入函数(带磨损均衡)
```c
int EEPROM_Write(uint8_t *data, uint16_t len) {
// 1. 寻找下一个可用槽(当前槽+1取模)
int next_slot = (current_slot + 1) % NUM_SLOTS;
// 2. 擦除下一个槽(如果非空)
Flash_EraseSlot(next_slot);
// 3. 写入数据并标记为COMMIT
Flash_WriteData(next_slot, data, len);
Flash_WriteHeader(next_slot, COMMIT);
// 4. 将旧槽标记为ERASED(擦除操作)
Flash_EraseSlot(current_slot); // 实际擦除整个扇区,但这里简化
// 5. 将新槽标记为VALID
Flash_WriteHeader(next_slot, VALID);
current_slot = next_slot;
return 0;
}
```
#### 掉电恢复函数
```c
void EEPROM_Recover(void) {
uint32_t addr = FLASH_SECTOR_START;
uint32_t status;
int valid_slot = -1;
int commit_slot = -1;
for (int i = 0; i < NUM_SLOTS; i++) {
status = *(__IO uint32_t*)addr;
if (status == VALID) valid_slot = i;
else if (status == COMMIT) commit_slot = i;
addr += SLOT_SIZE;
}
if (commit_slot != -1) {
// 有未完成的写入,将COMMIT槽提升为VALID,并擦除其他槽
Flash_WriteHeader(commit_slot, VALID);
// 擦除其他所有槽(除commit_slot)
for (int i = 0; i < NUM_SLOTS; i++) {
if (i != commit_slot) Flash_EraseSlot(i);
}
current_slot = commit_slot;
} else if (valid_slot != -1) {
current_slot = valid_slot;
} else {
// 无有效数据,初始化
Flash_EraseSector();
current_slot = 0;
Flash_WriteHeader(current_slot, EMPTY);
}
}
```
## 5. 完整代码示例(简化)
```c
// flash_emul.h
#ifndef FLASH_EMUL_H
#define FLASH_EMUL_H
#include "stm32f4xx_hal.h"
#define FLASH_SECTOR_START 0x080E0000
#define SLOT_SIZE 128
#define NUM_SLOTS 8
#define DATA_SIZE 64
#define EMPTY 0xFFFFFFFF
#define VALID 0xAAAAAAAA
#define COMMIT 0x55555555
#define ERASED 0x00000000
void EEPROM_Init(void);
int EEPROM_Write(uint8_t *data, uint16_t len);
int EEPROM_Read(uint8_t *data, uint16_t len);
void EEPROM_Recover(void);
#endif
```
```c
// flash_emul.c
#include "flash_emul.h"
static int current_slot = -1;
static void Flash_EraseSector(void) {
FLASH_EraseInitTypeDef erase;
uint32_t page_error = 0;
erase.TypeErase = FLASH_TYPEERASE_SECTORS;
erase.Sector = FLASH_SECTOR_11; // 根据实际调整
erase.NbSectors = 1;
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
HAL_FLASH_Unlock();
HAL_FLASHEx_Erase(&erase, &page_error);
HAL_FLASH_Lock();
}
static void Flash_WriteHeader(int slot, uint32_t status) {
uint32_t addr = FLASH_SECTOR_START + slot * SLOT_SIZE;
HAL_FLASH_Unlock();
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, status);
HAL_FLASH_Lock();
}
static void Flash_WriteData(int slot, uint8_t *data, uint16_t len) {
uint32_t addr = FLASH_SECTOR_START + slot * SLOT_SIZE + 4; // 跳过头部
HAL_FLASH_Unlock();
for (uint16_t i = 0; i < len; i += 4) {
uint32_t word = 0;
for (int j = 0; j < 4 && (i+j) < len; j++) {
word |= ((uint32_t)data[i+j]) << (8*j);
}
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + i, word);
}
HAL_FLASH_Lock();
}
static void Flash_EraseSlot(int slot) {
// 简化:擦除整个扇区,实际可只擦除槽所在扇区,但这里为演示
Flash_EraseSector();
// 重新初始化所有槽为EMPTY(除了当前有效槽)
for (int i = 0; i < NUM_SLOTS; i++) {
if (i != slot) Flash_WriteHeader(i, EMPTY);
}
}
void EEPROM_Init(void) {
EEPROM_Recover();
}
int EEPROM_Write(uint8_t *data, uint16_t len) {
if (len > DATA_SIZE) return -1;
int next_slot = (current_slot + 1) % NUM_SLOTS;
// 擦除下一个槽(如果非空)
Flash_EraseSlot(next_slot);
// 写入数据并标记为COMMIT
Flash_WriteData(next_slot, data, len);
Flash_WriteHeader(next_slot, COMMIT);
// 擦除旧槽(这里简化,实际只擦除旧槽所在扇区)
Flash_EraseSlot(current_slot);
// 将新槽标记为VALID
Flash_WriteHeader(next_slot, VALID);
current_slot = next_slot;
return 0;
}
int EEPROM_Read(uint8_t *data, uint16_t len) {
if (current_slot == -1) return -1;
uint32_t addr = FLASH_SECTOR_START + current_slot * SLOT_SIZE + 4;
for (uint16_t i = 0; i < len; i++) {
data[i] = *(__IO uint8_t*)(addr + i);
}
return 0;
}
void EEPROM_Recover(void) {
uint32_t addr = FLASH_SECTOR_START;
uint32_t status;
int valid_slot = -1, commit_slot = -1;
for (int i = 0; i < NUM_SLOTS; i++) {
status = *(__IO uint32_t*)addr;
if (status == VALID) valid_slot = i;
else if (status == COMMIT) commit_slot = i;
addr += SLOT_SIZE;
}
if (commit_slot != -1) {
Flash_WriteHeader(commit_slot, VALID);
for (int i = 0; i < NUM_SLOTS; i++) {
if (i != commit_slot) Flash_EraseSlot(i);
}
current_slot = commit_slot;
} else if (valid_slot != -1) {
current_slot = valid_slot;
} else {
Flash_EraseSector();
current_slot = 0;
Flash_WriteHeader(current_slot, EMPTY);
}
}
```
## 6. 注意事项
- **扇区擦除粒度**:STM32F4的擦除最小单位是扇区(16KB),因此磨损均衡的槽数量受限于扇区大小。若槽太小,擦除整个扇区会浪费寿命。建议槽大小至少为扇区的1/N,且N为槽数。
- **掉电时机**:在写入过程中,若掉电发生在擦除旧槽后、标记新槽为VALID前,恢复时COMMIT槽会被提升为VALID,但旧槽已被擦除,数据可能丢失。因此,实际设计应保证至少有一个槽始终有效,可增加冗余槽。
- **写保护**:操作Flash时需关闭中断,防止中断服务程序干扰。
- **性能**:Flash写入速度慢,频繁写入会阻塞系统,建议使用DMA或后台任务。
- **磨损均衡优化**:可记录每个槽的擦写次数,动态选择最少使用的槽,但会增加复杂度。
## 7. 总结
通过状态机管理Flash槽,实现了磨损均衡和掉电保护,显著延长了Flash寿命并提高了数据可靠性。该设计适用于对成本敏感且写入频率不高的嵌入式应用。实际项目中可根据需求调整槽大小和状态机,例如增加CRC校验或使用双扇区备份。希望本文能为你提供有价值的参考。