4、差分升级技术:差分算法原理(bsdiff/hdiff)、差分包生成流程、差分包合并流程
各位同学,今天我们来聊聊差分升级。说实话,这是OTA系统里最核心、也最考验技术功底的一环。我刚开始做嵌入式OTA时,第一版方案就是全量升级——简单粗暴,但每次升级都要传几兆甚至几十兆的数据。后来在量产阶段,客户反馈说流量费太贵了,我才意识到:差分升级不是锦上添花,而是刚需。
4.1 为什么需要差分升级?
你想想看,一个嵌入式设备,固件版本从v1.0升级到v1.1,可能只改了几个函数、修了几个bug。如果每次都传整个固件,那90%以上的数据都是重复的。对于NB-IoT、LoRa这类低带宽网络,全量升级简直就是灾难。
差分升级的核心思路很简单:只传变化的部分。设备端拿到差分包后,再和本地固件合并,生成新固件。这样传输量可能只有全量包的1/10甚至更少。
关键指标对比:
| 升级方式 | 传输数据量 | 适用场景 | 我个人的建议 |
|---|---|---|---|
| 全量升级 | 整个固件(如2MB) | 首次烧录、版本跨度大 | 能不用就别用 |
| 差分升级 | 差异部分(如50KB) | 小版本迭代、补丁更新 | 强烈推荐 |
4.2 差分算法原理:bsdiff vs hdiff
差分算法有很多种,但嵌入式领域最常用的就是bsdiff和hdiff。这两兄弟各有千秋,我分别说说。
4.2.1 bsdiff算法
bsdiff是基于后缀排序的差分算法。它的核心思想是:找到新旧固件中相同的子串,然后只记录差异。说白了,就是先找出两段二进制数据中「长得像」的部分,再压缩存储。
我在项目中遇到过一个问题:bsdiff生成的差分包虽然小,但合并时内存消耗很大。为什么?因为bsdiff在合并时需要把整个旧固件加载到内存里。对于只有几百KB RAM的MCU来说,这简直是噩梦。
避坑指南:我曾经在一个STM32F103项目上直接用bsdiff,结果合并时内存爆了。后来我改用流式合并,边读边写,才解决了问题。如果你也用bsdiff,记得评估一下设备的内存上限。
4.2.2 hdiff算法
hdiff是bsdiff的改进版,专门针对嵌入式场景做了优化。它的特点是:合并时不需要加载整个旧固件,而是用滑动窗口的方式逐段处理。这样内存占用就小多了。
嗯,这里要注意:hdiff的差分包体积通常比bsdiff大10%~20%,但合并速度更快、内存更省。怎么选?我个人习惯是:RAM大于1MB的设备用bsdiff,小于1MB的用hdiff。
算法对比表:
| 特性 | bsdiff | hdiff |
|---|---|---|
| 差分包大小 | 更小(约小10%~20%) | 稍大 |
| 合并内存需求 | 高(需加载整个旧固件) | 低(流式处理) |
| 合并速度 | 较慢 | 较快 |
| 适用设备 | 高配MCU、Linux设备 | 低配MCU、资源受限设备 |
4.3 差分包生成流程
差分包是在服务器端生成的。流程其实不复杂,但有几个坑要避开。我一步步说。
- 准备新旧固件:旧固件是设备当前运行的版本,新固件是要升级的目标版本。注意:两个固件必须是同一平台编译的,否则差分算法会崩溃。
- 运行差分工具:比如用bsdiff,命令就是
bsdiff old.bin new.bin patch.bin。生成的就是差分包。 - 压缩差分包:差分算法本身已经做了压缩,但为了进一步减小体积,我建议再用gzip或lzma压一遍。我在项目中试过,能再压缩30%~50%。
- 添加元数据:差分包头部要包含版本号、校验值、目标设备类型等信息。这样设备端拿到包后,可以先校验再合并,避免「升级变砖」。
警告:我曾经遇到过一个坑——新旧固件的编译优化选项不同(比如一个开了-O2,一个开了-Os),导致生成的差分包特别大。后来我规定:所有固件必须用相同的编译配置。你也要注意这一点。
4.4 差分包合并流程
合并是在设备端完成的。流程如下:
- 接收差分包:通过OTA通道下载到设备存储区(通常是外部Flash)。
- 校验差分包:检查CRC或SHA256,确保数据没被篡改或损坏。
- 读取旧固件:从当前运行分区读取旧固件数据。
- 执行合并算法:根据差分包中的指令,逐段合并生成新固件。bsdiff和hdiff的合并逻辑不同,但核心都是「复制旧数据 + 应用差异补丁」。
- 写入新分区:合并后的新固件写入备用分区(A/B分区方案),或者直接覆盖旧分区(单分区方案,风险较高)。
- 校验新固件:合并完成后,再算一次新固件的哈希值,和服务器端记录的对比。一致才算成功。
这里有个细节:合并过程中如果断电怎么办?我建议用原子操作——先写到一个临时分区,全部写完后才切换启动标志。这样即使中途断电,下次重启还能回滚到旧版本。
合并流程伪代码(以bsdiff为例):
// 伪代码,实际实现更复杂
void apply_bsdiff_patch(uint8_t* old_firmware, uint32_t old_size,
uint8_t* patch_data, uint32_t patch_size,
uint8_t* new_firmware, uint32_t* new_size) {
// 1. 解析差分包头部
patch_header_t* header = (patch_header_t*)patch_data;
// 2. 逐段合并
uint32_t old_pos = 0, new_pos = 0;
while (new_pos < header->new_size) {
// 复制旧数据段
memcpy(&new_firmware[new_pos],
&old_firmware[old_pos],
header->copy_len);
new_pos += header->copy_len;
old_pos += header->copy_len;
// 应用差异数据
apply_diff(&new_firmware[new_pos],
&patch_data[header->diff_offset],
header->diff_len);
new_pos += header->diff_len;
}
*new_size = header->new_size;
}
4.5 实际项目中的经验总结
做了这么多年OTA,我总结了几条差分升级的「潜规则」:
- 版本跨度不要太大:如果从v1.0直接升到v5.0,差分算法生成的包可能比全量包还大。我建议最多跨3个版本做差分。
- 差分包要支持断点续传:物联网设备网络不稳定,差分包下载到一半断了怎么办?我习惯把差分包分成多个小块,每块独立校验,这样断点续传就很容易实现。
- 合并失败要有回滚机制:不管算法多成熟,总有意外。我设计的系统里,合并失败后自动回滚到旧版本,并上报错误日志。这样至少设备还能用。
好了,差分升级的核心内容就这些。说白了,bsdiff和hdiff没有绝对的好坏,关键看你的设备资源。下一章我们会讲升级包的签名与校验,那是安全性的最后一道防线。