引言

STM32F4系列内置Flash具有1MB容量,但每个扇区擦写寿命仅约1万次(典型值),而EEPROM可达百万次。若直接模拟,频繁写入将迅速耗尽寿命。同时,写入过程中掉电可能导致数据半更新,引发系统故障。因此,设计磨损均衡(Wear Leveling)和掉电保护(Power-fail Protection)是嵌入式存储的关键。

一、原理剖析

1. Flash与EEPROM差异

  • 擦除粒度:Flash按扇区擦除(如F4的128KB扇区),EEPROM按字节。
  • 写入限制:Flash只能将1写为0,需先擦除(全1)。
  • 寿命:扇区擦写次数有限,磨损均衡需分散写入。

2. 磨损均衡机制

  • 思想:将逻辑地址映射到多个物理扇区,轮流使用。
  • 实现:维护一个写入计数器,每次写入切换扇区,擦除最旧扇区。
  • 效果:将擦写次数平均化,延长Flash寿命。

3. 掉电保护策略

  • 风险:写入过程中掉电,数据可能部分写入或扇区状态不一致。
  • 方案:采用双备份(Double Backup)或日志式(Log-based)存储。
  • 核心:写入前先备份旧数据,写入完成后更新标志,掉电后恢复时检测标志决定回滚或提交。

二、系统设计

本设计使用两个扇区(Sector0和Sector1,各16KB)模拟一个EEPROM区域,存储结构如下:

// 存储记录头(位于每个扇区起始)
typedef struct {
    uint32_t magic;      // 魔数,0xA5A5A5A5表示有效
    uint32_t write_cnt;  // 写入次数,用于磨损均衡
    uint32_t crc;        // 数据CRC校验
    uint8_t  data[512];  // 用户数据区
} FlashRecord;
  • 扇区状态:每个扇区有ACTIVE(当前写入)和STANDBY(备用)状态。

  • 写入流程

    1. 读取当前ACTIVE扇区的记录。
    2. 修改数据,更新write_cnt+1,计算CRC。
    3. 写入STANDBY扇区(先擦除)。
    4. 写入完成后,将STANDBY设为ACTIVE(通过更新扇区头标志)。
    5. 擦除旧ACTIVE扇区,作为新STANDBY。
  • 掉电恢复:上电时扫描两个扇区,检查magic和CRC。若只有一个有效,则使用之;若两个都有效,则选择write_cnt较大者(最新)。若一个损坏,则从另一个恢复。

三、代码实现

1. 底层Flash操作函数

#include "stm32f4xx.h"

#define FLASH_SECTOR0 0
#define FLASH_SECTOR1 1
#define FLASH_ADDR0   0x08000000  // 假设从起始地址开始
#define FLASH_ADDR1   0x08004000  // 16KB偏移

// 擦除扇区
void Flash_EraseSector(uint8_t sector) {
    FLASH_Unlock();
    while (FLASH_GetStatus() != FLASH_BUSY);
    FLASH_EraseSector(sector, VoltageRange_3);
    FLASH_Lock();
}

// 写入数据(半字为单位)
void Flash_WriteData(uint32_t addr, uint32_t *data, uint16_t len) {
    FLASH_Unlock();
    for (uint16_t i = 0; i < len; i++) {
        while (FLASH_GetStatus() != FLASH_BUSY);
        FLASH_ProgramWord(addr + i*4, data[i]);
    }
    FLASH_Lock();
}

// 读取数据(直接指针访问)
void Flash_ReadData(uint32_t addr, uint32_t *data, uint16_t len) {
    for (uint16_t i = 0; i < len; i++) {
        data[i] = *(volatile uint32_t *)(addr + i*4);
    }
}

2. 磨损均衡与写入逻辑

// 定义记录结构(需对齐)
#define RECORD_SIZE (sizeof(FlashRecord) + 3) & ~3  // 4字节对齐

// 查找有效扇区
uint8_t Flash_FindActiveSector(void) {
    FlashRecord rec0, rec1;
    Flash_ReadData(FLASH_ADDR0, (uint32_t*)&rec0, sizeof(rec0)/4);
    Flash_ReadData(FLASH_ADDR1, (uint32_t*)&rec1, sizeof(rec1)/4);
    
    uint8_t valid0 = (rec0.magic == 0xA5A5A5A5) && (CRC32(rec0.data) == rec0.crc);
    uint8_t valid1 = (rec1.magic == 0xA5A5A5A5) && (CRC32(rec1.data) == rec1.crc);
    
    if (valid0 && valid1) {
        return (rec0.write_cnt >= rec1.write_cnt) ? 0 : 1; // 取最新
    } else if (valid0) return 0;
    else if (valid1) return 1;
    else return 0; // 默认0,首次使用
}

