STM32L4内部Flash读写实战:原理、代码与避坑指南
2026/9/9 21:45:03 网站建设 项目流程

简介:针对STM32L4系列内部Flash读写场景,这份资源面向使用LL库或HAL库开展底层开发的嵌入式工程师,可帮助读者缩短底层驱动开发周期。作者基于STM32L452RET6型号完成寄存器级配置,并已通过LL库调试验证;由于核心操作均采用寄存器直写方式,迁移到HAL库时仅需调整相关定义即可复用。资源共2个文件,其中C源文件负责实现Flash擦除、写入与读取等接口,头文件提供函数声明和关键参数宏定义,整体压缩包大小仅2KB,结构精简,便于直接加入现有工程。目前已有1930人学习/下载。除基础读写代码外,资源还记录了实际调试中的典型排错经验:适配STM32L471VETx时发现擦页函数失效,根因是Flash页码不连续,只需按照芯片手册修正擦页函数的页码号即可恢复正常;同时提醒默认版本不适用于STM32L4x1系列,相关用户需要自行调整擦页逻辑,避免在硬件适配时走弯路。 有朋友下载了一个叫做“STM32L4xx_读写内部FLASH.rar”的工程压缩包,想让我聊聊STM32L4系列怎么操作内部Flash。这功能说起来不算复杂,但却是不少项目的隐形基础,IAP升级、参数掉电保存、日志记录、字库存储都绕不开它。网上一搜同类问题确实一大堆,很多人卡在擦除失败、写入不对齐、跑着跑着程序就硬死机这些坎上。这篇就把STM32L4内部Flash读写的原理、代码实现和踩坑经验一次讲透,给你一套可以直接抄作业的方案。

1. 为什么要在STM32L4内部Flash上做读写

1.1 内部Flash在项目里到底能干什么

可能有人觉得,现在外部SPI Flash又便宜又大,比如热词里提到的W25Q64,动不动就是8MB,为什么还要费劲去折腾芯片自带的内部Flash呢?答案很简单:你用内部Flash存关键数据,不需要额外接一颗芯片,不需要占用SPI接口,掉电不丢失,读取速度还快,而且安全性更高。

我归纳了一下,项目里用到内部Flash的场景主要有这么几类:

  • IAP在线升级:这是最经典、最刚需的场景。Bootloader把上位机发下来的固件写入App区域的Flash,写完后跳转执行。这要求你对Flash的擦除、写入、跳转异常熟练。
  • 参数掉电保存:相当于把Flash当EEPROM用,存设备编号、校准系数、工作模式这类关键配置。虽然L4系列也内置了真正的EEPROM,但是容量极小(一般是几个KB),像STM32L431只有2KB的EEPROM,存大一点的数据结构就得动Flash。
  • OTA固件备份:做双区OTA的时候,需要把新固件先存到Flash某个区域,校验通过后再搬移到运行区,这时候Flash读写能力就是核心。
  • 数据记录:部分需要掉电保存运行数据的设备,会把Flash当简易的日志存储器用。

1.2 L4片内外设和F1/F4的关键差异

STM32L4系列是ST主打的低功耗产品线,内核是Cortex-M4,主频最高80MHz,很多人在F1、F4上写过Flash,但拿到L4会发现有个明显区别:L4的Flash擦除以页为单位,每页大小是2KB,而F1系列的页只有1KB或者2KB,F4系列的页大小更是有4KB、16KB甚至128KB的不等。这个差异直接影响你计算扇区地址、规划存储区域的方式。

另一个更关键的差异是,L4的Flash编程(写入)支持双字(64位)写入操作,也就是说一次写入8字节,而F1系列是半字(16位)写入。Flash写入宽度不同,意味着你在构造写入函数、处理数据对齐时的思路必须跟着变。后面讲代码我会详细展开。

还有一个容易忽略的点:L4内部Flash支持双Bank模式(部分型号),可以把Flash分成两个独立的Bank,支持并行擦写、乒乓升级。在STM32L4系列的高配型号(比如L476、L496)上,双Bank功能非常实用,但默认工程一般只开启单Bank模式,需要操作FLASH_OPTR寄存器里的DBANK位才能切换。

2. 擦写原理与硬件约束

2.1 先擦后写,按页擦除的机制

