引言

在嵌入式产品中,EEPROM常用于保存配置参数、校准数据等。但独立EEPROM芯片增加成本和PCB面积,而STM32F4系列内部Flash容量大、速度快,通过模拟EEPROM可节省资源。然而,Flash的擦写寿命(典型10K次)远低于EEPROM(1M次),且写入前必须擦除整个扇区(通常16KB),掉电时可能造成数据丢失或损坏。因此,实现磨损均衡和掉电保护是模拟EEPROM的核心挑战。

一、Flash模拟EEPROM的基本原理

STM32F4的Flash以扇区为单位组织,每个扇区大小不等(如16KB、64KB等)。写入数据时,Flash只能将1变为0(编程),擦除则将所有位恢复为1。因此,模拟EEPROM需采用“写后擦”策略:

  • 将数据写入扇区中的空闲位置(追加写)。
  • 当扇区写满后,擦除整个扇区并重新写入有效数据。

这种策略减少了擦除次数,但若频繁写入同一地址,仍会快速耗尽该扇区寿命。磨损均衡旨在将擦写均匀分布到多个扇区,延长整体寿命。

二、磨损均衡策略

2.1 扇区管理结构

通常使用2个或更多扇区作为存储池。每个扇区划分为多个固定大小的记录槽(如32字节),每个记录包含:

  • 数据区:用户数据。
  • 状态区:标记记录状态(如有效、无效、擦除)。
  • 序号区:单调递增的序号,用于区分新旧数据。

以2个扇区为例,结构如下:

#define SECTOR_SIZE      (16 * 1024)  // 假设扇区大小16KB
#define RECORD_SIZE      32           // 每个记录32字节
#define RECORDS_PER_SECTOR (SECTOR_SIZE / RECORD_SIZE)

typedef struct {
    uint8_t data[24];    // 用户数据,24字节
    uint32_t seq;        // 序号,4字节
    uint8_t status;      // 状态:0xFF有效,0x00无效,0xAA擦除标记
} Record_t;

2.2 动态磨损均衡

每次写入时,在当前活动扇区中寻找空闲记录槽(状态为0xFF且seq为0xFFFFFFFF)。若找到,则写入新数据并更新序号。若扇区已满,则切换到另一个扇区:

  1. 擦除备用扇区。
  2. 将当前扇区中所有有效记录(状态为0xFF)复制到备用扇区,并保持序号不变。
  3. 擦除当前扇区,将其设为备用。

这样,每次切换扇区时,两个扇区的擦写次数交替增加,实现均衡。但若写入频率极不均匀(如只写一个地址),动态均衡仍可能导致某个扇区提前耗尽。

2.3 静态磨损均衡

静态均衡将不常修改的数据(如出厂参数)定期迁移到擦写次数较少的扇区。实现方法:

  • 维护每个扇区的擦写计数(存储在Flash中)。
  • 当某个扇区擦写次数超过阈值(如平均次数+10),触发迁移:将该扇区中的有效数据复制到最冷扇区,并擦除原扇区。

但静态均衡增加复杂度,对于大多数应用,动态均衡已足够。若需极致寿命,可结合静态均衡。

三、掉电保护策略

掉电可能发生在写入过程中,导致记录不完整或状态不一致。保护措施包括:

3.1 双备份与校验

  • 每个记录写入两次:先写主记录,再写备份记录(位于不同扇区或不同偏移)。
  • 读取时,先读主记录,校验CRC;若失败,则读备份。
  • 写入时,先写备份,再写主记录,确保至少一个有效。

3.2 状态机与原子操作

  • 写入记录时,先擦除目标槽(置为0xFF),然后写入数据,最后更新状态为有效。
  • 若掉电发生在写数据期间,状态仍为0xFF,读取时视为无效,可回滚。
  • 使用Flash的编程操作(半字或字)确保写入原子性,避免部分写入。

3.3 掉电检测与恢复

  • 利用STM32的PVD(可编程电压检测)中断,检测电压跌落时,禁止新写入并完成当前操作。
  • 上电后,扫描所有记录,找出序号最大的有效记录作为最新数据,并清理无效记录。

四、完整代码示例

以下代码实现基于2个扇区的动态磨损均衡和简单掉电保护(使用CRC校验)。

#include "stm32f4xx.h"
#include <string.h>

// 配置:使用扇区11和12(地址0x080E0000和0x080E4000,假设容量足够)
#define FLASH_SECTOR_ACTIVE   11
#define FLASH_SECTOR_BACKUP   12
#define ACTIVE_ADDR           0x080E0000
#define BACKUP_ADDR           0x080E4000

#define RECORD_SIZE           32
#define RECORDS_PER_SECTOR    (16 * 1024 / RECORD_SIZE)

typedef struct {
    uint8_t data[24];
    uint32_t seq;
    uint8_t status;  // 0xFF有效,0x00无效
} Record_t;

