2. 升级方案架构设计:Bootloader + App 双区架构原理

做固件升级,说白了就是解决一个问题:怎么安全地把新程序写进去,而且万一写失败了,设备还能活过来

我入行那会儿,第一次做OTA升级,就踩了个大坑。当时图省事,直接在App区里跑擦写Flash的操作,结果写到一半断电了...嗯,板子直接变砖。从那以后,我再也不敢轻视升级架构的设计了。

今天咱们聊的Bootloader + App双区架构,就是目前工业界最成熟、最稳妥的方案。你想想看,手机、路由器、智能家电,底层基本都是这套逻辑。

2.1 什么是双区架构?

双区架构,就是把单片机的Flash空间,逻辑上分成两个区域:

  • Bootloader区:负责启动、校验、升级管理。它一般只读,不轻易改。
  • App区:存放真正的应用程序。升级时,新固件就写到这里。

我习惯把Bootloader比作「看门大爷」。平时它不干活,一有动静(比如上电、复位、收到升级指令),它就出来检查一下App区是否完好。如果App坏了,它就等着你给它送新固件。

核心思想: Bootloader负责「怎么升级」,App负责「做什么功能」。两者职责分离,互不干扰。

2.2 双区架构的典型布局

咱们以一颗常见的STM32芯片为例,Flash通常是512KB或1MB。我一般这样划分:

区域 起始地址 大小 内容
Bootloader 0x08000000 32KB 启动代码、升级协议、Flash驱动
App 0x08008000 480KB 用户应用程序
参数区 0x0807E000 8KB 升级标志、版本号、校验值

注意看,Bootloader只占32KB,剩下的几乎全给App。参数区我习惯放在最后,因为擦写次数多,单独划出来不容易误伤App。

我的习惯: Bootloader大小最好留点余量。比如你算出来只需要20KB,我建议给32KB。为什么?因为后期可能要加加密、加签名校验,代码会膨胀。我曾经就因为算得太死,后面加功能时不得不重新调整分区,那叫一个痛苦。

2.3 启动流程:Bootloader到底干了什么?

上电后,CPU从0x08000000开始执行。Bootloader的启动流程,我总结为三步:

  1. 检查升级标志:看看参数区有没有「请求升级」的标记。如果有,进入升级模式。
  2. 校验App完整性:如果没有升级请求,就计算App区的CRC或哈希值,跟参数区存的值比对。
  3. 跳转执行:校验通过,直接跳转到App的入口地址(0x08008000 + 4)。

这里有个细节:跳转前要关掉所有中断,设置好栈指针。不然App一启动就崩了。

/* 伪代码:Bootloader跳转逻辑 */
void jump_to_app(void)
{
    uint32_t app_stack = *(uint32_t*)APP_ADDR;        // 取栈顶地址
    uint32_t app_reset = *(uint32_t*)(APP_ADDR + 4);  // 取复位向量

    __disable_irq();          // 关中断
    SCB->VTOR = APP_ADDR;    // 重定向中断向量表
    __set_MSP(app_stack);    // 设置主栈指针

    void (*app_main)(void) = (void (*)(void))app_reset;
    app_main();               // 跳转,不再返回
}

你看,代码其实很简单。但实际项目中,我见过有人忘了重定向VTOR,结果App里一触发中断就跑飞了。嗯,这种bug查起来特别隐蔽。

2.4 升级流程:新固件怎么写进去?

升级流程,我习惯画成一张图。下面这张SVG图,就是我自己项目里常用的升级状态机:

空闲状态 接收数据 校验写入 升级完成 收到升级指令 数据接收完毕 校验通过 复位/重新上电 校验失败 等待重试 超时/重试

这张图里,我特别标出了「校验失败」的分支。实际项目中,我见过很多新手只画了成功路径,没考虑失败怎么处理。结果一遇到校验失败,设备就卡死了。

升级的具体步骤,我拆解一下:

  • 第一步:握手。上位机发一个升级请求,Bootloader回复确认,然后擦除App区。
  • 第二步:分包传输。每包数据带序号和CRC,Bootloader收到后回ACK。丢包就重传。
  • 第三步:整体校验。所有包传完后,Bootloader对整个App区算一次哈希,跟上位机发来的比对。
  • 第四步:标记生效。校验通过,写一个「升级成功」标志到参数区。下次重启直接进App。

注意: 千万不要在App运行的时候擦写App区!我见过有人图方便,在App里直接调用Flash擦除函数,结果把自己正在跑的代码给擦了...那场面,直接死机。正确的做法是:App只负责把新固件存到另一个缓冲区(比如外部Flash或RAM),然后复位进Bootloader,由Bootloader来完成真正的写入。

2.5 双区架构的优缺点

任何架构都有取舍。双区架构也不是万能的,我列一下实际项目中的感受:

优点 缺点
升级失败可恢复,设备不砖 占用额外Flash空间(Bootloader区)
架构清晰,职责分离 Bootloader开发调试较麻烦
支持远程OTA升级 需要处理中断向量表重定向
可加入加密、签名等安全机制 启动流程多了一步校验,稍慢

我个人觉得,对于绝大多数产品来说,这点缺点完全可以接受。毕竟,一个变砖的设备,损失的可不只是Flash空间。

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

最后,分享几个我实际项目中遇到的坑,希望能帮你少走弯路:

  • 中断向量表没重定向:App里一开中断就死机。解决办法:跳转前设置SCB->VTOR。
  • Bootloader里用了App的全局变量:跳转后变量值乱掉。解决办法:Bootloader和App各自独立,不要共用全局变量。
  • 升级过程中断电:App区只写了一半。解决办法:用双备份区,或者至少保证Bootloader能检测到App不完整。
  • Flash擦写时间太长:导致看门狗复位。解决办法:在擦写循环里喂狗,或者用DMA方式。

嗯,这些坑说起来都是泪。但踩过一次之后,你就再也不会犯了。

双区架构是固件升级的基石。理解了它,后面咱们聊的差分升级、安全升级、多备份升级,都是在这个基础上做加法。说白了,先把地基打牢,后面盖楼才稳当。


专注资料整理