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 确认后清零。
注意:回滚计数器必须放在独立的、有 ECC 保护的 Flash 区域。我曾经遇到过 Flash 位翻转导致计数器异常递增,设备被误判为“不可恢复”——白白浪费了一整批产品。

2.4 架构总览:一张图说清楚

下面这张图概括了 Bootloader 架构的核心流程。我习惯把分区、启动决策、双备份画在一起,方便团队对齐思路。

Bootloader 架构总览图 Bootloader 架构总览:分区 + 启动决策 + 双备份 Flash 分区布局 Bootloader 参数区 App 主区 (Slot A) App 备份区 (Slot B) 日志区 启动决策流程 上电 / 复位 读取启动参数 验证 Slot A 启动 Slot A 尝试 Slot B 双备份核心规则 App 启动成功后必须确认状态 → 回滚计数器清零 → 下次升级才允许切换

2.5 避坑指南:我踩过的几个坑

  • 分区对齐问题:Flash 擦除是按扇区(Sector)来的。分区起始地址必须对齐到扇区边界,否则擦除时会误伤邻居。我吃过一次亏,Slot A 和 Slot B 没对齐,升级时把参数区擦了一半。
  • 启动参数校验:参数区的内容一定要做 CRC 或签名校验。我曾经遇到过 Flash 写入一半断电,参数区处于“半写”状态,Bootloader 读到一个错误的回滚计数器,直接触发了回滚。
  • 看门狗定时器:Bootloader 启动过程中一定要喂狗。我见过一个案例,Bootloader 在验证签名时卡住了(因为算法库有 bug),看门狗没喂,设备反复复位——最后只能拆机烧录。
  • 双备份的“伪双备份”:有些方案把 Slot B 放在同一个 Flash 芯片的不同页,但 Flash 芯片本身坏了,两个 Slot 都完蛋。真正的双备份应该是物理隔离——比如两个 Flash 芯片,或者一个 Flash 加一个外部存储。
我的习惯:每次设计 Bootloader 架构时,我都会先画一张类似上面的 SVG 图,然后拉着团队过一遍。画图的过程,就是发现问题的过程。很多坑,在图上就能看出来。