// 简单CRC8校验
uint8_t crc8(uint8_t *data, uint16_t len) {
    uint8_t crc = 0;
    while (len--) {
        crc ^= *data++;
        for (int i = 0; i < 8; i++) {
            crc = (crc & 0x80) ? (crc << 1) ^ 0x07 : (crc << 1);
        }
    }
    return crc;
}

// 擦除扇区
void flash_erase_sector(uint32_t sector) {
    FLASH_Unlock();
    FLASH_EraseSector(sector, VoltageRange_3);
    FLASH_Lock();
}

// 写入一条记录(带CRC)
void flash_write_record(uint32_t addr, Record_t *rec) {
    uint8_t buf[RECORD_SIZE];
    memcpy(buf, rec->data, 24);
    memcpy(buf + 24, &rec->seq, 4);
    buf[28] = rec->status;
    buf[29] = crc8(buf, 29);
    buf[30] = 0xFF; // 保留
    buf[31] = 0xFF;

    FLASH_Unlock();
    for (int i = 0; i < RECORD_SIZE; i += 2) {
        FLASH_ProgramHalfWord(addr + i, *(uint16_t*)(buf + i));
    }
    FLASH_Lock();
}

// 读取记录并校验
int flash_read_record(uint32_t addr, Record_t *rec) {
    uint8_t buf[RECORD_SIZE];
    memcpy(buf, (uint8_t*)addr, RECORD_SIZE);
    if (buf[28] != 0xFF) return 0; // 无效
    if (crc8(buf, 29) != buf[29]) return 0;
    memcpy(rec->data, buf, 24);
    memcpy(&rec->seq, buf + 24, 4);
    rec->status = 0xFF;
    return 1;
}

// 查找最新有效记录(遍历两个扇区)
uint32_t find_latest(Record_t *rec) {
    uint32_t max_seq = 0;
    uint32_t found = 0;
    Record_t tmp;
    for (int i = 0; i < RECORDS_PER_SECTOR; i++) {
        if (flash_read_record(ACTIVE_ADDR + i * RECORD_SIZE, &tmp) && tmp.seq > max_seq) {
            max_seq = tmp.seq;
            memcpy(rec, &tmp, sizeof(Record_t));
            found = 1;
        }
        if (flash_read_record(BACKUP_ADDR + i * RECORD_SIZE, &tmp) && tmp.seq > max_seq) {
            max_seq = tmp.seq;
            memcpy(rec, &tmp, sizeof(Record_t));
            found = 1;
        }
    }
    return found;
}

// 写入数据(磨损均衡+掉电保护)
void eeprom_write(uint8_t *data, uint16_t len) {
    Record_t rec;
    uint32_t seq = 0;
    // 获取当前最大序号
    Record_t latest;
    if (find_latest(&latest)) {
        seq = latest.seq + 1;
    }
    memcpy(rec.data, data, len);
    rec.seq = seq;
    rec.status = 0xFF;

    // 尝试写入活动扇区
    for (int i = 0; i < RECORDS_PER_SECTOR; i++) {
        uint32_t addr = ACTIVE_ADDR + i * RECORD_SIZE;
        if (*(uint8_t*)addr == 0xFF && *(uint32_t*)(addr + 24) == 0xFFFFFFFF) {
            flash_write_record(addr, &rec);
            return;
        }
    }

    // 活动扇区已满,切换扇区
    flash_erase_sector(FLASH_SECTOR_BACKUP);
    // 复制有效记录到备份扇区
    for (int i = 0; i < RECORDS_PER_SECTOR; i++) {
        Record_t tmp;
        if (flash_read_record(ACTIVE_ADDR + i * RECORD_SIZE, &tmp)) {
            flash_write_record(BACKUP_ADDR + i * RECORD_SIZE, &tmp);
        }
    }
    // 写入新记录
    flash_write_record(BACKUP_ADDR, &rec);
    // 擦除活动扇区,并交换角色
    flash_erase_sector(FLASH_SECTOR_ACTIVE);
    // 注意:实际中需交换地址,这里简化,使用宏定义切换
}

// 读取数据
void eeprom_read(uint8_t *data, uint16_t len) {
    Record_t rec;
    if (find_latest(&rec)) {
        memcpy(data, rec.data, len);
    }
}

五、注意事项

  • 扇区大小:STM32F4不同型号扇区大小不同,需根据实际调整宏定义。
  • 擦写时间:擦除一个16KB扇区约需1秒,写入半字约50us,需考虑实时性。
  • 磨损均衡粒度:记录槽越小,均衡效果越好,但管理开销增大。
  • 掉电保护:建议使用PVD中断,在电压跌落时禁止写入,并确保当前操作完成。
  • 测试:进行掉电测试时,需模拟不同阶段的掉电,确保恢复后数据一致。

结语

通过合理的扇区管理、磨损均衡和掉电保护机制,STM32F4内部Flash可以可靠地模拟EEPROM,满足大多数嵌入式应用需求。本文提供的策略和代码可作为基础,开发者可根据具体需求扩展,如增加静态均衡、多扇区管理等。掌握这些技术,能显著提升产品的数据存储可靠性。