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 存储布局

#define FLASH_SECTOR_START  0x080E0000
#define SLOT_SIZE           128  // 每个槽大小(字节),包含头部和数据
#define NUM_SLOTS           8    // 槽数量,总占用1KB
#define DATA_SIZE           64   // 实际数据长度(字节)

4.3 状态机实现

初始化函数

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);
    }
}

写入函数(带磨损均衡)

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;
}

掉电恢复函数

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. 完整代码示例(简化)

// 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
// 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校验或使用双扇区备份。希望本文能为你提供有价值的参考。