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

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

STM32F4的Flash具有擦除次数限制(典型10K次),且写入前必须擦除(按扇区)。直接单缓冲区存储时,每次更新需擦除整个扇区,频繁写入会快速耗尽寿命。更重要的是,若在擦除或写入过程中掉电,数据可能丢失或损坏。双缓冲区方案通过交替使用两个扇区,实现磨损均衡,并利用备份机制保证掉电安全。

2. 核心原理

2.1 磨损均衡

  • 将Flash划分为两个等大小的扇区(如Sector 11和Sector 12,各16KB)。
  • 每次写入时,选择当前活跃扇区(Active Sector),写入新数据并标记为最新。
  • 当活跃扇区写满或达到阈值时,切换至另一扇区,并将旧扇区擦除。这样两个扇区轮流使用,磨损均匀。

2.2 掉电保护

  • 每个扇区头部存储状态标志(如有效、待擦除、擦除完成)。
  • 写入流程:先写备份区(非活跃扇区),再更新活跃区标志,最后擦除旧数据。
  • 掉电时,系统重启后通过状态标志判断哪个扇区有效,并恢复数据。

3. 实现步骤

3.1 内存布局定义

#define FLASH_SECTOR_ACTIVE   11
#define FLASH_SECTOR_BACKUP   12
#define FLASH_SECTOR_SIZE     16 * 1024  // 16KB
#define DATA_SIZE             512         // 用户数据长度(字节)

typedef struct {
    uint32_t magic;        // 魔数,用于校验
    uint32_t status;       // 状态:0xFFFFFFFF有效,0x00000000待擦除
    uint8_t  data[DATA_SIZE];
    uint32_t crc32;        // 数据CRC校验
} FlashPage_t;

3.2 初始化与恢复

void Flash_Init(void) {
    FlashPage_t page;
    uint32_t active_sector = FLASH_SECTOR_ACTIVE;
    
    // 读取两个扇区头部,判断有效状态
    Flash_Read(FLASH_SECTOR_ACTIVE, &page);
    if (page.magic != MAGIC_NUM || page.status != 0xFFFFFFFF) {
        // 活跃扇区无效,尝试备份扇区
        Flash_Read(FLASH_SECTOR_BACKUP, &page);
        if (page.magic == MAGIC_NUM && page.status == 0xFFFFFFFF) {
            active_sector = FLASH_SECTOR_BACKUP;
        } else {
            // 两个都无效,格式化(擦除两个扇区,写初始数据)
            Flash_Erase(FLASH_SECTOR_ACTIVE);
            Flash_Erase(FLASH_SECTOR_BACKUP);
            Flash_WritePage(FLASH_SECTOR_ACTIVE, &default_data);
            active_sector = FLASH_SECTOR_ACTIVE;
        }
    }
    g_active_sector = active_sector;
}

3.3 写入操作(带磨损均衡)

void Flash_Write(uint8_t *data, uint32_t len) {
    FlashPage_t new_page;
    uint32_t backup_sector = (g_active_sector == FLASH_SECTOR_ACTIVE) ? FLASH_SECTOR_BACKUP : FLASH_SECTOR_ACTIVE;
    
    // 1. 构造新页数据
    new_page.magic = MAGIC_NUM;
    new_page.status = 0xFFFFFFFF;
    memcpy(new_page.data, data, len);
    new_page.crc32 = CRC32_Compute(data, len);
    
    // 2. 擦除备份扇区(若未擦除)
    Flash_Erase(backup_sector);
    
    // 3. 写入备份扇区(先写备份,保证掉电时旧数据仍在)
    Flash_WritePage(backup_sector, &new_page);
    
    // 4. 更新活跃扇区状态为待擦除(标记旧数据无效)
    Flash_UpdateStatus(g_active_sector, 0x00000000);
    
    // 5. 切换活跃扇区
    g_active_sector = backup_sector;
    
    // 6. 擦除旧活跃扇区(延迟擦除,减少磨损)
    Flash_Erase(g_active_sector == FLASH_SECTOR_ACTIVE ? FLASH_SECTOR_BACKUP : FLASH_SECTOR_ACTIVE);
}

