
简介STM32片内Flash读写测试工程面向嵌入式开发者和单片机存储应用学习者重点解决设备掉电后数据保存与Flash编程难题。资源为完整Keil工程包共140个文件以h/c源码文件为主涵盖系统时钟初始化、Flash驱动、串口输出、定时器等模块同时附带uvprojx工程配置、hex/axf烧录文件、map映射文件及相关调试辅助文件压缩包总大小2.96MB导入即可编译运行。已有1031人学习下载。代码封装了flash_write与flash_read等常用接口并围绕Flash初始化、扇区擦写、编程状态检查、备份存储等关键流程展开配合详细注释和读写自检逻辑帮助读者理解STM32内部Flash分扇区管理、有限擦写寿命及编程保护机制。通过实际读写循环与异常处理可快速掌握非易失性存储开发技巧提升嵌入式系统的数据可靠性与稳定性设计能力。1. 为什么要自己动手做片内Flash读写测试最近做一个小项目需要在STM32上保存一批设备校准参数。最开始我特别天真直接把参数定义成const数组想着烧录的时候写死在Flash里结果调试阶段每次改参数都要重新下载程序麻烦不说还差点把Bootloader区域冲掉。后来我索性把片内Flash的读写测试认认真真做了一遍把擦除、写入、读取、校验整条链路彻底摸清了。这篇内容就是我当时完整踩坑过程的总结适合正在做参数掉电存储、OTA升级标志位或简单日志记录的人看。我会先讲清楚Flash的物理规则和不同型号的差异再给出一套可以直接抄的HAL库读写代码最后是几个高频率出现的报错排查思路——比如很多人一上来就遇到的Flash Download Failed和No STM32 Target Found其实99%都是环境问题不是代码问题。片内Flash这个东西说难不难但和RAM完全不是一个用法。你可以把Flash想象成一块只能从白色画成黑色的白板画笔只能涂黑也就是把1写成0如果想把黑色变回白色只能把整块板子擦干净重来。这就是Flash最核心的物理特性不理解这一点后面你会踩无数坑。我现在每次焊完板子都会先花十分钟做一次Flash读写测试确认存储区域安全、读写逻辑正确再开始写业务代码这个习惯帮我省掉了大量排查时间。1.1 片内Flash到底能做什么片内Flash本质上是非易失存储掉电不丢数据。很多人一听到Flash第一反应是“这不是放程序代码的地方吗”没错但它也是芯片上唯一一块用户可写、掉电不丢、访问速度又快的存储区域。片内除了Flash还有OTP区、备份寄存器和外部E2PROM各有各的用途但论通用性Flash绝对是最常用的。我在实际项目中用Flash做过几类事情一是保存传感器校准系数比如温度补偿表、零偏值二是保存设备序列号、MAC地址、配网信息这样批量生产时每台设备可以有独立参数三是保存Bootloader和App之间的跳转标志位上电后先判断Flash里的某个字节是不是约定值决定要不要进入固件升级流程四是存简单的运行日志掉电重启后还能翻看上次运行状态。这些场景的共同特点是数据量不大、不需要频繁写入但要求掉电保存——Flash正好合适。有同学会问为什么不用片外的E2PROM或SPI Flash因为片内Flash免费啊而且读取不需要额外驱动直接指针解引用就能访问。它的缺点是不能按字节改、有擦写寿命限制但作为参数存储完全够用。如果你需要频繁记录日志比如每秒写一次那我劝你还是用外部SPI Flash或者干脆加片E2PROM片内Flash的擦写寿命一般是1万次左右扛不住这么频繁的写入。1.2 不同型号的Flash结构差异很大这是我踩过最深的一个坑。STM32不同系列的Flash内部结构完全不一样连“最小擦除单位”这个概念都有好几种叫法。以最常见的F103系列举例中容量型号比如C8T6Flash总共64KB按1KB一页划分大容量型号比如ZET6Flash有512KB前4页是16KB后面每页2KB。F1系列用“页”作为擦除单位所以FLASH_ErasePage传的是页地址。但到了F4系列就不一样了。F4的Flash按“扇区”管理同样是1MB容量分成了4个16KB扇区、1个64KB扇区、7个128KB扇区。你在F103上习惯了一页1KB、擦除特别精细换到F4直接给你来一个128KB的扇区一个不小心就会把不需要擦除的代码区带进去。H7系列更复杂按Bank分成两个区操作前还要确认自己在哪个Bank。所以动笔写代码之前请务必打开对应型号的《参考手册》翻到Flash章节把“页/扇区大小”和“起始地址”抄下来记到笔记本上。我个人的习惯是把常用型号的Flash参数整理成一张表写测试代码时直接查芯片系列典型型号Flash容量擦除单位擦除单位大小F103中容量STM32F103C8T664KB页1KBF103大容量STM32F103ZET6512KB页2KB前4页16KBF407STM32F407VET6512KB扇区16KB/64KB/128KBF429STM32F429ZIT62MB扇区多个Bank混合换芯片型号后复制以前代码的时候务必确认这些参数不要想当然。1.3 写Flash前必须搞懂的4条物理规则第一Flash写入只能用1写0不能把0写1。想恢复成1只能整体擦除。所以“读-改-写”在Flash上行不通你必须先擦掉整个页/扇区再写入新数据。如果页里还有别的数据不想丢只能先读到RAM里擦除完再一起写回去。第二写入的最小单位不是1字节而是半字16位部分系列支持32位字写入或64位双字写入。F103用HAL库操作时FLASH_TYPEPROGRAM_HALFWORD一次写16位FLASH_TYPEPROGRAM_WORD一次写32位。F4/H7还支持FLASH_TYPEPROGRAM_DOUBLEWORD一次写64位。写入地址必须对齐半字对齐2字节、字对齐4字节、双字对齐8字节不对齐直接HardFault或返回错误。第三Flash有锁定机制。芯片复位后Flash默认处于锁定状态要编程必须先调用HAL_FLASH_Unlock()解锁。这个锁由Flash控制寄存器里的LOCK位控制目的是防止程序跑飞时把Flash内容写坏。写完后记得调用HAL_FLASH_Lock()重新上锁我见过不少代码只解锁不上锁虽然平时没事但对于有安全要求的项目来说是一个隐患。第四擦除和编程操作期间Flash控制器会占用内部总线这时候如果CPU试图从Flash取指就会产生总线冲突。最直接的后果是擦除过程中来了一个中断中断服务函数在Flash里一看取不到指令直接HardFault死机。后面我会专门讲这个问题的解决方案。2. 读写测试的整体设计思路2.1 测试环境和工具选择我用的测试平台是一块非常常见的STM32F103C8T6最小系统板开发环境是Keil 5加STM32CubeMX生成的HAL库工程调试器是ST-Link V2。为什么推荐HAL库因为现在大部分新项目都是用CubeMX初始化外设HAL库函数对Flash操作的封装比较统一换芯片型号时改动最小。如果你还在用标准库也不用担心Flash操作的核心思路是一样的无非是FLASH_Unlock、FLASH_ErasePage、FLASH_ProgramWord、FLASH_Lock这几个函数。后面我会做一个对照你按自己的环境选择即可。我这里还要提一下调试器的选择。ST-Link V2便宜够用但有一个缺点连接稳定性一般。尤其是杜邦线插得松或者下载速度调太高经常出现“flash download failed”之类的报错。如果你手头有J-Link协议支持更好稳定性也会好不少。不过我这次测试用的是ST-Link后面会给出排查这类报错的具体方法即便你用J-Link遇到类似问题思路也是通用的。2.2 规划一个绝对安全的测试地址这是整个测试中最需要小心的一步。STM32F103C8T6的Flash从0x08000000开始总容量64KB最后1KB的地址范围是0x0800FC00到0x0800FFFF。大多数情况下我们的测试程序编译出来也就10KB到30KB直接烧录后会从0x08000000开始存放正常不会占用到最后1KB。所以我把用户数据区选在0x0800FC00也就是最后一页。为什么不往前选因为越靠前越接近代码区程序一旦变大就可能和你的数据区重叠。Flash不同于外部存储芯片地址重叠不会有任何提示只会悄悄把你的程序代码覆盖掉导致随机死机、跑飞排查起来非常折磨人。测试的时候我故意采用了“从后往前规划”的思路这也是量产项目里比较规范的做法程序代码从前放用户数据从后放中间留出一段安全间距。如果芯片容量小、程序又大可以适当减小间距但绝不能完全不留。对于C8T6这类64KB芯片我的建议是用户数据区固定放在倒数第2页到倒数第4页之间具体看你的数据量。2.3 测试方案拆解解锁、擦除、写入、校验我的完整测试流程分四步。第一步调用HAL_FLASH_Unlock()解锁Flash原理我前面讲过不解锁就写硬件会直接拒绝。第二步擦除目标页。调用HAL_FLASHEx_Erase()把刚才规划的0x0800FC00这一页擦干净擦除后整页数据都变成0xFF。这里要注意HAL库的擦除接口需要传一个FLASH_EraseInitTypeDef结构体里面填写擦除类型、起始地址和擦除页数。还有一个非常隐蔽的坑必须注意检查第二个参数PageError如果擦除失败PageError会返回出错的页地址。第三步调用HAL_FLASH_Program()把要写入的数据按字写入到页内偏移地址。一次写32位循环多次写多个数据。写入完成后调用HAL_FLASH_Lock()重新锁定。第四步校验。直接用指针读取Flash对应地址的内容与写入时使用的原始数据逐一对比。注意一定不要忽略了掉电测试写入成功后断电再重新上电执行读取程序确认数据仍然一致。只有掉电再上电验证过才能证明数据真正保存住了。这四步看着简单但每一步都有对应的坑。接下来我直接给出完整的测试代码然后逐个环节解释注意事项。3. 代码级实操HAL库实现Flash写入与读取3.1 按页擦除的完整写法先看擦除代码#define FLASH_USER_START_ADDR 0x0800FC00 // 最后一页起始地址 #define FLASH_USER_END_ADDR 0x0800FFFF // 最后一页结束地址 FLASH_EraseInitTypeDef eraseInit; uint32_t pageError 0; HAL_FLASH_Unlock(); eraseInit.TypeErase FLASH_TYPEERASE_PAGES; // 按页擦除 eraseInit.PageAddress FLASH_USER_START_ADDR; eraseInit.NbPages 1; // 擦除1页 if (HAL_FLASHEx_Erase(eraseInit, pageError) ! HAL_OK) { // 擦除失败pageError会指向出错的页地址 // 建议这里加一个明显提示比如串口打印或LED快闪 }这段代码里最容易被忽略的就是pageError参数。官方库的HAL_FLASHEx_Erase在擦除出错时返回HAL_ERROR但很多人的代码根本不检查返回值。我见过不少项目Flash擦除失败后继续写入导致数据全乱。我的建议是擦除和写入后至少要做一次失败检查哪怕是暂时打印一条串口日志也能帮你省下排查时间。擦除期间CPU会有一到几十毫秒的停顿F103擦一页典型耗时在20ms到40ms量级。对于大部分应用来说这个时间可以接受但如果你的系统里有实时性要求高的控制任务比如PWM输出、编码器计数这几十毫秒的停顿可能会造成可见的影响。后面我会讲怎么用关中断的方式规避风险。3.2 数据写入与对齐问题擦除完成后就可以写入数据了。这里给出一次写入4个32位数据的例子uint32_t testData[4] {0x12345678, 0xA5A5A5A5, 0x0000FFFF, 0x5A5A1234}; uint32_t addr FLASH_USER_START_ADDR; for (int i 0; i 4; i) { if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr i * 4, testData[i]) ! HAL_OK) { // 写入失败处理 } } HAL_FLASH_Lock();这里有一个我自己踩过很多次的坑地址对齐。HAL_FLASH_Program的写入地址必须按4字节对齐。如果写成addr i而不是addr i * 4那么第2次写入时地址就变成0x0800FC01直接不对齐。轻则写入失败重则触发HardFault程序当场死给你看。为什么要用0x12345678和0xA5A5A5A5这种测试值因为Flash擦除后的默认值是0xFFFFFFFF如果你测试数据也用0xFFFFFFFF写完和没写看起来一模一样校验完全无效。应该选择一些0和1混合得比较均匀的数值这样写完后能明显看出差异。我还会特意在数据里加一个0x0000FFFF用来测试高位低位是否写反了。还有一个经验不同系列的编程粒度不同。F103支持按字写F4/H7还支持双字写。优先使用当前系列支持的最大编程粒度比如F4用FLASH_TYPEPROGRAM_DOUBLEWORD一次写64位这样可以减少Flash编程操作的次数缩短整体写入时间。不过双字写入要求8字节对齐注意调整地址偏移。3.3 读回校验和掉电测试读取Flash内容比写入简单得多因为Flash在STM32里是内存映射的直接用指针访问即可uint32_t readback[4]; bool match true; for (int i 0; i 4; i) { readback[i] *(__IO uint32_t *)(FLASH_USER_START_ADDR i * 4); if (readback[i] ! testData[i]) { match false; } }这个for循环会在内存里跳4次把对应的值读出来和原始值对比。不要忘了加volatile类型的强转——__IO就是HAL库里的volatile宏防止编译器把读操作优化掉。我见过有人不用指针而用memcpy效果是一样的但要确认memcpy的长度没有越界别把Flash后面的区域也读了。真正关键的步骤是掉电测试。写入完成后把板子断电等几秒钟再上电执行同样的读取代码。如果读出来的数据依然是刚才写入的值说明Flash保存功能正常。如果读出来是0xFF说明数据其实没写进去——大概率是擦除失败或地址写错。我还建议做一次“重复擦写”测试比如连续写入、断电、读取20次中途不要修改任何代码。这样能验证地址区域是否稳定同时观察芯片是否出现偶发问题。Flash擦写是有寿命的这种测试数据最好用RAM里的变量自增生成这样可以避免反复修改源代码。3.4 标准库和HAL库函数对照很多老项目还在用标准库我这里给一个快速对照表方便你切换操作标准库HAL库解锁FLASH_Unlock()HAL_FLASH_Unlock()擦除页FLASH_ErasePage(addr)HAL_FLASHEx_Erase()写入16位FLASH_ProgramHalfWord(addr, data)HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr, data)写入32位FLASH_ProgramWord(addr, data)HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, data)锁定FLASH_Lock()HAL_FLASH_Lock()标准库的FLASH_ProgramWord只能写32位没有64位双字操作。如果你的项目从标准库迁移到HAL库最需要注意的是FLASHEx_Erase的结构体参数这个接口设计得比较绕但用习惯后发现填参数的过程相当于强制你去确认地址和页数能减少因疏忽导致的错误。标准库最大的问题是擦除接口没有返回值检查这一点HAL库做得更好所以我建议新项目尽量用HAL库。4. 测试过程中的高频问题与排查实录4.1 Flash Download Failed程序都下载不进去很多人在做Flash测试时遇到的第一个问题不是读写失败而是程序压根下载不进去。Keil弹出“Flash Download Failed - Target DLL has been cancelled”这个问题光我在论坛上就看到过无数次。排查顺序我建议这样来首先确认ST-Link的SWDIO和SWCLK杜邦线有没有接反或松动这是最大嫌疑。用万用表蜂鸣档量一下调试器到目标板的连通性很多“玄学问题”其实就是接触不良。其次打开Keil的Options for Target - Debug - Settings把下载速度从4MHz降到1MHz。高速模式下SWD抗干扰能力差尤其是杜邦线较长或者面包板连接的时候4MHz极易出错。降到1MHz后绝大多数连接不稳定的问题都能解决。我以前觉得降低下载速度显得不专业后来发现这是最省时间的做法。最后检查目标板供电。ST-Link V2的3.3V输出能力有限如果板子上还有其他外设比如OLED屏、蜂鸣器、ESP8266模块电流超了之后电压跌落下载必然失败。这种情况下换外部供电保持调试器和目标板共地。如果上述都不行用ST-Link Utility做一次整片擦除很多时候芯片内部有错误的Flash配置擦掉后就能恢复正常。4.2 No STM32 Target Found调试器连不上芯片“No STM32 Target Found”这个报错比Flash Download Failed更严重它意味着调试器压根没有发现芯片。常见原因有三个芯片被设置了读保护、程序把SWD引脚重新配置了、芯片进入低功耗模式。先说读保护。如果之前调试时不小心设置了RDP级别芯片会拒绝调试器连接。解决方法是按住目标板复位键不放让调试器在“连接时复位”的模式下抓住芯片。ST-Link Utility里面有“Connect under reset”选项勾选后再尝试连接。注意解除读保护会自动触发整片擦除所以别拿有重要代码的芯片乱试。再说程序把SWD引脚配置成普通GPIO。PA13和PA14默认是SWDIO和SWCLK但如果你在代码里把它们复用成别的功能下载一次后调试口就没了。遇到这种情况同样是按住复位键抢连接然后把程序擦掉。所以我建议测试Flash期间代码里不要碰PA13和PA14的复用配置。4.3 写进去的数据读出来全是0xFF如果Flash写入后读回的数据全是0xFF第一个要怀疑的就是没有擦除。Flash擦除后的状态是0xFF写入操作只能把某些位置0如果你写入前没有成功擦除写入时硬件会发现对应位已经是0了无法再改成0最终编程失败。排查方法很简单在写入前先单独调用一次擦除然后用调试器或ST-Link Utility读一下Flash目标地址确认确实变成0xFFFFFFFF。如果是再进行写入测试。另外选测试数据时别用0xFFFFFFFF原因前面说过写完和没写一样无法判断是否真正写入成功。还有一种情况是地址算错了。比如你想写最后一页0x0800FC00但实际写到了0x0800F800而0x0800F800那个区域没有擦除写进去自然失败或读出乱码。建议先计算好地址打印出来确认再执行写入。4.4 擦除时中断触发导致HardFault这个问题比较隐蔽一旦遇到会让人排查到怀疑人生。前面说过Flash擦除期间Flash控制器占用总线CPU此时不能从Flash取指。如果这时候来了一个中断中断服务函数在Flash里CPU无法从Flash读取指令直接触发HardFault。我遇到过一次特别典型的场景擦除Flash时蜂鸣器“滴”了一声系统立刻死机。排查到最后发现是定时器中断在擦除期间被触发而中断处理函数恰好放在Flash区。解决办法有两个方向。一是擦除前关中断擦完再恢复__disable_irq(); HAL_FLASHEx_Erase(eraseInit, pageError); __enable_irq();这个方法简单粗暴适合擦除时间短、系统能容忍短暂中断延迟的场景。二是把中断处理函数放到RAM里执行。在链接脚本里增加RAM执行段或者用__attribute__((section(.ramfunc)))标记关键函数。这个方法复杂但更优雅适合对实时性要求较高的系统。我的建议是刚开始测试Flash时直接采用关中断的方案先保证功能正确再考虑优化。5. 从测试到实战常见工程化扩展5.1 用Flash做参数存储永远不要只存一份测试通过之后就要考虑正式应用了。如果你的目的是用片内Flash保存参数我强烈建议做双备份方案。双备份的经典做法是参数区准备两个页页A存当前有效参数页B存上一次写入的参数。写入时先写页B校验通过后再写页A读取时先读页A如果校验失败就回退到页B。每份参数都带CRC校验确保数据没有被篡改或丢失。为什么这么设计因为产品在用户手里随时可能断电你无法保证断电的瞬间不是正在写Flash。如果只存一份写入到一半断电Flash里残留的数据就是半新半旧的垃圾重启后系统无法判断是否有效。双备份加CRC校验可以最大程度避免这种风险。我做第一个量产项目时没考虑这个问题结果有客户反馈“设置经常丢失”后来改成双备份才彻底解决。5.2 用ST-Link Utility/J-Flash直接查看Flash内容调试Flash内容时不要只依赖串口打印。ST-Link Utility可以直接读取芯片整个Flash并保存成bin文件还可以对比两个bin文件的差异。我用这个功能排查过好几次“为什么程序运行逻辑不对”的问题——最后发现是Flash里残留了上次的测试数据程序启动时把那块当有效参数读进来导致行为异常。看到真面目后问题一下就清晰了。J-Flash也提供类似功能而且批量操作速度更快。如果你要批量读取多片芯片的Flash内容做比对J-Flash配合脚本会非常方便。我在量产阶段用J-Flash导出每台设备的Flash镜像存档备案一旦后续出了质量问题可以直接翻出来比对当时的存储状态。这个习惯帮我快速定位过两次“出厂参数异常”的客诉。5.3 把Flash操作封装成独立模块我现在写项目已经不再把Flash操作散落在业务代码里了而是单独封装成flash_if.c和flash_if.h模块对外只暴露三个函数Flash_WriteParams、Flash_ReadParams、Flash_EraseParams。读函数直接从Flash映射地址memcpy到RAM结构体写函数内部按照“关中断 - 解锁 - 擦页 - 写入 - 校验 - 锁定 - 恢复中断”的顺序执行。所有Flash地址、页大小、数据结构体定义都放在头文件里换芯片型号时只需要改宏定义和结构体不用动任何业务逻辑。这个封装方式还有一个好处我可以随时给测试代码加几个串口命令比如通过串口发送“W”就执行写操作发送“R”就执行读操作发送“E”就执行擦除。调试的时候效率提升非常明显不用反复改代码、烧录、复位。你也可以在封装模块内部加一个状态机把每次写入的结果记录到一个全局变量里方便调试器实时观察。最后再分享一个我自己的体会STM32片内Flash读写测试这件事看着简单但值得当成一个正式的小项目来做。很多问题的根子不在于“不会调用库函数”而在于对Flash的物理规则不够敬畏——不擦除就写、地址重叠、中断不关这些都是会让你排查到怀疑人生的点。建议你拿到板子后先用整片擦除和全片读取把芯片的“底细”摸清楚再去做精细的读写测试。等你把这片Flash彻底摸透了后面再做参数存储、OTA、日志记录基本就是水到渠成的事情。本文还有配套的精品资源点击获取