# 引言 STM32F4系列微控制器内置Flash,但其擦写次数通常为1万次左右,远低于EEPROM的百万次。当需要频繁保存参数时,直接使用固定地址会导致Flash快速磨损。同时,掉电瞬间的写入操作可能造成数据损坏。因此,设计一个兼顾磨损均衡和掉电保护的模拟EEPROM方案至关重要。 # 原理分析 ## 磨损均衡(Wear Leveling) 磨损均衡的核心是避免固定地址反复擦写。常见策略包括: - **双扇区交替**:使用两个扇区,交替写入,延长整体寿命。 - **页内偏移**:在扇区内顺序写入新数据,直到扇区满再擦除。 - **动态映射**:维护逻辑地址到物理地址的映射表,每次写入选择最空闲的块。 ## 掉电保护 掉电保护确保写入过程中突然断电不会破坏已有数据。常用方法: - **写入前备份**:先将新数据写入临时区域,再更新主区域。 - **日志机制**:记录写入操作,上电后根据日志恢复。 - **双备份**:同一数据存两份,校验后选择有效版本。 ## 结合策略 将磨损均衡与掉电保护结合,可采用**双扇区+写入日志**方案: - 扇区A和扇区B交替使用,每次写入先写日志扇区,再更新数据扇区。 - 上电时检查日志,若发现未完成的操作,则回滚或完成。 - 通过计数器记录擦写次数,动态切换活动扇区。 # 配置步骤 1. **硬件准备**:选择两个Flash扇区(如扇区11和12,大小16KB),确保地址对齐。 2. **定义数据结构**:设计日志条目和数据头,包含CRC校验和状态标志。 3. **初始化**:上电扫描两个扇区,确定活动扇区和有效数据。 4. **写入流程**: - 在日志区写入新数据及CRC。 - 擦除数据扇区(若已满),写入新数据。 - 更新日志状态为完成。 5. **磨损均衡**:每次写入后递增计数器,当计数器达到阈值时切换活动扇区。 # 完整代码示例 以下代码基于STM32F4 HAL库,演示核心逻辑。 ```c // 扇区定义(以STM32F407为例) #define SECTOR_A_ADDR 0x080E0000 // 扇区11 #define SECTOR_B_ADDR 0x080F0000 // 扇区12 #define SECTOR_SIZE 0x4000 // 16KB // 数据结构 #pragma pack(1) typedef struct { uint32_t magic; // 魔数,用于识别有效数据 uint32_t counter; // 写入计数器,用于磨损均衡 uint8_t data[256]; // 用户数据 uint32_t crc; // CRC32校验 uint8_t status; // 0xAA=完成,0x55=写入中 } DataBlock; #pragma pack() // 全局变量 static uint32_t active_sector_addr; static uint32_t write_counter; // CRC32计算(简化,实际使用硬件CRC) uint32_t calc_crc(uint8_t *data, uint32_t len) { // 实现省略 return 0x12345678; } // 擦除扇区 void flash_erase_sector(uint32_t addr) { FLASH_EraseInitTypeDef erase; uint32_t sector_err; erase.TypeErase = FLASH_TYPEERASE_SECTORS; erase.Sector = (addr == SECTOR_A_ADDR) ? FLASH_SECTOR_11 : FLASH_SECTOR_12; erase.NbSectors = 1; erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; HAL_FLASH_Unlock(); HAL_FLASHEx_Erase(&erase, §or_err); HAL_FLASH_Lock(); } // 写入数据块 void flash_write_block(uint32_t addr, DataBlock *block) { HAL_FLASH_Unlock(); uint32_t *p = (uint32_t*)block; for (uint32_t i = 0; i < sizeof(DataBlock)/4; i++) { HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + i*4, p[i]); } HAL_FLASH_Lock(); } // 读取并校验数据块 int flash_read_block(uint32_t addr, DataBlock *block) { memcpy(block, (void*)addr, sizeof(DataBlock)); if (block->magic != 0xA5A5A5A5) return 0; uint32_t crc = calc_crc((uint8_t*)&block->data, sizeof(block->data)); if (crc != block->crc) return 0; if (block->status != 0xAA) return 0; return 1; } // 初始化:扫描两个扇区,选择有效且计数器较大的作为活动扇区 void eeprom_init(void) { DataBlock block_a, block_b; int valid_a = flash_read_block(SECTOR_A_ADDR, &block_a); int valid_b = flash_read_block(SECTOR_B_ADDR, &block_b); if (valid_a && valid_b) { active_sector_addr = (block_a.counter > block_b.counter) ? SECTOR_A_ADDR : SECTOR_B_ADDR; write_counter = (block_a.counter > block_b.counter) ? block_a.counter : block_b.counter; } else if (valid_a) { active_sector_addr = SECTOR_A_ADDR; write_counter = block_a.counter; } else if (valid_b) { active_sector_addr = SECTOR_B_ADDR; write_counter = block_b.counter; } else { // 无有效数据,初始化扇区A flash_erase_sector(SECTOR_A_ADDR); DataBlock init_block = {0}; init_block.magic = 0xA5A5A5A5; init_block.counter = 0; init_block.status = 0xAA; init_block.crc = calc_crc((uint8_t*)&init_block.data, sizeof(init_block.data)); flash_write_block(SECTOR_A_ADDR, &init_block); active_sector_addr = SECTOR_A_ADDR; write_counter = 0; } } // 写入数据(带磨损均衡和掉电保护) void eeprom_write(uint8_t *data, uint32_t len) { // 1. 准备新数据块 DataBlock new_block; new_block.magic = 0xA5A5A5A5; new_block.counter = write_counter + 1; memcpy(new_block.data, data, len); new_block.crc = calc_crc((uint8_t*)&new_block.data, sizeof(new_block.data)); new_block.status = 0x55; // 写入中 // 2. 写入日志区(使用非活动扇区) uint32_t log_addr = (active_sector_addr == SECTOR_A_ADDR) ? SECTOR_B_ADDR : SECTOR_A_ADDR; flash_erase_sector(log_addr); flash_write_block(log_addr, &new_block); // 3. 更新活动扇区(先擦除再写) flash_erase_sector(active_sector_addr); new_block.status = 0xAA; // 完成 flash_write_block(active_sector_addr, &new_block); // 4. 更新计数器,若达到阈值则切换活动扇区(此处简化,实际可擦除日志扇区) write_counter++; // 磨损均衡:当计数器达到1000次,切换活动扇区(示例) if (write_counter % 1000 == 0) { active_sector_addr = log_addr; } } // 读取数据 int eeprom_read(uint8_t *data, uint32_t len) { DataBlock block; if (!flash_read_block(active_sector_addr, &block)) return 0; memcpy(data, block.data, len); return 1; } ``` # 注意事项 - **掉电保护**:写入日志时,若掉电,上电后检测到status=0x55,可擦除日志扇区,恢复旧数据。 - **磨损均衡粒度**:上述代码在每次写入时擦除整个扇区,实际可优化为页内顺序写入,减少擦除次数。 - **CRC校验**:必须使用强校验,防止数据损坏。 - **Flash操作时间**:擦写期间应关闭中断,避免干扰。 - **扇区大小**:根据数据量选择合适扇区,避免浪费。 - **测试**:进行掉电测试,模拟不同阶段的断电,确保数据一致性。 # 总结 通过双扇区备份和写入日志机制,STM32F4内部Flash模拟EEPROM可以在有限寿命下实现可靠存储。磨损均衡通过计数器切换活动扇区,掉电保护通过状态标志和日志恢复。开发者可根据实际需求调整参数,如扇区数量、日志深度等,以平衡性能和复杂度。此方案在工业控制、物联网设备等场景中具有实用价值。