2. Bootloader 架构设计:分区规划、启动模式与双备份机制
Bootloader 的架构设计,说白了就是回答三个问题:代码放哪?怎么启动?坏了怎么办? 这三个问题没想清楚,后面的安全启动和 OTA 升级就是空中楼阁。我见过太多项目,上来就写业务代码,最后发现分区不够用、启动流程有死锁、升级失败变砖——嗯,那时候再回头改架构,代价就大了。
2.1 分区规划:给每段代码一个家
分区规划是 Bootloader 的基石。你想想看,Flash 空间就那么大,怎么分才合理?我个人习惯把 Flash 划分为以下几个区域:
| 分区名称 | 起始地址(示例) | 大小 | 内容 |
|---|---|---|---|
| Bootloader 区 | 0x08000000 | 64 KB | Bootloader 固件本身 |
| 参数存储区 | 0x08010000 | 16 KB | 启动参数、回滚计数器、签名公钥哈希 |
| App 主区(Slot A) | 0x08014000 | 512 KB | 当前运行的应用固件 |
| App 备份区(Slot B) | 0x08094000 | 512 KB | OTA 下载的新固件或回滚备份 |
| OTA 下载暂存区 | 0x08114000 | 256 KB | 下载过程中的固件包(可选) |
| 文件系统/日志区 | 0x08154000 | 128 KB | 升级日志、错误记录 |
分区大小怎么定?我一般遵循这几个原则:
- Bootloader 区:够用就行,通常 64~128 KB。功能越少越安全。
- App 主/备份区:大小必须一致,否则双备份逻辑会变得很复杂。
- OTA 暂存区:如果网络带宽够,可以直接下载到备份区,省掉这个区。
- 日志区:别小看它,调试时能救命。
2.2 启动模式选择:三种主流方案
启动模式决定了 Bootloader 怎么把控制权交给 App。我做过三种模式,各有优劣:
模式一:直接跳转模式
最简单。Bootloader 做完安全检查后,直接跳转到 App 入口地址。优点是快,缺点是如果 App 坏了,Bootloader 没法干预。
模式二:交互等待模式
Bootloader 启动后先等几秒,检测是否有升级请求(比如按键、串口命令)。超时后再跳转。适合调试和工厂烧录。
模式三:条件决策模式
这是我现在最常用的。Bootloader 根据参数区的标志位做决策:
// 伪代码示例
void bootloader_main(void) {
boot_params_t params = read_boot_params();
if (params.force_upgrade) {
enter_ota_mode(); // 强制进入 OTA
} else if (params.boot_count > MAX_RETRY) {
rollback_to_slot_b(); // 启动次数超限,回滚
} else if (verify_app_signature(SLOT_A)) {
jump_to_app(SLOT_A); // 正常启动
} else {
try_alternative_slot(); // 尝试备份区
}
}
2.3 双备份机制:别让设备变砖
双备份机制是 OTA 升级的保命符。说白了就是:永远保留一个能正常工作的固件版本。
我参与过一个智能家居项目,OTA 升级过程中突然断电。因为没有双备份,设备再也起不来了——用户只能返厂。那次之后,双备份就成了我的铁律。
两种主流双备份方案
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| Slot A/B 切换 | 两个完全独立的 App 分区,Bootloader 决定启动哪个 | 回滚快,安全性高 | Flash 占用翻倍 |
| 增量备份 | 只备份关键模块,其余从主区恢复 | 节省空间 | 恢复逻辑复杂,容易出错 |
我个人强烈推荐 Slot A/B 切换。虽然多占一倍 Flash,但换来的是确定性和可靠性。你想想看,设备变砖的代价,远不止那 512 KB Flash 的成本。
回滚计数器:防死循环的关键
双备份不是简单的“坏了就换”。你得防止一种情况:新固件有 bug,启动后马上崩溃,Bootloader 又切回旧固件,旧固件又尝试升级……形成死循环。
我的做法是引入回滚计数器:
- 每次升级后,App 启动成功必须主动“确认”状态。
- 如果 App 连续启动失败超过 N 次(比如 3 次),Bootloader 就锁定当前版本,不再尝试切换。
- 回滚计数器存储在参数区,每次 Bootloader 启动时递增,App 确认后清零。
2.4 架构总览:一张图说清楚
下面这张图概括了 Bootloader 架构的核心流程。我习惯把分区、启动决策、双备份画在一起,方便团队对齐思路。
2.5 避坑指南:我踩过的几个坑
- 分区对齐问题:Flash 擦除是按扇区(Sector)来的。分区起始地址必须对齐到扇区边界,否则擦除时会误伤邻居。我吃过一次亏,Slot A 和 Slot B 没对齐,升级时把参数区擦了一半。
- 启动参数校验:参数区的内容一定要做 CRC 或签名校验。我曾经遇到过 Flash 写入一半断电,参数区处于“半写”状态,Bootloader 读到一个错误的回滚计数器,直接触发了回滚。
- 看门狗定时器:Bootloader 启动过程中一定要喂狗。我见过一个案例,Bootloader 在验证签名时卡住了(因为算法库有 bug),看门狗没喂,设备反复复位——最后只能拆机烧录。
- 双备份的“伪双备份”:有些方案把 Slot B 放在同一个 Flash 芯片的不同页,但 Flash 芯片本身坏了,两个 Slot 都完蛋。真正的双备份应该是物理隔离——比如两个 Flash 芯片,或者一个 Flash 加一个外部存储。