STM32F4内部Flash模拟EEPROM:双缓冲区实现磨损均衡与掉电保护

1. 为什么需要双缓冲区?

STM32F4内部Flash具有擦写次数限制(通常10,000次),且写入前必须擦除(按扇区)。直接模拟EEPROM会导致频繁擦写同一扇区,迅速耗尽寿命。此外,写入过程中掉电可能造成数据损坏。双缓冲区方案通过两个扇区交替使用,分散擦写压力,并利用状态标志实现掉电恢复,兼顾寿命与可靠性。

2. 核心原理

2.1 磨损均衡

  • 使用两个扇区(如Sector 11和Sector 12,各128KB),每次写入选择“活跃”扇区。
  • 活跃扇区写满后,将最新数据复制到另一扇区,擦除旧扇区,并切换活跃标志。
  • 通过交替擦写,每个扇区的擦除次数减半,寿命延长至2倍。

2.2 掉电保护

  • 每个扇区头部存储状态字(如0xAAAAAAAA表示有效,0x55555555表示待擦除)。
  • 写入过程分两步:先写数据到临时区,再更新状态字。若掉电,通过状态字判断数据完整性。
  • 采用“备份-切换”策略:新数据先写入非活跃扇区,成功后更新活跃标志,避免旧数据丢失。

3. 配置步骤

3.1 硬件与软件准备

  • 使用STM32F407,内部Flash起始地址0x08000000,扇区大小128KB(注意不同型号扇区大小可能不同)。
  • 启用Flash接口时钟:__HAL_RCC_FLASH_CLK_ENABLE()
  • 定义扇区基址和状态字宏。

3.2 扇区划分

#define SECTOR_ACTIVE_1  0x080E0000  // 扇区11(假设从0x080E0000开始)
#define SECTOR_ACTIVE_2  0x08100000  // 扇区12
#define STATUS_VALID     0xAAAAAAAA
#define STATUS_ERASED    0xFFFFFFFF
#define STATUS_PENDING   0x55555555

3.3 初始化与状态检测

uint8_t Flash_EEPROM_Init(void) {
    uint32_t status1 = *(volatile uint32_t*)SECTOR_ACTIVE_1;
    uint32_t status2 = *(volatile uint32_t*)SECTOR_ACTIVE_2;
    
    if (status1 == STATUS_VALID && status2 != STATUS_VALID) {
        active_sector = 1;
    } else if (status2 == STATUS_VALID && status1 != STATUS_VALID) {
        active_sector = 2;
    } else if (status1 == STATUS_VALID && status2 == STATUS_VALID) {
        // 异常情况,选择状态较新的扇区(可比较写入计数)
        active_sector = (read_counter(1) > read_counter(2)) ? 1 : 2;
    } else {
        // 首次使用,擦除两个扇区并初始化
        Flash_EraseSector(SECTOR_ACTIVE_1);
        Flash_EraseSector(SECTOR_ACTIVE_2);
        Flash_WriteWord(SECTOR_ACTIVE_1, STATUS_VALID);
        active_sector = 1;
    }
    return 0;
}

4. 写入与读取实现

4.1 写入流程(带磨损均衡)

uint8_t Flash_EEPROM_Write(uint16_t addr, uint32_t data) {
    uint32_t target_sector = (active_sector == 1) ? SECTOR_ACTIVE_1 : SECTOR_ACTIVE_2;
    uint32_t backup_sector = (active_sector == 1) ? SECTOR_ACTIVE_2 : SECTOR_ACTIVE_1;
    
    // 1. 在备份扇区写入新数据(先擦除)
    Flash_EraseSector(backup_sector);
    Flash_WriteWord(backup_sector, STATUS_PENDING); // 标记写入中
    Flash_WriteWord(backup_sector + 4 + addr*4, data);
    
    // 2. 更新备份扇区状态为有效
    Flash_WriteWord(backup_sector, STATUS_VALID);
    
    // 3. 将旧扇区标记为待擦除(可选,用于掉电恢复)
    Flash_WriteWord(target_sector, STATUS_PENDING);
    
    // 4. 切换活跃扇区
    active_sector = (active_sector == 1) ? 2 : 1;
    
    // 5. 擦除旧扇区(延迟擦除可提高效率,但此处直接擦除)
    Flash_EraseSector(target_sector);
    return 0;
}

4.2 读取流程

uint32_t Flash_EEPROM_Read(uint16_t addr) {
    uint32_t sector = (active_sector == 1) ? SECTOR_ACTIVE_1 : SECTOR_ACTIVE_2;
    return *(volatile uint32_t*)(sector + 4 + addr*4);
}

4.3 掉电恢复处理

