深入解析STM32 Flash擦除机制:从F1到H7的兼容性挑战与实战设计
你有没有遇到过这样的问题?——在STM32F1上跑得好好的Flash擦除代码,移植到STM32F4或STM32L4后突然失败,甚至导致系统死机、程序跑飞?
这并不是偶然。尽管它们都叫“STM32”,也都用HAL库编程,但不同系列的Flash擦除行为其实大相径庭。如果你不了解这些底层差异,轻则数据丢失,重则固件损坏。
本文将带你穿透数据手册的厚厚文档,以一线工程师的视角,深入剖析STM32主流型号在Flash擦除操作中的核心差异与陷阱,并提供一套真正可复用、跨平台的解决方案。
为什么同一个HAL_FLASHEx_Erase()函数,在不同芯片上表现完全不同?
我们先来看一个真实场景:
FLASH_EraseInitTypeDef init = {0}; init.TypeErase = FLASH_TYPEERASE_PAGES; init.PageAddress = 0x08008000; init.NbPages = 1; HAL_FLASHEx_Erase(&init, &error);这段代码在STM32F1上运行正常,但在STM32F4上却返回错误 —— 因为F4根本不再支持“按页地址擦除”!
这就是问题所在:
虽然ST提供了统一的HAL接口,但底层Flash架构的设计逻辑完全不同。不理解这一点,再多的封装也只是空中楼阁。
Flash擦除的本质:不是“写0”,而是物理重置
在深入对比前,我们必须明确一点:
Flash只能从1变成0(编程),不能从0变回1 —— 所以必须先擦除成全1状态。
这意味着:
- 写入新数据前,必须先擦除;
- 擦除是以“块”为单位进行的,无法只擦几个字节;
- 一旦启动擦除,CPU不能从中断跳转到该区域执行代码(否则会硬故障);
- 擦除过程不可中断,耗时几十毫秒至数百毫秒。
而正是这个“块”的定义方式,成了各系列分道扬镳的关键。
不同STM32系列的Flash结构全景图
| 系列 | 最小擦除单位 | 单位大小 | Bank数量 | 特色功能 |
|---|---|---|---|---|
| STM32F1 | 页(Page) | 1KB / 2KB | 1 | 简单直观,适合入门 |
| STM32F4 | 扇区(Sector) | 16KB ~ 128KB | 1 或 2 | 支持双Bank OTA升级 |
| STM32L4 | 页(Page) | 2KB 和 4KB 混合 | 1 或 2 | 支持RWW,低功耗优化 |
| STM32H7 | 扇区 + 超级块 | 4KB ~ 128KB 不等 | 2 | TrustZone安全隔离、高速缓存 |
看到没?最小单位有叫“页”的,也有叫“扇区”的;大小从4KB到128KB不等;有的按地址操作,有的必须指定编号……混乱程度堪比“方言大战”。
下面我们逐个拆解。
STM32F1:最简单的页式结构,却是最容易踩坑的起点
F1是很多人的STM32启蒙芯片,它的Flash结构非常直接:
- 小容量:每页1KB,共64页(64KB)
- 中/大容量:前4页各1KB,后面每页2KB(如F103RCT6有128页 × 2KB = 256KB)
擦除函数也很简单:
HAL_StatusTypeDef erase_page_f1(uint32_t addr) { HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef cfg = {0}; cfg.TypeErase = FLASH_TYPEERASE_PAGES; cfg.PageAddress = addr; // 直接传入地址 cfg.NbPages = 1; uint32_t error; HAL_StatusTypeDef status = HAL_FLASHEx_Erase(&cfg, &error); HAL_FLASH_Lock(); return status; }✅优点:直观,易于理解和实现。
⚠️坑点:地址必须对齐到页边界!如果传入0x08000001,实际擦的是整个第一页(0x08000000~0x080007FF)。更危险的是,HAL不会报错,只会默默擦掉你不想要的数据。
💡建议:永远使用宏定义页边界检查:
#define IS_PAGE_ALIGNED(addr) (((addr) & (FLASH_PAGE_SIZE - 1)) == 0)STM32F4:扇区登场,双Bank开启OTA新时代
F4开始引入“扇区”概念,并且彻底抛弃了“按地址擦除”的方式 —— 你必须告诉它:“我要擦 Sector 2”。
例如STM32F407:
- Sector 0: 16KB
- Sector 1: 16KB
- …
- Sector 5: 64KB
- Sector 6–7: 各128KB
而且注意:擦除时要指定扇区编号,而不是地址!
HAL_StatusTypeDef erase_sector_f4(uint8_t sector_num, uint8_t bank) { HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef cfg = {0}; cfg.TypeErase = FLASH_TYPEERASE_SECTORS; cfg.Sector = sector_num; cfg.NbSectors = 1; cfg.Banks = bank; cfg.VoltageRange = FLASH_VOLTAGE_RANGE_3; // 必须设置电压范围! uint32_t error; HAL_StatusTypeDef status = HAL_FLASHEx_Erase(&cfg, &error); HAL_FLASH_Lock(); return status; }🚨致命细节:VoltageRange必须根据供电电压正确设置!
- Vcc=2.7V~3.6V →FLASH_VOLTAGE_RANGE_3
- 若设错,可能导致擦除失败或写保护异常!
🧠思考:如何根据地址找到对应的扇区编号?你需要自己建一张表:
typedef struct { uint32_t start; uint32_t size; } flash_sector_info_t; static const flash_sector_info_t sectors_f4[] = { {0x08000000, 0x4000}, // 16KB {0x08004000, 0x4000}, {0x08008000, 0x4000}, {0x0800C000, 0x4000}, {0x08010000, 0x4000}, {0x08020000, 0x20000}, // 128KB // ... };然后写一个通用查找函数:
int get_sector_index(uint32_t addr) { for (int i = 0; i < ARRAY_SIZE(sectors_f4); i++) { if (addr >= sectors_f4[i].start && addr < sectors_f4[i].start + sectors_f4[i].size) { return i; } } return -1; }这才是真正的跨平台基础。
STM32L4:低功耗下的精细控制艺术
L4主打低功耗应用,比如电池供电的传感器节点。这类设备常常需要频繁保存日志或状态,因此对擦除粒度要求极高。
幸运的是,L4采用了混合页结构:
- 前8页:每页2KB(用于配置存储)
- 后续60页:每页4KB(用于程序存储)
更重要的是:支持Read-While-Write(RWW)—— 即在一个Bank擦除/编程时,可以从另一个Bank读取指令!
这意味着你可以做到:
- 在后台擦除日志区的同时继续运行主程序;
- 实现无缝固件更新;
- 提升系统响应能力。
但代价是复杂性上升:页大小不一,需精确查表定位。
此外,L4还要求:
- 擦除期间禁止进入Stop模式;
- 推荐启用SMPS(开关电源)以获得稳定Vcore;
- 使用__HAL_FLASH_CLEAR_FLAG()清除可能残留的状态标志。
STM32H7:高性能背后的超级复杂架构
如果说F1是自行车,那H7就是战斗机。
H7拥有两个独立Flash Bank(Bank1和Bank2),每个Bank最多16个扇区,总共可达2MB存储空间。扇区大小从4KB到128KB不等,布局极为灵活。
但它也带来了前所未有的挑战:
1. 地址映射复杂
Bank1:0x08000000 ~ 0x080FFFFF
Bank2:0x08100000 ~ 0x081FFFFF
你要先判断地址属于哪个Bank,再计算扇区号。
2. 缓存必须处理
H7有强大的指令缓存和预取缓冲。但在擦除前必须关闭,否则可能引发总线错误:
__HAL_FLASH_INSTRUCTION_CACHE_DISABLE(); __HAL_FLASH_DATA_CACHE_DISABLE(); __HAL_FLASH_INSTRUCTION_CACHE_RESET(); __HAL_FLASH_DATA_CACHE_RESET();擦完后再重新使能。
3. 安全特性介入
若启用TrustZone,部分Flash区域被划为“安全区”,普通代码无权访问或擦除。此时需通过安全监控服务调用擦除。
4. 电压域选择关键
H7支持多种电压范围:
-FLASH_VOLTAGE_RANGE_1: 高速模式(1.28–2.7V)
-FLASH_VOLTAGE_RANGE_3: 标准模式(2.7–3.6V)
选错会导致操作失败!
如何构建真正可移植的Flash管理模块?
面对如此复杂的局面,我们不能再靠#ifdef STM32F1xx这种野路子了。
我们需要一个抽象层(FAL),让上层应用只需调用:
int flash_erase(uint32_t addr, uint32_t len);而底层自动完成:
- 查找地址所属的扇区
- 判断是否跨多个扇区
- 调用对应型号的擦除函数
- 处理Bank切换、缓存禁用等细节
设计方案:设备描述符 + 函数指针
typedef struct { uint32_t start_addr; uint32_t size; } flash_sector_t; typedef struct { const flash_sector_t *sectors; uint32_t sector_count; int (*erase_fn)(uint32_t addr); // 擦除指定地址所在的扇区 void (*prepare)(void); // 擦除前准备(如关缓存) void (*restore)(void); // 擦除后恢复 } flash_device_ops_t;然后根据不同型号注册不同的实例:
const flash_device_ops_t flash_f4_ops = { .sectors = sectors_f4, .sector_count = 8, .erase_fn = erase_sector_by_addr_f4, .prepare = NULL, .restore = NULL, }; const flash_device_ops_t flash_h7_ops = { .sectors = sectors_h7, .sector_count = 32, .erase_fn = erase_sector_by_addr_h7, .prepare = h7_flash_prepare, .restore = h7_flash_restore, };上层统一调用:
int flash_erase(uint32_t addr, uint32_t len) { const flash_device_ops_t *ops = get_current_flash_ops(); // 动态获取当前设备配置 ops->prepare(); // 如关闭缓存 uint32_t end = addr + len; for (uint32_t a = addr; a < end; ) { const flash_sector_t *sec = find_sector_containing(a, ops); if (!sec) return -1; if (ops->erase_fn(sec->start) != 0) { ops->restore(); return -1; } a = sec->start + sec->size; // 跳到下一个扇区 } ops->restore(); return 0; }这套设计不仅解决了兼容性问题,还为未来扩展留足空间 —— 加入U5、G0等新型号,只需新增一个flash_device_ops_t实例即可。
工程师必备的五大实战技巧
永远不要假设地址对齐
使用工具函数验证:c static inline bool is_aligned_to_sector(uint32_t addr, uint32_t size) { return (addr % size) == 0; }启用看门狗防卡死
擦除可能持续上百毫秒,务必喂狗:c while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY)) { HAL_IWDG_Refresh(&hiwdg); HAL_Delay(1); }保留参数区要用链接脚本
在.ld文件中预留一块Flash不被链接器使用:ld MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1M - 4K PARAM (r) : ORIGIN = 0x080FF000, LENGTH = 4K }高频擦写要做磨损均衡
对于日志类应用,不要固定擦同一块,应轮换使用多个扇区。调试时记录擦除日志
c LOG("Erasing sector at 0x%08X, size=%dKB", addr, size>>10);
可帮助发现误擦、重复擦等问题。
写在最后:兼容性的本质是认知深度
你会发现,ST虽然提供了HAL库,但真正的差异藏在数据手册的表格里、在参考手册的角落中、在每一个未被注释的寄存器位中。
F1简单易用,却不具备现代MCU的灵活性;
F4引入双Bank,打开了OTA的大门;
L4精细化控制,服务于低功耗世界;
H7集大成者,但也把复杂性推到了极致。
作为嵌入式开发者,我们的任务不仅是“让代码跑起来”,更是要理解硬件的行为边界。
当你能在F1、F4、L4、H7之间自由切换Flash操作而不犯错时,你就不再是“调API的人”,而是真正掌控系统的工程师。
如果你正在做多型号兼容项目,或者打算开发Bootloader、OTA模块,不妨从今天开始重构你的Flash管理模块 —— 把它做成一个真正健壮、可移植的核心组件。
毕竟,可靠的固件,始于一次正确的擦除。
欢迎在评论区分享你在STM32 Flash擦除中踩过的坑,我们一起避坑前行。