无论哪家MCU的Flash,工作原理都是一样的:擦除操作只能把位从0变成1,写入操作只能把位从1变成0。所以如果某个地址区域之前已经写过数据,直接再写新数据是写不进去的,必须先擦除再写入。擦除的最小单位是页(Page),L4系列的页大小是2KB。擦除之后整页内容全部变成0xFF,然后你再按32位、64位或连续数据去写入。

实际写代码时,HAL库提供了封装好的擦除接口,你只需要填好页起始地址、擦除页数就能执行。但有一点特别注意:擦除操作的时间和写入相比长得多,L4擦除一页的时间一般要以毫秒计,写入一个双字是几十微秒级别。如果你的系统里有中断频繁触发,Flash操作期间中断处理如果也访问Flash就会卡死,这个等会儿再说。

2.2 等待周期与ART缓存对读写的影响

Flash本身比CPU慢,这是从Cortex-M3时代就存在的客观问题,所以L4内置了等待状态控制器和ART(自适应实时存储器加速器)缓存。运行在80MHz主频时,如果供电电压正常工作(2.7V~3.6V),需要配置Flash等待周期为4个周期,否则CPU从Flash取指令会出错,表现为程序偶发跑飞、变量读取异常。

很多人初始化时钟的时候不重视这个配置,直接用STM32CubeMX生成的代码倒是没问题,但如果你手写时钟初始化或者移植旧工程,很可能在设置主频后忘了调FLASH_ACR寄存器的等待周期。在L4上我建议直接依赖HAL库的HAL_FLASH_Init()或者CubeMX生成的SystemClock_Config(),它会根据主频自动配置等待周期和ART使能状态。另外,需要缓存的数据如果存放在Flash地址,读写Flash时ART缓存可能带来数据陈旧的问题,所以读取Flash里的数据时最好加上volatile关键字修饰指针,防止编译器过度优化。

2.3 读保护与写保护的门槛

STM32L4的Flash控制器还支持两级读保护(RDP)和写保护(WRP)。读保护用来防止别人通过调试器/烧录器把Flash里的固件内容读出来,这是产品防抄板的基础手段。但注意:一旦设置了RDP Level 1,通过SWD接口就只能全片擦除后重新烧录,无法直接读内存。如果你的程序里读写Flash的代码在受保护状态下出问题,排查起来会比较痛苦。

写保护和读保护不一样,它保护的是指定扇区不被擦除或编程,防止程序运行中误操作损坏关键代码区。实际产品中,我一般建议把Bootloader区域加上WRP保护,避免App里的异常代码把Bootloader区冲掉。不过调试阶段千万别开WRP,否则你每次下载程序都会报错,还以为是调试器坏了。

3. 核心代码实现与流程拆解

3.1 初始化、解锁与关键结构体配置

在STM32L4上操作Flash,最核心的流程是:解锁Flash控制寄存器,执行擦除/写入指令,重新上锁。你可以理解成操作一个保险箱:HAL库默认Flash是锁住的,你必须先解锁,操作完再锁回去,防止程序跑飞时误修改Flash内容。

void Flash_Unlock(void) { HAL_FLASH_Unlock(); } void Flash_Lock(void) { HAL_FLASH_Lock(); }

解锁之后,配置擦除参数结构体。ST的HAL库里定义了一个名为FLASH_EraseInitTypeDef的结构体:

typedef struct { uint32_t TypeErase; /* 擦除类型:页擦除或全片擦除 */ uint32_t Banks; /* 选择哪个Bank */ uint32_t PageAddress; /* 页起始地址 */ uint32_t NbPages; /* 擦除页数 */ } FLASH_EraseInitTypeDef;

如果你要用页擦除,TypeEraseFLASH_TYPEERASE_PAGES,同时指定Banks(比如单Bank模式填FLASH_BANK_1),页地址和擦除页数按照规划填写。

3.2 双字写入的实现细节

STM32L4系列的写入单位是双字,也就是64位(8字节)。这意味着你在调用HAL_FLASH_Program时,Address必须8字节对齐,数据参数的类型是uint64_t

uint32_t Flash_Write_DoubleWord(uint32_t addr, uint64_t data) { HAL_StatusTypeDef status; status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr, data); if (status != HAL_OK) { /* 打印或记录错误码 */ return 1; } return 0; }

