3. Flash存储器原理:物理结构、读写擦除特性与分区规划
Flash 存储器,说白了就是嵌入式系统的硬盘。没有它,你的代码就没地方待。我最早接触 Flash 是在做一款工业控制器的时候,当时对它的擦写特性理解不深,结果产品量产半年后批量返修……嗯,从那以后我花了不少时间把 Flash 的脾气摸透了。
这一章我们就把 Flash 的物理结构、读写擦除特性、以及分区规划策略讲清楚。你搞懂了这些,后面做 Bootloader 升级才不会踩坑。
3.1 Flash 的物理结构
Flash 存储器的基本存储单元是浮栅晶体管。每个单元里有一个「浮栅」,被绝缘层包裹着。往浮栅里注入电荷,就代表存了 0;把电荷放掉,就代表存了 1。
为什么 Flash 能断电保存数据?因为浮栅被绝缘层包着,电荷跑不掉。理论上可以保持 10 年以上。我在项目中实测过,放了三年的板子读出来数据还在。
Flash 按结构分两大类:
| 类型 | 单元结构 | 特点 | 典型应用 |
|---|---|---|---|
| NOR Flash | 每个单元独立连接位线 | 随机读取快,可执行代码(XIP) | Bootloader、代码存储 |
| NAND Flash | 多个单元串联成页 | 密度高、成本低、写入快 | 大容量数据存储、文件系统 |
关键区别:NOR 可以像内存一样直接寻址读取,CPU 能直接在 NOR Flash 上执行代码。NAND 不行,必须先把代码读到 RAM 里再执行。做 Bootloader 时,如果你的芯片支持从 NOR 启动,那会省很多事。
3.2 读写擦除特性
Flash 有三个基本操作:读、写、擦除。这三个操作的脾气完全不同。
3.2.1 读操作
读是最简单的。NOR Flash 读一个字节通常几十纳秒,跟 SRAM 差不多。NAND Flash 读一个页(通常 2KB~16KB)需要几十微秒。读操作不改变 Flash 内容,也不会磨损 Flash。
3.2.2 写操作(编程)
写操作有个大坑:只能把 1 写成 0,不能把 0 写成 1。为什么?因为写入是靠往浮栅里注入电荷,电荷只能增加不能减少。要把 0 变回 1,必须先擦除。
写操作的单位是「页」或「字」。NOR Flash 通常按字(16位)或字节写,NAND Flash 按页写。写一个页的时间大约几百微秒到几毫秒。
我曾经踩过的坑:在写 Bootloader 时,我直接往 Flash 里写数据,没检查目标地址是不是全是 0xFF。结果写进去的数据跟预期完全不对。后来才意识到,必须先擦除再写。这个错误让我多花了两天调试。
3.2.3 擦除操作
擦除是把整个扇区(Sector)或块(Block)里的所有位都恢复成 1。擦除的单位比写大得多:NOR Flash 的扇区通常 4KB~128KB,NAND Flash 的块通常 128KB~2MB。
擦除时间也长,一个扇区擦除需要几百毫秒到几秒。而且擦除是有寿命的:
| Flash 类型 | 擦除寿命(典型值) | 说明 |
|---|---|---|
| NOR Flash | 10万次 | 每个扇区独立计数 |
| NAND Flash | 1万~10万次 | MLC 寿命短,SLC 寿命长 |
你想想看,如果一个扇区被频繁擦写,它很快就会报废。这就是为什么做 Bootloader 时要考虑「磨损均衡」。
3.3 Flash 分区规划策略
分区规划是 Bootloader 设计的核心。分不好,升级时可能把自己搞死。我见过一个产品,因为分区没规划好,升级过程中断电,结果 Bootloader 和应用程序都坏了,板子直接变砖。
3.3.1 经典分区方案
一个典型的 Bootloader 分区方案长这样:
+------------------+ <-- Flash 起始地址
| Bootloader | 大小:64KB~256KB
| (一级启动代码) |
+------------------+
| 参数区 | 大小:4KB~16KB
| (启动参数、配置) |
+------------------+
| 应用程序 A | 大小:根据实际需求
| (当前运行版本) |
+------------------+
| 应用程序 B | 大小:与 A 相同
| (升级备用版本) |
+------------------+
| 数据区 | 大小:剩余空间
| (日志、用户数据) |
+------------------+ <-- Flash 结束地址
我个人习惯:把 Bootloader 放在 Flash 的最开头,因为大多数芯片复位后从地址 0 开始执行。参数区紧跟着 Bootloader,方便 Bootloader 直接读取。应用程序 A 和 B 大小一致,这样升级时可以直接覆盖。
3.3.2 分区规划原则
- Bootloader 区要写保护:一旦烧录好,就不要再擦写它。可以用芯片的写保护寄存器锁住。
- 参数区单独一个扇区:参数经常更新,单独一个扇区可以避免影响其他区域。
- 应用程序区留冗余:至少保留两个版本(A/B 分区),升级失败还能回滚。
- 数据区放最后:数据区大小可变,不会影响代码布局。
- 扇区对齐:每个分区的大小必须是扇区大小的整数倍。否则擦除时会擦到别的分区。
我曾经犯过的错:做第一个 Bootloader 时,我把参数区放在应用程序区中间,结果应用程序升级时擦除了整个扇区,把参数也擦掉了。从那以后,我坚持每个分区独立占满整数个扇区。
3.3.3 分区大小计算
分区大小怎么定?我一般这样算:
- Bootloader:编译出来的二进制文件大小 + 20% 余量。通常 64KB 够用,复杂点的 128KB。
- 参数区:一个扇区就够了。NOR Flash 扇区最小 4KB,存几百个参数绰绰有余。
- 应用程序:编译出来的固件大小 + 30% 余量。两个版本各占一份。
- 数据区:剩下的全给它。
举个例子,一个 2MB 的 Flash:
| 分区 | 起始地址 | 大小 | 扇区数(4KB/扇区) |
|---|---|---|---|
| Bootloader | 0x000000 | 64KB | 16 |
| 参数区 | 0x010000 | 4KB | 1 |
| App A | 0x011000 | 512KB | 128 |
| App B | 0x091000 | 512KB | 128 |
| 数据区 | 0x111000 | 892KB | 223 |
3.4 Flash 分区结构图
下面这张图展示了典型的分区布局和升级流程:
3.5 避坑指南
做 Flash 分区规划时,有几个坑我帮你提前标记好:
- 不要用最后一个扇区存关键数据:有些 Flash 的最后几个扇区有坏块,尤其是 NAND。关键数据放中间扇区更安全。
- Bootloader 里不要依赖外部中断:升级过程中如果发生中断,可能会打断擦写操作,导致 Flash 状态异常。
- 写 Flash 前一定要关全局中断:擦写操作期间 CPU 不能响应中断,否则时序会乱。我习惯在擦写前后用
__disable_irq()和__enable_irq()包起来。 - 预留一个「恢复出厂设置」的入口:在 Bootloader 里加一个按键检测,按住某个键就不跳转应用程序,进入固件下载模式。这个功能救过我很多次。
一个小技巧:在参数区存一个「启动计数」字段。每次启动加 1,如果连续几次启动都失败(计数不增加),Bootloader 就自动切换到另一个应用程序分区。这就是最简单的看门狗回滚机制。