资讯详情

STM32L4xx内部FLASH读写实战:LL库实现参数掉电保存与避坑指南

📅 2026/10/26 11:10:46 | 华诺云谱 👁 阅读
STM32L4xx内部FLASH读写实战:LL库实现参数掉电保存与避坑指南
简介面向STM32L4xx系列MCU开发者的内部FLASH读写参考代码聚焦嵌入式底层存储操作这一常见需求。资源以LL库实现核心为寄存器级配置因此改用HAL库时只需调整相关宏定义即可复用适合已具备一定STM32基础、希望理解FLASH擦写底层机制的开发者参考。压缩包共2个文件包含1个c源文件与1个h头文件分别承载FLASH读写与擦除逻辑及对外接口声明整体仅2KB轻量易读。作者基于STM32L452RET6实测调通并补充说明在STM32L471VETx上因FLASH页码不连号导致擦页失败的问题指出需修改擦页函数中的页码号同时提示不适用于STM32L4x1系列这类排错经验对实际移植颇具参考价值。目前已有1935人学习下载可帮助读者快速掌握页擦除、编程写入等关键流程少走弯路。1. 从一次掉参数的现场说起STM32L4xx 内部 FLASH 读写到底能解决什么去年调一块低功耗采集板设备在实验室跑了一周都正常装到现场断电重启后标定系数全变回默认值。查了半天才发现参数只存在 RAM 里掉电就没了而板子上又没贴外部 EEPROM。这种场景下STM32L4xx 片内 FLASH 就是最省事的非易失存储不用改板、不用加料直接拿程序区之外的一页当参数区用。这份资源围绕的就是这件事——用 LL 库和寄存器操作把 L4 系列内部 FLASH 的擦除、编程、读取跑通适合正在做参数保存、固件配置、简易数据记录又不想外挂存储芯片的嵌入式开发者。它不教你 HAL 怎么点鼠标生成代码而是把双字编程、页擦除、解锁上锁这些底层动作摊开讲新手能照着跑熟手能直接抠进自己的工程。2. 技术选型与工程结构为什么是 LL 库加寄存器而不是 HAL 一把梭2.1 LL 库在 FLASH 操作上的取舍STM32L4xx 的 FLASH 控制器有几个硬性约束编程必须以双字64 位为单位擦除必须以页2KB为单位而且擦写期间取指会被阻塞。HAL 的HAL_FLASH_Program封装得比较厚出错时返回的错误码要再翻一层才能定位到是 PGAERR 还是 WRPERRLL 库则把LL_FLASH_Unlock、LL_FLASH_Program_DoubleWord、LL_FLASH_ErasePage这些动作直接暴露出来配合FLASH-SR状态寄存器读标志位排查问题时心里有数。这份资源走的就是后者能用 LL 就不碰 HAL个别 LL 没覆盖的位比如某些选项字节相关操作直接写寄存器。选 LL 还有一个现实原因——代码体积。L4 系列不少型号 FLASH 只有 128KB 或 256KBHAL_FLASH 模块连带依赖拉进来占用不算小LL 的 FLASH 部分基本是薄薄一层内联函数对资源紧张的工程更友好。代价是要自己管解锁、自己清标志、自己判忙少一步就翻车这也是后面避坑章节要重点说的。2.2 目录结构与文件职责这类工程通常不会太大但分层要清楚否则参数区和程序区混在一起改一次地址全乱。按常见组织方式目录大致是这样stm32l4_flash_demo/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── stm32l4xx_it.h │ │ └── stm32l4xx_ll_conf.h // 使能 LL FLASH 头文件 │ └── Src/ │ ├── main.c // 初始化与调用示例 │ ├── stm32l4xx_it.c │ └── system_stm32l4xx.c ├── Drivers/ │ ├── CMSIS/ // 内核与设备头文件 │ └── STM32L4xx_LL_Driver/ │ ├── Inc/stm32l4xx_ll_flash.h │ └── Src/stm32l4xx_ll_flash.c // 若使用源码版 LL ├── BSP/ │ ├── Inc/ │ │ └── bsp_internal_flash.h // 参数区读写接口声明 │ └── Src/ │ └── bsp_internal_flash.c // 擦除/编程/校验实现 └── App/ ├── Inc/app_param.h └── Src/app_param.c // 参数结构体与默认值BSP层是这份资源的核心把 FLASH 的物理细节页号、地址、双字对齐挡在接口后面App层只关心「存一个参数结构体、读回来校验」。这样换芯片型号时改的是 BSP 里的页地址宏上层不动。stm32l4xx_ll_conf.h里要确认LL_FLASH_MODULE_ENABLED是打开的否则编译时LL_FLASH_*全是未定义这个坑新手经常踩。2.3 参数区选址别和程序区抢地盘L4 的 FLASH 按页划分每页 2KB起始地址 0x08000000。参数区一般放在最后一页或倒数第二页具体取决于芯片容量。以 256KB 型号为例最后一页起始地址是 0x0803F800。选址时有两个硬要求一是这一页不能被链接器分配给代码二是擦除粒度决定了参数区最小就是 2KB哪怕你只存 16 字节也得占一整页。链接脚本里要把这一页排除掉常见做法是在.ld文件里把 FLASH 长度减去 2KB/* 以 256KB FLASH 为例留出最后一页给参数区 */ MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 254K RAM (xrw) : ORIGIN 0x20000000, LENGTH 96K }LENGTH 254K就是 256K 减 2K链接器不会把代码放到最后一页。如果忘了改程序一编译就可能占用参数页运行时擦除直接把代码擦了现象是设备跑着跑着 HardFault这种问题用调试器看 PC 落在 0x0803F8xx 附近就能确认。参数区地址建议用宏统一定义别在代码里散落魔法数字/* bsp_internal_flash.h */ #define PARAM_FLASH_PAGE_ADDR 0x0803F800UL /* 最后一页起始地址 */ #define PARAM_FLASH_PAGE_NUM 127U /* 页号按型号核对 */ #define PARAM_FLASH_SIZE 0x800UL /* 2KB */页号在不同容量型号上不一样128KB 型号最后一页是第 63 页256KB 是第 127 页512KB 是第 255 页。写代码前先翻对应型号参考手册的 FLASH 章节别照抄别的工程。3. 核心实现拆解解锁、擦除、双字编程、回读校验四步走3.1 解锁与上锁顺序错了直接锁死L4 的 FLASH 控制器上电默认锁定任何擦写操作前必须先解锁往FLASH-KEYR依次写两个固定密钥。顺序不能反中间不能被打断写完要检查FLASH-CR的LOCK位是否清零。LL 库把这两步包成了LL_FLASH_Unlock()但底层就是下面这个动作/* 解锁 FLASH 控制器擦写前必须调用 */ static void flash_unlock(void) { if (LL_FLASH_IsLocked()) { LL_FLASH_Unlock(); /* 内部依次写 KEY1、KEY2 到 KEYR */ } /* 解锁失败通常意味着之前有未清除的错误标志先清再试 */ if (LL_FLASH_IsLocked()) { __disable_irq(); FLASH-SR FLASH_SR_CLEAR; /* 清所有错误标志 */ LL_FLASH_Unlock(); __enable_irq(); } }LL_FLASH_IsLocked()读的是FLASH-CR的 LOCK 位。如果解锁后仍然是锁定状态多半是FLASH-SR里有残留错误标志比如上次操作失败留下的 PGSERR控制器拒绝再次解锁。这时先写FLASH-SR清标志再重试注意清标志是写 1 清零不是写 0。上锁用LL_FLASH_Lock()擦写完成后立刻调用别让 FLASH 长时间处于可写状态误操作窗口越小越好。3.2 页擦除先擦后写是铁律FLASH 的物理特性决定了只能把 1 写成 0不能把 0 写回 1所以改任何数据前必须先擦除整页。擦除流程是解锁 → 清标志 → 置 PER 位并写页号 → 置 STRT 启动 → 等 BSY 清零 → 查错误标志 → 上锁。用 LL 库写出来是这样/* 擦除指定页返回 0 成功负值失败 */ int flash_erase_page(uint32_t page) { flash_unlock(); /* 等待上一次操作结束避免在忙时改配置 */ while (LL_FLASH_IsActiveFlag_BSY()) { } /* 清掉可能残留的错误标志 */ LL_FLASH_ClearFlag(LL_FLASH_FLAG_ALL); /* 配置页擦除PER 置位页号写入 PNB */ LL_FLASH_EnablePageErase(); LL_FLASH_SetPageToErase(page); /* 启动擦除并等待完成 */ LL_FLASH_StartErase(); while (LL_FLASH_IsActiveFlag_BSY()) { } /* 检查错误PGSERR 配置错、WRPERR 写保护、PROGERR 编程错 */ int ret 0; if (LL_FLASH_IsActiveFlag_OPERATION_ERR()) { ret -1; } LL_FLASH_ClearFlag(LL_FLASH_FLAG_ALL); LL_FLASH_Lock(); return ret; }LL_FLASH_SetPageToErase(page)写的是FLASH-CR的 PNB 字段页号必须和地址对应写错页号会擦掉别的页这是最危险的一类错误。LL_FLASH_StartErase()置 STRT 位后硬件开始干活2KB 一页擦除时间在毫秒级轮询 BSY 就够了不用上中断。擦完必须查OPERATION_ERR因为擦除失败不会让程序跑飞只会让后续写入数据错乱不查标志等于把问题埋到几个月后。3.3 双字编程64 位对齐是硬约束L4 的 FLASH 编程最小单位是双字也就是 8 字节地址必须 8 字节对齐。想存一个 4 字节的uint32_t也得凑成 8 字节写进去。LL 库提供LL_FLASH_Program_DoubleWord(address, data0, data1)两个 32 位参数拼成一个 64 位双字。写一个参数结构体时常见做法是按 8 字节对齐逐块写/* 从指定地址开始写入 buf长度按 8 字节向上取整 */ int flash_write(uint32_t addr, const uint8_t *buf, uint32_t len) { if ((addr 0x7U) ! 0U) { return -1; /* 地址未 8 字节对齐直接拒绝 */ } flash_unlock(); while (LL_FLASH_IsActiveFlag_BSY()) { } LL_FLASH_ClearFlag(LL_FLASH_FLAG_ALL); uint32_t written 0; while (written len) { uint64_t dw 0xFFFFFFFFFFFFFFFFULL; /* 未用字节保持擦除态 */ uint32_t chunk (len - written 8U) ? 8U : (len - written); memcpy(dw, buf written, chunk); LL_FLASH_Program_DoubleWord(addr written, (uint32_t)(dw 0xFFFFFFFFULL), (uint32_t)(dw 32)); while (LL_FLASH_IsActiveFlag_BSY()) { } if (LL_FLASH_IsActiveFlag_OPERATION_ERR()) { LL_FLASH_ClearFlag(LL_FLASH_FLAG_ALL); LL_FLASH_Lock(); return -2; /* 编程失败多半是目标区未擦除 */ } written chunk; } LL_FLASH_ClearFlag(LL_FLASH_FLAG_ALL); LL_FLASH_Lock(); return 0; }几个参数要盯住addr必须 8 字节对齐不对齐直接返回错误别指望硬件帮你兜dw初始化为全 1是因为 FLASH 擦除后就是全 1未写满的双字剩余字节保持 1 才符合物理状态chunk处理尾部不足 8 字节的情况用memcpy把有效字节填进低地址高地址留 1。编程失败最常见的原因是目标地址没擦除写进去的位和原有 0 冲突硬件报 PROGERR所以「先擦后写」不是建议是必须。3.4 回读与校验写完不算完读回来对得上才算写入完成后直接从 FLASH 地址读数据即可FLASH 在读取上和普通内存没区别不需要解锁。但工程上不能只写不验参数区出错的代价往往是现场设备行为异常排查成本远高于多读一遍。常见做法是写入后逐字节比对再在参数结构体里加一个 CRC 或魔数/* 写入并校验返回 0 表示写入且比对一致 */ int flash_write_verify(uint32_t addr, const uint8_t *buf, uint32_t len) { if (flash_write(addr, buf, len) ! 0) { return -1; } /* 直接按内存读回比对 */ if (memcmp((const void *)addr, buf, len) ! 0) { return -2; } return 0; }memcmp的第二个参数直接强转 FLASH 地址在 Cortex-M4 上读 FLASH 和读 RAM 一样走总线没有额外步骤。参数结构体建议首字段放魔数比如 0x5A5AA5A5读取时先判魔数再判 CRC魔数不对说明这页从没写过或是被擦过直接加载默认值别拿随机数据当参数用。CRC 用软件实现即可参数区数据量小不必上硬件 CRC 单元。4. 跑通与验证从空工程到参数掉电不丢的完整步骤4.1 编译环境与最小验证工程这份资源是 Keil 工程的话打开后先确认三件事器件型号选对比如 STM32L431RCTx、stm32l4xx_ll_conf.h里LL_FLASH_MODULE_ENABLED已定义、链接脚本或 Keil 的 IROM 配置已经把参数页排除。用 Keil 的话在 Target 选项的 IROM1 里把 Size 改成 0x3F800256KB 减 2KB和前面 ld 脚本是一个意思。最小验证不用接任何外设主循环里做三件事上电读参数区魔数不对就写入一组默认值然后每隔几秒把计数值加一写回去。用调试器看参数区地址的内容变化或者把读回的值通过串口打印出来。串口配置用 LL 库的 USART 初始化波特率 115200这些和 FLASH 无关按平时习惯配就行。/* main.c 中的验证逻辑片段 */ int main(void) { /* 系统时钟、串口等初始化省略 */ app_param_t param; if (app_param_load(param) ! 0) { /* 首次上电或参数区损坏写入默认值 */ app_param_default(param); app_param_save(param); printf(param init done\r\n); } else { printf(param loaded, boot_cnt%lu\r\n, param.boot_cnt); } param.boot_cnt; app_param_save(param); while (1) { /* 业务逻辑 */ } }app_param_load内部先判魔数再算 CRC任一不过返回非 0app_param_save内部先擦页再写结构体再回读校验。跑一次断电重启串口打印的boot_cnt每次加一说明参数真的落到了 FLASH 里。这个验证看着简单但能把解锁、擦除、编程、校验四条链路全串起来任何一环有问题都会在这里暴露。4.2 用调试器确认页地址和内容跑通之后别急着收工用调试器的 Memory 窗口看参数页地址比如 0x0803F800确认三件事擦除后整页是 0xFF、写入后目标区域是预期数据、结构体尾部没有被意外改写。如果看到某几个字节是 0x00 而不是预期值多半是双字编程时memcpy长度算错把未初始化内存写进去了。还可以在擦除函数前后各打一个断点单步看FLASH-CR的 PER、PNB、STRT 位变化比读文档直观。另一个实用手段是看 map 文件搜索参数页地址确认没有任何函数或常量落在这一页。Keil 的.map文件在 Objects 目录下IAR 在 List 目录下搜0x0803f8没有结果就说明链接器没占用。这一步花两分钟能省掉后面「程序莫名跑飞」的几天排查。4.3 低功耗场景下的额外注意L4 系列主打低功耗如果工程里用了 STOP 模式FLASH 操作要在进入低功耗前完成别在唤醒中断里直接擦写。擦除期间电流会上升而且 BSY 轮询依赖系统时钟STOP 模式下时钟停了轮询会卡死。常见做法是把参数保存请求置个标志主循环检测到标志再执行擦写写完再允许进低功耗。另外擦写期间如果发生复位那一页可能处于半擦半写状态所以读取时的魔数加 CRC 双重校验不能省它是掉电中断后的最后一道保险。5. 避坑与排查五条现场踩出来的记录5.1 擦除后程序跑飞PC 落在参数页附近现象调用擦除函数后设备立刻 HardFault调试器显示 PC 在 0x0803F8xx 区间。原因链接器把某个函数或常量表放到了最后一页擦除时把代码擦了。根源是 IROM 长度没减掉参数页或者减了但分散加载文件没同步改。解决核对 Keil 的 IROM1 Size 或 ld 脚本的 FLASH LENGTH确保等于总容量减 2KB改完重新编译看 map 文件确认参数页地址无占用。改链接配置后一定要全量重编增量编译有时不会重新布局。5.2 写入返回成功但读回全是 0xFF现象flash_write返回 0回读比对却失败Memory 窗口看目标地址还是擦除态。原因编程前没清FLASH-SR里的错误标志或者LL_FLASH_Program_DoubleWord调用时 BSY 还没落硬件忽略了这次编程。也可能是页号写错擦的是别的页目标页仍是旧的锁定数据。解决每次擦写前统一执行LL_FLASH_ClearFlag(LL_FLASH_FLAG_ALL)并在编程后检查OPERATION_ERR页号和地址用宏绑定别手写两处。回读校验必须保留它是唯一能发现「假成功」的手段。5.3 解锁一直失败LOCK 位清不掉现象调用LL_FLASH_Unlock()后LL_FLASH_IsLocked()仍返回真后续擦写全部无效。原因FLASH-SR里有未清除的 PGSERR 或 SIZERR控制器处于错误状态拒绝解锁。也可能是代码在中断里重复调用解锁两次解锁之间状态被打断。解决解锁前先写FLASH-SR FLASH_SR_CLEAR清标志再解锁擦写函数加一个静态标志防重入或者在调用期间关中断。清标志是写 1 清零写 0 没效果这点和普通寄存器赋值习惯相反。5.4 参数结构体加字段后旧数据全乱现象给参数结构体新增一个成员重新烧录后读出的旧参数值错位CRC 校验失败。原因结构体布局变了旧数据按新布局解析字段偏移全错。FLASH 里存的还是老格式代码却按新格式读。解决参数结构体首字段放版本号读取时版本不匹配就走默认值并重写或者结构体末尾预留若干保留字节加字段时从保留区挪。升级固件时参数格式变更是常态版本号字段是成本最低的后悔药。5.5 擦写期间进低功耗导致卡死现象带 STOP 模式的工程里参数保存偶尔卡在while (LL_FLASH_IsActiveFlag_BSY())出不来。原因擦除启动后系统进入 STOPFLASH 操作依赖的时钟停了BSY 位不再更新轮询死循环。解决擦写动作放在主循环执行期间禁止进低功耗或者擦写前关掉低功耗唤醒源完成后恢复。别在中断里直接擦写中断上下文里做毫秒级阻塞操作本身就是隐患。6. 二次开发改造把参数区做成可扩展的小型存储层拿到这份代码后多数人不会停在「存一个结构体」上而是想把它扩成能存多组配置、能记录运行日志的小存储层。改造的核心思路是把「页」抽象成「槽」一页 2KB 切成若干固定大小的槽每个槽带独立魔数和 CRC读写按槽号寻址。这样改一组参数不影响另一组擦除粒度也从整页降到单槽逻辑。具体做法是先定义槽布局每槽 64 字节头部 8 字节放魔数、版本、长度、CRC后面 56 字节放数据一页正好 32 个槽。写入时先读目标槽判断是否已是相同内容相同就跳过减少擦写次数——FLASH 擦写寿命有限L4 的数据手册给的擦写次数是万级频繁写同一页会提前耗尽。不同内容才触发整页擦除加重写所有有效槽这个「读-改-写」流程是小型存储层的标准套路。/* 槽头结构8 字节和双字编程粒度对齐 */ typedef struct { uint32_t magic; /* 0x5A5AA5A5 表示槽有效 */ uint16_t version; /* 数据格式版本 */ uint16_t len; /* 有效数据长度最大 56 */ uint32_t crc; /* 数据区 CRC32 */ } slot_hdr_t; /* 按槽号写入内部做读-改-写返回 0 成功 */ int slot_write(uint32_t slot_id, const void *data, uint16_t len);slot_write内部先把整页读到 RAM 缓冲2KBL4 的 RAM 够用改掉目标槽擦页再把缓冲整体写回。这个流程的代价是每次写都搬 2KB但换来的是任意槽独立更新和掉电后单槽可恢复。如果写频率高可以再加一个「脏页合并」策略多次写只标记脏槽空闲时统一落盘。验证方法也简单写槽 0 到槽 31 各一组不同数据逐个读回比对然后随机改其中几个槽断电重启再全量校验确认只有被改的槽变化、其余槽数据完好。再模拟一次写入过程中复位在擦除后、写回前手动触发复位重启后应该能通过 CRC 识别出损坏槽并回退到默认值而不是整页数据报废。我自己的习惯是任何碰 FLASH 的改动先在 RAM 里跑一遍逻辑仿真把槽布局、CRC、读-改-写流程用普通数组验证通过再切到真实地址。直接上板调擦写一次失误就是一页数据没了调试器都救不回来。从那以后我每次改参数区代码都强制先在 RAM 仿真里走一遍全流程再烧板子。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