2. 固件存储结构:Flash存储器基础、Bootloader与App分区、分区表设计、冗余备份机制(A/B系统)
上一章我们聊了固件升级的整体概念。这一章,咱们得把目光聚焦到硬件底层——固件到底存在哪里?怎么组织的?
说白了,嵌入式设备的存储核心就是 Flash。你写的代码、跑的系统、存的配置,全得靠它。我刚开始做嵌入式那会儿,总觉得 Flash 就是个“大号 EEPROM”,结果踩了不少坑。今天咱们就把这块彻底讲透。
2.1 Flash 存储器基础
Flash 是一种非易失性存储器。断电不丢数据,这是它的基本盘。但跟 RAM 不一样,Flash 有三大特性你必须刻在脑子里:
- 按扇区擦除:不能像写 RAM 那样直接覆盖。想改一个字节?得先把整个扇区擦干净,再写进去。
- 擦写寿命有限:NOR Flash 一般 10 万次,NAND Flash 更少,几千到几万次。别拿它当日志盘用。
- 读快写慢:读一个字节几十纳秒,写一个字节几十微秒,擦一个扇区可能要几百毫秒。
嵌入式里最常见的是 NOR Flash 和 NAND Flash。NOR 适合存代码,因为支持 XIP(片上执行),CPU 可以直接从 Flash 里取指令跑。NAND 密度大、成本低,但坏块多,一般用来存文件系统或大容量数据。
2.2 Bootloader 与 App 分区
一个典型的固件存储布局,至少分成两大块:Bootloader 分区和App 分区。
Bootloader 是上电后第一个跑的程序。它负责初始化硬件、校验 App 的完整性,然后跳转到 App 执行。App 就是你的业务代码——传感器采集、通信协议、控制逻辑,全在里头。
为什么非要分开?
- 安全隔离:App 写崩了,Bootloader 还能救回来。
- 升级独立:升级时只擦 App 区,Bootloader 不动,保证设备不会变砖。
- 启动链清晰:Bootloader → App,责任分明。
我见过一些新手把 Bootloader 和 App 编译成一个固件烧进去。嗯,升级一次就变砖一次。后来他们改成了分区方案,世界清净了。
2.3 分区表设计
分区表就是一张“地图”,告诉 Bootloader 和 App:哪个区域存什么,从哪里开始,有多大。
一个典型的分区表长这样:
| 分区名称 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x08000000 | 64 KB | 启动代码、升级入口 |
| App A | 0x08010000 | 512 KB | 主应用(当前运行) |
| App B | 0x08090000 | 512 KB | 备用应用(升级目标) |
| Config | 0x08110000 | 16 KB | 设备参数、校准数据 |
| Log | 0x08114000 | 48 KB | 运行日志(循环写入) |
分区表的设计原则,我总结了几条:
- 对齐扇区边界:每个分区的起始地址必须是 Flash 扇区大小的整数倍。否则擦除时会擦到隔壁分区,数据就乱了。
- 预留冗余空间:App 分区别卡得太死。我有个项目 App 固件 480KB,分区只给了 512KB,结果加了两个功能就超了,还得改分区表重新烧 Bootloader。
- 分区表本身要保护:最好把分区表放在 Bootloader 区里,或者单独划一个只读区域。App 不能随便改分区表,否则升级时可能把自己搞死。
2.4 冗余备份机制(A/B 系统)
这是固件升级里最经典、最可靠的方案。说白了就是准备两份 App 分区:A 区和 B 区。当前运行的是 A,升级就写到 B;下次启动切到 B,如果 B 跑不起来,自动回退到 A。
为什么需要 A/B 系统?
- 升级失败零风险:断电、传输错误、固件损坏,都不怕。旧的版本还在。
- 支持回滚:新版本有 bug?Bootloader 可以自动切回旧版本。
- 后台升级:设备正常运行时,在后台把新固件下载到备用分区,下次重启生效。
A/B 系统的核心逻辑,我用一段伪代码来描述:
// Bootloader 中的 A/B 切换逻辑
void bootloader_main() {
// 读取启动标志(存在 Config 分区)
uint8_t boot_flag = read_boot_flag();
if (boot_flag == BOOT_FROM_A) {
if (verify_app(PARTITION_A)) {
jump_to(PARTITION_A);
} else if (verify_app(PARTITION_B)) {
// A 坏了,切到 B
set_boot_flag(BOOT_FROM_B);
jump_to(PARTITION_B);
}
} else { // BOOT_FROM_B
if (verify_app(PARTITION_B)) {
jump_to(PARTITION_B);
} else if (verify_app(PARTITION_A)) {
set_boot_flag(BOOT_FROM_A);
jump_to(PARTITION_A);
}
}
// 两个分区都坏了,进入恢复模式
enter_recovery_mode();
}
App 侧在升级完成后,需要做一件事:设置“升级成功”标志。Bootloader 会在下次启动时检查这个标志,如果 App 没设置,就认为新版本启动失败,自动回滚。
下面这张图展示了 A/B 系统的完整工作流程:
A/B 系统的代价也很明显:Flash 容量翻倍。如果你的 Flash 只有 1MB,App 占 600KB,那就没法用 A/B。这时候可以考虑压缩固件,或者改用“单分区 + 外部存储”的方案。
不过话说回来,对于工业设备、汽车电子、医疗仪器这些场景,A/B 系统几乎是标配。多花一倍 Flash 的钱,换来的是现场零返修。你想想看,一个设备部署在偏远基站,升级失败变砖了,派人出差修的成本远不止那几块钱的 Flash。