Bootloader设计基础:分区、启动、跳转与看门狗

各位同学,今天我们来聊聊Bootloader。这东西说白了,就是固件升级的「守门员」。我做了这么多年嵌入式,见过太多因为Bootloader没写好,导致设备变砖的案例。嗯,咱们今天就把这块硬骨头啃下来。

一、Bootloader与App分区

先说说分区。你想想看,MCU的Flash就那么大一块,怎么安排才合理?我个人习惯把Flash分成三个区:Bootloader区、App区、参数区。

分区名称 起始地址 大小 用途
Bootloader区 0x08000000 32KB 存放Bootloader代码
App区 0x08008000 192KB 存放应用程序
参数区 0x08038000 32KB 存放校准参数、配置信息

为什么Bootloader要单独占一个区?因为它的代码不能跟App混在一起。我在项目中遇到过,有同事把Bootloader和App放在同一个区,结果升级时写错了地址,直接把Bootloader覆盖了。设备当场变砖,只能拆机用烧录器救回来。

注意:Bootloader区必须写保护!至少要把写保护使能。否则一旦App跑飞,乱写Flash,Bootloader就没了。

二、启动流程

上电后,MCU先执行什么?当然是Bootloader。流程大概是这样的:

  1. 系统上电,MCU从0x08000000取复位向量
  2. 执行Bootloader的main函数
  3. 检查是否有升级请求(比如检测某个GPIO电平,或者检查标志位)
  4. 如果有升级请求,进入升级模式,等待接收固件
  5. 如果没有升级请求,校验App的完整性
  6. 校验通过,跳转到App;校验失败,停留在Bootloader等待升级

这里有个细节:启动时间。Bootloader不能跑太久,否则会影响设备的响应速度。我一般控制在200ms以内完成所有检查和跳转。

三、跳转逻辑

跳转是Bootloader的核心操作。说白了,就是把程序计数器PC指向App的起始地址。但这里有个坑:中断向量表

App的代码是在Bootloader之后编译的,它的中断向量表地址跟Bootloader不一样。所以跳转前,必须重新设置中断向量表偏移。以STM32为例:

/* 跳转到App */
void jump_to_app(uint32_t app_addr)
{
    uint32_t stack_ptr;
    void (*app_entry)(void);
    
    /* 1. 关闭全局中断 */
    __disable_irq();
    
    /* 2. 关闭所有外设时钟 */
    RCC->AHBENR = 0;
    RCC->APB1ENR = 0;
    RCC->APB2ENR = 0;
    
    /* 3. 从App起始地址读取栈指针 */
    stack_ptr = *(uint32_t*)app_addr;
    
    /* 4. 从App起始地址+4读取复位向量 */
    app_entry = (void (*)(void))(*(uint32_t*)(app_addr + 4));
    
    /* 5. 设置主栈指针 */
    __set_MSP(stack_ptr);
    
    /* 6. 跳转 */
    app_entry();
}

我曾经犯过一个错误:跳转前忘了关中断。结果跳过去之后,中断还在响应,但中断向量表还没更新,直接跑飞了。嗯,从那以后,我每次跳转前都会把外设和中断清理干净。

小技巧:跳转前最好把SysTick也关了。否则App那边重新初始化SysTick时,两个定时器打架,系统会卡死。

四、看门狗喂狗策略

看门狗这东西,用好了是保护神,用不好就是催命符。Bootloader里怎么喂狗?我总结了几条原则:

  • Bootloader启动时,先别开看门狗。因为Bootloader执行时间不确定,万一升级过程比较长,看门狗超时复位就麻烦了。
  • 跳转到App前,确保看门狗是关闭的。App那边会重新初始化看门狗,如果Bootloader开着狗,App没来得及喂,又复位了。
  • 如果一定要在Bootloader里开看门狗,那就把喂狗周期设长一点,比如10秒。然后在每个关键步骤后喂一次。

举个例子,我在做分拣机固件升级时,遇到过一个问题:升级过程中网络中断,Bootloader一直在等待数据包。看门狗没喂,3秒后复位了。复位后又进入Bootloader,又等数据包,又复位...无限循环。后来我改了策略:在等待数据包时,每500ms喂一次狗,同时加一个超时计数器,超过30秒没收到数据就退出升级模式,直接跳转到旧App。

/* 升级过程中的喂狗策略 */
void upgrade_loop(void)
{
    uint32_t timeout = 0;
    
    while(1)
    {
        /* 每500ms喂一次狗 */
        if(get_tick() - last_feed > 500)
        {
            feed_watchdog();
            last_feed = get_tick();
        }
        
        /* 检查是否有数据包 */
        if(receive_packet())
        {
            process_packet();
            timeout = 0;
        }
        else
        {
            timeout++;
            if(timeout > 60)  /* 30秒超时 */
            {
                /* 退出升级,跳转到旧App */
                jump_to_app(OLD_APP_ADDR);
            }
        }
    }
}
核心要点:Bootloader里的看门狗,原则是「能不开就不开,实在要开就长周期+超时保护」。千万别让看门狗把升级过程打断了。

五、避坑指南

最后,我把自己踩过的坑总结一下,你们记好了:

  • 分区地址要对齐。Flash的擦除是按扇区来的,分区起始地址必须是扇区对齐的。否则擦除时会擦到别的区。
  • App的链接脚本要改。App的起始地址必须跟分区地址一致,中断向量表偏移也要设置正确。
  • 跳转前清理外设。DMA、定时器、中断,一个都不能留。否则App那边初始化时会出问题。
  • 校验App完整性。跳转前至少算个CRC,确保App代码没被破坏。我一般用CRC32,速度快,校验强度也够。

好了,Bootloader的基础设计就讲到这里。下一章我们聊聊固件包的格式设计,包括怎么打包、怎么校验、怎么加密。你们先把今天的内容消化一下,有问题随时问我。