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)大概需要几十到几百毫秒。我在项目中遇到过,有人没注意这个时间,导致看门狗复位了,整个升级过程卡死。嗯,这是个经典坑。
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,就是告诉编译器:“别偷懒,每次都给我老老实实去读。”
4.4 保护机制:别让Bug把Bootloader自己擦掉了
这是个大问题。Bootloader本身也住在Flash里。如果升级过程中,一个指针跑飞了,把Bootloader所在的Sector给擦掉了,那MCU就变砖了。
所以,MCU厂商都提供了Flash保护机制。常见的有两种:
- 写保护(Write Protection):可以针对特定的Sector,禁止写入和擦除。一旦使能,除非复位或重新配置,否则无法修改。
- 读保护(Read Protection):防止别人通过调试接口(如SWD/JTAG)读取你的固件。这个对保护知识产权很有用。
我个人习惯,在Bootloader启动时,会立即把Bootloader所在的Sector设置为写保护。这样,即使应用程序的代码有Bug,也擦不掉Bootloader。
4.5 磨损均衡:Flash不是无限寿命的
Flash的擦写次数是有限的。NOR Flash通常支持10万次擦写,NAND Flash更少,只有几千到几万次。如果你的Bootloader需要频繁记录参数(比如系统运行日志、校准数据),那同一个Sector很快就会被写坏。
怎么办?磨损均衡(Wear Leveling)。说白了,就是不要把数据总写在一个地方,而是分散到多个Sector里。
我分享一个简单的实现思路:
- 把Flash分成多个Sector,比如4个。
- 每次写数据时,先检查当前Sector是否快满了。
- 如果快满了,就切换到下一个Sector。
- 记录一个索引,标明当前使用的是哪个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倍。
好了,Flash操作这块,咱们就聊到这儿。记住:擦除是前提,写入要解锁,读取要加volatile,保护要到位,磨损均衡不能省。下一章,咱们会把这些操作整合到Bootloader里,实现真正的固件升级流程。