
简介面向采用意法半导体STM32L4系列微控制器的嵌入式开发者这套内部闪存读写驱动方案已在项目中调通核心代码基于LL库与寄存器操作编写并在具体芯片型号上完成验证若切换到HAL库环境只需调整对应的宏定义即可复用。压缩包内共包含两个文件分别是一个C源文件和一个头文件整体体积仅2KB代码量精简便于直接嵌入现有工程并按需裁剪。作者还记录了后续适配其他型号芯片时的关键排错经验部分L4系列芯片的闪存页码不连续按默认逻辑擦页会失败修改擦页函数中的页码号即可解决。这一提醒对于使用L4x1系列或者打算在不同型号间移植代码的开发者尤其实用可减少反复排查的时间。目前已有1935人学习下载适合需要快速实现闪存读写功能的初中级嵌入式工程师参考。1. 内部FLASH不是黑匣子一个能把STM32L4读写链路跑通的Demo做Bootloader升级或者掉电参数存储时大部分人第一次碰内部Flash都会被擦写翻车劝退写进去读出来是0xFF擦除时程序卡死严重时仿真器都连不上芯片。这份基于STM32L4xx的读写内部Flash Demo用LL库和寄存器操作两条路线把解锁、页擦除、64位双字编程、读回校验这一整条链路全部打通——下载后烧录就能看到Flash地址从0xFF变成写入的测试数据全程有串口打印和错误码不用靠猜。适合正准备做OTA、想存校准则参数、或者刚接触L4系列想搞懂Flash底层时序的开发者。项目不大但每个环节都是做产品前必须趟平的坑。2. 项目形态与技术选型LL库、寄存器与工程结构2.1 这个Demo到底做了什么先明确这份资源不是高大上的完整工程框架而是一组聚焦的Flash读写工具代码加一个验证入口。它把操作内部Flash这件事拆成五个独立动作初始化解锁、页擦除、64位编程写入、读回校验、结果打印。测试地址默认放在0x08008000这是L4系列内部Flash主存储区的用户可用区域0x08000000起始L4标准页大小2KB不同型号容量不同避开低地址的启动代码区不容易把系统搞挂。执行流程很直白上电后先调Flash_Init解锁FLASH控制逻辑然后擦除测试地址所在页紧接着写入一个64位的测试数据再把这个地址上的内容读回来逐字比对串口把每一步操作的状态打出来。成功时能看到写入值和读回值完全一致失败时能看到具体错误码对齐错误、校验失败、BSY超时而不是面对一个黑匣子干瞪眼。这里有一个很容易误解的地方很多人以为写内部Flash像写SRAM那样直接赋值就行其实Flash必须先擦除再编程而且擦除的粒度是一整页不能像SRAM那样按字节清零。Demo把“擦除”和“编程”分成两个函数就是这个原因后续做Bootloader或参数存储时这两个动作也必须分开控制——否则你会在“为什么写入前要擦一整页”这个问题上浪费一整天。2.2 为什么选LL库而不是HAL选型原因值得多说两句。L4系列的官方驱动包里同时提供HAL库和LL库HAL库是厚而重的全功能封装初始化外设确实方便但Flash擦写这种时序敏感操作HAL库会在里面插入各种状态检查和超时等待出了问题反而不容易定位到硬件行为。LL库只做一层很薄的寄存器映射封装保留底层控制感代码精简、执行路径短适合用来理解Flash时序。对比项HAL库LL库寄存器直操作接口抽象高全功能封装低寄存器一一对应无封装代码体积大带超时和状态机小接近底层最小出错定位困难状态被封装容易寄存器可见最直接适用场景应用逻辑驱动外设时序敏感、学习底层极致精简或调试疑难问题三个档次各有用途。项目赶进度直接HAL想搞明白Flash的擦写时序和避坑点建议用LL库跟着走一遍寄存器流程。这份资源把LL库和寄存器两版都提供了同一份工程里能对照着看——LL库里某个操作到底改的是哪个寄存器翻一下寄存器版代码就清楚了这种双视角结构在网上的例程里很少见。2.3 工程目录结构与关键文件解压后是标准的STM32工程布局没有花哨的文件夹。核心代码集中在应用层两个文件里flash_if.c是Flash读写功能实现main.c负责调用顺序和串口打印。驱动层用L4系列的LL库和HAL库同包CMSIS提供寄存器地址定义。文件职责Core/Src/main.c调用Flash_Init、页擦除、双字写入、读回校验串口打印结果Core/Src/flash_if.cFlash解锁、擦除、编程、校验函数实体Core/Inc/flash_if.h函数声明与错误码宏定义Drivers/STM32L4xx_HAL_DriverLL库源码提供LL_FLASH_xxx接口Drivers/CMSIS芯片寄存器地址与位定义移植到自己板子上的时候注意LL库的函数签名在不同型号上有细微差别比如Bank枚举在部分L4型号上叫LL_FLASH_BANK_1某些衍生型号可能带LL_FLASH_BANK_ALL个别早期版本LL_FLASH_EraseFlashPage甚至只有地址参数没有Bank参数。移植时先打开头文件看一眼具体枚举和函数原型别盲抄。另外一个通用习惯在自己工程的芯片型号头文件里确认FLASH_PAGE_SIZE这个宏L4系列一般是2048个别型号有差异页大小直接决定了擦除地址要对齐到哪个边界。3. 核心模块拆解一次写入从解锁到读回的完整链路3.1 解锁KEYR密钥与FLASH_InitFLASH控制器在复位后处于锁定状态所有修改CR寄存器控制寄存器的操作都会被忽略。必须先往KEYR密钥寄存器依次写入两个固定的32位密钥——0x45670123和0xCDEF89ABFLASH才解锁。LL库把这步封装成了LL_FLASH_Unlock()反汇编看到的实际就是连续两次写KEYR寄存器。void Flash_Init(void) { if (LL_FLASH_IsActiveFlag_BSY()) { LL_FLASH_ClearFlags(LL_FLASH_FLAG_ALL); // 清掉残留错误标志 } LL_FLASH_Unlock(); // 连续写两次KEYR完成解锁 LL_FLASH_EnablePrefetch(); // 打开预取缓冲 }逻辑说明Flash_Init在系统上电后调用一次。第一段先检查BSY忙标志如果上一次异常复位留下了未完成的操作先清标志避免后续轮询读到脏状态。LL_FLASH_Unlock是解锁入口把两把密钥按顺序写进KEYR写错顺序或只写一把都会被硬件忽略。EnablePrefetch打开预取缓冲Flash读取速度会快一些L4跑较高系统时钟时属于推荐配置。参数说明LL_FLASH_IsActiveFlag_BSY不需要参数读的是FLASH_SR寄存器的BSY位LL_FLASH_ClearFlags的参数是位掩码写成LL_FLASH_FLAG_ALL表示把错误标志全部清掉。Demo为了上手简单用了全清实际产品里建议先读SR判断是什么错误再决定怎么清直接全清会把有效诊断信息抹掉。解锁这个动作是关键前提它决定了后续所有CR操作是否有效。常见的现象是没有解锁就调用擦除函数函数返回正常但Flash内容一点没变或者写KEYR的时序不对FLASH控制器直接忽略后续操作。遇到这种“擦了没反应”的问题先检查有没有调Flash_Init别急着怀疑硬件坏了。3.2 页擦除先擦后写2KB一页L4系列的Flash按页管理标准页大小2KB。擦除的最小单位是页无法按字节或半字擦除擦除后这一页内容全部变成0xFF这是Flash的物理特性。写入操作只能把1写成0不能把0写成1所以写入之前必须先擦除目标页否则写操作就是在旧数据上叠加结果必然错乱。#define FLASH_TEST_ADDR 0x08008000UL // 测试用目标地址 uint8_t Flash_ErasePage(uint32_t page_addr) { while (LL_FLASH_IsActiveFlag_BSY()); // 等待上一条Flash操作结束 LL_FLASH_EraseFlashPage(LL_FLASH_BANK_1, page_addr); // L4页擦除置PER模式 写AR目标地址 拉STRT触发LL库一次完成 while (LL_FLASH_IsActiveFlag_BSY()); // 等待本次擦除完成 return FLASH_IF_OK; }逻辑说明函数入参是目标页的起始地址不是页编号LL库内部会按地址换算到具体页。第一个while循环做互斥保证上一个Flash操作可能是编程也可能是擦除彻底结束之前新的操作指令不会被覆盖。LL_FLASH_EraseFlashPage内部做了三件事把CR寄存器置为页擦除模式、把目标地址写入AR寄存器、把STRT位拉起开始擦除。第二个while循环等BSY清零擦除过程一般需要十几毫秒量级阻塞轮询是合理的因为擦除期间Flash控制器的状态机就是忙的做不了别的事。参数说明LL_FLASH_BANK_1是Bank编号枚举L4系列地址空间分Bank1和Bank20x08000000到0x0807FFFF一般是Bank1。传入的page_addr必须对齐到页边界即2KB整数倍如果地址落在页中间硬件行为未定义。返回FLASH_IF_OK表示擦除正常完成实际擦除失败的场景多发生在电压跌落或温度异常时正常室温调试很少出现。不少人在这步翻车的原因是传了页内偏移地址而不是页起始地址比如往0x08008100擦除实际擦掉的是0x08008000这一页看起来像“擦错页了”。更隐蔽的是往Bank2的地址擦除代码却按Bank1的逻辑校验地址合法但数据和预期对不上。第4章会展开说这类问题。3.3 64位编程对齐、PGSIZE与LL_FLASH_Program擦除完就能写入了。L4系列的Flash编程支持三种宽度32位编程、64位双字编程和快速编程。最常用的是64位编程因为L4的Flash编程物理单元是双字8字节用32位编程反而要按硬件规则拆分成对的双字操作更容易踩对齐的坑。这个Demo用的就是64位编程。uint8_t Flash_WriteDoubleWord(uint32_t addr, uint64_t data) { uint64_t rd 0; if ((addr 0x07U) ! 0U) { // 8字节对齐检查 return FLASH_IF_ERR_ALIGN; } while (LL_FLASH_IsActiveFlag_BSY()); LL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr, data); // 内部置PG编程模式 设置PGSIZE为64位宽度 写数据到目标地址 while (LL_FLASH_IsActiveFlag_BSY()); rd *(volatile uint64_t *)addr; // 读回校验写入结果 if (rd ! data) { return FLASH_IF_ERR_VERIFY; } return FLASH_IF_OK; }逻辑说明函数开头先做对齐检查addr低3位必须为0因为64位编程要求8字节对齐。没有对齐直接返回错误码这是避免HardFault的第一道防线。中间的LL_FLASH_Program是写入核心LL库根据传入的FLASH_TYPEPROGRAM_DOUBLEWORD把CR寄存器配置成64位编程宽度再把data写入目标地址对应的存储单元硬件检测到地址写操作后自动触发编程状态机。两个while轮询保证写前写后都处在非忙状态。写完立刻用volatile指针把该地址强制读回与原数据比对。volatile在这里很关键Flash区域在编译器视角可能被优化成从缓存读加volatile强制走总线访问实际存储单元。参数说明FLASH_TYPEPROGRAM_DOUBLEWORD是LL库枚举对应寄存器里PGSIZE字段设为64位宽度data是64位无符号数一次写入8字节。返回值三种FLASH_IF_OK、FLASH_IF_ERR_ALIGN地址未对齐、FLASH_IF_ERR_VERIFY写后读回不一致。实际写的时候发现ERR_VERIFY大概率是擦除没成功或者写入前BSY没有真正清掉回头查Flash_ErasePage的地址参数。一个容易忽略的性能点64位双字写入一次最多写8字节如果要写一大片参数区循环调用这个函数即可每次地址加8。不要试图搞一个“内存拷贝”式的大块写入Flash编程必须按硬件规定的宽度一次一次来循环本身不慢慢的是擦除那一步。3.4 读回校验与main流程LL库版本把整个链路串起来在main里长这样int main(void) { uint8_t ret FLASH_IF_OK; SYSTEM_Init(); // 时钟、串口初始化略 Flash_Init(); printf([FLASH] init done\r\n); ret Flash_ErasePage(FLASH_TEST_ADDR); printf([FLASH] erase ret%u\r\n, ret); ret Flash_WriteDoubleWord(FLASH_TEST_ADDR, 0x1122334455667788ULL); printf([FLASH] write ret%u\r\n, ret); uint64_t val *(volatile uint64_t *)FLASH_TEST_ADDR; printf([FLASH] read back: 0x%08X%08X\r\n, (uint32_t)(val 32), (uint32_t)val); while (1); }逻辑说明main先初始化系统时钟和串口然后按解锁→擦除→写入→读回的顺序执行。每一步都打印返回值读回的val应该等于0x1122334455667788。这个流程已经是一个完整的冒烟测试没有多余功能方便新手验证自己的板子是否正常。如果串口打印的读回值是0xFFFFFFFFFFFFFFFF说明擦除失败或写入没触发回头查解锁和擦除环节。参数说明SYSTEM_Init是时钟和串口的初始化入口不同开发板串口引脚不一样移植时改成自己板子的串口配置即可。printf重定向到串口的实现依赖具体IDE和库有些嵌入式printf不支持%llX格式所以代码里把64位值拆成两个32位分别打印高32位在前低32位在后这个写法在任何C库下都能用。3.5 寄存器直操作疑难问题排查的照妖镜寄存器直操作版本的意义在于透明度。LL库虽然薄毕竟还是封装真正调试疑难问题时直接看寄存器最直观。L4擦除一页的寄存器操作是这样的void Flash_PageErase_Reg(uint32_t page_addr) { while (FLASH-SR FLASH_SR_BSY); // 等不忙 FLASH-CR | FLASH_CR_PER; // 页擦除模式 FLASH-AR page_addr; // 目标页地址 FLASH-CR | FLASH_CR_STRT; // 开始擦除 while (FLASH-SR FLASH_SR_BSY); // 等擦除完成 FLASH-CR ~FLASH_CR_PER; // 退出擦除模式 } void Flash_Write64_Reg(uint32_t addr, uint64_t data) { while (FLASH-SR FLASH_SR_BSY); FLASH-CR | FLASH_CR_PG; // 编程模式 FLASH-CR | (2U FLASH_CR_PGSIZE_Pos); // 64位编程宽度 *(volatile uint64_t *)addr data; // 写入即触发编程 while (FLASH-SR FLASH_SR_BSY); FLASH-CR ~FLASH_CR_PG; // 退出编程模式 }逻辑说明寄存器版本和LL库版逻辑一一对应。FLASH_CR_PER对应页擦除模式位FLASH_CR_STRT是启动位一次擦除流程必须按“置PER→写AR→置STRT→等BSY→清PER”顺序执行不能颠倒。编程流程是“置PG→设PGSIZE→写地址触发→等BSY→清PG”。注意写入操作本身是通过对目标地址赋值触发的不是通过CR寄存器触发的这是新手最容易懵的地方看起来像普通内存赋值实际上写这一行代码的瞬间Flash控制器接管总线开始编程。参数说明FLASH_CR_PGSIZE_Pos在CMSIS头文件里定义是PGSIZE字段的偏移量2U表示64位编程宽度如果用32位编程这里要改成1U。FLASH-SR的BSY位在第16位判断忙状态就是读这一位。寄存器版本写起来啰嗦但任何异常都能在调试器里直接看到CR和SR的状态比如发现CR置了PG但SR的BSY一直为1说明状态机被中断干扰卡住了这种问题在LL库里只能靠猜在寄存器版里一眼就能看出来。4. 避坑指南Flash擦写中的常见翻车现场Flash擦写是嵌入式领域最典型的“玄学重灾区”很多问题不是逻辑写错而是对硬件行为理解有偏差。下面几条都是踩过的坑按现象→原因→解决顺序写碰到类似症状可以顺着排查。4.1 现象写入后读回全是0xFF这个是最常见的翻车现场。代码逻辑看起来完全没问题——先擦除再写入读回却是0xFFFFFFFF像是写入操作根本没有生效。原因地址参数传错了。擦除函数要求传页起始地址也就是2KB的整数倍很多人直接传目标数据的地址比如0x08008100而0x08008000这一页才是实际被擦除的页。随后写入操作写到0x08008100上这个地址刚被擦成0xFF写入的内容又被覆盖成0xFF。另一种情况是调用了擦除但擦的是Bank2的页写入却写到了Bank1两个地址看起来都合法串起来就错位了。解决擦除前打印一下页起始地址和Bank编号确认和目标地址在同一个Bank。调试器里直接看FLASH-AR寄存器的值就知道硬件实际收到的擦除地址是什么。我的习惯是定义一个宏把目标地址注释上“页起始0x08008000页内偏移0”避免过两周自己都不知道当初传的是什么。4.2 现象擦写过程中程序卡死像死机一样程序跑到Flash擦除或写入的while等BSY循环里出不来串口不再打印调试器暂停后看到程序停在轮询BSY的下一条指令。原因绝大多数是中断惹的祸。擦写Flash期间来一个中断中断服务函数里的代码正好在Flash上取指执行和正在进行的编程操作抢同一个Flash控制器。虽然L4支持RWW读-写-同时执行但条件是读和写需要在不同Bank或者代码在RAM里执行。系统默认的SysTick处理函数、串口中断都在Flash一进中断就撞车。另一个原因是擦除Bank1的同时当前程序正运行在Bank1等于在擦自己的代码区等BSY永远等不到头。解决擦写过程前后用关中断指令包住让擦写在原子环境中完成。如果擦除的不是当前执行所在Bank可以把中断服务函数和主循环代码全部搬到RAM执行。还在卡死的话确认一下当前PC指针所在的地址段是不是正好落在目标Bank里——调试器里查看PC寄存器和SP寄存器对照地址区间就能判断。4.3 现象写入后仿真器连不上芯片这个最吓人。程序跑了一次写入然后调试器再也连不上报错提示连接失败感觉芯片被写坏了。其实芯片没坏只是Flash的选项字节Option Bytes被意外改写了。原因写入地址越界写到了选项字节区域或者代码里动了RDP读保护等级。RDP等级从Level 0改成Level 1之后调试接口直接被锁死外部工具无法再读写Flash。某些非法写入操作会让芯片进入保护状态用编程器连接时握手失败。解决别慌这个状态用Boot引脚拉高配合全擦除可以救回来。把Boot0拉高重新上电芯片从系统存储器启动用配套工具执行整片擦除Mass Erase选项字节恢复成出厂值。这之后我给自己定了个规矩每次测试Flash写入代码之前先读一遍RDP等级确认是Level 0再开跑调试器连不上这种事一次就够受的了。4.4 现象写入时触发HardFault程序在执行Flash_WriteDoubleWord或者寄存器直操作版本时直接跳进HardFault_Handler。原因地址访问违规。最常见的是地址没有对齐——64位编程对8字节对齐有硬性要求地址低3位非0时总线直接报错。其次是写到了Flash地址空间的保留区域或者容量边界之外比如某型号实际只有512KB Flash地址范围0x08000000到0x0807FFFF往0x08090000写就是访问不存在的存储区总线错误。解决函数入口做对齐检查不对齐直接返回错误码而不是继续执行。写之前运行时判断目标地址是否落在Flash合法范围内编译期也可以用静态断言卡住越界地址。HardFault的现场在调试器里能看到出错地址把出错地址减去FLASH_BASE得到偏移量再对照这个型号的Flash容量能快速定位是越界还是对齐问题。4.5 现象擦写完成后系统变慢外设行为乱掉Flash读写都成功了但程序主频掉了跑起来明显变慢或者某些外设直接乱码。原因Flash等待周期LATENCY配置问题。L4系统时钟跑得比较高Flash读取需要插入等待周期才能保证时序这个等待周期存在FLASH_ACR寄存器里。某些代码在时钟初始化后把FLASH_ACR覆盖了或者低功耗模式切换时钟后没有重新配置LATENCYFlash读取时序错乱表现为整体变慢或者指令取错。解决检查时钟初始化代码确保HCLK频率和FLASH_ACR里的LATENCY匹配。L4系列手册里有对应关系表比如HCLK跑到80MHz需要4个等待周期。改完时钟树之后重新确认一遍LATENCY寄存器没有被覆盖尤其是低功耗唤醒路径上。这条问题最难排查因为写Flash本身是成功的症状出现在Flash之外遇到系统慢半拍的诡异问题先查这里。5. 二次开发把读写Demo改造成可靠的参数存储模块5.1 从裸函数升级到带校验的存储结构直接裸写Flash验证时序没问题但放进产品代码里活不过第一轮掉电测试。产品断电不会挑“你刚好写完”的时候断可能写到一半断电重启后读到的就是半截数据。改造办法是给存储区定义一个带魔数、版本、CRC的结构体每次写入前算CRC读的时候先验魔数再看CRC校验不过就回滚到备份数据。typedef struct { uint32_t magic; /* 固定魔数如 0xA5A5A5A5 */ uint32_t version; uint32_t crc; /* 对 data 数组的 CRC32 结果 */ uint8_t data[64]; /* 业务参数区 */ } param_block_t;写入流程变成先擦除目标页构造param_block_t并计算CRC再用Flash_WriteDoubleWord按64位宽度分块写完整个结构体最后读回校验。读流程变成读整块数据先检查magic再重算CRC和存储的crc比对不一致说明写入被断电打断这时用备用槽的数据恢复。这套模式就是掉电安全参数存储的最小实现比裸写可靠得多。5.2 双槽轮换与Bootloader衔接再往上走一层用两个备份槽做A/B切换固定Slot A和Slot B写之前先看当前活跃槽写到另一个槽里去写完校验通过再切换活跃标记。这样任何一次写入失败总有上一次完整数据可以恢复代价是Flash容量换可靠性而且天然实现磨损均衡避免总擦同一页把页寿命用光。L4的Flash擦写次数典型值是1万次量级用双槽能把等效寿命拉高一倍。和Bootloader衔接时有两个细节跳转到App之前必须关全局中断并把SysTick复位防止App启动时中断向量还是旧的如果App要擦除Bootloader所在Bank代码必须整个在另一个Bank或RAM里跑这条规则和4.2提到的RWW限制是同一个道理。换地址时记得同步更新链接脚本的FLASH起始地址别让App自己把自己覆盖了。整套改造做完这个Demo已经能当一个小型参数模块用。以后我每次换目标页或者动Flash选项字节都会强制走一遍“确认RDP等级→擦除→写回→读校验”的完整流程宁可多花几分钟也不想再被仿真器连不上折磨。希望帮到你。本文还有配套的精品资源点击获取