在初始化时,若检测到状态为STATUS_PENDING的扇区,说明上次写入未完成,需根据另一扇区状态恢复:

  • 若备份扇区有效,则直接切换活跃扇区到备份扇区,擦除损坏扇区。
  • 若两个扇区均无效,则重新初始化。

5. 完整代码示例(简化版)

// flash_eeprom.h
#ifndef FLASH_EEPROM_H
#define FLASH_EEPROM_H
#include "stm32f4xx_hal.h"

#define SECTOR1_BASE 0x080E0000
#define SECTOR2_BASE 0x08100000
#define STATUS_OFFSET 0
#define DATA_OFFSET   4
#define STATUS_VALID  0xAAAAAAAA
#define STATUS_PENDING 0x55555555

void Flash_EEPROM_Init(void);
uint32_t Flash_EEPROM_Read(uint16_t addr);
void Flash_EEPROM_Write(uint16_t addr, uint32_t data);

#endif

// flash_eeprom.c
#include "flash_eeprom.h"

static uint8_t active_sector;

static void Flash_EraseSector(uint32_t sector) {
    FLASH_EraseInitTypeDef erase = {0};
    uint32_t page_error = 0;
    erase.TypeErase = FLASH_TYPEERASE_SECTORS;
    erase.Sector = (sector == SECTOR1_BASE) ? FLASH_SECTOR_11 : FLASH_SECTOR_12;
    erase.NbSectors = 1;
    erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
    HAL_FLASH_Unlock();
    HAL_FLASHEx_Erase(&erase, &page_error);
    HAL_FLASH_Lock();
}

static void Flash_WriteWord(uint32_t addr, uint32_t data) {
    HAL_FLASH_Unlock();
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, data);
    HAL_FLASH_Lock();
}

void Flash_EEPROM_Init(void) {
    uint32_t s1 = *(volatile uint32_t*)SECTOR1_BASE;
    uint32_t s2 = *(volatile uint32_t*)SECTOR2_BASE;
    
    if (s1 == STATUS_VALID && s2 != STATUS_VALID) active_sector = 1;
    else if (s2 == STATUS_VALID && s1 != STATUS_VALID) active_sector = 2;
    else if (s1 == STATUS_VALID && s2 == STATUS_VALID) {
        // 选择写入计数较大的(此处简化,可比较数据区末尾的计数器)
        active_sector = 1; // 实际需更复杂判断
    } else {
        Flash_EraseSector(SECTOR1_BASE);
        Flash_EraseSector(SECTOR2_BASE);
        Flash_WriteWord(SECTOR1_BASE, STATUS_VALID);
        active_sector = 1;
    }
}

void Flash_EEPROM_Write(uint16_t addr, uint32_t data) {
    uint32_t target, backup;
    if (active_sector == 1) { target = SECTOR1_BASE; backup = SECTOR2_BASE; }
    else { target = SECTOR2_BASE; backup = SECTOR1_BASE; }
    
    // 写入备份扇区
    Flash_EraseSector(backup);
    Flash_WriteWord(backup + STATUS_OFFSET, STATUS_PENDING);
    Flash_WriteWord(backup + DATA_OFFSET + addr*4, data);
    Flash_WriteWord(backup + STATUS_OFFSET, STATUS_VALID);
    
    // 切换活跃扇区
    active_sector = (active_sector == 1) ? 2 : 1;
    
    // 擦除旧扇区
    Flash_EraseSector(target);
}

uint32_t Flash_EEPROM_Read(uint16_t addr) {
    uint32_t sector = (active_sector == 1) ? SECTOR1_BASE : SECTOR2_BASE;
    return *(volatile uint32_t*)(sector + DATA_OFFSET + addr*4);
}

6. 注意事项

  • 扇区大小:不同STM32F4型号扇区大小不同(如F407为128KB,F411为128KB但扇区数不同),需查阅参考手册。
  • 擦除时间:Flash擦除耗时较长(约1-2秒),写入操作可能阻塞,建议在RTOS中或使用中断处理。
  • 磨损均衡优化:可增加写入计数或使用环形缓冲区,进一步均衡磨损。
  • 掉电保护:状态字写入顺序至关重要,确保先写数据后更新状态,避免中间状态。
  • 地址对齐:Flash写入必须按字(4字节)对齐,且地址不能跨扇区。
  • 中断安全:写入过程中应关闭中断或使用临界区,防止并发访问。
  • 寿命估算:若每秒写1次,10K次寿命约2.7小时,双缓冲区可延长至5.4小时,需根据应用评估。

7. 总结

双缓冲区方案通过交替使用两个扇区,实现了磨损均衡,并通过状态机管理确保了掉电时的数据一致性。本文提供的代码框架可直接应用于STM32F4项目,但实际生产环境需根据具体需求增强错误处理和性能优化。掌握此技术,可有效利用内部Flash资源,降低硬件成本。