十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32L4内部Flash读写实战:原理、代码与避坑指南

STM32L4内部Flash读写实战:原理、代码与避坑指南 简介针对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或者2KBF4系列的页大小更是有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。所以如果某个地址区域之前已经写过数据直接再写新数据是写不进去的必须先擦除再写入。擦除的最小单位是页PageL4系列的页大小是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;如果你要用页擦除TypeErase填FLASH_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 1Keil/IAR下载固件时都会弹窗报错。很多人不知道这个保护是软件设置的一时半会儿找不到解锁入口。ST-Link Utility或STM32CubeProgrammer里可以执行全片擦除来解除RDP保护但会清空整个芯片数据。调试阶段我建议保持RDP为Level 0量产烧录时再考虑开保护。还有一个常见问题是开了看门狗之后擦写Flash时间太长导致看门狗复位。如果你在真机上跑擦写多页Flash耗时可能上百毫秒这段时间喂狗的任务被卡住了看门狗就响了。解决方案就是在擦写前暂停看门狗或者把Flash操作拆分成小块在擦写间隙喂狗。后者实现更优雅但要注意关中断的时间窗口控制。5. 从模块到产品级的升级建议5.1 磨损均衡与掉电保护的设计Flash的擦写寿命是有限的L4系列的官方数据标称一般是10万次擦写P/E Cycles。10万次听起来不少但如果你每秒都写一次参数不到三天就报废了。所以做参数存储功能时“磨损均衡”必须安排上。我常用的一个做法是循环写入策略在Flash里规划一个比较大的存储区比如8KB4页每次写数据写到新地址而不是覆盖固定地址。读到数据时遍历找到最新的一份。等整个区域写满之后再擦除一遍重新从头写。这样总擦写次数被分散到了多个页寿命乘以页数。掉电保护则更依赖软件设计。关键参数可以考虑交替双备份写入前先备份旧值写入新值后校验校验失败就回滚旧值。我在实际项目里就是两步写入先写一个固定的头标记比如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的退耦电容很多莫名其妙的写数据错误能直接消失。本文还有配套的精品资源点击获取
返回列表