STM32F4内部Flash模拟EEPROM:磨损均衡与掉电保护的协同设计
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式系统中,使用STM32F4内部Flash模拟EEPROM是常见需求,但Flash的擦写寿命有限且掉电可能导致数据丢失。本文深入探讨如何在模拟EEPROM时兼顾磨损均衡与掉电保护,通过双扇区备份、动态地址映射和写入日志机制,实现可靠且持久的数据存储。文章包含原理分析、配置步骤、完整代码示例及关键注意事项,帮助开发者构建健壮的存储方案。
# 引言
STM32F4系列微控制器内置Flash,但其擦写次数通常为1万次左右,远低于EEPROM的百万次。当需要频繁保存参数时,直接使用固定地址会导致Flash快速磨损。同时,掉电瞬间的写入操作可能造成数据损坏。因此,设计一个兼顾磨损均衡和掉电保护的模拟EEPROM方案至关重要。
# 原理分析
## 磨损均衡(Wear Leveling)
磨损均衡的核心是避免固定地址反复擦写。常见策略包括:
- **双扇区交替**:使用两个扇区,交替写入,延长整体寿命。
- **页内偏移**:在扇区内顺序写入新数据,直到扇区满再擦除。
- **动态映射**:维护逻辑地址到物理地址的映射表,每次写入选择最空闲的块。
## 掉电保护
掉电保护确保写入过程中突然断电不会破坏已有数据。常用方法:
- **写入前备份**:先将新数据写入临时区域,再更新主区域。
- **日志机制**:记录写入操作,上电后根据日志恢复。
- **双备份**:同一数据存两份,校验后选择有效版本。
## 结合策略
将磨损均衡与掉电保护结合,可采用**双扇区+写入日志**方案:
- 扇区A和扇区B交替使用,每次写入先写日志扇区,再更新数据扇区。
- 上电时检查日志,若发现未完成的操作,则回滚或完成。
- 通过计数器记录擦写次数,动态切换活动扇区。
# 配置步骤
1. **硬件准备**:选择两个Flash扇区(如扇区11和12,大小16KB),确保地址对齐。
2. **定义数据结构**:设计日志条目和数据头,包含CRC校验和状态标志。
3. **初始化**:上电扫描两个扇区,确定活动扇区和有效数据。
4. **写入流程**:
- 在日志区写入新数据及CRC。
- 擦除数据扇区(若已满),写入新数据。
- 更新日志状态为完成。
5. **磨损均衡**:每次写入后递增计数器,当计数器达到阈值时切换活动扇区。
# 完整代码示例
以下代码基于STM32F4 HAL库,演示核心逻辑。
```c
// 扇区定义(以STM32F407为例)
#define SECTOR_A_ADDR 0x080E0000 // 扇区11
#define SECTOR_B_ADDR 0x080F0000 // 扇区12
#define SECTOR_SIZE 0x4000 // 16KB
// 数据结构
#pragma pack(1)
typedef struct {
uint32_t magic; // 魔数,用于识别有效数据
uint32_t counter; // 写入计数器,用于磨损均衡
uint8_t data[256]; // 用户数据
uint32_t crc; // CRC32校验
uint8_t status; // 0xAA=完成,0x55=写入中
} DataBlock;
#pragma pack()
// 全局变量
static uint32_t active_sector_addr;
static uint32_t write_counter;
// CRC32计算(简化,实际使用硬件CRC)
uint32_t calc_crc(uint8_t *data, uint32_t len) {
// 实现省略
return 0x12345678;
}
// 擦除扇区
void flash_erase_sector(uint32_t addr) {
FLASH_EraseInitTypeDef erase;
uint32_t sector_err;
erase.TypeErase = FLASH_TYPEERASE_SECTORS;
erase.Sector = (addr == SECTOR_A_ADDR) ? FLASH_SECTOR_11 : FLASH_SECTOR_12;
erase.NbSectors = 1;
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
HAL_FLASH_Unlock();
HAL_FLASHEx_Erase(&erase, §or_err);
HAL_FLASH_Lock();
}
// 写入数据块
void flash_write_block(uint32_t addr, DataBlock *block) {
HAL_FLASH_Unlock();
uint32_t *p = (uint32_t*)block;
for (uint32_t i = 0; i < sizeof(DataBlock)/4; i++) {
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + i*4, p[i]);
}
HAL_FLASH_Lock();
}
// 读取并校验数据块
int flash_read_block(uint32_t addr, DataBlock *block) {
memcpy(block, (void*)addr, sizeof(DataBlock));
if (block->magic != 0xA5A5A5A5) return 0;
uint32_t crc = calc_crc((uint8_t*)&block->data, sizeof(block->data));
if (crc != block->crc) return 0;
if (block->status != 0xAA) return 0;
return 1;
}
// 初始化:扫描两个扇区,选择有效且计数器较大的作为活动扇区
void eeprom_init(void) {
DataBlock block_a, block_b;
int valid_a = flash_read_block(SECTOR_A_ADDR, &block_a);
int valid_b = flash_read_block(SECTOR_B_ADDR, &block_b);
if (valid_a && valid_b) {
active_sector_addr = (block_a.counter > block_b.counter) ? SECTOR_A_ADDR : SECTOR_B_ADDR;
write_counter = (block_a.counter > block_b.counter) ? block_a.counter : block_b.counter;
} else if (valid_a) {
active_sector_addr = SECTOR_A_ADDR;
write_counter = block_a.counter;
} else if (valid_b) {
active_sector_addr = SECTOR_B_ADDR;
write_counter = block_b.counter;
} else {
// 无有效数据,初始化扇区A
flash_erase_sector(SECTOR_A_ADDR);
DataBlock init_block = {0};
init_block.magic = 0xA5A5A5A5;
init_block.counter = 0;
init_block.status = 0xAA;
init_block.crc = calc_crc((uint8_t*)&init_block.data, sizeof(init_block.data));
flash_write_block(SECTOR_A_ADDR, &init_block);
active_sector_addr = SECTOR_A_ADDR;
write_counter = 0;
}
}
// 写入数据(带磨损均衡和掉电保护)
void eeprom_write(uint8_t *data, uint32_t len) {
// 1. 准备新数据块
DataBlock new_block;
new_block.magic = 0xA5A5A5A5;
new_block.counter = write_counter + 1;
memcpy(new_block.data, data, len);
new_block.crc = calc_crc((uint8_t*)&new_block.data, sizeof(new_block.data));
new_block.status = 0x55; // 写入中
// 2. 写入日志区(使用非活动扇区)
uint32_t log_addr = (active_sector_addr == SECTOR_A_ADDR) ? SECTOR_B_ADDR : SECTOR_A_ADDR;
flash_erase_sector(log_addr);
flash_write_block(log_addr, &new_block);
// 3. 更新活动扇区(先擦除再写)
flash_erase_sector(active_sector_addr);
new_block.status = 0xAA; // 完成
flash_write_block(active_sector_addr, &new_block);
// 4. 更新计数器,若达到阈值则切换活动扇区(此处简化,实际可擦除日志扇区)
write_counter++;
// 磨损均衡:当计数器达到1000次,切换活动扇区(示例)
if (write_counter % 1000 == 0) {
active_sector_addr = log_addr;
}
}
// 读取数据
int eeprom_read(uint8_t *data, uint32_t len) {
DataBlock block;
if (!flash_read_block(active_sector_addr, &block)) return 0;
memcpy(data, block.data, len);
return 1;
}
```
# 注意事项
- **掉电保护**:写入日志时,若掉电,上电后检测到status=0x55,可擦除日志扇区,恢复旧数据。
- **磨损均衡粒度**:上述代码在每次写入时擦除整个扇区,实际可优化为页内顺序写入,减少擦除次数。
- **CRC校验**:必须使用强校验,防止数据损坏。
- **Flash操作时间**:擦写期间应关闭中断,避免干扰。
- **扇区大小**:根据数据量选择合适扇区,避免浪费。
- **测试**:进行掉电测试,模拟不同阶段的断电,确保数据一致性。
# 总结
通过双扇区备份和写入日志机制,STM32F4内部Flash模拟EEPROM可以在有限寿命下实现可靠存储。磨损均衡通过计数器切换活动扇区,掉电保护通过状态标志和日志恢复。开发者可根据实际需求调整参数,如扇区数量、日志深度等,以平衡性能和复杂度。此方案在工业控制、物联网设备等场景中具有实用价值。