4、Bootloader设计原理:启动流程、分区表设计、双备份机制(A/B分区)

说到Bootloader,很多刚入行的朋友觉得它就是个「跳板」——上电后跳一跳,跳到应用程序就完事了。其实不然。

我在农业空气质量站的项目里吃过亏。有一次设备部署在新疆的棉田里,OTA升级到一半,网络断了。结果呢?设备变砖了。运维人员得开车几百公里去现场刷机。那之后,我彻底重新设计了Bootloader。

今天我们就聊聊,一个靠谱的Bootloader到底该怎么设计。

4.1 启动流程:从复位到跑起来

芯片上电后,第一件事不是跑你的业务代码,而是执行Bootloader。这个流程我习惯分成三个阶段:

  1. 硬件初始化:关看门狗、设置时钟、初始化堆栈。这一步要快,别磨蹭。
  2. 启动决策:检查启动标志、校验固件完整性、决定从哪个分区启动。
  3. 跳转执行:设置好向量表偏移,然后跳转到应用程序入口。

这里有个细节很多人会忽略——中断向量表的重映射。你想想看,Bootloader里用了中断,跳转到APP后,中断向量表如果不重新指向APP的地址,那中断一进来就跑到Bootloader里去了,系统直接崩溃。

核心原则:Bootloader越简单越好。它只负责「启动」这件事,别往里面塞业务逻辑。我在项目中见过有人把传感器驱动写在Bootloader里,结果每次升级都得重新适配驱动,简直是给自己挖坑。

4.2 分区表设计:把Flash安排明白

Flash分区就像给房子隔房间。房间分得好,住着舒服;分得乱,找东西都费劲。

以我常用的STM32F4系列为例,Flash总共1MB。我是这么分的:

分区名称 起始地址 大小 用途
Bootloader 0x08000000 64KB 启动代码、升级逻辑
App A (主分区) 0x08010000 384KB 当前运行的固件
App B (备份分区) 0x08070000 384KB 备用固件
参数区 0x080D0000 64KB 校准参数、网络配置
日志区 0x080E0000 128KB 运行日志、错误记录

为什么Bootloader只给64KB?因为它的代码量真的不需要太大。我曾经见过有人给Bootloader分了256KB,结果里面空荡荡的,白白浪费了宝贵的Flash空间。

我的习惯:分区表定义好后,用宏或者枚举写在头文件里,并且加上版本号。这样以后改分区布局时,能通过版本号做兼容性检查。别问我为什么强调这个——有一次我改了分区大小,忘了更新旧设备的Bootloader,结果升级时把参数区给覆盖了,所有设备需要重新校准。

4.3 双备份机制(A/B分区):让升级不再「提心吊胆」

说白了,A/B分区就是准备两套固件。一个在跑,一个待命。升级时往空闲分区写,写完了再切换。

具体流程是这样的:

  1. 设备当前在A分区运行,收到升级包后,下载到B分区。
  2. 下载完成后,校验B分区的完整性(CRC32或SHA256)。
  3. 校验通过,设置启动标志为「下次从B启动」,然后复位。
  4. Bootloader检查标志,从B分区启动。
  5. B分区运行后,上报「升级成功」。如果上报失败,Bootloader会在超时后回滚到A分区。

这里有个关键点——「冒烟测试」。新固件启动后,不能立刻确认它没问题。我一般会留一个「确认窗口」,比如30秒。在这30秒内,如果设备没有上报心跳或者收到了回滚指令,Bootloader就自动切回旧分区。

注意:千万不要在升级过程中断电!A/B分区虽然能防止变砖,但如果正在擦写B分区时断电,B分区就坏了。不过没关系,A分区还在,设备还能正常跑。这就是双备份的价值——你最多损失一个备份分区,主系统不受影响。

4.4 代码示例:Bootloader跳转逻辑

嗯,这里我贴一段实际用过的跳转代码。简洁明了,没有花架子:

/* 跳转到应用程序 */
void jump_to_app(uint32_t app_addr)
{
    /* 1. 关闭全局中断 */
    __disable_irq();
    
    /* 2. 关闭外设时钟(重要!) */
    /* 我在项目中遇到过,不关外设时钟,跳转后外设状态混乱 */
    RCC->AHB1ENR = 0;
    RCC->APB1ENR = 0;
    RCC->APB2ENR = 0;
    
    /* 3. 重置所有外设 */
    /* 这一步很多人会漏掉,但必须做 */
    for (int i = 0; i < 8; i++) {
        NVIC->ICER[i] = 0xFFFFFFFF;
        NVIC->ICPTR[i] = 0xFFFFFFFF;
    }
    
    /* 4. 设置主堆栈指针 */
    __set_MSP(*(__IO uint32_t*)app_addr);
    
    /* 5. 设置向量表偏移 */
    SCB->VTOR = app_addr;
    
    /* 6. 跳转到复位中断向量 */
    void (*app_reset_handler)(void) = 
        (void (*)(void))(*((__IO uint32_t*)(app_addr + 4)));
    app_reset_handler();
}

这段代码看起来简单,但每一步都有它的道理。尤其是第2步和第3步——关闭外设时钟和重置中断。如果不做,跳转后外设可能还在上次的状态,新固件根本控制不了它们。

4.5 避坑指南

最后,分享几个我踩过的坑:

  • 分区对齐问题:Flash的擦除是按扇区来的。分区起始地址必须是扇区对齐的。我曾经因为没对齐,擦除时把隔壁分区的数据也抹掉了。
  • 升级包完整性校验:不要只校验固件本身,还要校验分区头信息。我遇到过升级包下载完整,但分区头里的长度字段被篡改了,结果Bootloader读到了错误的数据。
  • 回滚策略:不要只做一次回滚。如果B分区连续启动失败3次,就彻底锁定A分区,并上报错误。否则设备会陷入「反复重启-回滚-再重启」的死循环。

好了,Bootloader的设计思路就这些。说白了,它就是一个「守门员」——平时不显山不露水,但关键时刻能救你的设备一命。下一章我们聊聊OTA升级的传输协议怎么选,到时候见。