3. 硬件平台与开发环境

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 分区规划原则

  1. Bootloader 区要写保护:一旦烧录好,就不要再擦写它。可以用芯片的写保护寄存器锁住。
  2. 参数区单独一个扇区:参数经常更新,单独一个扇区可以避免影响其他区域。
  3. 应用程序区留冗余:至少保留两个版本(A/B 分区),升级失败还能回滚。
  4. 数据区放最后:数据区大小可变,不会影响代码布局。
  5. 扇区对齐:每个分区的大小必须是扇区大小的整数倍。否则擦除时会擦到别的分区。

我曾经犯过的错:做第一个 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 分区结构图

下面这张图展示了典型的分区布局和升级流程:

Flash 分区结构与升级流程 Bootloader (64KB) 参数区 (4KB) App A (512KB) App B (512KB) 数据区 (892KB) 下载新固件 校验固件完整性 (CRC/SHA 校验) 写入 擦除 App B 写入新固件 升级失败时回滚到 App A

3.5 避坑指南

做 Flash 分区规划时,有几个坑我帮你提前标记好:

  • 不要用最后一个扇区存关键数据:有些 Flash 的最后几个扇区有坏块,尤其是 NAND。关键数据放中间扇区更安全。
  • Bootloader 里不要依赖外部中断:升级过程中如果发生中断,可能会打断擦写操作,导致 Flash 状态异常。
  • 写 Flash 前一定要关全局中断:擦写操作期间 CPU 不能响应中断,否则时序会乱。我习惯在擦写前后用 __disable_irq() 和 __enable_irq() 包起来。
  • 预留一个「恢复出厂设置」的入口:在 Bootloader 里加一个按键检测,按住某个键就不跳转应用程序,进入固件下载模式。这个功能救过我很多次。

一个小技巧:在参数区存一个「启动计数」字段。每次启动加 1,如果连续几次启动都失败(计数不增加),Bootloader 就自动切换到另一个应用程序分区。这就是最简单的看门狗回滚机制。