## 一、为什么用 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 ```c #include "stm32f4xx.h" void Flash_Unlock(void) { HAL_FLASH_Unlock(); // 或使用标准库:FLASH_Unlock(); } ``` ### 2. 擦除扇区 ```c 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. 写入数据 ```c 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 读取和普通内存一样,直接通过指针访问: ```c uint32_t value = *(volatile uint32_t *)address; ``` ## 四、实现轻量级 EEPROM 模拟 ### 设计思路 - 使用一个扇区(例如 16KB),分成 N 个 8 字节记录槽。 - 每个槽包含:**状态标记**(0xAA 表示已写入,0x00 表示已删除)+ **数据**(4 字节)+ **CRC**(可选)。 - 写入时从当前有效槽的下一个空槽开始写;读时扫描最后一个有效槽。 - 当空槽耗尽时,将最新数据搬移到新扇区并擦除旧扇区。 ### 代码示例 ```c #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 校验等高级特性,为系统提供稳定可靠的掉电存储能力。