有人可能会问:“我想写一个字符串,长度不是8的整数倍怎么办?”答案是:把你想要写入的数据打包成8字节倍数,不够的部分用0xFF填充。比如写一个结构体,结构体大小用#pragma pack(1)对齐到1字节,再定义一个8字节对齐的缓冲区,把结构体memcpy进去,最后按8字节块写入Flash。

这里我给出一个写数组的常见写法,把任意长度数据按双字写入:

uint32_t Flash_Write_Buffer(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; uint64_t tmp; /* 1. 确保地址8字节对齐 */ if ((addr % 8) != 0) return 1; /* 2. 用16字节/8字节凑整处理 */ for (i = 0; i < len; i += 8) { tmp = 0; for (int j = 0; j < 8; j++) { if ((i + j) < len) tmp |= ((uint64_t)buf[i + j]) << (8 * j); else tmp |= ((uint64_t)0xFF) << (8 * j); } if (Flash_Write_DoubleWord(addr + i, tmp) != 0) return 1; } return 0; }

这里有个细节:补0xFF而不是补别的值,因为Flash擦除后本来就是0xFF,这样补出来的数据在存储上是“空余”的状态,校验起来也直观。

3.3 擦除校验与数据回读验证

写入Flash之后,不能想当然认为写成功了,必须回读校验。实际工程里我见过太多“写的时候返回OK,重启之后数据全是0xFF”的案例,原因多半是写入地址超出芯片实际Flash大小,或者擦除时序不对。

回读校验的方式很简单:用指针直接读,读到再和源数据比对。

uint32_t Flash_Verify(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; uint8_t *p = (uint8_t *)addr; for (i = 0; i < len; i++) { if (p[i] != buf[i]) { return 1; /* 校验失败 */ } } return 0; }

注意,读Flash的时候地址没有对齐要求,任意字节都可以直接访问,这跟写Flash的双字对齐要求完全不同。另外读Flash时务必加上volatile,防止编译器优化掉访问操作。

完整擦写流程就是:先解锁,再擦除对应的页,然后按双字写入,最后上锁,再做一次整体回读校验。

4. 实操过程中踩过的坑

4.1 擦写期间一定要关中断

这个坑我当年调试时踩过,后来发现是社区里最常见的问题。前面提过,L4的Flash控制器在擦除/编程期间会暂停CPU对Flash的访问,如果这时有中断触发,中断服务函数本身也存储在Flash里,CPU去取中断向量的过程就会卡住,轻则恢复慢,重则整个系统死机。

稳妥的做法是在擦写Flash前关闭全局中断,擦写完成后恢复。L4内核是Cortex-M4,直接操作PRIMASK寄存器即可:

uint32_t primask; primask = __get_PRIMASK(); __disable_irq(); /* 擦写操作 */ __set_PRIMASK(primask);

但要注意,如果擦写时间较长(比如擦除多页),中断关闭太久会影响实时性。更好的方案是把擦写Flash的代码放到RAM里执行,这样擦写期间CPU可以从RAM取指,中断也不会卡死,但工程上这个方案实现起来稍复杂,新手可以先从关中断开始,运行一段时间确认没问题再说。

4.2 地址对齐、越界与页边界陷阱

L4的Flash起始地址固定在0x08000000,不同型号容量不同。以STM32L431为例,它有256KB Flash,末尾地址就是0x0803FFFF。写地址时千万不能超过这个范围,否则HAL库底层虽然会返回错误,但也可能直接触发HardFault。

页边界问题更隐蔽:如果存储区设计没考虑2KB页对齐,你写入的数据可能横跨两个页边界,擦除时就要多擦一页,甚至误伤相邻页的数据。解决办法很简单,规划存储区时让起始地址按2KB对齐。比如你想在L431的Flash末尾存参数,可以定义如下宏:

#define APP_END_ADDR 0x08040000 /* 1MB 型号的末尾示例,实际按型号改 */ #define PARAM_BASE_ADDR (APP_END_ADDR - 2 * 2048) /* 预留4KB */

这里我故意用“2 * 2048”是为了强调页对齐概念,你可以直接用宏定义成具体数值也可以。

4.3 仿真调试时的Flash保护问题

