一、为什么用 Flash 模拟 EEPROM

很多 STM32 型号没有真正的 EEPROM,但提供了大容量 Flash。Flash 的写入以“页”或“扇区”为单位,且只能将 1 写为 0,擦除后恢复为 0xFF。模拟 EEPROM 的核心思路是:将一个扇区划分为多个记录槽,每次写入只修改一个槽,避免频繁擦写。

二、STM32 Flash 硬件特性

  • 擦除最小单位:不同型号差异大。F1 系列为 1KB/2KB 页,F4 系列为 16KB 扇区,L4 系列为 2KB 页。
  • 编程最小单位:通常为 16 位或 32 位(半字/字)。
  • 写寿命:典型 10K 次擦写,但通过模拟 EEPROM 可分散磨损。
  • 操作电压:编程/擦除时要求 VDD 稳定,禁止在低电压下操作。

三、直接写入 Flash 的步骤(以 STM32F4 为例)

1. 解锁 Flash

#include "stm32f4xx.h"

void Flash_Unlock(void) {
    HAL_FLASH_Unlock();
    // 或使用标准库:FLASH_Unlock();
}

2. 擦除扇区

void Flash_EraseSector(uint32_t sector) {
    FLASH_EraseInitTypeDef erase = {0};
    uint32_t pageError = 0;
    erase.TypeErase = FLASH_TYPEERASE_SECTORS;
    erase.Sector = sector;
    erase.NbSectors = 1;
    erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
    HAL_FLASHEx_Erase(&erase, &pageError);
}

注意:擦除后整个扇区变为 0xFF,不可恢复。

3. 写入数据

void Flash_WriteData(uint32_t address, uint32_t *data, uint32_t len) {
    for (uint32_t i = 0; i < len; i++) {
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address + i*4, data[i]);
    }
}

4. 读取数据

Flash 读取和普通内存一样,直接通过指针访问:

uint32_t value = *(volatile uint32_t *)address;

四、实现轻量级 EEPROM 模拟

设计思路

  • 使用一个扇区(例如 16KB),分成 N 个 8 字节记录槽。
  • 每个槽包含:状态标记(0xAA 表示已写入,0x00 表示已删除)+ 数据(4 字节)+ CRC(可选)。
  • 写入时从当前有效槽的下一个空槽开始写;读时扫描最后一个有效槽。
  • 当空槽耗尽时,将最新数据搬移到新扇区并擦除旧扇区。

代码示例

#define EEPROM_SECTOR       FLASH_SECTOR_5   // 例如 0x08020000
#define EEPROM_SECTOR_SIZE  0x4000           // 16KB
#define SLOT_SIZE           8                // 8字节/槽
#define SLOT_COUNT          (EEPROM_SECTOR_SIZE / SLOT_SIZE)

#define STATUS_VALID        0xAA
#define STATUS_ERASED       0xFF

uint32_t eeprom_previous_slot = 0;

uint32_t EEPROM_FindLastSlot(void) {
    uint32_t address = EEPROM_SECTOR_BASE;
    uint32_t last = 0;
    for (uint32_t i = 0; i < SLOT_COUNT; i++) {
        uint8_t status = *(volatile uint8_t *)(address + i * SLOT_SIZE);
        if (status == STATUS_VALID) {
            last = i;
        } else if (status == STATUS_ERASED) {
            return last; // 遇到空槽则返回
        }
    }
    return last;
}

uint32_t EEPROM_Read(uint32_t address) {
    uint32_t slot = EEPROM_FindLastSlot();
    uint32_t data_addr = EEPROM_SECTOR_BASE + slot * SLOT_SIZE + 1; // 跳过状态字节
    return *(volatile uint32_t *)data_addr;
}

void EEPROM_Write(uint32_t address, uint32_t value) {
    uint32_t slot = EEPROM_FindLastSlot();
    // 如果已写满,则搬移+擦除(简化处理:直接擦除整个扇区,并写入第一槽)
    if (slot >= SLOT_COUNT - 1) {
        Flash_EraseSector(EEPROM_SECTOR);
        slot = 0;
    } else {
        slot++;
    }

    uint32_t write_addr = EEPROM_SECTOR_BASE + slot * SLOT_SIZE;
    uint8_t status = STATUS_VALID;
    // 先写状态字节,再写数据,确保掉电一致性
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, write_addr, status);
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, write_addr + 1, value);
}

实际工程需加锁保护、CRC 校验、多扇区冗余等,上述代码仅为教学演示。

五、注意事项

  • 写操作中禁止中断:若在 Flash 写入时发生中断,尤其是中断里也有 Flash 操作,会导致硬件错误。建议 __disable_irq() / __enable_irq() 保护,或使用 Flash 操作完成中断。
  • 地址对齐:STM32 写 32 位数据时,地址必须 4 字节对齐,否则触发 Fault。
  • 掉电时序:断电时电压跌落很快,必须通过检测到掉电后再写入。常见做法是使用 RV 调节器/电源监控 + VBAT 后备寄存器标记,或在掉电中断中立即写入关键数据。
  • 磨损均衡:1KB 页的型号(如 F103)可拆分为多个小页轮流使用,避免同一页频繁擦除。
  • 读改写风险:写数据前必须确保该区域已擦除,否则写入值可能不正确(只能从 1 改 0)。
  • 使用 HAL/LL 库:标准库、HAL、LL 各有优势,但 HAL 的 Flash 驱动更安全,已处理等待周期和电压范围。

六、实际应用建议

  • 只在关键数据变化时写入(如系统参数、计数值),不要每秒都写。
  • 结合 BKP 后备寄存器,保存无需频繁擦写的实时时钟、唤醒标志。
  • 若数据量小但要求高可靠,可考虑外部 I2C EEPROM(如 AT24C02),但成本稍高。
  • 务必测试多次掉电场景,并用示波器观察电源跌落时序,确保写入区间电压足够。

七、总结

STM32 内部 Flash 在正确管理下完全可以替代 EEPROM。开发者需理解扇区擦除、写入对齐和掉电保护机制,通过“槽位”+“状态标记”实现磨损均衡和断点续写。本文提供的框架可扩展到多扇区备份、CRC 校验等高级特性,为系统提供稳定可靠的掉电存储能力。