STM32固件更新中Flash Erase流程的全面技术分析
你有没有遇到过这样的场景:OTA升级包已经完整接收,校验也通过了,可一执行擦除,设备就“哑火”——再也无法启动?或者更诡异的是,擦除操作看似成功,但写入新固件后程序跑飞,调试器连不上,Bootloader彻底失联?
这不是玄学,而是Flash擦除这个看似最基础的操作,在真实硬件世界里埋下的深坑。它不像内存清零那样“写个0就行”,而是一场由电荷泵驱动、受温度电压严控、被Option Bytes锁死、还可能被自己代码误伤的精密物理过程。
今天我们就把STM32 Flash erase从数据手册的铅字里拽出来,放到示波器探头下、万用表表笔间、量产产线的温箱里,一层层剥开它的物理本质、时序逻辑与工程陷阱。不讲概念堆砌,只谈你烧录失败时真正该看哪一行寄存器、该测哪一点电压、该改哪一段HAL调用。
擦除不是“清零”,而是一次高压隧穿手术
先破一个常见误解:Flash擦除 ≠ RAM memset(0xFF)。
RAM清零是电子开关切换电平;而Flash擦除,是在微米级浮栅晶体管上,人为制造一场可控的“雪崩”——通过片内电荷泵升压至12–20 V(远高于VDD的3.3 V),强行让浮栅中存储的电子穿过10 nm厚的氧化层,泄放到衬底。这个过程不可逆,且有寿命上限:ST官方标称每个Sector最多擦写10,000次(AN4821实测老化曲线显示,超8,000次后EOP标志开始偶发丢失)。
这意味着:
- 你在Bootloader里每做一次HAL_FLASHEx_Erase_Sector(),都在给那个Sector的寿命倒计时;
- 如果OTA升级逻辑错误地反复擦同一Sector(比如因校验失败重试三次),等于直接把它送进ICU;
- 那些号称“支持无限次OTA”的产品,背后一定藏着Sector磨损均衡算法,或干脆把关键区段(如中断向量表)挪到永不擦除的Bank0固定区。
更关键的是,这场“高压手术”对供电极其敏感。RM0468白纸黑字写着:VDD必须 ≥ 2.7 V才能保证擦除可靠性。但现实是:很多工业现场电源纹波高达±150 mV,擦除瞬间电流突增50 mA,LDO输出跌落至2.55 V——此时硬件可能悄悄置位BSY,却永远不拉高EOP。你轮询10秒,它沉默10秒,最后系统卡死。
所以,真正的擦除准备,从来不只是调一个HAL函数。它始于PCB设计阶段的电源路径阻抗计算,成于Bootloader中对VDDA引脚的实时ADC采样,终于擦除前那句被很多人忽略的判断:
if (HAL_ADC_GetValue(&hadc1) < ADC_VDD_THRESHOLD) { // 比如对应2.7V的ADC值 ERROR_Handler("VDD too low for erase!"); }Sector擦除 vs Chip擦除:选错模式=自废武功
STM32提供两种擦除粒度:Sector Erase(扇区擦除)和Chip Erase(整片擦除)。别小看这个选择,它直接决定你的OTA是优雅升级,还是灾难恢复。
Sector擦除:精准外科手术
适用场景:日常固件增量更新、双Bank切换、局部配置修复。
核心约束:必须严格对齐硬件定义的Sector边界。以STM32H743为例:
- Sector0: 0x0800_0000 ~ 0x0801_FFFF(128 KB)
- Sector1: 0x0802_0000 ~ 0x0803_FFFF(128 KB)
- Sector8: 0x0810_0000 ~ 0x0813_FFFF(256 KB)
如果你传入地址0x0802_1000调用Sector擦除,硬件不会帮你“向上取整到Sector1起始”,而是静默截断为Sector1起始地址0x0802_0000。更危险的是:若你误传0x0800_0001(Sector0内偏移1字节),某些早期H7 Errata会触发BUSY错误,而FLASH_SR寄存器甚至不报WRPERR——因为地址根本没落到保护区,只是硬件拒绝响应。
正确做法永远是:
// 手动对齐到Sector起始地址(HAL库不自动做!) uint32_t sector_base = GetSectorBaseAddress(sector_num); // 自定义查找表 HAL_FLASHEx_Erase_Sector(sector_num, FLASH_VOLTAGE_RANGE_3);Chip擦除:核弹级重置
适用场景:产线首次烧录、Bootloader损坏后的强制恢复、安全擦除(需配合RDP Level 1)。
关键真相:Chip Erase不擦除Option Bytes区域(地址0x1FFF_7800起)。这是ST刻意设计的安全机制——防止用户一按“全擦除”就把写保护、RDP等级全清掉,导致芯片变砖。
但这也带来一个经典坑点:某客户在产线测试中发现,Chip Erase后读取Option Bytes,WRP0值仍是0xFFFF(全保护),以为擦除失败。其实恰恰相反——Chip Erase本就不动Option Bytes。真正要擦Option Bytes,必须走独立流程:
HAL_FLASH_OB_Launch(); // 先解锁Option Bytes编程 HAL_FLASHEx_OBErase(); // 再单独擦除而这个操作一旦执行,若RDP处于Level 2,芯片将永久锁定。所以产线工装必须带硬件跳线,确保只有在特定治具上才允许触发Option Bytes擦除。
Option Bytes不是“设置项”,而是Flash的宪法
很多开发者把Option Bytes当成BIOS里的普通配置菜单,直到某天OTA失败,才翻开手册发现:WRP(Write Protection)位是硬件级熔丝,复位即生效,且优先级高于一切软件指令。
以STM32H743为例,Option Bytes中WRP0–WRP3共4字节,每bit控制2个Sector。例如WRP0[0] = 0表示保护Sector0和Sector1(注意:0=protect,1=unprotect,反直觉!)。当你执行擦除时,硬件在FLASH_CR置位STRT前,会瞬间比对目标地址所属Sector与WRP掩码——匹配即拉高WRPERR,并立即终止整个擦除流程,连EOP都不会置位。
这就解释了为什么你看到HAL_FLASHEx_Erase()返回HAL_ERROR,但FLASH_SR里既没有BSY也没有EOP——因为操作根本没开始,就在门口被哨兵拦下了。
更隐蔽的陷阱在于:出厂默认WRP值通常是0xFFFF。这意味着所有Sector默认受保护!很多开发者用ST-Link Utility烧录完Bootloader就直接OTA,结果第一次擦除Bank2就失败——因为Sector8~15(Bank2)被默认WRP锁死了。
解决方案不是“关掉写保护”,而是精准解封:
// 仅解封Bank2对应Sector(H743中为Sector8~15) OBInit.WRPState = OB_WRPSTATE_ENABLE; OBInit.WRPSector = OB_WRP_SECTOR_8 | OB_WRP_SECTOR_9 | ... ; // 显式列出 OBInit.WRPSector |= OB_WRP_SECTOR_15; HAL_FLASHEx_OBProgram(&OBInit); HAL_FLASH_OB_Launch(); // 必须调用,否则不生效记住:每次修改Option Bytes,芯片必须复位才能生效。所以你的Bootloader OTA流程里,得预留一次“写完WRP后主动复位”的步骤,否则后续擦除依然失败。
超时监控不是锦上添花,而是生死线
擦除超时(Timeout)是现场故障率最高的环节之一。原因很现实:
- 厂家标称“单Sector最大40 ms”,但这是在25°C、VDD=3.3V、全新Flash下的理想值;
- 实际产线环境:夏季机柜内温度达75°C,擦除时间延长30%;
- 电池供电设备:VDD从3.3V跌至2.9V,电荷泵建立时间翻倍;
- Flash老化:使用5年后,擦除失败率从0.001%升至0.1%。
如果代码里还用着教科书式的轮询:
while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY)); // 危险!可能永远等待那恭喜你,设备将在某个高温午后,卡在这个while里,再也不会醒来。
真正的工业级防护,必须三重保险:
第一重:硬件级看门狗兜底
在擦除前启动独立看门狗(IWDG),超时时间设为ERASE_TIME_MAX × 3(如H743设为120 ms):
IWDG_HandleTypeDef hiwdg; hiwdg.Instance = IWDG; hiwdg.Init.Prescaler = IWDG_PRESCALER_32; // 32.768kHz / 32 = 1ms hiwdg.Init.Reload = 120; // 120ms超时 HAL_IWDG_Init(&hiwdg); // ... 执行擦除 ... HAL_IWDG_Refresh(&hiwdg); // 擦除成功后及时喂狗这样即使软件卡死,IWDG也会强制复位,避免设备“假死”。
第二重:软件超时+降频策略
轮询时用HAL_GetTick()计时,但不要用HAL_Delay()(它依赖SysTick,而擦除时可能关闭中断):
uint32_t start = HAL_GetTick(); while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY)) { if (HAL_GetTick() - start > 80) { // H743推荐80ms超时 __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL); return HAL_TIMEOUT; } __NOP(); // 空操作,避免总线冲突 }第三重:物理验证,拒绝“虚假成功”
擦除返回成功,不代表Flash单元真被清空。某些老化Flash会出现“擦除伪成功”:EOP置位了,但部分bit仍残留电荷,读出来是0xFFFFFFFE而非0xFFFFFFFF。
所以必须验证:
// 验证Sector首地址(成本最低) if (*(volatile uint32_t*)sector_base != 0xFFFFFFFFU) { return HAL_ERROR; // 物理擦除失败 } // 进阶方案:随机读取Sector内3个地址(首/中/尾)这步验证增加约200 μs耗时,但能提前拦截99%的现场升级事故。
双Bank OTA中,擦除是容错能力的分水岭
STM32H7的双Bank架构(Bank1/Bank2)常被宣传为“无缝升级”,但真相是:擦除的原子性,才是双Bank真正可靠的底层保障。
典型流程:
1. Bootloader接收新固件,存入Bank2;
2.擦除Bank2全部Sector(Sector8~15);
3. 将新固件写入Bank2;
4. 修改Option Bytes中的nSWBOOT0位,下次启动从Bank2加载。
这里的关键是第2步:Sector擦除是独立原子操作。即使Sector8擦除成功,Sector9因电压跌落失败,Sector8的数据依然完好,Bootloader可以继续尝试重擦Sector9,而不会污染Sector8。这种“失败隔离”能力,是单Bank方案(擦除整个应用区)完全不具备的。
但工程师常犯的致命错误是:在擦除Bank2前,忘记检查Bank1的Vector Table是否被意外擦除。
因为STM32H7的中断向量表默认位于0x0800_0000(Sector0),而Sector0属于Bank1。如果OTA逻辑有bug,把擦除范围错写成SECTOR_0,整个系统将立即失去中断响应能力——连串口打印都收不到。
防御方案很简单:
// 擦除前强制校验:目标地址是否落在保护白名单外? uint32_t vector_sector = GetSectorNumber(SCB->VTOR); // 获取当前VTOR所在Sector if (target_sector == vector_sector) { ERROR_Handler("Attempt to erase active Vector Table sector!"); }把这段加进你的Safe_SectorErase(),就能避开90%的“擦完就变砖”事故。
最后一句实在话
Flash擦除没有“银弹”,只有细节。
- 不是看懂了RM0468的时序图就能搞定,而是要拿示波器抓VDDA在擦除瞬间的跌落波形;
- 不是调通了HAL库函数就万事大吉,而是要在-40°C冷凝箱里连续跑72小时老化测试;
- 不是写了超时保护就高枕无忧,而是要把FLASH_SR寄存器快照存进备份SRAM,让现场工程师能拿着日志反推故障根因。
如果你正在设计一款需要5年免维护的工业网关,建议把本文提到的每一个检查点——电压采样、WRP动态读取、Sector边界对齐、IWDG兜底、物理验证——全部变成Bootloader里的强制编译断言(#error "Missing VDD check!")。
因为真正的嵌入式鲁棒性,从来不在宏大的架构图里,而在那一行行与Flash控制器搏斗的寄存器操作中。
如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。