
简介一份面向嵌入式初学者的STM32片内Flash读写测试工程适合需要使用片内Flash保存参数、实现掉电不丢失的开发者。资源从Flash扇区结构讲起完整覆盖初始化、地址定位、编程、错误检查、数据读取与掉电保护等关键环节并在测试例程中展示HAL_FLASH_Program/Erase接口的典型调用以及写保护、超时、校验错误等异常应对方式。压缩包共140个文件以c源码和h头文件为主另含uvprojx/uvoptx工程文件、axf/hex编译输出和sct/map链接配置方便在Keil中结合链接文件分析存储布局整体仅2.96MB。该资源已有1031人学习适合开发前先验证Flash读写流程通过阅读并移植其中的flash_write和flash_read函数还能复用其异常处理与掉电备份设计有效缩短存储模块的上手与调试时间。 最近在做一个需要掉电保存参数的小项目板子上用的是STM32F103C8T6画图的时候没预留外部EEPROM等程序写完了才发现参数没地方存。重新改板一是麻烦二是不划算于是就把目光盯在了片内Flash的剩余空间上。反正几十K的Flash放着也是放着拿来存参数既能省掉一颗外挂芯片还能少几条走线。方案看起来简单但真正动手写读写测试的时候才发现里面有不少细节不试不知道。这篇就把我整个验证过程和踩过的坑整理出来给正在考虑用片内Flash存数据的朋友做个参考。先说结论片内Flash完全能做掉电保存读写速度虽然不比RAM但在绝大多数参数存储场景里绰绰有余。关键是得搞清楚它的擦除粒度、写入限制、地址划分以及最容易被坑的“在正在执行的扇区上擦写”这种问题。下面我按自己的测试流程一步步说。1. 片内Flash与外部存储怎么选1.1 片内Flash能干什么片内Flash在STM32里主要有两个用途一个是放程序代码本身另一个就是像我现在这样把剩余空间拿出来存运行参数比如设备地址、校准系数、开关状态、用户配置这些掉电不能丢的数据。相比外部存储片内Flash最大的优势就是“白嫖”——硬件上零成本不用改板也不用在固件里初始化额外的SPI或I2C外设。对很多量产产品来说省一颗外部存储芯片省的不只是芯片本身的钱还有贴片费、PCB面积和备料复杂度这些隐性成本加在一起就相当可观了。1.2 与外部EEPROM、SPI Flash的对比我用一个表格把几种常见掉电存储方案的对比列出来方便大家根据项目需求来选方案容量擦写寿命擦除粒度写入单位成本适用场景片内Flash几十KB~几MB1万次左右按页/扇区(1KB~128KB)16位/32位零小容量参数、掉电保存外部EEPROM几KB~几百KB100万次按字节字节中频繁小量写入、数据重要SPI NOR Flash几百KB~几十MB10万次按扇区(4KB)字节/页写低大容量日志、固件升级外部铁电FRAM几KB~几MB千亿次按字节字节高高频写入且要求可靠从表格能看出来片内Flash的擦写寿命是约束最大的点。它的擦写次数常见标称是1万次如果你在程序里每隔几秒就写一次很快就会被写穿。所以我自己定的原则是片内Flash只存低频变化的参数比如设备配置、累计流量之类的高频数据该用RAM缓存还是用RAM需要频繁写入的再用EEPROM或铁电。1.3 什么时候不建议用片内Flash这里要先泼一盆冷水。如果你的产品需要频繁掉电保存状态一分钟得写十几次那别用片内Flash。还有如果单个数据很小但每次改动都要单独保存尽量还是用外部EEPROM因为片内Flash最小擦除单位一般是1KB以F103中容量为例你都擦一整页就为改一个字节这种消耗很心疼。另外片内Flash和程序共用一块存储区域如果在擦写操作里出了问题比如指向了程序当前运行所在的扇区或者擦除过程中掉电导致程序代码被擦掉一半那设备就直接变砖了。没有外部Bootloader救场的话就只能用下载器强制擦除整片重新烧。所以片内Flash的方案适合有一定调试手段、程序已经稳定跑起来的阶段再上。2. Flash的基本工作规律2.1 存储映射与页划分要操作片内Flash首先得把整个存储映射搞清楚。拿我手里的STM32F103C8T6举例它内置64KB Flash起始地址是0x08000000结束地址是0x0800FFFF。这个地址范围其实是被映射到Cortex-M3的0x00000000开始的统一地址空间里的所以CPU可以直接像读内存一样读Flash内容不需要额外配置。F103中容量每页大小是1KB64KB就是64页。页的编号从0开始程序从0x08000000开始存放实际项目编译出来Hex大概十几到二十几KB所以前面的页都用掉了。我打算把参数放到最后几页也就是地址0x0800F000附近这样离程序区远一点在擦除时误伤程序代码的概率小一些。这里提醒一句不同系列、不同容量的STM32页大小和划分完全不一样。F1系列小容量是1KB每页大容量是2KB每页F4系列则是按扇区分前四个扇区各16KB后面还有64KB、128KB的。用哪个型号先去参考手册里查清楚Memory Map那张图别想当然用通用代码。2.2 为什么只能擦成0xFF再写入片内Flash的本质决定了它的两个硬性特性擦除操作会把整个页全部置为0xFF而编程操作只能把1写成0不能把0写成1。这意味着如果你想修改某个地址上的数据不能直接像改RAM那样覆盖写而要先把这个地址所在的那一整页擦掉让所有位都回到1再按需把某些位写成0。这种特性决定了写入流程一定是“先备份、再擦除、再整页回写”。如果你只想更新页里的一个字节就得先把整页参数读进RAM缓冲区在缓冲区里改掉目标字节然后擦除Flash页再把缓冲区整页写回去。这也是片内Flash写小数据的痛点读写放大太严重了。2.3 编程必须按半字对齐写入F103的Flash编程操作要求按16位半字为单位写入也就是每次至少写2个字节而且起始地址必须是2字节对齐的。虽然HAL库里有按字节写的函数接口但底层实现其实是读了半字再拼装回去写的。所以如果你想存一些结构体数据建议在定义结构体时就注意对齐或者直接用uint16_t、uint32_t数组来组织缓冲避免搞出奇数地址访问错误。另外F103内部Flash擦除和编程时CPU会暂停等待操作完成这是正常的不用害怕延时就怕你在擦写期间开着中断。因为中断服务函数代码本身也在Flash里如果擦写过程中恰好触发了中断CPU没法同时执行Flash操作和取指指令就会卡死或者访问异常。所以我建议在擦写Flash之前关中断写完再恢复这个后面代码里会体现。3. 测试前的准备工作3.1 工具链与工程选择我这次用的是标准的STM32标准外设库做的工程其实不管用标准库、HAL库还是LL库原理都一样区别只是封装程度。测试用的板子是市面上最常见的“最小系统板”板上有一颗STM32F103C8T6自带ST-Link下载器不需要额外接任何外设就能测片内Flash串口用来打印测试日志。如果是第一次接触片内Flash我建议先用最笨的办法验证直接在线调试在Keil的Memory窗口里输入0x0800F000观察这一片地址的内容变化。这样做的好处是能看到最原始的数据不用通过串口打印绕弯子等到调试通过以后再写串口日志也不迟。3.2 划出一块安全的存储地址这一步很关键绝对不能随便挑个地址就用。我建议在工程里明确划定一个Flash存储区域宏比如#define PARAM_FLASH_ADDR 0x0800F000 #define PARAM_FLASH_PAGE 60然后在链接脚本或编译输出里确认一下你的程序实际占用的Flash结束地址是多少。比如Keil编译完显示Program Size: Code18000, RO-data1000, RW-data200, ZI-data1500那总Flash占用大约是Code加RO加RW算出来不超过20KB那0x0800F000这个地址已经避开了程序区可以放心用。保险起见再留出至少2~4KB的余量防止后续代码膨胀把程序区顶到存储区里。3.3 最小测试硬件和时间预估这个测试全程只需要一块能跑的STM32最小板加一个下载器连LED都不需要状态全靠串口或者调试器看。整体测试时间大概一个下午就能搞定主要时间花在调地址越界和写保护这类问题上。如果你手里正好有带ST-Link的板子这个项目几乎是零成本。4. 读写测试的实现细节4.1 操作流程总览片内Flash的读写流程概括起来就是六个步骤解锁、查标志、擦除、编程、校验、上锁。其中解锁和上锁是为了防止程序误操作因为Flash控制寄存器在复位后默认是锁定状态必须先写入两个固定的解锁键值才能操作。我整理了一张流程图来描述整个操作逻辑但考虑到博客里贴图不方便就用文字描述一遍调用解锁函数写入解锁键值检查BSY标志位等待上一次Flash操作彻底完成如果需要写入的页之前有数据先执行页擦除按半字为单位执行编程写入逐地址读回数据和源缓冲区比较全部校验通过后锁定Flash。把这个流程吃透了后面不管换什么芯片写的代码都是换汤不换药。4.2 标准库实现的核心代码下面这段代码是我在工程里实际用到的Flash写入函数省略了返回值的过度处理只保留核心逻辑方便阅读#include stm32f10x_flash.h #define FLASH_START_ADDR 0x0800F000 #define FLASH_PAGE_NUM 60 #define DATA_SIZE 64 // 存储的数据块大小单位字节 static uint16_t write_buf[DATA_SIZE / 2]; void Flash_WriteData(uint32_t addr, uint16_t *buf, uint16_t halfword_len) { __disable_irq(); // 擦写期间关中断 FLASH_Unlock(); // 检查BSY标志 while (FLASH_GetFlagStatus(FLASH_FLAG_BSY) SET) ; // 擦除目标页 FLASH_ErasePage(addr); // 按半字写入 for (uint16_t i 0; i halfword_len; i) { FLASH_ProgramHalfWord(addr i * 2, buf[i]); while (FLASH_GetFlagStatus(FLASH_FLAG_BSY) SET) ; } FLASH_Lock(); __enable_irq(); }这段代码里有个容易被忽略的点我用了一个write_buf静态数组来承接需要写入的数据。为什么不用局部数组因为局部数组在栈上一旦函数退出栈就被释放了如果你的数据源是结构体或者变量必须先memcpy到这个对齐的缓冲里再传入写函数避免出现奇数地址或者对齐问题。4.3 读取与校验方法读取片内Flash非常简单因为Flash是直接映射在寻址空间里的直接按指针取值就行。比如要读取0x0800F000处的半字数据可以这样uint16_t read_val *(volatile uint16_t *)0x0800F000;注意这里加了volatile防止编译器把读操作优化掉尤其是你在做循环校验的时候不加volatile很容易踩坑。为了验证写入是否可靠我的测试代码里写了一个完整校验流程把源缓冲区和Flash里读回的数据逐字节比较任何一个字节不一致就直接报错并停止uint8_t Flash_Verify(uint32_t addr, uint16_t *buf, uint16_t halfword_len) { for (uint16_t i 0; i halfword_len; i) { uint16_t read_data *(volatile uint16_t *)(addr i * 2); if (read_data ! buf[i]) { return 0; // 校验失败 } } return 1; // 校验成功 }4.4 测试功能代码我在主函数里写了一个简单的测试逻辑第一次上电时如果检测到Flash存储区域是空的全是0xFF就往里面写一串模拟的配置数据比如设备ID、校准值、开机次数计数下次上电时先读出来打印然后把开机次数加1再写回去。这样反复断电重启就能验证“掉电保存”和“累计计数”两个功能是否正常typedef struct { uint16_t magic; uint16_t device_id; uint16_t boot_count; } AppConfig; int main(void) { // 初始化串口... AppConfig cfg; uint16_t cfg_buf[4]; // 读取结构体 memcpy(cfg, (void *)FLASH_START_ADDR, sizeof(AppConfig)); if (cfg.magic ! 0x5A5A) { // 首次上电初始化配置 cfg.magic 0x5A5A; cfg.device_id 0x01; cfg.boot_count 1; memcpy(cfg_buf, cfg, sizeof(cfg)); Flash_WriteData(FLASH_START_ADDR, cfg_buf, sizeof(cfg) / 2); printf(第一次上电参数已初始化\r\n); } else { // 后续上电累加次数 cfg.boot_count; memcpy(cfg_buf, cfg, sizeof(cfg)); Flash_WriteData(FLASH_START_ADDR, cfg_buf, sizeof(cfg) / 2); printf(设备ID:%d 开机次数:%d\r\n, cfg.device_id, cfg.boot_count); } while (1) { // 主循环 } }这里有个很重要的细节我用了memcpy把结构体拷贝到uint16_t数组再传给写入函数而不是直接从结构体地址传。原因就是前面说的对齐问题。F103的Flash编程要求半字对齐如果结构体里出现了奇数字节成员编译器填充后整个结构体对齐通常没问题但直接取地址强转成uint16_t指针传进去风险就大了。稳妥起见统一走缓冲中转。5. 实际测试与踩坑记录5.1 测试过程和结果我实际跑下来整个流程还是比较顺利的。第一次上电串口打印“第一次上电参数已初始化”断电重启后打印“设备ID:1 开机次数:2”再来一次变成3。校验函数每次也都返回成功说明数据写入和读出完全一致。用ST-Link的在线调试窗口看0x0800F000地址的内存能看到数据确实固化在Flash里断电后不丢。我还做了一个耐久性测试让程序自动连续擦写同一页500次每写一次读一次校验。跑完以后Flash依然能正常写入和读取没有出现提前损坏的迹象。当然500次离1万次寿命还很远但至少能证明连续擦写的稳定性。建议大家在做完功能测试后也跑一下这个耐久性测试用连续擦写直到写坏的办法测出当前芯片的真实寿命余量。我见过一些劣质芯片标称1万次实际几千次就出问题。5.2 最容易踩的坑擦了自己代码区这是我在调试过程中最惊险的一次。一开始我图方便直接选了0x08008000这个地址测试写完以后程序立刻跑飞下载器也连不上了。一看地址我程序的代码段已经占到了0x08005000左右0x08008000正好在我程序区的中间等于我把正在执行的代码擦掉了单片机当然直接挂。后来只能按着板子的复位键同时用ST-Link强制连接趁着代码没跑起来的一瞬间整片擦除重新烧录才把板子救回来。从那以后我再也不随手填地址了每块板子我都先看编译生成的map文件确认程序占用Flash的精确范围再选一个离程序末尾至少空出几KB的地址。5.3 写保护导致的坑另一个容易遇到的问题是Flash写保护。一些量产板子出厂时会设置读保护或写保护导致你在调试时无法擦写Flash。报错信息往往不是“写保护未解锁”而是Unsupported command或者FLASH_ErasePage超时非常迷惑。正确的处理方式是先用ST-Link Utility或者STM32CubeProgrammer把芯片的读保护等级改为Level 0全片擦除一遍解除保护后再继续调试。不过要注意改读保护等级会触发全片擦除等于把原有程序也清掉了所以量产阶段的板子别轻易动这个设置。5.4 掉电损坏的思考测试中还发现一个现实中的隐患写Flash一半时断电。因为在擦除或写入过程中断电Flash里存的位很可能处于不确定状态轻则参数损坏重则整页数据不可用。虽然片内Flash从硬件机制上比外部EEPROM对掉电更敏感外部EEPROM内部有掉电检测和写周期管理但我们还是能在软件上做一些补救比如在数据结构里加上magic标志、CRC校验如果发现校验不对就回退到默认参数千万别用脏数据跑业务逻辑。6. 常见问题排查清单6.1 下载程序时报Flash Download Failed这个报错要注意区分。如果是在用下载器烧录时弹出“Flash Download failed - Target DLL has been cancelled”那多半不是片内Flash代码的问题而是下载器连接不稳、芯片型号选错、读保护开了导致的。处理方法很简单把下载器重新插拔、确认Keil里Device型号和芯片一致、用STM32CubeProgrammer检查连接状态。我在排查这种问题时最常用的招是按住板子复位键点击下载在弹出来的进度条出现瞬间松开复位十次有八次能连上。6.2 写入后读回全是0xFF如果写入函数返回成功但读回来全是0xFF大概率是写入地址和擦除地址对不上。比如你擦除了0x0800F000所在页却往0x0800F200写那这部分内容根本没被擦除写入又只能把1变0原来有数据的话就会得到一堆错乱的值。还有一种可能是写保护生效了编程操作被硬件忽略函数却因为不检查标志位而“假成功”。6.3 程序在擦写期间卡死擦写期间程序卡死优先检查中断。前面说了擦写Flash时CPU要停下等待如果此时中断触发中断里的代码没办法从Flash取指整个MCU就僵在那里。最简单的解决办法是在Flash操作前后关中断优先级高的定时器中断最好也关掉。另外如果写Flash的代码本身在Flash上执行而你要擦除的页正好包含当前正在执行的代码那也会卡死这就要回头检查地址规划。6.4 参数保存后经常随机错乱随机错乱的情况多半是没有做备份机制。我这里给一个简单但有效的做法在Flash里规划两块区域交替使用比如这次写在页60下次写在页61每块数据开头打上序号哪个序号大就使用哪个。这样即使一次写了一半断电也还有上一份完整的数据能顶上。等两块区域都写满一轮再做一次全局整理。这个方案的代价是Flash空间多占一倍但对关键参数来说非常值得。7. 后续扩展建议片内Flash测通了以后还可以做一些功能扩展。比如做一个简易的日志记录系统把系统运行状态以环形的形式轮流写入不同的页记录一段时间的事件历史方便故障诊断。再比如可以结合外部的SPI Flash做一个分级存储片内Flash存关键配置外部Flash存批量日志这样既省钱又能满足大容量存储需求。另外一个趁手的工具思路是写一个类似EEPROM模拟层的组件封装成接口上层程序只调用save_param和load_param不用关心底层擦写逻辑。这样以后换芯片型号只要改底层实现就行。我这次测试完就顺手把Flash操作抽成了独立模块后续项目拿来就能用。最后分享一点个人体会片内Flash虽然不像外部存储那么灵活但用好了能给项目省下实实在在的BOM成本和PCB面积。关键是把地址规划、擦写保护和掉电处理这三件事想清楚再配合完整读写测试基本就能放心投入使用了。如果你们项目也在纠结该不该用片内Flash按我这个流程先拿块板子跑一遍读写测试用数据说话比拍脑袋决定靠谱得多。本文还有配套的精品资源点击获取