用ST-Link在线调试时,如果芯片设了读保护(RDP Level 1),Keil/IAR下载固件时都会弹窗报错。很多人不知道这个保护是软件设置的,一时半会儿找不到解锁入口。ST-Link Utility或STM32CubeProgrammer里可以执行全片擦除来解除RDP保护,但会清空整个芯片数据。调试阶段我建议保持RDP为Level 0,量产烧录时再考虑开保护。

还有一个常见问题是,开了看门狗之后擦写Flash时间太长,导致看门狗复位。如果你在真机上跑,擦写多页Flash耗时可能上百毫秒,这段时间喂狗的任务被卡住了,看门狗就响了。解决方案就是在擦写前暂停看门狗,或者把Flash操作拆分成小块,在擦写间隙喂狗。后者实现更优雅,但要注意关中断的时间窗口控制。

5. 从模块到产品级的升级建议

5.1 磨损均衡与掉电保护的设计

Flash的擦写寿命是有限的,L4系列的官方数据标称一般是10万次擦写(P/E Cycles)。10万次听起来不少,但如果你每秒都写一次参数,不到三天就报废了。所以做参数存储功能时,“磨损均衡”必须安排上。

我常用的一个做法是循环写入策略:在Flash里规划一个比较大的存储区(比如8KB,4页),每次写数据写到新地址,而不是覆盖固定地址。读到数据时遍历找到最新的一份。等整个区域写满之后,再擦除一遍重新从头写。这样总擦写次数被分散到了多个页,寿命乘以页数。

掉电保护则更依赖软件设计。关键参数可以考虑交替双备份,写入前先备份旧值,写入新值后校验,校验失败就回滚旧值。我在实际项目里就是两步写入:先写一个固定的头标记(比如0xA5A5A5A5),再写数据体,读数据时先检查头标记,如果头标记不对就认定为上一次写了一半,直接丢掉。这个思路简单有效,性价比很高。

5.2 移植到其他L4子系列时要注意什么

STM32L4家族子型号非常多,L431、L451、L476、L496、L4R5等等,Flash容量从128KB到2MB都有,页大小也略有不同。移植代码最关键的是确认三件事:Flash总容量、起始地址、页大小。这三样东西全部能在对应型号的参考手册(RM0394、RM0351等)里翻到。

另外,部分L4系列如果在双Bank模式下,擦除时Banks参数填错了,也会影响擦除行为。建议在系统初始化时做一个FLASH_GetFlashSize()查询,动态确认可用空间,再用宏定义把存储区规划好,不要在代码里写死地址。这样以后换芯片子型号,只需要改板级配置文件,不用翻代码。

关于高速写入还要提一个细节:L4的Flash编程限制FLASH_CR寄存器里的PG位在编程期间必须保持置位,HAL库内部会处理,但如果你参考网上F1系列的寄存器操作代码(F1写半字)改过来的,一定要把写入宽度改成双字,否则会报编程错误(Programming Error)。这类移植问题排查起来很费劲,我建议L4一律用HAL库或者LL库,不要抄F1的老代码硬改。

5.3 性能验证与量产自检

量产阶段,Flash读写功能最好做成自检项。上电时写一个随机数到指定地址,读回来比对,不一致就报错,这样能筛掉一部分体质差的芯片。另外,烧录完Bootloader之后,建议做一次全Flash区域循环校验,确保出厂固件的每个字节都和编译产物一致,这一步能有效避免某些“烧录成功但是跑不起来”的诡异问题。

我一般会写一个简单的“写读0x55/0xAA交替”测试,对每个存储页做一遍快速写读,验证硬件状态。测试脚本很简单,就是用Bootloader在跳转App之前跑一次,只占用几十毫秒,但对出货质量的保障非常明显。

最后分享一个验证Flash稳定性的土办法

写完Flash读写模块后,我想分享一个我常用来做压力测试的小方法,对排查“偶发数据错乱”这种问题特别有效。写一个循环,每写满一块区域就把所有数据读出来校验,同时跑一整夜,第二天看错误计数器。如果一夜跑下来几万次擦写没有一次错误,基本上Flash驱动这一块就可以放心交付了。凡是校验失败,优先查中断配置和电源稳定性,不要特别着急怀疑芯片本身。电源波动是Flash写入失败的元凶之一,尤其用电池供电的设备,擦写瞬间电流需求变大,电压跌落就会导致写入数据异常。给Flash供电引脚加一个100nF加10uF的退耦电容,很多莫名其妙的写数据错误能直接消失。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询