3.4 读取操作

void Flash_Read(uint8_t *data, uint32_t len) {
    FlashPage_t page;
    Flash_ReadPage(g_active_sector, &page);
    if (page.magic == MAGIC_NUM && page.status == 0xFFFFFFFF && CRC32_Check(page.data, len, page.crc32)) {
        memcpy(data, page.data, len);
    } else {
        // 数据损坏,返回默认值或错误
        memset(data, 0xFF, len);
    }
}

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

// flash_ee.h
#define MAGIC_NUM 0xA5A5A5A5
#define FLASH_SECTOR_ACTIVE   11
#define FLASH_SECTOR_BACKUP   12
#define DATA_SIZE             512

typedef struct {
    uint32_t magic;
    uint32_t status;
    uint8_t  data[DATA_SIZE];
    uint32_t crc32;
} FlashPage_t;

void Flash_Init(void);
void Flash_Write(uint8_t *data, uint32_t len);
void Flash_Read(uint8_t *data, uint32_t len);

// flash_ee.c
#include "flash_ee.h"
#include "stm32f4xx_hal.h"

static uint32_t g_active_sector;

// 底层Flash操作(使用HAL库)
static void Flash_Erase(uint32_t sector) {
    FLASH_EraseInitTypeDef erase = {0};
    uint32_t error = 0;
    erase.TypeErase = FLASH_TYPEERASE_SECTORS;
    erase.Sector = sector;
    erase.NbSectors = 1;
    erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
    HAL_FLASH_Unlock();
    HAL_FLASHEx_Erase(&erase, &error);
    HAL_FLASH_Lock();
}

static void Flash_WritePage(uint32_t sector, FlashPage_t *page) {
    uint32_t addr = FLASH_BASE + sector * FLASH_SECTOR_SIZE;
    HAL_FLASH_Unlock();
    // 写入头部和用户数据(注意对齐)
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, page->magic);
    addr += 4;
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, page->status);
    addr += 4;
    for (int i = 0; i < DATA_SIZE; i += 4) {
        uint32_t word = *(uint32_t*)&page->data[i];
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + i, word);
    }
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + DATA_SIZE, page->crc32);
    HAL_FLASH_Lock();
}

static void Flash_ReadPage(uint32_t sector, FlashPage_t *page) {
    uint32_t addr = FLASH_BASE + sector * FLASH_SECTOR_SIZE;
    memcpy(page, (void*)addr, sizeof(FlashPage_t));
}

static void Flash_UpdateStatus(uint32_t sector, uint32_t status) {
    uint32_t addr = FLASH_BASE + sector * FLASH_SECTOR_SIZE + 4;
    HAL_FLASH_Unlock();
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, status);
    HAL_FLASH_Lock();
}

// 初始化、写入、读取函数实现见上文

5. 注意事项

  • 扇区大小匹配:确保所选扇区大小足够存放FlashPage_t结构体,且地址对齐(4字节)。
  • 擦除时间:Flash擦除耗时较长(约1-2秒),写入期间应避免中断或任务切换,必要时关中断。
  • 磨损均衡粒度:每次写入都擦除备份扇区,实际寿命仍受限于擦除次数。可增加多页缓冲或使用日志式存储进一步优化。
  • 掉电检测:若系统有掉电检测(如BOD),可在掉电瞬间完成关键写入,提高可靠性。
  • CRC校验:使用硬件CRC或软件CRC,确保数据完整性。
  • Flash编程电压:确保供电稳定,编程时电压波动会导致写入失败。

6. 总结

双缓冲区方案通过交替使用两个Flash扇区,实现了磨损均衡,并通过先写备份再切换的策略,保证了掉电时数据不丢失。该实现简单可靠,适用于大多数需要模拟EEPROM的场景。开发者可根据实际需求调整缓冲区大小和策略,以平衡寿命、速度和复杂度。