差分升级原理:核心思想与数据块级别的差异比较
差分升级,说白了就是「只传变化的部分」。
我刚开始接触OTA时,也想过一个问题:为什么不能直接全量升级?后来在某个物联网项目中,设备只有256KB的Flash,固件却有200KB。每次升级都要传200KB,网络差的时候,传一半断连,设备就变砖了。嗯,那时候我才真正理解差分升级的价值。
差分算法的核心思想
差分升级的核心,就三个字:找不同。
你想想看,固件升级通常只是改了几个功能,修复了几个bug。新旧版本之间,90%以上的数据是一样的。我们没必要把整个固件重新传一遍,只需要传「旧版本没有、新版本才有」的那部分数据。
这个思路,其实和git的diff很像。我习惯把差分算法比作「拼图游戏」:
- 旧固件:已经拼好的拼图
- 新固件:目标拼图
- 差分补丁:告诉你哪些拼图块需要换掉,哪些需要移动位置
差分算法要做的,就是找出新旧固件之间的「最小差异集合」。这个集合越小,补丁文件就越小,升级效率就越高。
核心公式:
补丁大小 ≈ 新固件大小 - 旧固件中可复用的数据大小
理想情况下,补丁大小 = 新增代码 + 修改代码 - 删除代码
数据块级别的差异比较
差分算法不是逐字节比较的——那样太慢了,而且补丁会很大。我常用的做法是分块比较。
具体怎么分?我举个例子:
旧固件: [A1][A2][A3][A4][A5][A6][A7][A8]
新固件: [A1][A2][B3][A4][A5][A6][B7][A8]
其中:
- A1、A2、A4、A5、A6、A8 完全相同(可复用)
- A3 被修改为 B3(需要更新)
- A7 被修改为 B7(需要更新)
差分算法会这样处理:
- 分块:把新旧固件都切成固定大小的块(比如64字节一块)
- 哈希计算:对每个块计算哈希值(MD5或CRC32)
- 匹配查找:用新固件的哈希值去旧固件里找匹配
- 记录差异:匹配上的块记录「从哪里复制」,没匹配上的块记录「新数据」
我在项目中遇到过一个问题:块大小怎么选?
块太小(比如8字节),匹配精度高,但补丁文件里的「复制指令」会很多,反而增大补丁体积。块太大(比如1024字节),匹配率低,很多块都匹配不上,补丁也会变大。
我个人习惯用64~256字节的块大小。这个范围在大多数嵌入式设备上表现都不错。
避坑指南:
我曾经在一个项目中,把块大小设成了32字节。结果补丁文件比全量升级还大。后来分析发现,因为块太小,每个块都要记录「从哪里复制」的地址信息,这些元数据反而成了大头。后来改成128字节,补丁大小直接降了40%。
补丁文件的生成与合并
补丁文件长什么样?说白了就是一个「操作指令列表」。我常用的补丁格式包含三种指令:
| 指令类型 | 含义 | 示例 |
|---|---|---|
| COPY | 从旧固件复制一段数据 | COPY 0x1000, 256 |
| INSERT | 插入新数据 | INSERT 0x2A, 0x3B, 0x4C... |
| DELETE | 跳过旧固件的一段数据 | DELETE 128 |
补丁生成过程:
// 伪代码示例
function generatePatch(old_firmware, new_firmware):
patch = []
i = 0
while i < len(new_firmware):
// 尝试在旧固件中匹配当前块
match = find_match(old_firmware, new_firmware[i:i+BLOCK_SIZE])
if match:
// 匹配成功,生成COPY指令
patch.append(COPY, match.offset, BLOCK_SIZE)
i += BLOCK_SIZE
else:
// 匹配失败,生成INSERT指令
patch.append(INSERT, new_firmware[i])
i += 1
return patch
补丁合并过程:
// 伪代码示例
function applyPatch(old_firmware, patch):
new_firmware = []
for instruction in patch:
if instruction.type == COPY:
// 从旧固件复制数据
new_firmware.append(old_firmware[instruction.offset:instruction.offset+instruction.length])
elif instruction.type == INSERT:
// 插入新数据
new_firmware.append(instruction.data)
elif instruction.type == DELETE:
// 跳过旧固件数据(不复制)
skip(old_firmware, instruction.length)
return new_firmware
注意:
合并过程中,旧固件的数据是只读的。你不能一边合并一边修改旧固件,否则后面的COPY指令会读到错误的数据。我见过有人踩过这个坑——合并到一半,旧固件被覆盖了,结果整个升级失败。
实际应用中的考量
差分升级不是万能的。我总结了几点经验:
- 版本跨度不宜过大:如果旧版本和新版本之间改动太大,差分补丁可能比全量升级还大。我建议版本跨度控制在3~5个版本以内。
- 内存要够用:差分合并需要额外的内存来存放补丁文件和临时数据。我一般预留固件大小的1.5倍内存。
- 校验不能省:合并完成后,一定要做完整性校验。我曾经因为Flash写入错误,导致合并后的固件跑不起来,后来加了CRC校验才解决。
嗯,差分升级的原理其实不复杂。核心就是「找相同、记不同」。你只要理解了分块比较和指令合并这两个关键点,剩下的就是工程实现上的细节了。
下一章我会讲具体的差分算法实现——bsdiff和hdiffpatch,这两个是嵌入式领域最常用的差分工具。到时候我会分享一些实际项目中的调优经验。