# 引言 在嵌入式产品中,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个扇区为例,结构如下: ```c #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校验)。 ```c #include "stm32f4xx.h" #include // 配置:使用扇区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,满足大多数嵌入式应用需求。本文提供的策略和代码可作为基础,开发者可根据具体需求扩展,如增加静态均衡、多扇区管理等。掌握这些技术,能显著提升产品的数据存储可靠性。