// 写入数据(带磨损均衡)
void Flash_WriteData_WithWearLeveling(uint8_t *newData, uint16_t len) {
    uint8_t activeSector = Flash_FindActiveSector();
    uint8_t standbySector = (activeSector == 0) ? 1 : 0;
    uint32_t standbyAddr = (standbySector == 0) ? FLASH_ADDR0 : FLASH_ADDR1;
    
    // 读取旧记录(若存在)
    FlashRecord oldRec;
    Flash_ReadData((activeSector==0)?FLASH_ADDR0:FLASH_ADDR1, (uint32_t*)&oldRec, sizeof(oldRec)/4);
    
    // 构建新记录
    FlashRecord newRec;
    newRec.magic = 0xA5A5A5A5;
    newRec.write_cnt = (oldRec.magic==0xA5A5A5A5) ? oldRec.write_cnt + 1 : 1;
    memcpy(newRec.data, newData, len);
    newRec.crc = CRC32(newData, len);
    
    // 擦除备用扇区
    Flash_EraseSector(standbySector);
    
    // 写入新记录(先写数据,最后写magic和cnt,确保完整性)
    // 注意:为简化,这里一次性写入,实际可分段
    Flash_WriteData(standbyAddr, (uint32_t*)&newRec, sizeof(newRec)/4);
    
    // 更新状态:备用扇区成为ACTIVE,旧扇区可擦除(下次备用)
    // 此处通过擦除旧扇区实现(但需确保新扇区写入成功)
    Flash_EraseSector(activeSector);
}

// 读取数据
void Flash_ReadData_WithWearLeveling(uint8_t *data, uint16_t len) {
    uint8_t activeSector = Flash_FindActiveSector();
    FlashRecord rec;
    Flash_ReadData((activeSector==0)?FLASH_ADDR0:FLASH_ADDR1, (uint32_t*)&rec, sizeof(rec)/4);
    memcpy(data, rec.data, len);
}

3. 掉电恢复函数

void Flash_PowerFailRecovery(void) {
    FlashRecord rec0, rec1;
    Flash_ReadData(FLASH_ADDR0, (uint32_t*)&rec0, sizeof(rec0)/4);
    Flash_ReadData(FLASH_ADDR1, (uint32_t*)&rec1, sizeof(rec1)/4);
    
    uint8_t valid0 = (rec0.magic == 0xA5A5A5A5) && (CRC32(rec0.data) == rec0.crc);
    uint8_t valid1 = (rec1.magic == 0xA5A5A5A5) && (CRC32(rec1.data) == rec1.crc);
    
    if (!valid0 && !valid1) {
        // 两个都无效,初始化扇区0
        Flash_EraseSector(0);
        FlashRecord initRec = {0xA5A5A5A5, 1, 0, {0}};
        initRec.crc = CRC32(initRec.data, sizeof(initRec.data));
        Flash_WriteData(FLASH_ADDR0, (uint32_t*)&initRec, sizeof(initRec)/4);
    } else if (valid0 && !valid1) {
        // 扇区1损坏,从0复制到1(可选)
        Flash_EraseSector(1);
        Flash_WriteData(FLASH_ADDR1, (uint32_t*)&rec0, sizeof(rec0)/4);
    } else if (!valid0 && valid1) {
        // 扇区0损坏,从1复制到0
        Flash_EraseSector(0);
        Flash_WriteData(FLASH_ADDR0, (uint32_t*)&rec1, sizeof(rec1)/4);
    }
    // 若都有效,无需处理
}

四、配置步骤

  1. 开启Flash访问:在stm32f4xx_hal_conf.h中启用HAL_FLASH_MODULE_ENABLED
  2. 定义存储区域:在链接脚本中预留两个扇区(如从0x08000000开始,但注意避开代码区)。
  3. 初始化:系统启动时调用Flash_PowerFailRecovery()进行恢复。
  4. 读写调用:使用Flash_WriteData_WithWearLeveling()Flash_ReadData_WithWearLeveling()
  5. 优化:根据数据大小调整记录结构,可增加多页存储。

五、注意事项

  • 写入顺序:先写数据区,最后写magic和write_cnt,避免半写状态被误认为有效。
  • CRC校验:建议使用硬件CRC单元(如STM32F4内置CRC)加速。
  • 中断保护:写入过程中应关闭中断或使用临界区,防止干扰。
  • 磨损均衡粒度:若数据频繁小量更新,可增加扇区数量或使用日志式。
  • 测试:通过反复上下电和写入测试验证可靠性。

结语

通过上述策略,STM32F4的Flash模拟EEPROM可显著延长寿命并保证数据安全。实际项目中可根据需求调整扇区大小和数量,甚至结合RTOS实现异步写入。嵌入式开发中,细节决定成败,希望本文能助你构建更稳健的存储系统。