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的启动流程,我总结为三步:
- 检查升级标志:看看参数区有没有「请求升级」的标记。如果有,进入升级模式。
- 校验App完整性:如果没有升级请求,就计算App区的CRC或哈希值,跟参数区存的值比对。
- 跳转执行:校验通过,直接跳转到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方式。
嗯,这些坑说起来都是泪。但踩过一次之后,你就再也不会犯了。
双区架构是固件升级的基石。理解了它,后面咱们聊的差分升级、安全升级、多备份升级,都是在这个基础上做加法。说白了,先把地基打牢,后面盖楼才稳当。