资讯动态

STM32L4内部Flash读写工程实现:LL库操作与掉电保存避坑指南

发布时间:2026/10/17 12:15:17 来源:尧图企业网站定制
简介针对STM32L4系列微控制器内部FLASH的读写需求这套基于实际操作验证的代码可帮助嵌入式开发者快速实现参数保存与固件升级等功能。作者在STM32L452RET6芯片上使用LL库顺利完成调试因底层采用寄存器操作换用HAL库时也只需简单修改即可移植。压缩包内共有两个文件分别对应主程序与头文件整个资源包仅有两KB大小代码精简且便于阅读目前该资源已被一千九百三十五人学习下载具有一定的参考热度。值得一提的是作者后续在STM32L471VETx上测试时发现擦除页函数会因Flash页码不连号而失效因此特别提醒使用STM32L4x1系列的用户需要调整擦页函数的页码号这一经验可有效避免同类芯片的调试陷阱。整体而言这套资源不仅给出了内部FLASH的读写与擦除实现思路还完整记录了型号适配中的关键注意点适合正在学习或实际使用STM32L4系列的开发者参考借鉴。1. 把内部FLASH当成外设来理解先问自己三个问题做嵌入式的都知道STM32L4xx系列芯片的内部FLASH既能存代码也能存用户数据可它读写起来不像RAM那样直接砸地址就行——要先解锁、按页擦除、再按双字写入任何一步没对齐都可能直接HardFault。这套读写内部FLASH的工程包就是一套把LL库和寄存器操作结合起来的可移植实现覆盖解锁、擦除、写入、校验、掉电保存这一整条链路。适合做设备参数保存、Bootloader、OTA升级或者日志存储的从业者直接参考。新人拿过来能对着例程改地址和页号老手可以跳过讲解部分直接看LL_FLASH_Program的时序处理和最后的避坑清单。不管你是刚接手L4系列的项目还是已经在用HAL库想换成寄存器操作这份资源都值得先下载解压看一眼。2. 原理先行L4的Flash组织形式与LL库操作接口2.1 片内Flash的地址分布与页组织STM32L4系列的内部Flash不是一块“大RAM”它的地址空间是分段管理的。主存储区从0x08000000开始用于放代码和常量系统存储区在0x1FFF0000附近出厂Bootloader住在那选项字节区在0x1FFF7000附近管读保护、写保护这些策略配置。普通应用程序正常情况下能读写的只有主存储区其他区域访问权限受限制操作不当就触发总线错误。主存储区内部又按“Bank”和“页”两级来组织。一个Bank包含若干页页大小常见的是2KB部分低密度型号是1KB具体以你手上型号的规格书为准。擦除操作的最小单位是一页写入操作的最小单位是双字也就是64位、8字节。这意味着你想改一个字节不能直接写得先把整个页读出来、在RAM里改好、再把整页擦掉、最后写回去——这个“读-改-擦-写”流程是所有Flash参数存储方案的基础。代码里我习惯先定义好用户数据区的边界不让编译器把程序代码和参数区混在一起#define FLASH_BASE_ADDR 0x08000000UL #define USER_DATA_SECTOR_START 4U /* 避开向量表和前几页代码区 */ #define USER_DATA_PAGE_SIZE 0x800UL /* 2KB一页按实际型号调整 */ #define USER_DATA_ADDR (FLASH_BASE_ADDR (USER_DATA_SECTOR_START * USER_DATA_PAGE_SIZE))USER_DATA_SECTOR_START取4的原因很简单L4的启动代码、中断向量表、以及编译出来的.text段通常会占据低地址空间我把参数区放到第4页之后减少覆盖代码的概率。如果你的程序很大或者Bootloader占了前16KB那就得把这个起始页号继续往后推不能照抄。2.2 LL库到底是不是寄存器操作很多人看到“LL库”三个字默认它跟HAL库一样是个大封装。实际不是。LL库全称Low Layer它是寄存器操作的薄封装你在LL_FLASH_Program函数体里看到的直接就是FLASH-CR、FLASH-SR这些寄存器位的读写。相比HAL_FLASH_ProgramLL少了好几层状态机和超时检查换来的是更确定的时序这在Flash擦写这种对时间敏感的场景里非常重要。我常用到的LL库Flash接口主要有这几个函数作用对应寄存器操作LL_FLASH_Unlock() / LL_FLASH_Lock()解锁/锁定Flash控制寄存器向KEYR写KEY1、KEY2LL_FLASH_Erase()按页擦除或全片擦除操作CR的PER、STRT位LL_FLASH_Program()写半字、字或双字操作CR的PG位和写入数据寄存器LL_FLASH_IsActiveFlag_BSY()查询忙标志读SR的BSY位LL_FLASH_IsActiveFlag_WRPERR()查询写保护错误标志读SR的WRPERR位用LL库不等于放弃寄存器操作它的底层就是寄存器。遇到库里没有覆盖的细节比如要读选项字节直接用寄存器读就行。我一般把LL库调用当成“带注释的寄存器操作”来读出了BUG查代码时跳进函数看一眼就知道它动了哪个位。2.3 工程骨架与文件组织这套工程包拿到手之后不要急着编译先花十分钟确认三件事工程里是否包含了stm32l4xx_ll_flash.c源文件链接脚本里是否给用户参数区留了独立段启动文件和系统时钟配置是否正常。LL库驱动文件在HAL库包的Drivers/STM32L4xx_HAL_Driver/Src目录下工程里要把LL库的源文件加进去同时把USE_HAL_LL这个编译宏打开否则LL接口不会暴露出来。链接脚本.ld文件我建议做一件事在Flash区域尾部划出一段独立的MEMORY区域专门给参数存储。不划段的后果是链接器可能把常量池或初始化数据布局到你的参数区导致你辛辛苦苦写进去的数据被启动代码覆盖查半天查不出原因。修改方式大致长这样MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 96K PARAM (rw) : ORIGIN 0x08018000, LENGTH 32K RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K }把Flash拆成FLASH和PARAM两段之后在你的参数管理代码里直接用绝对地址访问PARAM段即可链接器保证这段区间不会被代码占用。这种做法牺牲了一点点Flash容量换来了调试期的绝对安全感个人觉得非常值。3. 代码实战解锁、擦除、写入、校验的标准链路3.1 先解锁再操作KEY顺序不能错内部Flash的控制寄存器CR在复位后是锁定的你不解锁写CR寄存器根本写不进去——这是芯片的防误操作机制。解锁动作是向Flash密钥寄存器KEYR依次写入两个固定密钥LL_FLASH_Unlock(); /* 内部等价于FLASH-KEYR 0x45670123; FLASH-KEYR 0xCDEF89AB; */密钥写入顺序不能反。反了或者中间被其他代码打断Flash会进入Busy状态后续所有操作全部无效。解锁之后操作完成要立刻锁回来LL_FLASH_Lock(); /* 内部等价于FLASH-CR | FLASH_CR_LOCK; */关于这个Lock我要多说一句它不是“退出前象征性加锁”而是实打实的保护。如果项目里开了读保护RDP加锁状态下的任何写操作都会触发硬错误所以调试阶段发现写不进去第一反应应该去看是不是忘记解锁或者RDP级别被改过了。3.2 页擦除的标准写法写入之前必须确保目标区间是0xFFFFFFFF这只能通过擦除实现。L4的擦除以页为单位先选中Bank和页地址再置位STRT位启动uint32_t page_addr USER_DATA_ADDR; LL_FLASH_Erase(LL_FLASH_TYPEERASE_PAGES, LL_FLASH_BANK_1, page_addr); while (LL_FLASH_IsActiveFlag_BSY()) { /* 等待BSY位清零擦除期间不能做任何Flash操作 */ }LL_FLASH_Erase的第三个参数是页的起始地址不是页编号这个容易搞混。比如第4页的起始地址就是0x08000000 4 * 0x800 0x08002000你把0x04传进去结果是擦掉第0页而不是第4页。函数内部会根据地址自动算出页号因此传地址即可。擦除操作的时间比写入长得多2KB一页的擦除时间一般在几十毫秒量级。等待BSY位清零这一步是必须的别跳过去也别用delay代替——读取BSY标志本身就是最快最准的状态判断。3.3 写入双字与参数区完整函数L4支持半字(16位)、字(32位)、双字(64位)三种编程粒度。考虑到Flash写入的可靠性以及参数区经常要存结构体数据我推荐统一用双字写入一次写8字节地址必须8字节对齐。下面这段代码是我在实际项目中一直用的参数区写入与校验逻辑uint8_t flash_write_param(uint32_t addr, uint64_t *data, uint32_t len) { uint32_t i; uint32_t page_start addr ~(USER_DATA_PAGE_SIZE - 1U); if ((addr % 8U) ! 0U) { return 1; /* 地址未对齐到双字 */ } LL_FLASH_Unlock(); while (LL_FLASH_IsActiveFlag_BSY()) { } /* 先把所在整页擦掉 */ LL_FLASH_Erase(LL_FLASH_TYPEERASE_PAGES, LL_FLASH_BANK_1, page_start); while (LL_FLASH_IsActiveFlag_BSY()) { } /* 逐双字写入 */ for (i 0; i len; i) { LL_FLASH_Program(LL_FLASH_TYPEPROGRAM_DOUBLEWORD, addr i * 8U, data[i]); while (LL_FLASH_IsActiveFlag_BSY()) { } } LL_FLASH_Lock(); /* 读回校验 */ for (i 0; i len; i) { if (*(uint64_t *)(addr i * 8U) ! data[i]) { return 2; /* 校验失败 */ } } return 0; }几个参数说明data是uint64_t类型的数组指针len是双字的个数不是字节数。写一个8字节参数len传1写一个24字节结构体len传3。addr必须8字节对齐这个由调用方保证函数入口做了防御式检查。擦除整页会导致同页其他位置的数据全部丢失所以调用这个函数之前调用方要把整页数据备份到RAM改完再整页回写或者像我后面第5章一样做双槽位冗余。读回校验那一段有人觉得多余我可以负责任地说Flash偶发写入失败在量产现场是真实存在的不校验会让设备在客户手里出现“参数存了但没存进去”的玄学故障。读回比对成本极低务必保留。4. 避坑清单擦写Flash的五个经典翻车现场4.1 现象写入时触发PGAERR硬错误程序跳进HardFault原因写入地址没有按当前编程粒度对齐。L4的双字写入要求地址是8的倍数字节对齐只对半字编程有效。很多人从F1系列转过来F1只要求半字对齐没改代码直接放到L4上跑当场翻车。 解决写入前统一做地址对齐检查(addr % 8U) ! 0U直接返回错误码不进入写入流程。同时确保结构体在定义时就按__attribute__((aligned(8)))对齐避免成员偏移量破坏地址对齐。4.2 现象写入成功但读回来全是FF数据根本没进去原因目标页在写入前不是全FF状态。Flash写入只能把1写成0没办法把0写成1。如果上电后该页残留旧数据你又没执行页擦除就直接写那写入的结果是旧数据和新数据的“按位与”读回来自然不对。 解决写入前先检查起始地址处8字节是否为0xFFFFFFFFFFFFFFFF不是就执行页擦除。我通常把擦除动作封进写入函数的内部而不是留给调用方决定从机制上杜绝忘记擦除的可能。4.3 现象擦除/写入过程中来一个中断系统卡死在BSY位等待原因中断服务函数里访问了Flash。擦写期间Flash控制器处于忙状态此时任何对Flash存储区的读访问都会让总线挂起直到BSY清零。如果中断里恰好有从Flash取指或读取常量表的动作CPU就停在那里主循环的BSY轮询永远跑不到整个系统看起来像死机。 解决擦写期间关闭所有可能访问Flash的中断或者至少在擦写函数入口处用临界区保护。我一般用__disable_irq()包裹擦写临界区完事再__enable_irq()如果系统开了FreeRTOS还需要用taskENTER_CRITICAL()配合。最保险的做法是把擦写函数放到SRAM里执行彻底避开Flash取指问题这个后面讲。4.4 现象写数据写到一半发现高地址区域的数据不见了甚至系统起不来原因双Bank架构的L4型号擦写时Bank选择错了。比如你想擦主Bank第8页实际上擦的是Bank2对应偏移的页。更隐蔽的是代码里直接用线性地址计算页号没有考虑到双Bank的映射规则小容量单Bank型号上能跑换到双Bank的L4R系列就翻车。 解决查看目标型号的参考手册确认Flash是单Bank还是双BankLL库的擦写函数显式传入LL_FLASH_BANK_1或LL_FLASH_BANK_2。如果你希望代码在单双Bank型号间通用就把Bank和页地址都做成宏定义换型号只改宏不改函数逻辑。4.5 现象掉电或复位瞬间正在擦写再上电参数区数据全乱原因擦写过程中电源跌落Flash内部状态机没有完成整个操作该页处于部分擦除的状态既不是旧数据也不是新数据。这类问题在实验室用调试器供电很难复现一旦到现场用开关电源就频繁出现。 解决硬件上保证电源纹波和掉电保持时间软件上做双槽位交替写入方案。简单说就是准备A、B两个参数区每次写入先写备份槽再更新主槽启动时校验主槽的CRC坏了就从备份槽恢复。这套设计能把掉电损坏的概率降低几个量级代价只是多占一页Flash非常划算。5. 进阶验证用CRC回读和双槽位切换把最后一道关守住参数存储的可靠性最终比拼的不是哪一次写入有多快而是写入过程中各种意外发生后能不能自愈。我在做某设备的校准参数存储时吃过一次亏——设备在现场跑了三个月某台机器校准数据掉了客户反馈“参数没存住”。从那以后我每次做Flash参数区都强制走一遍“写双槽CRC校验”的流程。双槽位的核心思路是把Flash里一页拆成两个槽位每个槽位前半部分存数据后半部分存CRC32校验值。写入时按固定顺序先写槽A再写槽B读取时先校验槽ACRC不对就尝试槽B两个都不对才报参数丢失。掉电无论落在哪个槽的写入窗口另一个槽都还是完好的系统启动时始终能找到一份可用数据。#define SLOT_A_ADDR (USER_DATA_ADDR) #define SLOT_B_ADDR (USER_DATA_ADDR USER_DATA_PAGE_SIZE) #define PARAM_DATA_LEN 8U /* 实际单条参数的长度按需调整 */ uint32_t calc_crc32(uint8_t *buf, uint32_t len); uint8_t param_save(uint8_t *data) { uint32_t crc calc_crc32(data, PARAM_DATA_LEN); /* 先写备份槽B再写主槽A */ flash_write_param(SLOT_B_ADDR, (uint64_t *)data, PARAM_DATA_LEN); flash_write_param(SLOT_B_ADDR PARAM_DATA_LEN * 8U, crc, 1); flash_write_param(SLOT_A_ADDR, (uint64_t *)data, PARAM_DATA_LEN); flash_write_param(SLOT_A_ADDR PARAM_DATA_LEN * 8U, crc, 1); return 0; }这个方案里唯一的开销是每次保存要写双份数据Flash擦写寿命翻倍但对于参数存储这种不频繁的操作L4的Flash擦写寿命通常10万次起步完全够用。CRC32计算可以用查表法也可以逐位法重点是校验时要把数据和CRC分开读别一次读一整块否则数据损坏时连CRC一起错没法区分是数据坏了还是校验值坏了。验证时还有一个容易被忽略的动作写入完成后的读回比对一定要在关中断之后再校验吗不一定读回校验阶段Flash已经退出忙状态中断访问Flash不会产生总线挂起。所以校验代码放在临界区外面执行即可这样中断延迟不会因为Flash写入而被拖长实时性任务也不会受到明显影响。我把这个经验写进本次分享也是给那些准备把这段代码直接搬进RTOS工程的人提个醒——临界区要尽量短擦写事大但系统调度的事也耽误不起。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