4、Bootloader设计原理(下):双备份启动策略、看门狗喂狗机制、启动超时处理

好,咱们接着聊Bootloader。上一节我们把基础框架搭起来了,这一节要聊的,才是真正让产品在医疗现场「扛得住」的关键。

说实话,医疗设备最怕什么?最怕升级到一半断电,或者启动时卡死。病人躺在手术台上,设备起不来,那可不是闹着玩的。所以双备份、看门狗、超时处理,这三板斧必须得硬。

一、双备份启动策略:给系统上双保险

什么叫双备份?说白了,就是Flash里存两份固件。一份是当前运行的,一份是备份的。我习惯叫它们:App_AApp_B

你想想看,如果只有一份固件,升级写到一半断电了,那系统就彻底变砖了。医疗设备变砖?那后果我不敢想。

1.1 双备份的布局方式

我一般这样划分Flash:

|------------------|
| Bootloader       |  (固定区域,不参与升级)
|------------------|
| App_A (主分区)   |  (当前运行)
|------------------|
| App_B (备份分区) |  (备用)
|------------------|
| 配置参数区       |  (存放启动计数、版本号等)
|------------------|

这里有个细节:配置参数区很重要。我会在里面存一个字节叫 boot_flag,用来标记下次该启动哪个分区。

1.2 启动流程怎么走?

我设计的流程大概是这样的:

  1. 上电后,Bootloader先读配置参数区的 boot_flag
  2. 如果 boot_flag 指向 App_A,就校验 App_A 的CRC。
  3. 校验通过 → 跳转到 App_A 运行。
  4. 校验失败 → 自动切到 App_B 启动。
  5. 如果 App_B 也失败 → 进入恢复模式(等待串口或USB重新烧录)。

嗯,这里要注意:每次成功启动后,要把 boot_flag 更新一下。比如这次从 App_A 启动了,就把标志改成下次优先启动 App_A。但如果 App_A 连续启动失败,就要切到 App_B。

核心原则:永远保证至少有一份可用的固件。哪怕两份都坏了,Bootloader本身不能坏。

1.3 我在项目中遇到的坑

有一次,我发现设备偶尔会莫名其妙地切到备份区启动。查了好久,最后发现是CRC校验的时机不对。我在Bootloader里校验时,用的是App_A的CRC值,但App_A在运行过程中会修改自己的代码区(比如写参数到Flash末尾),导致CRC对不上。

从那以后,我学乖了:CRC校验只校验代码段,不校验可写的参数区。或者干脆把参数区单独划出来。

二、看门狗喂狗机制:别让系统死得不明不白

看门狗这东西,说白了就是一个定时器。你不定期喂它,它就复位系统。医疗设备里,看门狗是标配,不是选配。

但问题来了:Bootloader阶段要不要喂狗?

我个人习惯:要喂,但不能乱喂

2.1 Bootloader阶段的喂狗策略

我一般这样设计:

  • Bootloader启动后,立即初始化看门狗,超时时间设为 5 秒。
  • 在关键步骤(如CRC校验、Flash擦写)完成后,喂一次狗。
  • 跳转到App之前,把看门狗重新配置一下(或者交给App去管)。

为什么要这样?因为Bootloader阶段如果死循环了,看门狗能把它拉回来。但如果你每行代码都喂狗,那看门狗就形同虚设了——系统卡在某个地方,你还在喂狗,它永远不会复位。

我的经验:只在「确定性」的步骤完成后喂狗。比如「CRC校验完成」、「Flash擦除完成」、「跳转准备就绪」。不要在循环里喂狗。

2.2 避坑指南:我曾经犯过的错

我曾经在一个项目里,把看门狗的喂狗操作放在了 while(1) 主循环里。结果有一次,程序卡在了一个中断服务函数里,主循环还在跑,看门狗一直被喂,系统就是不复位。最后是现场工程师发现设备死机了,但看门狗没起作用。

从那以后,我改成:喂狗只放在关键路径上,不放在循环里。而且我会在喂狗前加一个标志位检查,确保程序确实在正常执行。

三、启动超时处理:别让用户等太久

医疗设备启动,一般要求 3秒内进入主界面。如果Bootloader卡住了,用户看到的就是黑屏。你想想看,医生急着用设备,结果等了10秒还没反应,他会怎么想?

3.1 超时处理的两种场景

我一般把超时分为两种:

场景 超时时间 处理方式
等待用户按键(进入升级模式) 1~2秒 超时后直接启动当前固件
等待外部通信(如串口OTA指令) 5~10秒 超时后进入默认启动流程

嗯,这里要注意:等待用户按键的时间不能太长。我见过有些设计等5秒,用户以为设备坏了,直接拔电。我个人习惯:1.5秒,够用。

3.2 超时处理的代码实现

我一般用SysTick来做超时计数,不用硬件定时器(省资源)。大概这样:

// 启动超时处理示例
#define BOOT_TIMEOUT_MS  1500  // 1.5秒超时

uint32_t tick_start = get_systick_ms();

while(1) {
    if (check_uart_cmd()) {
        // 收到升级指令,进入升级模式
        enter_ota_mode();
        break;
    }
    
    if ((get_systick_ms() - tick_start) > BOOT_TIMEOUT_MS) {
        // 超时,直接启动
        jump_to_app();
        break;
    }
}

这里有个小技巧:超时时间内,要顺便喂狗。不然看门狗先超时了,系统复位,又回到这个循环,永远进不了App。

警告:不要在超时循环里做Flash擦写等耗时操作。否则超时时间会不准,而且可能影响看门狗。

四、把三者结合起来:一个完整的启动流程

好了,我们把双备份、看门狗、超时处理串起来,看看完整的Bootloader启动流程是什么样的:

  1. 上电复位:初始化时钟、GPIO、看门狗(超时5秒)。
  2. 检查启动标志:读配置参数区的 boot_flag,决定启动哪个分区。
  3. 等待用户按键:1.5秒超时,期间喂狗。如果收到升级指令,进入OTA模式。
  4. 校验固件:对目标分区做CRC校验。校验通过后喂狗。
  5. 校验失败:切到备份分区,重新校验。如果备份也失败,进入恢复模式。
  6. 跳转前准备:关闭中断、关闭外设、重置看门狗配置。
  7. 跳转到App:修改栈指针,跳转。

你看,每一步都有兜底。双备份兜底固件损坏,看门狗兜底程序卡死,超时处理兜底用户等待。三个机制互相配合,才能保证医疗设备在恶劣环境下也能可靠启动。

总结一句话:Bootloader设计的核心不是「怎么启动」,而是「启动失败了怎么办」。双备份、看门狗、超时处理,就是给失败准备的退路。

好,这一节就聊到这儿。下一节我们开始讲OTA升级的通信协议设计,那又是另一个有意思的话题了。