资讯动态

STM32L4内部FLASH读写实战:从擦除对齐到掉电安全

发布时间:2026/9/9 22:16:06 来源:尧图企业网站定制
简介面向STM32L4系列嵌入式开发者这份代码包提供基于LL库的寄存器级内部FLASH读写实现已在STM32L452RET6芯片上调通。作者将FLASH解锁、擦除、写入、读取等操作封装在独立的C源文件与头文件中底层均为寄存器配置因此使用HAL库时只需调整少量定义即可复用适合需要精细控制存储流程的进阶开发者。值得关注的是实践中的兼容性问题改用STM32L471VETx后遇到擦页失败最终定位为L4系列部分型号的FLASH页码不连续仅需修改擦页函数的页码号即可解决但该代码不直接适用于STM32L4x1系列需自行调整。压缩包共2个文件分别为FLASH.c与FLASH.h整体仅2KB轻量无冗余查看代码即可理解完整流程。目前已有1930人学习或下载说明这一小巧实现能帮助读者避开内部FLASH操作中的典型坑点。 做嵌入式的谁没在“保存参数却一掉电就丢”这件事上栽过跟头。网上一搜“STM32L4xx 读写内部FLASH”下载下来往往是这样一个压缩包一个HAL库工程、几个.c和.h文件、一段很简短的示例。可真正自己跑起来才发现“能跑”和“能用”之间隔着好几道坎擦除粒度多大、为什么写入要按64位、双Bank工程怎么分区、操作到一半掉电会不会把固件搞坏。这篇文章就来把这些事讲透结合我实际调这块的经验把代码、参数、坑位一次说清楚。1. 项目拆解这个压缩包到底在解决什么问题1.1 内部FLASH读写本质上是在解决“掉电不丢参数”的需求STM32L4xx系列内部都带了一块NOR FLASH程序就存放在这里。程序写进去了当然不用天天改它但在实际项目里我们往往需要把一批“掉电不能丢”的数据也塞进去设备校准系数、累计运行时间、用户配置、蓝牙配对信息、最后关机前的状态等等。这类需求听起来很简单——“把数据存到FLASH里嘛”但它和单片机里RAM变量最大的区别就是CPU不能像写内存那样直接往FLASH地址写数据。写入前必须先擦除擦除的最小单位不是字节而是页Sector/Page而且L4系列还有ECC校验、等待周期、双Bank切换等一系列约束。这就是为什么会有诸如“hdfs读写流程”“W25Q64读写 掉电不丢失”“匿名地面站 STM32参数读写”这类搜索词本质上大家都在找一套可靠的“非易失存储方案”。内部FLASH的好处是零外围成本、读取速度快坏处是擦写次数有限、操作不当会直接带崩程序而外置W25Q64则是反过来。所以先把内部FLASH玩明白是每个做低功耗/物联网设备的人绕不开的基本功。1.2 和常见外设方案放在一起看才能理解方案的选型边界热词里不少人在找“SPI读写W25Q64”“I2C读写EEPROM”“RC522读写卡程序”这类代码看得出大家最终目的都一样找一种稳定的介质存数据。内部FLASH和外置EEPROM/SPI FLASH最常见的对比维度有三点对比项STM32L4内部FLASH外置EEPROM如AT24C02外置SPI FLASH如W25Q64擦写寿命约1万次官方标称约100万次约10万次写入粒度64位双字字节页一般256字节擦除粒度页L4多为2KB视型号而定无扇区一般4KB是否需要外围电路不需要需要I2C引脚需要SPI引脚掉电数据保持好但写一半掉电有风险好好能否执行代码可以XIP不能部分型号支持映射执行成本已包含在MCU内低低如果只是存几个字节的配置参数、擦写频率很低内部FLASH完全够用还能省掉一颗外部存储器、一个引脚和一段驱动代码。但如果要存大量日志、字库、音频资源就该用W25Q64这种大容量外部存储。如果参数需要频繁更新、每次只改一个字节EEPROM更合适。搞清楚这个边界你才会明白自己真正需要的是哪份参考代码。2. 原理先行L4内部FLASH的关键特性与踩坑点2.1 双Bank结构与擦除粒度别把扇区管理搞错STM32L4系列这里以常见的STM32L476/496为例内部FLASH容量大多是1MB划分成两个BankBank1从0x08000000开始Bank2从0x08080000开始。每个Bank内部又分成若干个2KB的扇区部分型号页大小不同拿到具体型号后第一件事就是查参考手册里的Flash organization表别凭感觉。这个结构带来两个直接影响。第一擦除操作是“按扇区”进行的你哪怕只想改一个字节也得先把整个2KB扇区擦掉。所以设计存储布局时一般把“频繁修改的一组数据”集中放在同一个扇区里避免跨扇区操作把不常改的校准值、版本号分开存放。第二L4支持双Bank交叉访问内置了Read-While-Write能力可以做“在线升级时继续执行代码”这类应用。但如果你没用到OTA只用内部FLASH存参数那大多数时候是“程序在Bank1跑参数擦写在Bank2”互不干扰。我实际用过的芯片里L4的擦除是最慢的一类擦一个2KB扇区耗时在几十毫秒级别具体时间取决于时钟配置和芯片主频。这一特性直接影响你在哪一层调用擦除函数——绝不能放在定时器中断里更不能放在对实时性要求苛刻的路径上。2.2 每64位一组ECC校验为什么写入不能只写一个字节STM32L4的内部FLASH带有ECCError Correction Code校验机制数据在写入时按“双字64位”为单位计算校验码。这造成一个非常关键的限制写入时地址必须按8字节对齐数据长度必须是8字节的整数倍。如果你直接往一个非8字节对齐的地址写数据或者用HAL_FLASH_Program往FLASH里写一个uint32_t轻则写失败重则触发ECC错误甚至进HardFault。那项目里明明只需要存一个“温度阈值”就是一个uint16_t怎么办正确做法是“读-改-写”先把目标双字地址里原来的64位数据读出来把要修改的字节对应位置换掉再把完整64位一次性写回。做这个操作有两个前提一是这段地址已经擦除过全0xFF二是你读出来的旧数据和要写的新数据组合后是完整64位。顺带一提L4的ECC还带来一个行为差异读取FLASH内容时如果某个双字区域从来没被写入过也就是擦除后的全0xFF状态读出来是正常的0xFF但如果是“擦除后只写了一部分字节”这块区域的ECC信息可能不完整此时读取可能触发NMI或HardFault。这是很多人在自行封装“写任意字节”函数时踩得最深的一个坑——自己拼数据时没按8字节整块处理。2.3 时钟等待周期和解锁流程缺一不可内部FLASH虽然挂在CPU的地址空间上但它的读取速度是有限制的。当CPU主频提高时CPU需要插入等待周期Wait States才能正确读取FLASH内容。STM32L4手册里给出了一个表格比如在3.3V供电下80MHz主频通常需要4个等待周期这个参数在CubeMX生成工程时会自动配好。你要是手工改过时钟树把频率调上去了却忘了同步修改FLASH延迟寄存器程序就会莫名其妙地跑飞、死机而且毫无规律。另外内部FLASH在复位后默认是“上锁”状态直接调用擦写指令会被硬件拒绝。标准操作流程是先调用HAL_FLASH_Unlock()往KEYR寄存器写入解锁密钥然后执行擦除、编程操作结束后再用HAL_FLASH_Lock()重新上锁。从安全角度讲写完立刻上锁是一个好习惯能避免程序跑飞后误擦FLASH。早期我做产品时偷懒没上锁结果一次电源跌落导致芯片异常复位维护代码里碰巧有擦写FLASH的指令直接把整片程序区给干掉了。从那以后我写内部FLASH的驱动第一件事就是规定“用完后必须Lock”。3. 实操过程从HAL初始化到读写回读的完整流程3.1 工程搭建以及FLASH驱动初始化前必须确认的三件事不管是基于STM32CubeMX生成工程还是裸写寄存器我建议你先确认三件事芯片具体型号、内部FLASH总容量、每个扇区的起始地址和大小。以STM32L476RG为例FLASH基址0x08000000总容量1MBBank1含256个2KB扇区实际上具体扇区数量看型号L476RG是256页256页Bank2从0x08080000开始。在代码里优先用头文件里提供的宏定义而不是直接写死地址比如用FLASH_BASE作为基址再自己定义一个“参数存储区”的宏#define APP_FLASH_BASE 0x08000000u #define PARAM_SECTOR_ADDR 0x0807F000u /* 选在Bank1末尾空闲区域注意别覆盖代码区 */这里要特别强调“别覆盖代码区”。链接脚本决定了你的程序占用多长FLASH取反就是最大可用地址。STM32L4内置的Bootloader和选项字节都占据特定地址自己写参数时更要避开这些区域。稳妥的做法是在编译后查看.map文件找到最后只读数据的结束地址把参数区定义在它之后、且对齐到扇区边界。初始化其实没有额外的“FLASH初始化函数”HAL库在SystemInit时已经把时钟等待周期配置好了。你真正要做的只是调用HAL_FLASH_Unlock()和HAL_FLASH_Lock()。但有一点容易被忽略如果开启了指令缓存ICache和数据缓存DCache对FLASH写入后缓存里可能还残留旧值。调试时用读回验证往往看不到问题等到认真校验时就发现读回来的居然是旧数据。我的习惯是在擦写前后调用SCB_InvalidateDCache()做一次缓存失效。3.2 擦除和写入HAL库API如何使用才不出错L4的HAL库提供了两个核心函数HAL_FLASHEx_Erase用于擦除页HAL_FLASH_Program用于写入数据。擦除时先构造一个FLASH_EraseInitTypeDef结构体FLASH_EraseInitTypeDef eraseInit {0}; uint32_t pageError 0; HAL_StatusTypeDef status; eraseInit.TypeErase FLASH_TYPEERASE_PAGES; eraseInit.PageAddress PARAM_SECTOR_ADDR; eraseInit.NbPages 1; HAL_FLASH_Unlock(); status HAL_FLASHEx_Erase(eraseInit, pageError); if (status ! HAL_OK) { /* 处理擦除失败pageError会给出出错的页地址 */ }这里参数的含义要说明白。TypeErase可以是页擦除或批量擦除FLASH_TYPEERASE_MASSERASE批量擦除会把整个主存储区都清掉一般只在Bootloader里用千万别在应用里乱调。PageAddress是你要擦除的那个扇区的起始地址必须落在扇区边界上如果你传了一个扇区中间的地址函数行为就是不明确的。NbPages要擦除的扇区数如果在Bank1和Bank2各选了一个扇区就要分两次擦除。页擦除完成后才能写入。L4的HAL_FLASH_Program在L4上要传的数据类型是uint64_tuint64_t writeData 0x1122334455667788ULL; status HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, PARAM_SECTOR_ADDR, writeData);注意看第三个参数不是一个指针而是一个值。这意味着HAL库默认你是“准备好了一个双字再整块写入”的。如果你的数据结构是多个字段比如一个结构体里面有float、uint8_t、uint16_t就自己先把它memcpy到一个临时uint64_t缓冲区再调用HAL_FLASH_Program。用memcpy而不是手工拼字节可以避免因为结构体对齐问题导致的字节错位这也是从实战中总结出来的经验。写完之后记得调用HAL_FLASH_Lock()。如果还需要做代码读回校验直接用volatile指针读取对应地址即可uint64_t readBack *(volatile uint64_t *)PARAM_SECTOR_ADDR;读内部FLASH不需要任何解锁操作但使用volatile是必须的否则编译器可能在优化时直接复用旧值让你看到“写入没生效”的假象。3.3 一套简单的“写入任意长度数据”封装思路实际项目中你不太可能每次都手动拼64位。我会在驱动层封装一个“写参数块”的函数基本流程是这样先检查参数长度是否超过一个扇区容量超过就返回错误然后把整个扇区读入RAM缓冲区一块静态数组在RAM里修改对应字段最后整片擦除、整片写回。说句实话“先搬回RAM再写回”这个方案在内存足够时最省心因为它天然满足8字节对齐要求也不会出现“写一半掉电导致扇区里有新有旧”的尴尬局面。代价是擦写窗口变长但只要调用频率低就可以接受。更高级的做法是“双扇区轮流存储”参数区用两个扇区交替写入每次先读旧扇区、改写、写新扇区、最后标记旧扇区失效。即使中途掉电重启后也能根据标记自动从有效扇区恢复。如果做的是需要OTA升级的产品还可以把参数区单独放在一个Bank里利用L4的双Bank特性实现“升级代码时不丢参数”这个扩展方向很值得研究。4. 常见问题与排查技巧实录4.1 写进去之后读出来全是0xFF或偶发HardFault读出来0xFF基本就是两种情况一是根本没写成功二是写了但读到的是擦除后的旧数据。先检查写入前是否做了擦除操作——FLASH只能把1写成0擦除后全为1如果你在旧数据区域上直接写想写的位本来就是1的会保持1想从0改成1的则写不进去最终读出来就是下半字节是0xFF。第二检查是否因为ICache/DCache导致读回缓存旧数据调用缓存失效函数再看。第三如果你的数据长度不是8的倍数比如只写了4个字节那另一半0x00000000会被当成有效数据参与ECC计算某些情况下读的时候会触发ECC错误。HardFault则大概率是写入地址越界或地址非对齐。排查时可以打开Keil/IAR的Fault报告看触发异常的PC和访问地址更直接的办法是在HAL_FLASH_Program之前加断言确保地址满足(addr FLASH_BASE) (addr FLASH_BASE FLASH_SIZE) (addr % 8 0)。这类错误在写内联汇编或寄存器版本时特别常见HAL库帮你挡掉了一部分。4.2 程序在FLASH操作时卡死或跑飞这个问题的高发原因有三个中断、时钟、代码执行位置。先说中断HAL的擦除和写入函数内部会等待BSY位清零在这个等待循环期间如果来了一个高优先级中断而且中断服务程序里又去操作了FLASH比如在中断里执行参数存取就会形成死锁。所以我在项目里明确规定FLASH擦写必须放在主循环或低优先级任务中执行操作期间用临界区保护禁止嵌套调用。如果是RTOS环境还要考虑给这段操作加互斥信号量。再说时钟前面提到过主频调高后必须同步配置FLASH等待周期。这个错误很隐蔽因为大多数时间程序运行正常只有在执行长跳转、大量连续取指时才突然死机。用CubeMX重新生成时钟配置和你的源码对比一下FLASH_ACR寄存器值基本就能找到问题。最后说代码执行位置如果你的程序是从内部FLASH运行的那么擦写操作期间同一个Bank的FLASH暂时无法被CPU访问不同Bank可以RWW如果你擦的扇区恰好是当前正在执行的代码区CPU取不到下一条指令必然跑飞。这种事情常发生在“在应用代码里全片擦除”这种错误设计里。因此牵涉到代码区升级的操作一定要在RAM中运行或者跳到另一个Bank执行。4.3 擦写寿命与掉电安全正式产品必须考虑的可靠性设计L4内部FLASH擦写寿命标称通常是一万次听起来不少但如果某个整机每小时存一次参数一天24次一年不到就逼近8000次了。再加上调试阶段反复擦写寿命压力是真实存在的。我的建议是高频率参数放在RAM里只有在关机、掉电检测、或定时落盘时写入FLASH每次写入前先判断数据是否发生变化没变化就直接跳过。别小看这个判断函数它能把擦写次数降一个数量级。掉电安全方面L4的FLASH有两个天然优势一个是ECC能在一定程度上发现数据错误另一个是双Bank设计。但ECC只是检错纠错不能防止“写到一半掉电”造成的数据错乱。为了应对掉电我一般在擦写操作前先关掉全局中断、等待外部掉电检测引脚拉高后延时几毫秒再开始擦写如果实在来不及完成就在另一个扇区保留上一次的有效副本。启动时读取两个副本的CRC选合法的那份用。这个方法在老式EEPROM产品里叫“双备份滚动存储”放到内部FLASH上一样很实用。4.4 内部FLASH和外置W25Q64、EEPROM怎么选最后再聊聊选型这件事。有一个很常见的误区单片机的FLASH价格“包含在MCU里”所以优先用内部FLASH结果把寿命和容量都算进去发现产品改一版数据存储逻辑要花很长时间不如一开始用一颗W25Q64来得痛快。反过来有些人无脑上外部存储结果因为SPI驱动有bug、掉电时序设计不当反而引入更多故障。我的判断标准很简单看写入频率、数据量、以及对实时性的要求。2KB以内的结构化参数、每天写入不超过几次的内部FLASH完全够优先推荐省BOM省代码需要存几十条历史记录、字库、升级包的上W25Q64需要每次改一个字节且写入次数极多的选EEPROM。没有哪个方案是绝对最优的只有“在这个产品约束下更合适”的方案。代码调试方面拿到“STM32L4xx_读写内部FLASH”这类参考工程后不要直接往产品里搬。先把它的读、写、擦三个函数逐个单步过一遍确认扇区地址、数据对齐规则和你实际需求一致再改造成自己的驱动。很多时候参考工程本身就是“能编译过但没考虑掉电”的demo代码真正的可靠性要靠自己补齐。本文还有配套的精品资源点击获取

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

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

免费获取报价