STM32F4内部Flash模拟EEPROM:磨损均衡与掉电保护的工程实现

1. 为什么需要模拟EEPROM?

STM32F4系列内置Flash容量大(如F407有1MB),但擦写寿命有限(典型10K次/扇区)。当系统需要频繁保存配置参数(如校准值、运行状态)时,直接擦写Flash会迅速耗尽寿命。外部EEPROM(如AT24C02)虽可靠,但增加BOM成本和PCB面积。因此,在成本敏感或空间受限的场景,用内部Flash模拟EEPROM成为优选。

核心挑战:

  • 寿命限制:Flash擦除次数远低于EEPROM(1M次)。
  • 掉电风险:写入过程中掉电可能导致数据损坏或半写状态。
  • 擦除粒度:Flash按扇区擦除(F4通常16KB/扇区),而写入可字节/半字/字。

2. 硬件基础与关键特性

STM32F4的Flash控制器支持:

  • 编程:以16位(半字)为单位写入,需先置位PG位。
  • 擦除:按扇区或整片擦除,需置位SER位并等待BSY位清零。
  • 选项字节:可配置读保护等,但模拟EEPROM时通常不用。

关键寄存器:FLASH_CR(控制)、FLASH_SR(状态)、FLASH_ACR(延迟)。

3. 磨损均衡策略

3.1 扇区划分与日志结构

将Flash划分为多个逻辑扇区(如4个物理扇区,每个16KB)。采用日志结构:数据以记录(Record)形式顺序写入当前活动扇区,每个记录包含:

  • 头部:魔数(0xA5A5)、数据长度、CRC16校验。
  • 数据:用户参数(固定长度或变长)。
  • 状态:有效/无效标记。

当活动扇区写满时,触发扇区切换

  1. 选择下一个空闲扇区作为新活动区。
  2. 将最新有效数据从旧扇区复制到新扇区。
  3. 擦除旧扇区,并标记为待用。

3.2 磨损均衡算法

采用轮询+计数器方式:

  • 维护一个全局擦写计数(存储于Flash首扇区)。
  • 每次切换扇区时,递增计数并选择擦写次数最少的扇区作为新活动区。
  • 若所有扇区擦写次数接近,则顺序轮询。

示例代码(简化):

#define SECTOR_NUM 4
#define SECTOR_SIZE 0x4000  // 16KB

uint32_t erase_count[SECTOR_NUM];

uint32_t select_next_sector(uint32_t current) {
    uint32_t min_count = erase_count[(current+1)%SECTOR_NUM];
    uint32_t next = (current+1)%SECTOR_NUM;
    for (int i=0; i<SECTOR_NUM; i++) {
        if (erase_count[i] < min_count) {
            min_count = erase_count[i];
            next = i;
        }
    }
    return next;
}

4. 掉电保护策略

4.1 双区备份与原子操作

采用双区(A/B)备份

  • 数据写入时,先写备份区(B区),写成功后更新主区(A区)的指针。
  • 掉电时,若B区写入未完成,则主区数据仍有效;若B区完成但主区未更新,则下次启动时检测到B区有效,恢复之。

4.2 写失败恢复

在写入过程中,若检测到掉电(如通过ADC监测VDD),立即停止写入并标记当前记录为“待完成”。启动时扫描所有扇区,发现“待完成”记录则回滚或重写。

4.3 启动校验流程

void flash_eeprom_init(void) {
    // 1. 扫描所有扇区,找到最近有效的活动扇区
    // 2. 检查是否有未完成的切换(如旧扇区未擦除)
    // 3. 若存在,则完成切换或回滚
    // 4. 加载最新数据到RAM缓存
}

5. 完整代码示例

以下为简化但可用的实现框架(基于STM32F4 HAL库):

// flash_eeprom.h
#define EEPROM_SECTOR_START 4  // 从扇区4开始,避免覆盖代码
#define EEPROM_SECTOR_NUM   4
#define RECORD_HEADER_SIZE  8   // 魔数(2)+长度(2)+CRC(4)

typedef struct {
    uint16_t magic;
    uint16_t len;
    uint32_t crc;
} RecordHeader;

// flash_eeprom.c
static uint32_t active_sector;
static uint32_t write_offset;

uint32_t flash_write_record(uint8_t *data, uint16_t len) {
    // 检查剩余空间,若不足则切换扇区
    if (write_offset + RECORD_HEADER_SIZE + len > SECTOR_SIZE) {
        switch_sector();
    }
    // 构造头部
    RecordHeader hdr = {0xA5A5, len, crc32(data, len)};
    // 写入头部和数据(注意Flash写入需半字对齐)
    flash_write_halfword(active_sector*SECTOR_SIZE + write_offset, hdr.magic);
    flash_write_halfword(active_sector*SECTOR_SIZE + write_offset+2, hdr.len);
    flash_write_word(active_sector*SECTOR_SIZE + write_offset+4, hdr.crc);
    // 写入数据(按32位对齐)
    for (int i=0; i<len; i+=4) {
        uint32_t word = *(uint32_t*)(data+i);
        flash_write_word(active_sector*SECTOR_SIZE + write_offset+8+i, word);
    }
    // 更新偏移
    write_offset += RECORD_HEADER_SIZE + len;
    // 写入完成标记(可选)
    return 0;
}

void switch_sector(void) {
    uint32_t new_sector = select_next_sector(active_sector);
    // 1. 将有效数据从旧扇区复制到新扇区(遍历记录,过滤无效)
    // 2. 擦除旧扇区,更新擦写计数
    // 3. 设置active_sector = new_sector,write_offset = 0
    // 4. 写新扇区头部标记
}

6. 注意事项与优化建议

  • 对齐与原子性:Flash编程必须半字对齐,且写入过程中掉电可能导致半字损坏。建议使用双字(64位)写入或增加冗余校验。
  • CRC校验:使用硬件CRC单元(F4内置)加速计算,避免软件CRC占用CPU。
  • 磨损均衡粒度:若参数更新频繁(如每秒一次),建议增加扇区数(如8个)以延长寿命。
  • 掉电检测:利用PVD(可编程电压检测器)中断,在电压跌落时快速进入紧急保存流程。
  • 测试验证:编写压力测试脚本,模拟随机掉电(如用继电器控制电源),验证数据完整性。

7. 总结

通过日志结构、扇区轮换和双区备份,STM32F4内部Flash可有效模拟EEPROM,满足大多数工业应用。关键在于平衡寿命、可靠性和复杂度。本文提供的框架可直接移植,但需根据具体需求调整参数(如扇区大小、记录格式)。记住:没有银弹,只有针对场景的工程权衡。