4、端侧架构设计:Bootloader设计、双分区方案、差分升级原理
好,咱们进入正题。
端侧OTA升级,说白了就是让设备自己能给自己“换脑子”。这事儿听着简单,做起来坑特别多。我最早做智能电表项目时,就因为升级过程中突然断电,导致设备变砖,被客户追着骂了三天。从那以后,我对端侧架构设计就特别较真。
今天咱们聊三个核心点:Bootloader怎么设计、双分区方案怎么搭、差分升级到底是个什么原理。这三个东西,是端侧OTA的基石。
4.1 Bootloader设计:升级的“守门员”
Bootloader是什么?它就像设备的“第一口奶”。芯片上电后,第一个跑的就是它。它的任务很简单:决定这次该跑哪个固件,以及要不要进入升级模式。
我个人习惯把Bootloader分成三个阶段:
- 硬件初始化:时钟、内存、串口这些,得先让它能动起来。
- 升级检测:检查有没有新固件要刷。比如某个GPIO电平、或者Flash里某个标志位。
- 跳转执行:如果不需要升级,就直接跳转到主固件入口;如果需要,就启动升级流程。
这里有个关键点:Bootloader本身不能太大。我见过有人把Bootloader搞到100KB,结果Flash不够用,还得换芯片。你想想看,Bootloader只是负责引导,功能越少越安全。一般控制在16KB到32KB就够用了。
核心原则:Bootloader要“小而稳”。它不需要花里胡哨的功能,只需要保证两件事——能正常启动,能安全升级。
我在项目中遇到过一个问题:Bootloader里放了完整的Flash擦写驱动,结果驱动有bug,升级时把Bootloader自己给擦掉了。嗯,这属于“自杀式升级”。后来我学乖了,Bootloader的代码区一定要写保护,物理上锁死。
避坑指南:我曾经因为Bootloader没有做“升级失败回滚”,导致设备在升级中途断电后,既进不了主固件,也进不了升级模式。最后只能拆机用烧录器救砖。所以,Bootloader里一定要留一个“强制升级入口”,比如按住某个按键再上电。
4.2 双分区方案:升级的“安全气囊”
双分区,说白了就是准备两份固件空间。一份是当前在跑的,另一份是备用的。升级时,新固件写到备用区,写完了再切换过去。
常见的双分区布局是这样的:
| 分区名称 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| Bootloader | 0x08000000 | 32KB | 引导程序,写保护 |
| App_A(当前区) | 0x08008000 | 256KB | 当前运行的主固件 |
| App_B(备用区) | 0x08048000 | 256KB | 升级时写入新固件 |
| 参数区 | 0x08088000 | 16KB | 存储升级标志、版本号等 |
为什么需要双分区?你想想看,如果只有一个分区,升级时写坏了怎么办?设备就彻底废了。双分区的好处是:升级失败,还能回滚到旧版本继续跑。
我建议的切换逻辑是这样的:
// 伪代码:双分区切换逻辑
if (升级成功标志 == TRUE) {
// 新固件校验通过,切换启动分区
current_boot_partition = APP_B;
// 擦除旧固件区,准备下次升级
erase_partition(APP_A);
} else {
// 升级失败,继续跑旧固件
current_boot_partition = APP_A;
}
这里有个细节:切换分区不是靠“复制数据”,而是靠修改启动地址。Bootloader读取参数区里的“当前启动分区”标志,然后跳转到对应地址。这样切换速度极快,几乎无感。
个人经验:双分区方案里,我建议把“升级标志”放在一个独立的、有ECC校验的Flash区域。我曾经遇到过Flash位翻转,导致升级标志被误读,设备反复在A区和B区之间跳转,跟抽风似的。加个CRC校验就能解决。
4.3 差分升级原理:省流量、省时间、省Flash
差分升级,说白了就是只传输“变化的部分”。比如旧固件1MB,新固件1.01MB,但实际只改了10KB的代码。如果全量升级,你得传1.01MB;差分升级,只传那10KB的差异。
差分升级的核心算法是bsdiff和bspatch。bsdiff在云端生成差分包,bspatch在设备端还原新固件。
原理其实不复杂:
- 云端:对比旧固件和新固件,找出差异,生成一个很小的差分包。
- 设备端:下载差分包后,用bspatch算法,把旧固件和差分包合并,还原出新固件。
- 写入备用区:还原出的新固件直接写入App_B分区。
差分升级的流程大致是这样的:
// 设备端差分还原伪代码
void apply_delta_update(uint8_t *old_firmware, uint8_t *delta_patch, uint8_t *new_firmware) {
// 调用bspatch算法
bspatch(old_firmware, old_size, delta_patch, delta_size, new_firmware, &new_size);
// 校验新固件完整性
if (crc32(new_firmware, new_size) == expected_crc) {
// 写入App_B分区
flash_write(APP_B_ADDR, new_firmware, new_size);
// 设置升级成功标志
set_boot_flag(APP_B);
} else {
// 校验失败,不切换
log_error("差分还原校验失败");
}
}
差分升级能省多少?我做过实测:一个512KB的固件,如果只改了几个bug,差分包可能只有10KB-50KB。对于NB-IoT这种低带宽、高延迟的网络,省下来的流量就是钱。
关键点:差分升级要求设备端必须保留“旧固件”的完整副本。因为还原时需要拿旧固件做基准。所以双分区方案天然适合差分升级——App_A里就是旧固件,直接拿来用。
不过差分升级也有坑。我记得有一次,客户在旧固件上打了太多补丁,导致新旧固件差异巨大,差分包反而比全量包还大。嗯,这种情况就别用差分升级了,直接全量更划算。
注意事项:差分升级对设备端的RAM和算力有一定要求。bspatch算法需要一块足够大的内存来存放还原过程中的临时数据。如果MCU只有几十KB的RAM,跑差分还原可能会很吃力。我建议RAM小于64KB的设备,优先考虑全量升级。
4.4 三者如何配合?
Bootloader、双分区、差分升级,这三者不是孤立的。它们配合起来,才是一个完整的端侧OTA方案。
我画个简单的配合流程:
- 正常运行时:设备在App_A里跑主固件。Bootloader已经完成了引导,处于休眠状态。
- 收到升级指令:App_A里的OTA客户端下载差分包,然后调用bspatch,在内存中还原出新固件,写入App_B。
- 重启:设备复位,Bootloader启动。它检查参数区,发现App_B里有新固件,并且校验通过。
- 切换:Bootloader把启动分区指向App_B,然后跳转过去。设备开始跑新固件。
- 回滚:如果新固件跑不起来(比如看门狗超时),Bootloader检测到异常,自动切回App_A。
这个流程里,Bootloader是“裁判”,双分区是“场地”,差分升级是“快递员”。各司其职,缺一不可。
最后说一句:端侧架构设计,没有银弹。双分区虽然安全,但多占了一倍Flash空间。差分升级虽然省流量,但增加了设备端的计算负担。具体怎么选,得看你的产品定位和硬件资源。我个人的原则是:宁可多花点Flash,也要保证升级不翻车。毕竟,设备变砖的代价,比多买一颗Flash芯片大多了。