3. Flash编程原理:STM32 Flash的擦除、写入、读取操作,以及编程时序和电压要求
STM32 内部 Flash 是固件升级、参数存储和启动引导的核心载体。理解其编程原理,是设计可靠 OTA 升级方案的前提。本章将深入剖析 STM32 Flash 的物理特性、擦除/写入/读取操作的本质区别、编程时序约束以及电压要求,帮助你在模型更新方案中规避底层风险。
上图展示了 STM32 Flash 编程的三个核心维度:存储单元结构、操作链路和时序电压约束。下面逐一展开。
3.1 Flash 存储单元结构与操作本质
核心原理: STM32 内部 Flash 基于 NOR 型浮栅晶体管技术。每个存储单元通过控制栅和浮栅之间的电荷存储来表征逻辑状态。出厂时所有位均为逻辑 1(0xFF)。编程操作只能将 1 变为 0,而将 0 恢复为 1 的唯一方式是执行擦除操作。这一特性决定了 Flash 的读写策略:先擦除,后写入。
STM32 的 Flash 组织为多个扇区(Sector),不同型号扇区大小不同。以 STM32F103 系列为例,主存储区分为 4 个 16KB 扇区、1 个 64KB 扇区和 7 个 128KB 扇区。擦除操作的最小单位是扇区,而写入操作的最小单位是半字(16 位)或字(32 位),取决于具体型号和配置。
| 操作类型 | 最小单位 | 数据方向 | 典型耗时 | 注意事项 |
|---|---|---|---|---|
| 擦除 (Erase) | 扇区 (Sector) | 全部置 0xFF | 20ms ~ 2s(视扇区大小) | 擦除期间不可中断,否则数据损坏 |
| 写入 (Program) | 半字/字 (16/32bit) | 1 → 0 转变 | 约 20~50μs/字 | 写入前必须已擦除 |
| 读取 (Read) | 字节/半字/字 | 直接寻址 | 零等待或 1~3 等待周期 | 与 CPU 时钟频率相关 |
关键认知: Flash 的“写”并不是覆盖写。如果你向一个已写入 0x5A 的地址再次写入 0xA5,结果不是 0xA5,而是 0x00(因为 0x5A & 0xA5 = 0x00)。因此,任何修改操作都必须先擦除整个扇区,再重新写入完整数据。
3.2 擦除操作:流程与风险控制
操作流程: STM32 标准库和 HAL 库均提供了封装好的擦除函数,但底层原理一致。以 HAL 库为例,擦除一个扇区的典型流程如下:
- 检查 Flash 是否处于忙状态(BSY 位)。
- 解锁 Flash 控制器(写入 KEY1 和 KEY2 到 FLASH_KEYR)。
- 设置 FLASH_CR 寄存器的 SER 位(扇区擦除)和选择扇区号。
- 设置 STRT 位启动擦除操作。
- 等待 BSY 位清零,检查 EOP(结束操作)标志。
- 重新锁定 Flash 控制器。
案例拆解: 某物联网设备在 OTA 升级时,需要擦除 128KB 的应用程序扇区。工程师使用 HAL_FLASHEx_Erase() 函数,但未检查返回值。结果在擦除过程中,系统因看门狗超时复位,导致 Flash 处于半擦除状态,固件损坏。复盘发现:擦除 128KB 扇区耗时约 1~2 秒,远超看门狗默认超时时间。解决方案是:在擦除前暂停看门狗,或使用独立看门狗并延长超时时间,同时增加擦除完成标志位。
风险提示: 擦除操作不可中断。如果在擦除过程中发生复位或掉电,该扇区可能处于不确定状态(部分 0、部分 1)。因此,OTA 设计中必须使用双备份方案或至少保留一个可用的引导扇区。
3.3 写入操作:编程时序与数据对齐
核心原理: 写入操作本质上是将数据“与”到目标地址。STM32 的 Flash 编程接口要求数据宽度为半字(16 位)或字(32 位),且地址必须对齐。写入时序由硬件自动控制,但软件必须遵循以下步骤:
// HAL 库写入示例(以 STM32F4 为例)
uint32_t address = 0x08020000; // 目标地址
uint64_t data = 0x123456789ABCDEF0;
HAL_FLASH_Unlock();
FLASH_WaitForLastOperation(1000); // 等待上次操作完成
FLASH_Program_Word(address, (uint32_t)data);
FLASH_WaitForLastOperation(1000);
HAL_FLASH_Lock();
时序要求: 写入操作期间,CPU 会暂停执行 Flash 中的代码(因为 Flash 控制器忙)。如果代码本身运行在 Flash 中,写入过程中会出现短暂的“卡顿”。对于实时性要求高的系统,应将写入函数放在 RAM 中执行,或使用 Flash 双体结构(如 STM32F7/H7 系列)。
| 参数 | 最小值 | 典型值 | 最大值 | 说明 |
|---|---|---|---|---|
| VDD 电压 | 2.0V | 3.3V | 3.6V | 低于 2.0V 时禁止编程 |
| 编程时间 (半字) | — | 20μs | 50μs | 与温度、电压相关 |
| 擦除时间 (扇区) | 20ms | — | 2s | 大扇区耗时更长 |
| 等待周期 (WS) | 0 | — | 3 | 由 HCLK 频率决定 |
3.4 读取操作:等待周期与性能优化
核心原理: 读取 Flash 是 CPU 取指和数据的必经之路。STM32 的 Flash 接口支持预取缓冲和等待周期(Wait States)配置。当 CPU 主频提高时,Flash 读取速度跟不上 CPU 速度,必须插入等待周期。例如,STM32F103 在 72MHz 主频下需要 2 个等待周期,而 STM32F407 在 168MHz 下需要 5 个等待周期。
判断方法: 等待周期配置错误会导致程序运行异常或随机死机。正确做法是:在系统时钟初始化时,先设置 Flash 等待周期,再切换系统时钟源。HAL 库的 HAL_RCC_ClockConfig() 函数会自动处理这一顺序。
经验总结: 在 OTA 升级中,写入新固件时建议将 Flash 等待周期临时调大(如从 2 调到 3),以确保写入操作的稳定性。写入完成后再恢复原配置。虽然会略微降低性能,但能显著减少写入错误。
3.5 电压要求与低电压保护
核心原理: Flash 编程对供电电压有严格要求。VDD 低于 2.0V 时,Flash 控制器可能无法产生足够的编程电压,导致写入不完整或数据保持时间缩短。STM32 内置了 低电压检测器(LVD),当 VDD 降至阈值以下时,可产生复位或中断。
实战场景: 某便携设备使用锂电池供电,OTA 升级时电池电量不足,电压跌至 2.8V。虽然高于 2.0V 的最低要求,但写入过程中电池内阻导致瞬时压降,实际电压可能低于 2.0V,造成 Flash 写入失败。解决方案:
- 升级前检查 VDD 电压,低于 3.0V 时禁止升级。
- 写入期间关闭大电流外设(如射频模块、LED 驱动)。
- 使用外部电容或电源管理芯片稳定电压。
严重警告: 在电压不稳定的情况下进行 Flash 编程,不仅可能导致本次写入失败,还可能缩短 Flash 的寿命(数据保持时间从 10 年降至数月)。对于关键设备,务必增加电压监测和掉电保护逻辑。
3.6 常见误区与复盘总结
| 误区 | 后果 | 正确做法 |
|---|---|---|
| 直接向已写入的地址再次写入 | 数据变成两者相与的结果 | 先擦除整个扇区再写入 |
| 忽略擦除时间,看门狗超时 | 系统复位,Flash 半擦除 | 擦除前暂停看门狗或延长超时 |
| 写入时未检查 BSY 标志 | 写入冲突,数据错误 | 每次操作前等待 BSY 清零 |
| 电压不足时强行编程 | 写入失败,Flash 寿命缩短 | 增加电压检测,低于阈值禁止操作 |
| 等待周期配置错误 | 随机死机、程序跑飞 | 时钟切换前正确配置 WS |
复盘总结: Flash 编程看似简单,实则暗藏诸多陷阱。设计 OTA 升级方案时,必须将 Flash 操作视为一个完整的状态机:从解锁、擦除、写入、校验到重新锁定,每一步都要有超时保护和错误处理。建议在工程中封装统一的 Flash 驱动层,屏蔽底层寄存器操作,并提供以下接口:
Flash_EraseSector(sector_id)— 带超时和状态返回Flash_WriteData(addr, buf, len)— 自动处理对齐和分页Flash_ReadData(addr, buf, len)— 支持任意地址读取Flash_Verify(addr, buf, len)— 写入后校验