4. Bootloader 核心设计:启动模式判断、版本校验、跳转逻辑、看门狗喂狗策略
这一章,我们聊聊 Bootloader 最核心的几个动作。说白了,就是芯片上电后,Bootloader 怎么决定下一步该干什么。
我刚开始做 Bootloader 的时候,觉得这玩意儿不就是跳转一下嘛。后来踩了几个坑才明白——启动模式判断、版本校验、跳转逻辑、看门狗喂狗,这四个环节环环相扣,任何一个出问题,设备都可能变砖。
4.1 启动模式判断:上电后第一件事
芯片复位后,Bootloader 最先跑起来。它得先搞清楚:我是该去升级固件,还是直接启动现有程序?
判断依据通常有三种:
- 外部引脚电平:比如检测某个 GPIO 是否被拉低。我在项目里常用这个做强制升级入口。
- 标志位检查:在备份区或特定 RAM 地址存一个魔法数。应用层可以主动写标志,告诉 Bootloader 下次启动要升级。
- 版本区有效性:如果两个区都有效,按优先级选;如果都无效,进入等待升级模式。
我个人的习惯:优先检查外部引脚,再查标志位,最后才看版本区有效性。这样既保留了强制升级的物理手段,又兼顾了软件触发的灵活性。
你想想看,如果只依赖标志位,万一应用层崩溃了写不了标志,设备就永远升不了级了。所以物理引脚是最后的保底手段。
4.2 版本校验:别让旧固件覆盖新固件
版本校验不只是比大小。我见过不少方案,只比较主版本号,结果副版本号或修订号不一致,导致回退时覆盖了更新的固件。
我的做法是这样的:
- 版本号结构:主版本号 + 副版本号 + 修订号 + 编译时间戳。时间戳用来做最终裁决。
- 校验规则:先比主版本,再比副版本,最后比修订号。如果都一样,取时间戳最新的那个。
- 完整性校验:用 CRC32 或 SHA256 对整个固件区做校验。校验通过才算版本有效。
我曾经踩过的坑:有一次只做了 CRC 校验,没做版本号比较。结果两个区都存了同一份固件,但 CRC 都正确,Bootloader 就随机选了一个启动。后来加了版本号比较,才彻底解决这个问题。
版本校验的流程,我画了张图,你看一眼就明白了:
4.3 跳转逻辑:从 Bootloader 到应用的最后一跳
跳转看起来简单,不就是改 PC 指针嘛。但这里有几个细节,不注意就会出问题。
标准的跳转流程是这样的:
/* 跳转到应用区 */
typedef void (*app_entry_t)(void);
void jump_to_app(uint32_t app_addr)
{
uint32_t stack_addr;
app_entry_t entry;
/* 1. 关闭全局中断 */
__disable_irq();
/* 2. 关闭外设时钟(关键!) */
deinit_all_peripherals();
/* 3. 恢复系统时钟到默认状态 */
SystemClock_Config_Default();
/* 4. 读取向量表 */
stack_addr = *(volatile uint32_t *)app_addr;
entry = (app_entry_t)(*(volatile uint32_t *)(app_addr + 4));
/* 5. 设置主栈指针 */
__set_MSP(stack_addr);
/* 6. 设置向量表偏移(Cortex-M 系列) */
SCB->VTOR = app_addr;
/* 7. 跳转 */
entry();
/* 不会返回 */
}
这里有个坑:跳转前一定要关闭所有外设时钟。我遇到过一例,UART 的 DMA 还在传输,跳转后 DMA 把应用区的内存写乱了。后来我加了个 deinit_all_peripherals(),问题就解决了。
另外,向量表偏移(VTOR)的设置也很关键。Cortex-M0/M0+ 不支持 VTOR,需要用其他方式重映射。嗯,这个后面讲具体芯片适配时会细说。
4.4 看门狗喂狗策略:别让 Bootloader 被复位
看门狗是嵌入式系统的最后一道防线。但在 Bootloader 里,喂狗策略要特别小心。
我总结了几条原则:
- 原则一:Bootloader 启动后立即喂一次狗,确保有足够的时间做初始化。
- 原则二:在耗时操作(如擦除 Flash、校验固件)中,分段喂狗。比如每擦除一个扇区就喂一次。
- 原则三:跳转到应用前,把看门狗配置恢复到默认状态。否则应用可能因为不知道看门狗已开启而不断复位。
我曾经遇到的情况:有一次升级大固件,擦除 Flash 花了 3 秒,看门狗超时只有 2 秒。结果每次擦到一半就被复位了,升级永远完不成。后来我在擦除循环里加了喂狗,才搞定。
喂狗的时机,我一般这样安排:
| 阶段 | 操作 | 喂狗时机 |
|---|---|---|
| 启动初始化 | 时钟、GPIO、UART 初始化 | 初始化完成后立即喂狗 |
| 版本校验 | 读取 Flash、计算 CRC | 每校验 64KB 喂一次 |
| 固件下载 | 接收数据、写入 Flash | 每收到一包数据后喂狗 |
| 固件擦除 | 擦除旧固件扇区 | 每擦除一个扇区喂一次 |
| 跳转前 | 关闭外设、恢复时钟 | 跳转前最后一次喂狗 |
你想想看,如果擦除 Flash 时不喂狗,看门狗超时复位,Bootloader 又从头开始——这不就成了死循环了吗?
4.5 把这些串起来:完整的启动流程
好了,我们把上面四个环节串起来,看看完整的启动流程是什么样的:
- 上电复位:Bootloader 开始执行。
- 喂狗初始化:配置看门狗,第一次喂狗。
- 启动模式判断:检查引脚、标志位,决定是升级还是启动。
- 版本校验:如果启动,比较 A/B 区的版本和 CRC,选出正确的区。
- 跳转准备:关闭中断、外设、恢复时钟。
- 最后一次喂狗:确保跳转前看门狗不会复位。
- 跳转:设置向量表,跳转到应用入口。
我个人建议:在跳转前加一个短延时(比如 10ms),让看门狗计数器有足够的时间复位。有些芯片的看门狗在关闭后还有余波,这个延时能避免意外复位。