4. Flash存储器操作:擦除、写入、读取、保护与磨损均衡

好,咱们进入第四章。Flash操作,这可以说是Bootloader的“心脏”。你想想看,Bootloader的核心任务之一,就是把新固件写到Flash里。如果这块搞不定,那整个升级流程就全垮了。

我个人习惯,在讲任何Flash操作之前,都会先强调一点:Flash不是RAM。它不能像SRAM那样随便写。它有自己的一套脾气,你得顺着它来。嗯,咱们今天就把它捋顺了。

4.1 Flash擦除:为什么不能直接覆盖写?

很多刚入行的朋友会问我:“我直接往Flash地址写数据不行吗?” 不行。绝对不行。

Flash存储单元的特性决定了,它只能从“1”变成“0”。如果你想写一个字节,比如把0xFF改成0x00,那没问题。但如果你想从0x00改回0xFF,那就办不到了。因为从“0”变回“1”,需要一次擦除操作。

擦除操作,说白了就是把一大块区域(通常是一个Sector或一个Page)全部复位成0xFF。这个过程很慢,以STM32为例,擦除一个Sector(16KB)大概需要几十到几百毫秒。我在项目中遇到过,有人没注意这个时间,导致看门狗复位了,整个升级过程卡死。嗯,这是个经典坑。

关键点: Flash写入前,必须先擦除。擦除的最小单位是Sector/Page,不是字节。

4.2 Flash写入:按字还是按页?

擦除完了,接下来就是写。写入操作,通常可以按字节、半字(16位)或字(32位)进行。但要注意,不同MCU的Flash控制器,支持的写入粒度不一样。

我建议,尽量使用MCU支持的最大写入粒度。比如,如果你的MCU支持32位写入,那就别用8位。为什么呢?因为写入操作本身也耗时,32位写入和8位写入的时间差不多,但数据量差了4倍。效率更高。

这里有个代码示例,展示在STM32上如何写入一个字:

// 假设我们已经擦除了目标Sector
uint32_t *flash_addr = (uint32_t *)0x08010000;
uint32_t data = 0x12345678;

// 解锁Flash控制器
HAL_FLASH_Unlock();

// 清除标志位
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPERR);

// 写入一个字
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, (uint32_t)flash_addr, data);

// 锁定Flash控制器
HAL_FLASH_Lock();

你看,代码很简单。但有个细节:写入前一定要解锁。Flash控制器默认是锁住的,防止意外写入。解锁操作通常需要写入一个特定的Key序列。嗯,这个Key序列如果写错了,MCU可能会直接HardFault。我曾经手抖写错过一次,排查了半天。

4.3 Flash读取:直接寻址就行

读取Flash就简单多了。它和读取RAM一样,直接通过地址访问就行。不需要解锁,也不需要任何特殊操作。

uint32_t value = *((volatile uint32_t *)0x08010000);

这里我加了个volatile关键字。为什么?因为编译器可能会优化掉对Flash的读取,尤其是你在循环里反复读同一个地址时。加上volatile,就是告诉编译器:“别偷懒,每次都给我老老实实去读。”

小技巧: 读取Flash时,尽量使用32位对齐的地址。非对齐访问虽然也能工作,但会触发异常处理,性能会下降。

4.4 保护机制:别让Bug把Bootloader自己擦掉了

这是个大问题。Bootloader本身也住在Flash里。如果升级过程中,一个指针跑飞了,把Bootloader所在的Sector给擦掉了,那MCU就变砖了。

所以,MCU厂商都提供了Flash保护机制。常见的有两种:

  • 写保护(Write Protection):可以针对特定的Sector,禁止写入和擦除。一旦使能,除非复位或重新配置,否则无法修改。
  • 读保护(Read Protection):防止别人通过调试接口(如SWD/JTAG)读取你的固件。这个对保护知识产权很有用。

我个人习惯,在Bootloader启动时,会立即把Bootloader所在的Sector设置为写保护。这样,即使应用程序的代码有Bug,也擦不掉Bootloader。

警告: 写保护一旦使能,你自己也写不了。所以,如果你需要更新Bootloader本身,必须先解除保护。这通常需要通过全片擦除或特殊命令来实现。设计时一定要留好退路。

4.5 磨损均衡:Flash不是无限寿命的

Flash的擦写次数是有限的。NOR Flash通常支持10万次擦写,NAND Flash更少,只有几千到几万次。如果你的Bootloader需要频繁记录参数(比如系统运行日志、校准数据),那同一个Sector很快就会被写坏。

怎么办?磨损均衡(Wear Leveling)。说白了,就是不要把数据总写在一个地方,而是分散到多个Sector里。

我分享一个简单的实现思路:

  1. 把Flash分成多个Sector,比如4个。
  2. 每次写数据时,先检查当前Sector是否快满了。
  3. 如果快满了,就切换到下一个Sector。
  4. 记录一个索引,标明当前使用的是哪个Sector。

这样,每个Sector的擦写次数就平均了。整个Flash的寿命也就延长了。

举个例子,假设你有一个参数需要每秒更新一次:

Sector编号 擦写次数(无磨损均衡) 擦写次数(有磨损均衡)
Sector 0 100,000次(很快报废) 25,000次
Sector 1 0次 25,000次
Sector 2 0次 25,000次
Sector 3 0次 25,000次

你看,总寿命从10万次变成了40万次。虽然每个Sector的寿命没变,但整体寿命翻了4倍。

避坑指南: 我曾经在一个项目中,为了省事,把参数直接写在一个固定地址。结果产品用了半年,那个Sector就坏了。后来加了磨损均衡,问题才解决。所以,如果你的产品需要频繁写Flash,千万别偷懒。

好了,Flash操作这块,咱们就聊到这儿。记住:擦除是前提,写入要解锁,读取要加volatile,保护要到位,磨损均衡不能省。下一章,咱们会把这些操作整合到Bootloader里,实现真正的固件升级流程。