告别外置EEPROM!手把手教你用MCU内部Flash实现持久化存储(以AT32F413为例)
告别外置EEPROM!手把手教你用MCU内部Flash实现持久化存储(以AT32F413为例)
在嵌入式开发中,数据持久化存储是一个常见需求。传统解决方案是使用外置EEPROM芯片,但这会增加硬件成本和PCB复杂度。对于成本敏感型项目或空间受限的设计,利用MCU内部Flash模拟EEPROM功能是一个极具吸引力的替代方案。
AT32F413作为一款性价比极高的ARM Cortex-M4 MCU,内置256KB Flash存储器。本文将深入讲解如何在这类MCU上实现可靠的Flash模拟EEPROM方案,涵盖存储结构设计、磨损均衡算法实现,以及针对意外断电的数据保护机制。我们提供的代码示例可直接移植到STM32、GD32等同类MCU上使用。
1. Flash与EEPROM特性对比
在开始实现之前,我们需要清楚理解Flash和EEPROM的关键差异:
| 特性 | Flash存储器 | EEPROM |
|---|---|---|
| 写入方式 | 页擦除后写入 | 字节可写 |
| 擦除次数 | 约10,000次 | 约100,000次 |
| 访问速度 | 快(内部总线) | 慢(I2C/SPI接口) |
| 成本 | 已集成在MCU中 | 需额外芯片 |
| 容量 | 通常较大(KB级) | 通常较小(字节级) |
关键差异带来的挑战:
- Flash必须按页擦除,无法像EEPROM那样直接覆盖单个字节
- Flash的写入寿命较短,需要特殊设计来延长使用寿命
- Flash页大小固定(通常2KB-128KB),而EEPROM可以按需使用
2. Flash模拟EEPROM的核心设计
2.1 双页式存储结构
我们采用双页式设计来平衡存储效率和寿命:
[页0状态区][数据记录0][数据记录1]...[数据记录N] [页1状态区][空闲空间]...[空闲空间]工作流程:
- 系统始终在一个"活跃页"进行读写操作
- 每条数据记录包含:2字节地址 + 2字节数据
- 写入新数据时,总是追加到页的末尾空闲位置
- 读取数据时,从后向前扫描找到指定地址的最新记录
- 当活跃页写满时,执行页切换和数据迁移
注意:页大小应根据实际需求选择,通常为1-4个Flash扇区。AT32F413的扇区大小为2KB。
2.2 磨损均衡算法实现
延长Flash寿命的关键在于均衡各页的擦写次数。我们实现了一个简单的轮换算法:
#define PAGE_SIZE 2048 // 2KB per page #define PAGE0_BASE 0x0801F000 #define PAGE1_BASE 0x0801F800 uint16_t EE_ReadStatus(uint32_t page) { return *(volatile uint16_t*)page; } void EE_WriteStatus(uint32_t page, uint16_t status) { FLASH_ProgramHalfWord(page, status); } void EE_PageTransfer(uint32_t new_page) { // 1. 标记新页为"转移中" EE_WriteStatus(new_page, EE_PAGE_TRANSFER); // 2. 复制有效数据 uint32_t active_page = (new_page == PAGE0_BASE) ? PAGE1_BASE : PAGE0_BASE; for(int addr=0; addr<MAX_VARIABLES; addr++) { uint16_t value = EE_ReadVariable(addr); if(value != 0xFFFF) { // 有效数据 EE_WriteVariable(addr, value); } } // 3. 擦除旧页 FLASH_ErasePage(active_page); // 4. 更新新页状态 EE_WriteStatus(new_page, EE_PAGE_VALID); }关键优化点:
- 每次页切换只复制有效数据,跳过已删除或覆盖的记录
- 使用状态标志确保掉电恢复时能检测到中断的传输操作
- 擦除操作前检查页是否真的需要擦除,避免不必要的磨损
3. 数据完整性与掉电保护
意外断电是嵌入式系统常见问题,我们通过以下机制确保数据安全:
3.1 写操作原子性保证
每个数据记录写入是原子的(32位写入),不会被部分写入。在ARM Cortex-M架构上,32位对齐的写入是原子的。
3.2 状态机恢复机制
定义三种页状态,并在页开头存储状态标志:
typedef enum { EE_PAGE_ERASED = 0xFFFF, // 已擦除 EE_PAGE_VALID = 0x0000, // 有效数据页 EE_PAGE_TRANSFER = 0xCCCC // 数据传输中 } EE_PageStatus;上电恢复流程:
- 检查两页的状态标志
- 如果发现一页处于EE_PAGE_TRANSFER状态,说明上次传输中断
- 重新执行未完成的页传输操作
- 擦除无效页,确保系统回到一致状态
3.3 数据校验机制
每条记录可附加CRC校验码,在读取时验证数据完整性:
uint16_t EE_CalculateCRC(uint16_t addr, uint16_t data) { return (addr ^ data); // 简化示例,实际应使用标准CRC算法 } int EE_WriteVariable(uint16_t addr, uint16_t data) { uint32_t flash_addr = FindNextFreeAddress(); uint16_t crc = EE_CalculateCRC(addr, data); FLASH_ProgramHalfWord(flash_addr, addr); FLASH_ProgramHalfWord(flash_addr+2, data); FLASH_ProgramHalfWord(flash_addr+4, crc); return FLASH_COMPLETE; }4. AT32F413具体实现
4.1 Flash操作基础
AT32F413的Flash控制器提供了必要的接口函数:
void FLASH_Unlock(void); void FLASH_Lock(void); FLASH_Status FLASH_ErasePage(uint32_t Page_Address); FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);关键配置步骤:
- 解锁Flash写保护
- 设置正确的等待周期(根据主频调整)
- 执行擦除/编程操作
- 重新锁定Flash
提示:Flash操作期间必须禁止中断,避免代码在Flash中执行时被中断。
4.2 完整工程结构
建议的工程文件组织:
/eeprom_emul ├── inc │ ├── eeprom_emul.h // 接口定义 │ └── flash_if.h // Flash底层驱动 ├── src │ ├── eeprom_emul.c // 核心逻辑 │ └── flash_if.c // MCU特定实现 └── demo └── main.c // 示例应用接口设计示例:
// eeprom_emul.h typedef enum { EE_OK, EE_NO_DATA, EE_CRC_ERROR, EE_FLASH_ERROR } EE_Status; EE_Status EE_Init(void); EE_Status EE_ReadVariable(uint16_t addr, uint16_t* data); EE_Status EE_WriteVariable(uint16_t addr, uint16_t data); EE_Status EE_EraseAll(void);4.3 性能优化技巧
- 缓存热点数据:对频繁读取的变量,可在RAM中缓存最新值
- 批量写入:累积多个写入请求后一次性执行,减少Flash操作次数
- 智能页切换:根据剩余空间预测页切换时机,避免在关键时刻触发
- 后台维护:在系统空闲时执行数据整理和页回收操作
// 批量写入示例 #define MAX_BATCH 16 typedef struct { uint16_t addr; uint16_t data; } EE_WriteOp; EE_Status EE_WriteBatch(EE_WriteOp* ops, uint8_t count) { FLASH_Unlock(); for(int i=0; i<count; i++) { EE_Status status = EE_WriteVariable(ops[i].addr, ops[i].data); if(status != EE_OK) { FLASH_Lock(); return status; } } FLASH_Lock(); return EE_OK; }5. 实际应用案例
5.1 参数存储系统
在工业控制器中存储设备参数:
#define PARAM_TEMPERATURE 0x0001 #define PARAM_PRESSURE 0x0002 #define PARAM_FLOW_RATE 0x0003 void SaveParameters(float temp, float press, float flow) { EE_WriteOp ops[3]; ops[0].addr = PARAM_TEMPERATURE; ops[0].data = (uint16_t)(temp * 100); ops[1].addr = PARAM_PRESSURE; ops[1].data = (uint16_t)(press * 10); ops[2].addr = PARAM_FLOW_RATE; ops[2].data = (uint16_t)(flow * 1000); EE_WriteBatch(ops, 3); }5.2 事件日志记录
实现一个简单的黑匣子功能:
#define LOG_START_ADDR 0x0100 #define LOG_MAX_ENTRIES 128 typedef struct { uint32_t timestamp; uint16_t event_id; uint16_t event_data; } LogEntry; int LogEvent(uint16_t id, uint16_t data) { static uint16_t log_index = 0; uint32_t ts = HAL_GetTick(); uint16_t base_addr = LOG_START_ADDR + (log_index * sizeof(LogEntry)/2); EE_WriteVariable(base_addr, ts & 0xFFFF); EE_WriteVariable(base_addr+1, ts >> 16); EE_WriteVariable(base_addr+2, id); EE_WriteVariable(base_addr+3, data); log_index = (log_index + 1) % LOG_MAX_ENTRIES; return log_index; }5.3 固件配置存储
存储用户配置和校准数据:
typedef struct { uint16_t serial_num; uint16_t calib_date; int16_t offset_x; int16_t offset_y; uint16_t crc; } DeviceConfig; void SaveConfig(const DeviceConfig* cfg) { uint16_t crc = CalculateCRC((uint8_t*)cfg, sizeof(DeviceConfig)-2); EE_WriteOp ops[5]; ops[0].addr = CONFIG_BASE; ops[0].data = cfg->serial_num; ops[1].addr = CONFIG_BASE+1; ops[1].data = cfg->calib_date; ops[2].addr = CONFIG_BASE+2; ops[2].data = cfg->offset_x; ops[3].addr = CONFIG_BASE+3; ops[3].data = cfg->offset_y; ops[4].addr = CONFIG_BASE+4; ops[4].data = crc; EE_WriteBatch(ops, 5); }在多个实际项目中应用这种Flash模拟EEPROM的方案后,我们发现最关键的是合理规划存储布局和严格控制写操作频率。对于日均写入不超过100次的应用,这种方案可以轻松达到5年以上的使用寿命。
