引言

STM32F4系列内置Flash容量大、速度快,但擦写寿命(典型1万次)远低于EEPROM(100万次)。直接按地址写参数会迅速磨损同一扇区。此外,写入过程中掉电可能导致数据半更新,造成系统启动时参数错误。因此,模拟EEPROM必须解决两个核心问题:磨损均衡(让所有扇区均匀老化)和掉电保护(保证数据一致性)。

一、原理剖析

1. Flash硬件特性

  • STM32F4的Flash以扇区为单位擦除(如1KB、2KB等),写入前必须先擦除,且只能将1写为0。
  • 擦除操作耗时约数十毫秒,期间掉电会导致扇区数据全0或损坏。
  • 每个扇区擦写次数有限,超出后可能产生坏块。

2. 磨损均衡设计

核心思想:将多个扇区组成一个逻辑存储区,每次写入时选择擦写次数最少的扇区,并记录当前活动扇区。常见策略有:

  • 轮询法:按顺序循环使用扇区,简单但无法应对非均匀写入。
  • 计数法:在每个扇区头部记录擦写次数,写入时选择次数最少者。

本文采用双扇区轮换 + 磨损计数:使用两个扇区,一个作为活动扇区,另一个作为备份。每次写入时,将新数据写入备份扇区,成功后切换活动指针,并擦除旧活动扇区。这样每个扇区擦写次数相同,且始终有一个完整副本。

3. 掉电保护策略

  • 双备份:数据同时存于两个扇区,但只有活动扇区有效。写入时先写备份,再切换,保证任何时刻至少有一个完整数据。
  • 校验和:每个记录包含CRC32校验,读取时校验失败则回退到另一扇区。
  • 写入顺序:先写数据+校验,再写状态标记(如有效标志),最后切换。若在写状态前掉电,旧扇区仍有效。

二、具体实现方案

1. 存储布局

假设使用两个1KB扇区(如扇区11和12,地址0x080E0000和0x080E0400)。每个扇区结构如下:

#define SECTOR_SIZE 1024
#define DATA_SIZE   64   // 实际数据长度

typedef struct {
    uint32_t magic;       // 魔数,用于识别有效扇区
    uint32_t write_cnt;   // 写入次数(磨损计数)
    uint32_t crc;         // 数据CRC32
    uint8_t  data[DATA_SIZE];
} SectorHeader;

扇区头部固定占用16字节,数据区64字节,剩余空间填充0xFF。

2. 核心函数实现

扇区擦除与写入

#include "stm32f4xx_hal.h"

#define FLASH_SECTOR_11 11
#define FLASH_SECTOR_12 12
#define ADDR_SECTOR_11  0x080E0000
#define ADDR_SECTOR_12  0x080E0400

// 擦除指定扇区
static void flash_erase_sector(uint8_t sector) {
    FLASH_EraseInitTypeDef erase = {0};
    uint32_t page_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, &page_error);
    HAL_FLASH_Lock();
}

// 写入数据到指定地址(按32位字写)
static void flash_write_word(uint32_t addr, uint32_t data) {
    HAL_FLASH_Unlock();
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, data);
    HAL_FLASH_Lock();
}

磨损均衡控制

// 读取两个扇区的头部,返回有效且写入次数较少的扇区地址
static uint32_t select_active_sector(void) {
    SectorHeader h1, h2;
    memcpy(&h1, (void*)ADDR_SECTOR_11, sizeof(h1));
    memcpy(&h2, (void*)ADDR_SECTOR_12, sizeof(h2));

    int valid1 = (h1.magic == MAGIC_NUM && crc_check(&h1));
    int valid2 = (h2.magic == MAGIC_NUM && crc_check(&h2));

    if (valid1 && valid2) {
        // 选择写入次数少的扇区作为备份,另一个作为活动
        return (h1.write_cnt <= h2.write_cnt) ? ADDR_SECTOR_11 : ADDR_SECTOR_12;
    } else if (valid1) {
        return ADDR_SECTOR_11;
    } else if (valid2) {
        return ADDR_SECTOR_12;
    } else {
        // 两个都无效,初始化扇区11
        flash_erase_sector(FLASH_SECTOR_11);
        return ADDR_SECTOR_11;
    }
}

