Flash存储器基础:物理特性、读写擦除与磨损均衡
做嵌入式这么多年,我接触过的Flash少说也有几十种。从NOR到NAND,从SPI到并行,形态各异但底层逻辑相通。今天咱们就把它掰开揉碎,看看Flash到底是怎么工作的。
Flash的物理特性
Flash存储器的基本单元是浮栅晶体管。说白了,就是在一个普通MOS管的栅极下面,多了一层被氧化物包裹的浮栅。这层浮栅可以存储电荷,电荷的存在与否就代表了0和1。
我个人习惯把Flash的物理特性归纳为三点:
- 非易失性:掉电不丢数据。浮栅里的电荷被氧化物包裹,理论上可以保持10年以上。
- 块结构:Flash不能像RAM那样按字节改写。它被划分为多个块(Block),每个块又包含若干页(Page)。
- 有限寿命:每次擦除操作都会对氧化物造成损伤。NOR Flash一般10万次,NAND Flash在1万到10万次之间。
关键认知:Flash的读、写、擦除是三种完全不同的操作。读最快,写次之,擦除最慢。而且擦除必须按块进行,不能只擦一个字节。
我在项目中遇到过一个问题:某款产品用了3年突然无法启动。排查下来,是Bootloader所在的Flash块擦写次数超标了。嗯,这就是没理解Flash物理特性的后果。
读写擦除操作详解
咱们以最常见的SPI NOR Flash为例,看看这三种操作到底怎么玩。
读操作
读操作最简单。你给Flash发一个读命令(通常是0x03或0x0B),再给个地址,它就把数据吐出来。读操作不改变Flash内部状态,所以没有寿命限制。你读一亿次也没问题。
// SPI NOR Flash 读操作示例
uint8_t read_byte(uint32_t addr) {
uint8_t cmd = 0x03; // 读命令
uint8_t data;
CS_LOW();
spi_transfer(cmd);
spi_transfer((addr >> 16) & 0xFF);
spi_transfer((addr >> 8) & 0xFF);
spi_transfer(addr & 0xFF);
data = spi_transfer(0x00); // 读一个字节
CS_HIGH();
return data;
}
写操作
写操作就麻烦多了。Flash只能把1写成0,不能把0写成1。要把0变回1,必须擦除。所以写操作之前,目标区域必须是已擦除状态(全1)。
写操作以页为单位,常见的页大小是256字节。写之前要先发写使能命令(0x06),然后发页编程命令(0x02),再跟上地址和数据。
避坑指南:我曾经在量产测试时发现,一批板子的Flash写入总是失败。查了两天才发现,是写操作前忘了检查Flash的忙状态。写入命令发出后,Flash需要时间把数据真正存进去,这段时间你不能发新命令。一定要轮询状态寄存器,等它不忙了再继续。
擦除操作
擦除是Flash最耗时的操作。擦除一个64KB的块,可能需要几百毫秒甚至几秒。擦除命令也分几种:
| 擦除类型 | 命令码 | 典型耗时 | 适用场景 |
|---|---|---|---|
| 扇区擦除(4KB) | 0x20 | 45ms | 小范围更新 |
| 块擦除(32KB) | 0x52 | 120ms | 中等范围 |
| 块擦除(64KB) | 0xD8 | 250ms | 大范围更新 |
| 整片擦除 | 0xC7 | 数秒 | 出厂初始化 |
你想想看,如果Bootloader升级时用整片擦除,那得等好几秒。用户可能以为设备死机了。所以实际项目中,我通常只用扇区擦除,一次只擦4KB,配合进度条给用户反馈。
Flash磨损均衡概念
磨损均衡(Wear Leveling)这个概念,最早是从NAND Flash那边来的。NOR Flash虽然寿命长一些,但在Bootloader场景下,频繁更新的分区同样面临这个问题。
说白了,磨损均衡就是不让某个块被频繁擦写,而其他块闲着。把擦写操作均匀分布到所有块上。
为什么需要这个?我举个例子:
- Bootloader的参数区,每次启动都可能写一次
- 日志区,每记录一条日志就写一次
- OTA升级的临时存储区,每次升级都擦写
如果不做均衡,参数区可能一年就报废了,而其他区还是全新的。
常见的磨损均衡策略
我总结了几种常用的策略,按复杂度排序:
- 静态映射:把固定地址映射到不同物理块。简单但不够灵活。
- 动态映射:每次写操作都找擦写次数最少的块。需要维护一张映射表。
- 日志结构:所有写操作都追加到空闲块,旧块标记为无效。垃圾回收时再擦除。
实战建议:在Bootloader场景下,我推荐用动态映射。因为Bootloader的代码量不大,参数区通常只有几个块。维护一张小映射表,开销可以接受。我曾经在一个STM32项目上实现了这个方案,映射表放在Flash的最后两个块,每次启动时加载到RAM里。
Flash内部结构示意图
下面这张图展示了Flash存储器的层级结构,从芯片到存储单元:
Bootloader场景下的Flash分区策略
讲完基础,咱们回到Bootloader这个场景。我一般把Flash分成这么几个区:
| 分区名称 | 起始地址 | 大小 | 擦写频率 | 磨损均衡需求 |
|---|---|---|---|---|
| Bootloader区 | 0x000000 | 64KB | 几乎不擦写 | 无 |
| 参数区 | 0x010000 | 64KB | 每次启动 | 高 |
| 应用区A | 0x020000 | 512KB | OTA升级时 | 低 |
| 应用区B | 0x0A0000 | 512KB | OTA升级时 | 低 |
| 日志区 | 0x120000 | 128KB | 频繁 | 高 |
参数区和日志区是磨损均衡的重点。我习惯在参数区用动态映射,每次写参数都换一个页。日志区用日志结构,只追加不覆盖,满了再整体垃圾回收。
重要提醒:磨损均衡不是万能的。它只能延长Flash寿命,不能消除物理限制。设计时一定要留足余量。我一般按理论寿命的20%来设计,比如标称10万次,我就按2万次来规划。这样即使有意外情况,也不会在保修期内出问题。
好了,Flash的基础知识就聊到这儿。这些概念在后续的CRC校验实现和分区策略验证中都会用到。特别是磨损均衡,在Bootloader的可靠性和寿命设计中,是个绕不开的坎儿。