资讯动态

APM32E103 FLASH模拟EEPROM:磨损均衡与掉电恢复实现详解

发布时间:2026/9/13 0:44:14 来源:尧图企业网站定制
简介面向嵌入式开发者的APM32E103闪存模拟EEPROM工程基于APM32E1系列单片机实现参数非易失性保存适合在工业控制、仪器仪表、智能家电中保存配置数据与校准信息。工程提供可直接编译运行的MDK示例覆盖闪存控制器初始化、页擦除、字编程、读取校验等关键驱动并针对闪存擦写寿命和掉电安全设计多区块备份、循环写地址、状态标志及异常恢复机制可作为产品存储模块的参考实现。整套资源共91个文件以40个C源文件与45个H头文件为主体搭配MDK工程文件、启动汇编、HEX固件与说明文档压缩包约347KB。源码按标准外设驱动、芯片启动、用户应用和调试组件分层组织目录清楚可直接打开工程验证功能也能快速移植到APM32E1系列其他型号。目前已有137人学习适合正在学习APM32E1系列开发、或准备在项目中引入闪存模拟EEPROM方案的工程师参考通过完整源码可缩短底层驱动调试周期。1. 一句话讲清APM32E103为什么要做FLASH模拟EEPROM调试一台需要掉电保存标定值、序列号或生产参数的设备系统上电复位后参数全丢这是APM32E103这类Cortex-M3 MCU项目里很常见的最后一公里。APM32E103片内没有独立的EEPROM数据保存在FLASH里但FLASH只支持先擦后写、按扇区擦除直接把它当EEPROM用最直接的后果就是几万次写入后整个扇区报废而设备掉电瞬间可能只擦了一半。FLASH模拟EEPROM的核心是把扇区切成若干小页用磨损均衡、有效页标记和掉电恢复机制在芯片自带的FLASH上实现一个可反复写入的小型数据存储区。这套方案对APM32E1系列单片机通用思路和STM32F103完全同源移植成本极低适合有RTOS、裸机和低功耗需求的工业与消费类固件。2. 为什么EEPROM通常比FLASH寿命长以及模拟方案的取舍2.1 先搞清APM32E103的FLASH硬件约束页、扇区与擦写寿命APM32E103内部的FLASH和大多数Cortex-M3芯片一样写入时只能把1写成0不能把0写成1要把数据改回去必须先把整块区域擦除成0xFF。擦除操作的最小单位是扇区一个扇区往往是1KB、2KB或4KB而编程操作的最小单位通常是一个字或半字的宽度。也就是说哪怕只想改变一个字节也要先把这个字节所在的整个扇区读出来、在RAM中修改然后擦除扇区再整体写回。这个机制带来了两个直接结果。第一频繁修改一小块数据会反复擦写整个扇区FLASH的擦写寿命一般在1万到10万次之间以10万次计每100ms写一次大约3小时就会把同一扇区写坏。第二掉电瞬间如果扇区只擦除了一半整片数据就全丢了没有任何恢复余地。EEPROM则不同它按字节擦写、寿命可达100万次但APM32E103没有内置EEPROM。外挂24C02这类I2C EEPROM是备选方案但要增加一颗芯片、两个IO引脚还要处理I2C总线故障和上电时序。在很多对成本和可靠性要求都高的场合官方推荐的替代路径就是“用FLASH模拟EEPROM”。2.2 模拟方案的两种形态备份切换式与日志式业界做FLASH模拟EEPROM大体有两种思路。第一种是备份切换式分配两个或多个扇区一个作为活动区一个作为影子区。写入时先往影子区写新数据写完后更新一个状态标记再擦除活动区下一次写入时角色互换。这种方式实现简单、记录清晰但每次写入都可能触发一次整扇区擦除寿命受限于扇区擦除次数。第二种是日志式把一个扇区分成多个小页每次写入只在扇区末尾追加一个新页旧页标记为废弃。页面用完后才擦除整个扇区。这种方式把一次物理擦除分摊到多次逻辑写入上寿命提高若干倍也是目前更主流的做法。APM32E103的FLASH容量足够大完全可以分两个扇区做日志式环形存储一个扇区写满后擦除另一个扇区继续写。选择哪种方案取决于参数写入频次和数据量。如果设备一天只写一次参数备份切换式足够如果像仪表、工控设备那样频繁记录运行状态日志式几乎是必须的。下面我按日志式方案展开这套设计对APM32E1系列单片机内部FLASH的兼容性也最好。2.3 模拟EEPROM的寿命估算与关键指标设计前先做一个简单的寿命模型。假设总共有两个扇区每扇区32KB单页64字节每页带16字节管理头那么一个扇区可以分 32KB / 80B ≈ 409页两扇区共约819页。每次参数写入消耗一页。如果单次擦除寿命按1万次算总写入次数约为 819 × 10000 819万次。这个数字已经超过了绝大多数EEPROM方案的实际需求。实际上这个估算偏乐观因为页管理和磨损均衡还有额外开销但量级已经足够说明问题。要特别注意的是擦除寿命和编程操作电压、温度相关APM32E103的FLASH擦写寿命参数要以数据手册为准设计余量建议留50%以上。3. APM32E103上FLASH模拟EEPROM的具体实现3.1 把两个扇区规划成环形缓冲区以APM32E103内置FLASH为例我习惯在链接脚本里把地址最高的两个扇区预留出来不放入代码段。假设每个扇区大小为4KB基地址分别记为EE_SECTOR_A和EE_SECTOR_B。扇区内部不用单独建文件系统而是按“页数组”看待。区域起始地址大小用途扇区AEE_SECTOR_A4KB当前写区追加有效页扇区BEE_SECTOR_B4KB备用区A满后擦除待用页内管理头每页起始16Bmagic、逻辑地址、长度、CRC、状态页内数据区管理头之后48B用户参数负载这样划分后一个扇区大约可以容纳 4096 / (4816) 64页。磨损均衡的逻辑很直接始终在A区写入A区没有空页时先把B区擦除再把A区中的最新有效页复制到B区最后擦除A区角色互换。两个扇区轮换使用避免任何一块区域被集中消耗。3.2 页记录格式与状态标记每个页需要一个明确的状态机用来区分空闲、有效、废弃。所有状态转换都遵循同一个原则状态字段必须放在页的最后写入。这样做能保证掉电时不会出现数据有效但内容不完整的情况。#define EE_MAGIC 0xFA55 #define EE_PAGE_SIZE 48 // 单页数据区最大字节数 #define EE_HEAD_SIZE 16 // 管理头大小 #define EE_SECTOR_A 0x0801F000 // 实际地址以链接脚本为准 #define EE_SECTOR_SIZE 4096 #define EE_PAGES_PER_SECTOR (EE_SECTOR_SIZE / (EE_PAGE_SIZE EE_HEAD_SIZE)) typedef struct { uint16_t magic; // 固定为EE_MAGIC用于识别有效页 uint16_t logical_addr; // 用户参数编号例如0x0001 uint16_t len; // 本次实际写入的数据长度 uint16_t crc; // 对data区做CRC16 uint32_t valid_tag; // 0xFFFFFFFF表示空闲0xA5A5A5A5表示页已写完整 uint8_t data[EE_PAGE_SIZE]; } ee_page_t;valid_tag是整页最关键的保护字段。写页时先写magic、addr、len、data最后才写valid_tag。掉电时如果程序停在任意一步valid_tag仍然是0xFFFFFFFF上电扫描时会认为该页无效不会读出不完整的数据。3.3 读、写、擦除三个核心接口的C代码这一节给出可直接移植的C骨架。驱动层的FLASH擦除和编程函数以实际SDK为准我在这里统一用flash_erase_sector()和flash_program_word()占位替换成你工程里的底层函数即可。static uint32_t cur_sector EE_SECTOR_A; static uint32_t write_offset 0; static uint32_t ee_read_word(uint32_t addr) { return *(volatile uint32_t *)addr; } static void ee_write_word(uint32_t addr, uint32_t val) { flash_program_word(addr, val); // 调用SDK的FLASH编程接口 while (ee_read_word(addr) ! val); // 写后回读确认 } // 在cur_sector内查找第一个valid_tag为0xFFFFFFFF的空闲页 static int ee_find_free_slot(void) { uint32_t p; for (int i 0; i EE_PAGES_PER_SECTOR; i) { p cur_sector i * (EE_PAGE_SIZE EE_HEAD_SIZE); if ((ee_read_word(p 8) 0xFFFFFFFF) 0xFFFFFFFF) { return i; } } return -1; } int ee_write(uint16_t logical_addr, uint8_t *buf, uint16_t len) { if (len EE_PAGE_SIZE) return -1; int slot ee_find_free_slot(); if (slot 0) { // 当前扇区已满做扇区切换 if (ee_switch_sector() ! 0) return -2; slot 0; } uint32_t base cur_sector slot * (EE_PAGE_SIZE EE_HEAD_SIZE); ee_page_t page; page.magic EE_MAGIC; page.logical_addr logical_addr; page.len len; page.crc ee_crc16(buf, len); page.valid_tag 0xFFFFFFFF; memset(page.data, 0xFF, EE_PAGE_SIZE); memcpy(page.data, buf, len); // 顺序写入valid_tag放最后 ee_write_word(base, (page.magic 16) | page.logical_addr); ee_write_word(base 4, (page.len 16) | page.crc); for (int i 0; i EE_PAGE_SIZE; i 4) { ee_write_word(base EE_HEAD_SIZE i, *(uint32_t *)page.data[i]); } ee_write_word(base 8, 0xA5A5A5A5); // 置有效标记 return 0; }这段代码的核心是把一次整页写入拆成了若干次独立的32位编程操作。先写页头、再写数据、最后写状态这个顺序不能颠倒。ee_find_free_slot()每次从头扫描找空闲页线性开销在64页规模下可以忽略。3.4 掉电恢复与扇区切换掉电恢复的逻辑在上电时一次性执行扫描两个扇区中所有页找到valid_tag为有效且CRC正确的页把最新一页的logical_addr和数据恢复到RAM缓存。扫描时遇到valid_tag为0xFFFFFFFF的页就停止当前扇区的扫描后面的页一定是空的。int ee_init(void) { int found 0; for (int s 0; s 2; s) { uint32_t sector_base (s 0) ? EE_SECTOR_A : EE_SECTOR_B; for (int i 0; i EE_PAGES_PER_SECTOR; i) { uint32_t p sector_base i * (EE_PAGE_SIZE EE_HEAD_SIZE); ee_page_t page; memcpy(page, (void *)p, sizeof(page)); if (page.valid_tag ! 0xA5A5A5A5) { if (page.valid_tag 0xFFFFFFFF) break; continue; } if (page.magic ! EE_MAGIC) continue; if (ee_crc16(page.data, page.len) ! page.crc) continue; // 同步最新数据到RAM缓存 ee_cache[page.logical_addr].len page.len; memcpy(ee_cache[page.logical_addr].buf, page.data, page.len); found 1; cur_sector sector_base; write_offset i 1; } } return found; }需要特别提醒的是ee_init()里对两个扇区都完整扫描是为了处理“扇区切换过程中掉电”的极端情况。ee_switch_sector()的流程是先把当前扇区中所有有效页复制到备用扇区擦除当前扇区再交换cur_sector指向。如果复制过程中掉电重新上电后两个扇区里都有一部分数据此时必须在ee_init()里做“合并去重”选取地址更新的有效页作为最终数据。4. 参数设置、中断保护与FLASH编程时的三个坑4.1 写FLASH前关闭全局中断防止取指失败APM32E103的FLASH在编程和擦除期间CPU如果发生中断中断向量和中断服务程序所在的代码区域若正好处于被编程的扇区就会导致取指失败常见现象是程序跑飞到HardFault或者写了一半卡死。最稳妥的做法是在每次FLASH write/erase操作前关全局中断操作完成后恢复。__disable_irq(); flash_erase_sector(cur_sector); __enable_irq();如果工程里用了RTOS仅仅关中断还不够还要防止任务调度把当前任务切走否则另一个任务可能在这期间调用同一个FLASH驱动造成并发写冲突。裸机环境下直接关中断即可。4.2 页大小、对齐方式和地址边界的配合APM32E103的FLASH编程最小粒度通常是32位或64位但模拟EEPROM的页设计不必非要对齐到32位因为底层写入是按字写的。真正的约束是扇区地址必须按扇区大小对齐否则擦除时会报地址错误。EE_SECTOR_A和EE_SECTOR_B的值必须是扇区大小的整数倍。页大小也不是越大越好。页越大单次写入的数据量越大但是每页的磨损浪费也越大合理的页大小在16到128字节之间覆盖绝大多数标定参数场景。如果参数超过255字节通常的做法是把一个逻辑对象拆成多页用logical_addr区分或者放弃模拟EEPROM、直接在应用层做文件系统。4.3 擦除时间与写后校验的参数说明FLASH擦除一个扇区的时间通常在几十毫秒量级写入一个字在几十微秒量级。这些时间取决于芯片内部电荷泵和电压代码里不应该使用固定延时而应该轮询FLASH状态寄存器。操作类型典型耗时要求失败检测方式扇区擦除数十毫秒以状态寄存器为准擦除后回读全部字节为0xFF字编程数十微秒写后回读比较页复制与页大小线性相关逐字校验CRC掉电恢复毫秒级扫描即可无有效页时返回默认值写完一个页后立即回读整页是最便宜也最有效的可靠性手段。我在量产固件里通常会做两层校验驱动层每次编程后回读应用层定时对全部有效页做一次CRC巡检。巡检间隔可以放在低优先级任务里不影响正常逻辑。5. 仿真下载失败与大数据量参数的排错清单5.1 Keil下载时报target dll has been cancelled怎么查很多APM32E103开发者在调试setup阶段就卡在下载环节。报错信息形如Error: Flash Download failed - Target DLL has been cancelled这个错误和模拟EEPROM本身无关但很容易把问题误判成FLASH驱动配置错误。排查顺序我一般固定为三步查Debug设置里的Flash Download算法是否添加了对应容量型号查目标板供电和复位电路是否稳定尤其注意下载器供电能力查SWD线是否过长、时钟是否过高把SWD时钟从10MHz降到1MHz再试。APM32E103在Keil中的FLASH算法和STM32F103不完全通用如果工程是从STM32F103直接复制过来的算法文件必须替换。这个动作在Debug页面下方的“Flash Download”选项里操作选择对应厂商的算法文件后地址范围会自动更新。5.2 模拟EEPROM写完后数据不稳定的两个隐蔽原因一类是编译优化问题写了volatile但没有在结构体读取时保持volatile属性导致编译器把FLASH数据缓存到寄存器读出来的是旧值。全工程范围内对FLASH映射地址的访问建议全部使用volatile指针。另一类是参数存储区和程序代码共用扇区。如果链接脚本没有把两个EEPROM扇区排除出代码区编译出来的bin文件会填满整个FLASH镜像下载时把EEPROM扇区也覆盖成代码内容。这个问题在调试时不会立刻暴露只有在量产烧录完整镜像后才出现且每次断电重启表现不一样。排查方法是打开map文件确认EE_SECTOR_A和EE_SECTOR_B地址段没有任何.text符号。5.3 用压力测试验证磨损均衡是否真的在轮换写完模拟EEPROM后至少跑一轮快速磨损测试循环写入同一逻辑地址每写一次在RAM里计数同时记录当前扇区切换次数。写10万次后读取该逻辑地址的最新值确认数据完整且两个扇区的擦除次数差不超过1。测试时把扇区切换处的打印打开观察切换是否均匀交替。如果某一侧扇区始终不切换说明ee_find_free_slot()返回逻辑有误通常是状态标记写错位置导致旧页被反复当成空闲页。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价