资讯动态

STM32L4内部Flash读写:LL库与寄存器双版本实现与避坑指南

发布时间:2026/10/21 5:56:40 来源:尧图企业网站定制
简介面向STM32L4xx系列的内部FLASH读写实现适合需要在片内FLASH上进行数据存储、参数保存或OTA升级的嵌入式开发者也适合想深入理解寄存器配置的初学者。代码基于LL库并直接操作寄存器相比HAL库更精简已在STM32L452RET6上实际调通由于底层配置均为寄存器级换用HAL库时也只需调整相关类型定义即可复用方便不同工程间迁移。压缩包共2个文件包含1个C源文件与1个头文件分别承载FLASH读写函数实现与接口声明整体大小仅2KB结构紧凑便于快速移植或逐行理解读写流程。目前已有1935人学习/下载说明该实现具备实际参考价值。资源还额外注明一个关键坑点在STM32L4x1系列如L471VETx上擦页可能失败原因是FLASH页码不连续修改擦页函数中的页码号即可成功这一排错思路能帮助使用者根据具体芯片型号调整代码避免重复踩坑也提醒了不同型号间FLASH布局差异便于快速定位问题。1. 把内部 Flash 读写讲透一份 LL 库与寄存器双版本代码包能帮你避开哪些坑有开发者把页擦除函数一调用就卡死在 BSY 等待上有人往 Flash 里存参数掉电重启后读出来全是 0x00。这类问题我在几个嵌入式群里见过不止一次多数不是硬件坏了而是没搞懂 STM32L4 系列内部 Flash 的擦写时序和最小编程单位。这份压缩包里包含 LL 库和寄存器操作两套读写内部 Flash 的工程代码覆盖解锁、页擦除、双字编程、读取校验以及掉电存储的完整链路。无论你要做参数保存、OTA 升级还是运行日志都能直接对照移植。本文把代码包的实现逻辑、关键参数和翻车现场一并拆给你看。2. 先看懂 L4 的内部 Flash双 Bank、2KB 页与 ECC 带来的三个边界2.1 存储器组织主 Flash、系统 Flash 与选项字节STM32L4 系列内部 Flash 从地址 0x08000000 开始映射前面是主存储器用于放代码和用户数据0x1FFF0000 附近是系统存储器出厂 Bootloader 就在这里再往上还有选项字节区域用来配置读保护等级、看门狗选项和双 Bank 模式。读写内部 Flash大多数时候碰的是主存储器这一段选项字节一般只在出厂配置或升级时才动。主存储器的页大小在 L4 系列上统一是 2KB这点和 F1 系列的 1KB 或 4KB 都不一样。擦除操作的最小单位就是一整页没有半页擦除的说法。你哪怕只想改一个字节也必须先把所在的整页擦掉再重新把整页内容写回去所以做参数存储前一定要先在 RAM 里缓存整个页的数据。另一个容易被忽略的点是双 Bank 结构。L4 的 Flash 分成 Bank1 和 Bank2如果选项字节里没切换成单 Bank 模式那么从 0x08000000 开始的前一半属于 Bank1后一半属于 Bank2。擦写其中一个 Bank 时如果代码正在另一个 Bank 里执行通常不受影响但如果你的代码和数据在同一 Bank 里擦写期间取指令就会冲突。这是一个非常重要的边界条件后面第 4 章会展开讲。2.2 关键参数页大小、双 Bank 边界与 ECC下面这张表是 L4 内部 Flash 最常用到的一组硬件参数代码包里的地址宏和擦写循环都依赖这些数字。不同型号 Flash 总容量有差异但页大小和双字编程单位在全系列是统一的。参数名称典型值说明页大小2 KB擦除的最小单位页擦除一次清掉 2KB编程最小单位双字 64 位8 字节写入必须按 8 字节对齐否则报错双 Bank 边界容量一半处例如 512KB 型号边界在 0x08040000擦写寿命典型 10 万次数据区页磨损后需要换页ECC每双字附带校验位读出时硬件自动校验错误会置位标志L4 的 Flash 自带 ECC 校验每写入一个双字8 字节会附带生成校验位。这意味着读出时如果数据本身损坏硬件会在读总线阶段就报错并可能触发 NMI而不是像 F1 那样直接给你一个错值。好处是数据可靠性高坏处是你不能把任意碎数据硬塞进去编程时地址和长度都要对到双字边界。2.3 擦写寿命与写入最小单位官方标称的 10 万次擦写寿命针对的是每个页。如果一个参数页被频繁写入可能几个月就到寿命。所以工业产品里基本都会做磨损均衡把数据循环写到一组页里而不是每次都写同一个地址。L4 擦写寿命还有一个隐藏前提编程前必须先擦除。你在已写入的页上直接编程虽然硬件会报错但不会主动帮你处理旧数据必须先擦后写。如果有项目里出现“写入成功但读回是旧数据”的怪问题先检查是不是擦除时序被跳过了。2.4 底层接口选型寄存器、HAL 与 LL 怎么选同一份功能官方给了 HAL、LL 和寄存器三层写法。HAL 封装最厚函数调用方便但代码量大Flash 擦写这种高频时序操作里还夹带了不少超时检查寄存器操作最直接一条语句就能触发擦除但你必须自己维护 PEKEY、BSY 和错误标志少写一个步骤就是翻车LL 库介于两者之间它本质是对寄存器操作的薄封装函数名和寄存器一一对应又没有 HAL 那层抽象开销。我一般会这样选项目里本来就依赖 HAL 做外设初始化就顺手用 LL 做 Flash 读写省得混用两套风格如果只是写个 Bootloader 或者对体积敏感的小模块直接用寄存器操作把解锁、擦除、编程封装成三四个函数就够了。这份压缩包里两套都给了正好对照着看底层寄存器到底做了什么。3. 从解锁到校验LL 库与寄存器操作对照实现读写3.1 先看完整时序解锁、等待 BSY、操作、上锁内部 Flash 在复位后默认是上锁状态直接写控制寄存器不会生效。读写 Flash 的标准流程可以概括为四步先写两个解锁键值解除锁然后等待 BSY 标志为 0确保 Flash 空闲执行页擦除或双字编程操作完成后重新上锁。这一步的顺序问题比想象中更重要。解锁键值必须连续写入中间不能插入别的 Flash 操作等待 BSY 也不能省否则上一个操作还没结束你就往 CR 里写新命令行为是未定义的。下面的解锁代码分别给出 LL 和寄存器写法// 解锁LL 库方式 LL_FLASH_Unlock(); // 内部实际做了两件事 // 向 FLASH-PEKEYR 依次写入 0x89ABCDEF 和 0x02030405 // 解锁寄存器方式 FLASH-PEKEYR 0x89ABCDEF; FLASH-PEKEYR 0x02030405; // 写完后建议回读 FLASH-CR 的 LOCK 位确认确实解锁成功代码逻辑上PEKEYR 是 Flash 编程/擦除解锁寄存器两个固定键值顺序写错任何一个LOCK 位都不会清掉。LL 库的LL_FLASH_Unlock()内部就是把这两条语句封装起来你少记一个键值也能写对。需要留意的是解锁只对当前 Bank 生效L4 的双 Bank 模式里Bank1 和 Bank2 是独立的锁操作哪个 Bank 就要解哪个 Bank 的锁。3.2 页擦除寄存器版与 LL 库版对照页擦除的触发时序是在 CR 寄存器里把 PER 位置 1选对 Bank把要擦除的页号写到地址寄存器最后把 STRT 位置 1。硬件收到命令后会清掉整页同时把 BSY 置位完成后自动清 BSY。代码里必须循环等待 BSY 清零并且顺手检查写保护错误标志。// 页擦除寄存器方式目标地址为 page_addr void flash_erase_page(uint32_t page_addr) { // 1. 解锁前面已做 // 2. 等待上次操作完成 while (FLASH-SR FLASH_SR_BSY); // 3. 配置擦除命令页擦除、选择 bank FLASH-CR | FLASH_CR_PER; // 使能页擦除 if (page_addr 0x08040000) // 根据备份边界选 bank FLASH-CR | FLASH_CR_BNK; else FLASH-CR ~FLASH_CR_BNK; // 4. 写入要擦除的页地址低 12 位忽略 FLASH-ACR ~FLASH_ACR_PROGRAM_ADDRESS_MASK; FLASH-ACR | (page_addr 0x000FFFFF) ~0x7FF; // 5. 触发擦除 FLASH-CR | FLASH_CR_STRT; // 6. 等待完成 while (FLASH-SR FLASH_SR_BSY); // 7. 清错误标志、关闭页擦除命令 FLASH-SR FLASH_SR_EOP | FLASH_SR_WRPERR | FLASH_SR_PGAERR; FLASH-CR ~FLASH_CR_PER; }这段代码有几个参数值得说明。FLASH_CR_BNK位只在双 Bank 模式才有意义如果你启用了单 Bank 模式这个位不存在需要按参考手册确认page_addr 0x000FFFFF取出 Flash 地址的低 20 位而~0x7FF是为了把页内偏移清零因为擦除命令只关心页号不关心页内地址。严格来说擦除命令里更常用的是直接写FLASH-CR的 PNB 页号段但用地址换算页号更容易读。LL 库版更简单官方把命令拼接都封装好了LL_FLASH_Erase(LL_FLASH_BANK_1, page_addr); // 参数1bank 编号LL_FLASH_BANK_1 或 LL_FLASH_BANK_2 // 参数2页起始地址函数内部会换算成页号 while (LL_FLASH_IsActiveFlag_BSY()); // 等待擦除完成 LL_FLASH_ClearFlag_EOP(); // 清结束标志LL 库版的参数很直接第一个参数选 Bank第二个参数传页地址。要注意擦除前 BSY 检查和擦除后 BSY 检查都不能省LL 库里没有帮你等待只是封装了寄存器的读写。擦除动作本身要几十毫秒量级这段期间主循环别去读同一 Bank 的 Flash否则取指会阻塞。3.3 双字编程注意 8 字节对齐L4 的编程命令是双字64 位编程。不是 F1 那种半字写入也不是按字节写入。这意味着你每次写数据前必须把 64 位数据打包好地址必须是 8 的倍数。如果你的数据长度不是 8 的倍数最后一段要补零凑满一个双字。// 双字编程寄存器方式 uint64_t data *(uint64_t *)buffer; // 把两个 32 位拼成 64 位 FLASH-CR | FLASH_CR_PG; // 使能编程 if (addr 0x08040000) FLASH-CR | FLASH_CR_BNK; else FLASH-CR ~FLASH_CR_BNK; // 写入地址双字对齐 FLASH-ACR ~FLASH_ACR_PROGRAM_ADDRESS_MASK; FLASH-ACR | (addr 0x000FFFFF); // 地址低 20 位 // 写入数据寄存器 FLASH-PD_64 data; // 等待完成 while (FLASH-SR FLASH_SR_BSY); if (FLASH-SR FLASH_SR_PGAERR) { // 地址对齐错误或擦除不完整 } FLASH-CR ~FLASH_CR_PG;代码里FLASH-PD_64就是 64 位数据寄存器写入一个整双字。需要注意编程前页必须是擦除状态也就是全 0xFF否则硬件会置位 PGAERR表示编程对齐错误或页上有旧数据。LL 库对应的写法LL_FLASH_Program(LL_FLASH_PROGRAM_DOUBLEWORD, addr, data); // 第一参数固定传 LL_FLASH_PROGRAM_DOUBLEWORD // 第二参数是目标绝对地址需 8 字节对齐 // 第三参数是 uint64_t 数据 while (LL_FLASH_IsActiveFlag_BSY()); LL_FLASH_ClearFlag_PGAERR();LL 库里的LL_FLASH_Program要求你自己拼好uint64_t。常见错误是把两个uint32_t按数组顺序分别传那只会写入第一个 32 位第二个数据整个丢失。如果你用结构体存参数建议直接在声明时加__attribute__((aligned(8)))或者编译选项里做结构体对齐。3.4 读取与校验直接地址访问与 CRC 配合读 Flash 不需要任何命令直接*(uint32_t *)addr就能拿数据。L4 的 Flash 是挂在内部总线上的读操作和 SRAM 一样快但受 Flash 等待状态影响频率高时要配好延迟。校验逻辑一般分两级写完后立刻回读比对以及周期性对整个存储区做 CRC 校验。// 写入后立即回读校验 uint64_t read_back *(uint64_t *)addr; if (read_back ! data) { // 回读不一致记录错误并重试 } // 周期性 CRC 校验 uint32_t crc HAL_CRC_Calculate(hcrc, (uint32_t *)flash_addr, 32); // 参数2数据起始地址 // 参数3要校验的 32 位字数量不是字节数这个回读步骤的价值在于很多写入失败是静默的等你掉电重启后才暴露。回读时发现不一致至少知道是擦除没完成还是编程没完成。CRC 校验则适合验证整段数据完整性比如程序版本信息或者配置参数集。要注意HAL_CRC_Calculate第三个参数是字数量不是字节数量写成 32 会校验 128 字节而不是 32 字节。3.5 从 RAM 执行关键函数放进 SRAM第四个边界条件也是 Bootloader 开发绕不开的问题当 Flash 擦写操作和目标地址在同一 Bank 时CPU 从 Flash 取指令的行为会和擦写过程冲突。解决方法是把擦写函数整体搬进 SRAM 执行。// 把函数放到 RAM 执行的典型声明 void flash_erase_page_sram(uint32_t page_addr) __attribute__((section(.ramfunc))); // 函数体与普通擦除函数完全一致 // 但编译器会把代码段放到 SRAM启动时从 Flash 拷贝实际工程里我会用链接脚本把一组擦写函数放进固定 RAM 段然后在跳转前用memcpy从 Flash 里的镜像拷贝到 SRAM 再调用。也有个土办法利用 L4 的双 Bank 特性把数据放在 Bank2 而代码在 Bank1 跑只要不同时访问同一个 Bank也不会冲突。两种方案看项目复杂度Bootloader 里我推荐 RAM 执行方式因为升级时会面临 Bank1/Bank2 同时都要擦的情况。4. 避坑排查五个实测翻车现场的教训与对策4.1 解锁失败CR 寄存器写不进去LOCK 位一直为 1现象用寄存器方式直接给FLASH-CR写0x01回读发现寄存器纹丝不动芯片像没反应一样。原因没有先写 PEKEYR 解锁键值或者两个键值之间的顺序反了。L4 的 Flash 控制寄存器在 LOCK 位为 1 时对所有写操作置为无效不管你怎么赋值都会弹回去。解决先把解锁流程单独抽成一个函数写完后回读 LOCK 位判断是否成功。LL 库用户直接调用LL_FLASH_Unlock()然后马上读LL_FLASH_IsActiveFlag_LOCK()做断言。如果确认键值没错仍解锁失败检查是不是已经开启了读保护级别 1RDP 开启状态下调试访问受限看上去像解锁不生效。4.2 BSY 卡死擦除函数死循环在 while (BSY)现象页擦除触发后程序一直停在while (FLASH-SR FLASH_SR_BSY)看门狗复位都救不回来。原因最典型的是擦除期间 CPU 还在从同一 Bank 的 Flash 取指令。L4 在擦除时如果总线侧还在访问该 Bank 的 Flash擦除命令会一直等待总线空闲表现就是 BSY 永远为 1。另一个常见原因是 Flash 控制器时钟没有使能或者频率设置异常导致命令未真正触发。解决把擦除函数挪到 RAM 执行或者确保擦除期间代码跑在另一 Bank。同时检查RCC-AHBENR里 Flash 接口时钟是否打开。代码里加一个超时退出的手段比如等待 BSY 超过 1 秒就强制复位至少能保住现场可排查。实际项目里我在擦除和编程后都会加超时判断不裸等。4.3 只写 16 位或 32 位导致写入无效现象通过FLASH-PD_32 value或直接用某 HAL 函数写 32 位数据写完回读地址发现全是 0x00甚至相邻数据也被破坏。原因L4 的编程命令最小单位是双字。你只提供 32 位数据时Flash 控制器不会帮你补齐另一半写入操作要么被拒绝要么产生未定义结果某些库把 32 位当成非法参数直接返回错误。解决所有写入统一走 64 位数据接口。先用两个 32 位变量拼一个uint64_t或者定义一个 8 字节对齐的联合体一次性写入。数据不足 8 字节的部分用 0xFF 填充。代码包里给出一个flash_write_buffer函数内部自动补齐双字并对齐地址。4.4 掉电瞬间写入导致参数区损坏现象产品在写 Flash 参数时突然断电重启后参数区读出来是乱码部分页变成 0xFF 或全 0x00。原因写 Flash 过程中掉电正好停在擦除后编程前或者编程只完成一半。此时页里的内容就是不确定的ECH 标志也许没触发但数据已经废了。解决结构上做两层防护。第一参数区留两个副本每次先写备份区再写主区第二写入流程里先擦备份页写备份校验成功后再擦主页。代码包里默认采用双页备份方案主区和备份区各占一页。如果掉电下次启动检测到主区 CRC 不正确就自动从备份区恢复。这个方案简单可靠比单纯加电容持久化更省成本。4.5 擦出错误标志不清导致后续操作全部失败现象一次页擦除触发了 PGAERR 或 WRPERR但你没有手动清除错误标志后续第二次擦除、编程操作全部异常。原因Flash 的 SR 错误标志一旦置位会在下次命令执行时干扰命令状态机。有工程师调试时发现第一页能写、第二页怎么都写不进去一查 SR 里 WRPERR 还是 1。解决每次擦除、编程结束后统一清标志用FLASH-SR FLASH_SR_WRPERR | FLASH_SR_PGAERR | FLASH_SR_EOP一次性清掉。注意这些标志是写 1 清除不是写 0。写 0 不会有任何效果。LL 库对应调用LL_FLASH_ClearFlag_WRPERR()等函数。养成这个习惯后Flash 读写的大部分诡异问题都能提前暴露而不是越挫越深。5. 进阶用法把 Flash 当掉电存储前先做这三件事第一件事写一个存储管理器而不是裸调擦写函数。很多开发者一开始图省事直接在主流程里flash_erase然后flash_write数据少没问题数据量一大就乱。我会在代码包里单独划一个storage.c把所有擦写动作收敛成两个接口读一块数据和写一块数据。内部做页缓存、双页备份和 CRC 校验。上层应用完全不知道页号和 Bank 的存在只关心数据是不是读对了。第二件事做掉电测试不要只按复位键。按复位键是完整复位的冷启动和真正拔电的瞬间行为不一样。掉电时 VDD 是缓降的外设可能在半电压状态下跑几毫秒。要复现“擦除中掉电”这种场景应该用程控电源把电压在擦写过程中直接切断。我自己测试时会在擦除后、编程前留一个 GPIO 翻转点用逻辑分析仪看掉电发生在擦写流程的哪个阶段。这个习惯能帮你验证双页备份到底能不能兜住最坏情况。第三件事磨损均衡不复杂一个计数器就能实现。选一组 4 页 Flash 轮流存放参数包每页头部写序列号启动时选序列号最大的页作为当前有效页写入时写到下一页。当写满一圈再做整组擦除。这样 10 万次擦写寿命被摊到 4 个页上实际可用次数翻了四倍。这个思路比复杂的动态分配算法实用得多代码量也小。最后的收尾技巧是加一个“工厂复位”开关。在参数区第一个双字里存一个魔数比如0xA5A5A5A5启动时如果发现魔数不对就用默认参数重写整个存储区。项目调试阶段这个开关帮我避开了无数次“参数被改乱但又不知道默认值是什么”的局面。从那以后我每次写掉电存储相关代码都强制把双页备份、CRC 校验和魔数复位这三件事先做完再开始调业务逻辑。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