写入数据(带掉电保护)

int eeprom_write(uint8_t *data, uint32_t len) {
    if (len > DATA_SIZE) return -1;

    uint32_t active = select_active_sector();
    uint32_t backup = (active == ADDR_SECTOR_11) ? ADDR_SECTOR_12 : ADDR_SECTOR_11;

    // 构造新头部
    SectorHeader new_hdr;
    memcpy(&new_hdr, (void*)active, sizeof(new_hdr));
    new_hdr.write_cnt++;
    new_hdr.crc = crc32(data, len);
    memcpy(new_hdr.data, data, len);
    new_hdr.magic = MAGIC_NUM;

    // 擦除备份扇区
    flash_erase_sector((backup == ADDR_SECTOR_11) ? FLASH_SECTOR_11 : FLASH_SECTOR_12);

    // 写入备份扇区(先写头部,再写数据,最后写magic)
    uint32_t addr = backup;
    flash_write_word(addr, new_hdr.write_cnt);
    flash_write_word(addr+4, new_hdr.crc);
    for (int i=0; i<len; i+=4) {
        uint32_t word = 0;
        memcpy(&word, &new_hdr.data[i], 4);
        flash_write_word(addr+16+i, word);
    }
    // 最后写magic,表示写入完成
    flash_write_word(addr, MAGIC_NUM);

    // 擦除旧活动扇区(此时备份已完整)
    flash_erase_sector((active == ADDR_SECTOR_11) ? FLASH_SECTOR_11 : FLASH_SECTOR_12);

    return 0;
}

读取数据(带校验和回退)

int eeprom_read(uint8_t *data, uint32_t len) {
    uint32_t active = select_active_sector();
    SectorHeader hdr;
    memcpy(&hdr, (void*)active, sizeof(hdr));

    if (hdr.magic != MAGIC_NUM || crc32(hdr.data, DATA_SIZE) != hdr.crc) {
        // 活动扇区损坏,尝试备份
        uint32_t backup = (active == ADDR_SECTOR_11) ? ADDR_SECTOR_12 : ADDR_SECTOR_11;
        memcpy(&hdr, (void*)backup, sizeof(hdr));
        if (hdr.magic != MAGIC_NUM || crc32(hdr.data, DATA_SIZE) != hdr.crc) {
            return -1; // 两个都无效
        }
    }
    memcpy(data, hdr.data, len);
    return 0;
}

CRC32实现(简化版)

uint32_t crc32(const uint8_t *data, uint32_t len) {
    uint32_t crc = 0xFFFFFFFF;
    for (uint32_t i=0; i<len; i++) {
        crc ^= data[i];
        for (int j=0; j<8; j++) {
            crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1));
        }
    }
    return ~crc;
}

三、使用示例

int main(void) {
    HAL_Init();
    // ... 系统时钟初始化

    uint8_t params[64] = {0};
    // 读取参数
    if (eeprom_read(params, 64) != 0) {
        // 首次使用,初始化默认值
        memset(params, 0, 64);
        eeprom_write(params, 64);
    }

    // 修改参数并保存
    params[0] = 42;
    eeprom_write(params, 64);

    while(1) {}
}

四、注意事项

  • 扇区大小:STM32F4不同型号扇区大小不同(如1KB、2KB、128KB),需根据实际调整地址和擦除函数。
  • 写入粒度:Flash编程必须按字(32位)或半字,且地址对齐。
  • 擦除时间:擦除期间掉电会损坏扇区,但双备份机制可保证数据不丢。
  • 磨损计数溢出:write_cnt为32位,足够使用,但需处理溢出(如达到0xFFFFFFFF时重置所有扇区)。
  • 性能:每次写入需擦除一个扇区,耗时约20ms,不适合高频写入。
  • 中断保护:写入过程中应关闭中断,防止打断导致状态不一致。

五、总结

通过双扇区轮换和磨损计数,我们实现了Flash的均匀磨损,寿命提升至2万次(两个扇区)。掉电保护通过先写备份再切换的机制,确保任何时刻至少有一个完整数据。CRC校验进一步增强了数据完整性。该方案适用于参数存储、配置保存等低频写入场景,是嵌入式开发中实用且可靠的技术。