简介:面向STM32L4xx系列MCU开发者的内部FLASH读写参考代码,聚焦LL库与寄存器级配置,解决嵌入式项目中参数存储、固件升级等场景下的片上FLASH擦写需求。作者基于STM32L452RET6实测调通,因全部为寄存器操作,改用HAL库时仅需调整相关定义即可复用,适合具备一定底层开发基础、希望摆脱库函数黑盒、深入理解FLASH控制寄存器的工程师参考。压缩包共2个文件,包含1个c源文件与1个h头文件,分别承载擦除、写入、读取等核心函数实现与接口声明,整体仅2KB,轻量易集成。已有1935人学习下载,具备一定实践参考价值。资源还记录了作者后续在STM32L471VETx上遇到的擦页失效问题,指出根源在于FLASH页码不连号,需修改擦页函数中的页码号方可成功,并提示不适用于STM32L4x1系列,为读者排查类似芯片差异提供了具体线索。
1. STM32L4xx 内部 FLASH 读写:一份能直接跑通的寄存器级资源
手里这块 STM32L4xx 板子要存一组掉电不丢的标定参数,EEPROM 又不想外挂,最省事的办法就是把内部 FLASH 当小容量存储用。可真动手才发现,L4 系列的 FLASH 和 F1/F4 那套完全不是一回事:双 Bank 结构、页擦除粒度、编程位宽、还有那个让人头大的 72 位 PEC 校验,照抄老代码基本必翻车。这份STM32L4xx_读写内部FLASH.rar就是冲着这个场景来的——它把 LL 库和寄存器两套写法都摆出来,从解锁、擦页、编程到校验一条龙,适合已经能点灯、想跨过“内部 FLASH 到底怎么安全写”这道坎的嵌入式从业者。下面我按自己拆包复现的顺序,把能抄的部分、参数怎么定、坑在哪,一条条摊开讲。
2. 先搞懂 L4 的 FLASH 结构:双 Bank、页粒度与编程位宽
2.1 为什么不能照搬 F1 的写法
STM32L4xx 的 FLASH 控制器和早期 F1/F4 最大的区别在于它是双 Bank架构,而且擦除单位是页(Page)而不是扇区。以常见的 512KB 型号为例,两个 Bank 各 256KB,每页 2KB,也就是每个 Bank 128 页。这个“页”是擦除的最小单位,你哪怕只想改 4 个字节,也得把整页 2KB 先擦掉再回写。很多从 F1 迁过来的代码直接调FLASH_EraseSector(),在 L4 上根本对不上号,编译能过、跑起来全错,这就是第一个血泪点。
另一个容易忽略的是编程位宽。L4 支持 64 位双字(double word)编程,也就是一次必须写 8 个字节,且目标地址要 8 字节对齐。你要是按字节一个个写,硬件会直接给你置错误标志。这一点在 LL 库里体现得很明显:LL_FLASH_Program()的第二个参数是编程类型,常见取值是LL_FLASH_TYPEPROGRAM_DOUBLEWORD,传错了要么不生效要么报 PGERR。
2.2 关键寄存器与 LL 库的对应关系
寄存器级操作的核心就那几个:FLASH_ACR(访问控制,含等待周期和预取)、FLASH_KEYR(解锁密钥)、FLASH_SR(状态,错误标志都在这)、FLASH_CR(控制,擦除/编程/启动位)、FLASH_CCR(清标志)。LL 库把这些封装成了LL_FLASH_Unlock()、LL_FLASH_ErasePage()、LL_FLASH_Program()这类函数,但底层动作完全一致。我一般建议新手先用 LL 库跑通,再对照寄存器手册看它到底动了哪几位,这样出问题才知道去FLASH_SR里查哪个错误位。
| 操作 | 寄存器动作 | LL 库函数 | 常见错误标志 |
|---|---|---|---|
| 解锁 | 向 KEYR 写 KEY1/KEY2 | LL_FLASH_Unlock() | 无(写错则 CR 的 LOCK 位不清) |
| 擦页 | CR 置 PER、PNB,置 STRT | LL_FLASH_ErasePage(page) | PERERR、WRPERR |
| 编程 | CR 置 PG,写双字 | LL_FLASH_Program(type, addr, data) | PGERR、SIZEERR、PGSERR |
| 清标志 | 向 CCR 对应位写 1 | LL_FLASH_ClearFlag() | — |
提示:L4 的
FLASH_SR里错误标志是“写 1 清零”,不是读清零。清标志必须走FLASH_CCR,直接读SR不会清掉,很多人卡在这以为擦除失败。
2.3 页地址怎么算
页号PNB是相对当前 Bank 的偏移,不是全局页号。比如你要擦 Bank2 的第 0 页,PNB填 0,同时FLASH_CR的BKER位要选 Bank2。地址换算上,Bank1 起始0x08000000,Bank2 起始0x08040000(512KB 型号)。算页地址的常见写法是bank_base + page_index * 2048。这个 2048 就是页大小,型号不同可能变,动手前一定翻对应型号的参考手册确认,别拿一个数到处套。
3. 用 LL 库跑通擦写:从解锁到校验的完整流程
3.1 环境与工程准备
资源包里是标准的外设驱动风格工程,核心文件是flash.c/flash.h,依赖 STM32L4xx 的 LL 头文件。我一般把它挂到自己工程里,确认三件事:stm32l4xx_ll_flash.h已包含、系统时钟已配好(FLASH 等待周期要跟主频匹配,否则高频下读写会出玄学错误)、链接脚本里预留了要写的页不被程序占用。最后这条最容易被忽略——你把参数写到程序自己所在的页,擦除时把代码擦了,现象就是“跑着跑着 HardFault”,查半天查不到原因。
/* flash.h 精简接口,按资源包风格整理 */ #ifndef __FLASH_H #define __FLASH_H #include "stm32l4xx_ll_flash.h" #define FLASH_START_ADDR 0x0803F000U /* 选一个不冲突的页,按实际工程改 */ #define FLASH_PAGE_SIZE 2048U uint32_t Flash_ReadWord(uint32_t addr); int Flash_WriteBuffer(uint32_t addr, const uint8_t *buf, uint32_t len); int Flash_ErasePageByAddr(uint32_t addr); #endif这段头文件把起始地址和页大小抽成宏,好处是换型号只改两处。FLASH_START_ADDR我特意选在 Bank1 靠后的页,实际用的时候你要对照自己的.map文件确认这块没被代码或常量占用。Flash_ReadWord直接解引用地址即可,内部 FLASH 读和普通内存读没区别,不需要解锁。
3.2 擦除一页的标准动作
int Flash_ErasePageByAddr(uint32_t addr) { uint32_t page; uint32_t bank; /* 判断落在哪个 Bank,512KB 型号 Bank2 从 0x08040000 起 */ if (addr >= 0x08040000U) { bank = FLASH_BANK_2; page = (addr - 0x08040000U) / FLASH_PAGE_SIZE; } else { bank = FLASH_BANK_1; page = (addr - 0x08000000U) / FLASH_PAGE_SIZE; } LL_FLASH_Unlock(); /* 清掉历史错误标志,避免误判 */ LL_FLASH_ClearFlag(FLASH_FLAG_ALL_ERRORS); if (LL_FLASH_ErasePage(bank, page) != SUCCESS) { LL_FLASH_Lock(); return -1; } /* 擦除后必须等 BSY 清零再返回 */ while (LL_FLASH_IsActiveFlag_BSY()) { } LL_FLASH_Lock(); return 0; }逻辑上分四步:算 Bank 和页号、解锁、清错误标志、发起擦除并等忙。这里有两个参数要盯紧——bank和page。page是相对该 Bank 的页号,不是全局页号,算错就会擦到隔壁页。LL_FLASH_ErasePage内部会置PER和STRT,返回SUCCESS只代表命令发出去了,不代表擦完了,所以后面那个while等BSY不能省。我见过有人省掉这步,紧接着就编程,结果PGSERR报错,还以为是数据问题。
3.3 双字编程与回读校验
int Flash_WriteBuffer(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; uint64_t dw; if ((addr % 8U) != 0U || (len % 8U) != 0U) { return -2; /* 必须 8 字节对齐,长度也按 8 的倍数传 */ } LL_FLASH_Unlock(); LL_FLASH_ClearFlag(FLASH_FLAG_ALL_ERRORS); for (i = 0; i < len; i += 8U) { /* 小端拼双字,和硬件写入顺序一致 */ dw = (uint64_t)buf[i] | ((uint64_t)buf[i + 1] << 8) | ((uint64_t)buf[i + 2] << 16) | ((uint64_t)buf[i + 3] << 24) | ((uint64_t)buf[i + 4] << 32) | ((uint64_t)buf[i + 5] << 40) | ((uint64_t)buf[i + 6] << 48) | ((uint64_t)buf[i + 7] << 56); if (LL_FLASH_Program(LL_FLASH_TYPEPROGRAM_DOUBLEWORD, addr + i, dw) != SUCCESS) { LL_FLASH_Lock(); return -3; } while (LL_FLASH_IsActiveFlag_BSY()) { } } LL_FLASH_Lock(); /* 回读校验,别省这一步 */ for (i = 0; i < len; i++) { if (*(volatile uint8_t *)(addr + i) != buf[i]) { return -4; } } return 0; }这段的关键参数是LL_FLASH_TYPEPROGRAM_DOUBLEWORD和那个 8 字节对齐检查。拼双字时按小端顺序移位,和 Cortex-M 的内存字节序一致,写进去回读才对得上。每写一个双字都要等BSY清零,不能连续猛写,否则后一个会覆盖前一个的状态。最后的回读校验是我强烈建议保留的——内部 FLASH 写失败不一定报错,有时候标志位干净但数据就是错的,回读是最后一道后悔药。返回值我用了负数区分错误类型,方便上层打日志定位。
3.4 一个最小验证 main
uint8_t tx[16] = {0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88, 0x99,0xAA,0xBB,0xCC,0xDD,0xEE,0xFF,0x00}; uint8_t rx[16]; Flash_ErasePageByAddr(FLASH_START_ADDR); Flash_WriteBuffer(FLASH_START_ADDR, tx, sizeof(tx)); for (int i = 0; i < 16; i++) { rx[i] = *(volatile uint8_t *)(FLASH_START_ADDR + i); } /* 断点看 rx 是否等于 tx,或串口打印比对 */这段就是复现入口:先擦、再写、再读,三步走完用调试器看rx。如果rx和tx一致,说明整条链路通了;不一致就回到FLASH_SR看错误位,按第 5 章的排查表对号入座。
4. 寄存器直写版:绕开 LL 库时要注意的位操作
4.1 解锁与清标志的裸写
有些老项目或极致精简场景不想引 LL 库,那就得自己碰寄存器。解锁就是往FLASH->KEYR依次写两个魔数,顺序不能反,中间不能被打断。清标志是往FLASH->CCR对应位写 1。这里有个细节:FLASH->CR的LOCK位在解锁成功后自动清 0,你不用手动清;但如果你解锁前就有错误标志挂着,某些操作会被拒绝,所以解锁后第一件事是清SR里的错误位。
#define FLASH_KEY1 0x45670123U #define FLASH_KEY2 0xCDEF89ABU static void flash_unlock_raw(void) { if (FLASH->CR & FLASH_CR_LOCK) { FLASH->KEYR = FLASH_KEY1; FLASH->KEYR = FLASH_KEY2; } /* 清所有错误标志:CCR 对应位写 1 */ FLASH->CCR = FLASH_CCR_CLR_ALL; /* 按头文件宏或手写位掩码 */ }FLASH_KEY1/KEY2是固定魔数,写错顺序或写错值,LOCK位不会清,后续所有擦写都被硬件挡掉,现象是“函数返回成功但啥也没发生”。这种静默失败最坑,所以解锁后我习惯读一下CR确认LOCK真的为 0 再往下走。
4.2 擦除与编程的位域拼装
寄存器版擦除要自己拼CR:选 Bank 置BKER、置PER、把页号写进PNB、最后置STRT。编程则是清PER、置PG、然后往目标地址写双字。拼位域时最容易错的是PNB的位宽和位置,不同型号手册里画得清清楚楚,照着填就行,别凭记忆。
static int flash_erase_raw(uint32_t bank, uint32_t page) { while (FLASH->SR & FLASH_SR_BSY) { } FLASH->CR &= ~FLASH_CR_PNB; /* 先清页号域 */ FLASH->CR |= (page << FLASH_CR_PNB_Pos); /* 填新页号 */ FLASH->CR |= FLASH_CR_PER; /* 页擦除模式 */ if (bank == 2U) { FLASH->CR |= FLASH_CR_BKER; } else { FLASH->CR &= ~FLASH_CR_BKER; } FLASH->CR |= FLASH_CR_STRT; /* 启动擦除 */ while (FLASH->SR & FLASH_SR_BSY) { } FLASH->CR &= ~FLASH_CR_PER; /* 退出擦除模式 */ return (FLASH->SR & FLASH_SR_ERRORS) ? -1 : 0; }这段和 LL 库版一一对应,区别只是手动拼位。FLASH_CR_PNB_Pos是页号域的起始位,用头文件宏比自己数位靠谱。擦完清PER是个好习惯,否则后续编程可能被当成擦除命令处理。返回值里查FLASH_SR_ERRORS掩码,能一次覆盖PERERR/WRPERR/PGSERR等,比逐个判断省事。
4.3 两种写法的取舍
LL 库版可读性好、跨型号移植时改宏就行,适合大多数业务工程;寄存器版代码量小、无库依赖,适合 bootloader 或对体积敏感的场景。我的习惯是业务层用 LL 库,bootloader 里用寄存器直写,两边都保留回读校验。资源包里两套都有,对照着看能更快理解每个位到底在干什么,这比单看一种写法收获大。
5. 避坑与排查:五个真实会踩的坑
5.1 擦完立刻写,报 PGSERR
现象:擦除函数返回成功,紧接着编程返回失败,FLASH_SR的PGSERR置位。原因:擦除命令发出后没等BSY清零就发起编程,硬件认为上一条操作还没结束。解决:擦除和编程之间强制加while (LL_FLASH_IsActiveFlag_BSY()),并且擦完清一次错误标志再进编程。
5.2 写进去读出来全是 0xFF
现象:编程函数返回成功,回读却是擦除态0xFF。原因:目标页没真正擦掉,或者写地址落在了被写保护的页(WRPERR)。解决:先单独调擦除并回读确认整页为0xFF,再编程;同时检查选项字节里的写保护配置,别把要写的页保护上了。
5.3 跑着跑着 HardFault
现象:程序运行一段时间后进 HardFault,复位后又正常。原因:写的页和程序代码/常量区重叠,擦除时把正在执行的代码擦了。解决:对照.map文件确认目标页空闲,或在链接脚本里显式预留一段 FLASH 给参数区,从根上隔离。
5.4 高频下读写偶发错误
现象:降频跑正常,满频跑偶尔读出错或编程失败。原因:FLASH_ACR的等待周期(LATENCY)没跟主频匹配,高频访问 FLASH 需要插入等待。解决:按参考手册的频率-等待周期表配置LATENCY,改主频时同步改,别只改 PLL 不改 FLASH 等待。
5.5 双 Bank 页号算错擦错区
现象:想擦 Bank2 的页,结果 Bank1 的数据没了。原因:PNB填了全局页号,或BKER位没设对。解决:页号一律按“相对当前 Bank”算,擦之前打印bank和page两个值核对,别嫌麻烦。
6. 进阶:把参数区做成带版本和 CRC 的小存储
跑通单页读写只是起点,真到产品里,参数区得能扛住掉电、能识别旧数据、能判断完整性。我一般会在页头放一个固定结构:魔数、版本号、长度、CRC32,后面才是数据本体。写入时先擦页、写头、写数据、最后回读算 CRC 比对;读取时先验魔数和 CRC,不通过就当没存过、走默认值。这样即使写到一半掉电,下次上电也能识别出脏数据而不是拿着半截参数跑飞。
typedef struct { uint32_t magic; /* 固定 0x50415241,标识参数区 */ uint16_t version; /* 结构版本,升级时判断兼容 */ uint16_t len; /* 数据字节数 */ uint32_t crc; /* 对 data 部分算 CRC32 */ } ParamHeader; /* 写入流程:擦页 -> 写 header(补齐到8字节) -> 写 data -> 回读校验 CRC */ /* 读取流程:读 header -> 验 magic -> 验 len 范围 -> 算 data CRC 比对 */结构体本身要注意对齐:ParamHeader是 12 字节,不是 8 的倍数,直接当双字写会踩对齐坑。常见做法是把它__attribute__((aligned(8)))或手动补到 16 字节再写。CRC 用软件查表或硬件 CRC 外设都行,关键是写入和读取用同一套算法、同一个初值,否则永远对不上。版本号的作用是结构升级时能识别老数据并做迁移,而不是直接当垃圾丢掉。
验证方法我固定走三步:正常写读比对、模拟掉电(写到一半手动复位)后上电读、连续擦写一百次看是否稳定。第三步能暴露擦写寿命和标志清理的问题,别省。从那以后我每次动内部 FLASH 相关代码,都强制先跑这三步再合入,吃过太多次“单次测试通过、批量出货翻车”的亏。希望这份拆解帮到你,资源包里的两套写法对照着看,比单啃手册快得多。
本文还有配套的精品资源,点击获取