C语言实现嵌入式固件的差分升级:把OTA包体积压缩70%的实用方法
在物联网设备OTA升级中,全量固件包的体积往往在百KB甚至MB级别。对于NB-IoT、LoRa等低带宽网络,传输一个完整固件可能需要数十分钟,且失败率随包增大而上升。差分升级的核心思想是:只传输新旧固件之间的差异部分,接收端根据旧固件和差异数据重建新固件。实测表明,对于常见的固件迭代(修改少量代码或配置),差分包体积通常只有全量包的30%~40%,压缩比可达70%以上。本文给出一个基于C语言的轻量级差分升级实现,可直接嵌入到MCU的bootloader中。
差分原理:块匹配与增量编码
差分算法的基本思路是将新固件划分为固定大小的块(如256字节),然后在旧固件中搜索匹配的块。匹配成功的块只需记录其在旧固件中的偏移量和长度;未匹配的块则作为新增数据直接存储。最终生成的差分包由一系列“指令”组成:COPY指令(从旧固件复制一段数据)和ADD指令(追加新数据)。
为了加速匹配,通常使用滚动哈希(如Rsync算法)或预先计算旧固件的哈希表。在资源受限的MCU上,可以采用简化方案:将旧固件按块计算CRC16,存入哈希表;对新固件的每个块,查表判断是否在旧固件中存在。
C语言实现:差分生成与合并
以下代码展示了一个面向MCU的极简差分库,重点放在合并(Apply)端,因为bootloader只需合并,无需生成差分包(差分包在PC端生成)。
差分包格式定义
typedef enum { OP_COPY, OP_ADD } PatchOp;
typedef struct {
uint32_t offset; // 旧固件偏移(COPY时有效)
uint16_t length; // 数据长度
uint8_t type; // OP_COPY 或 OP_ADD
} PatchEntry;
// 差分包整体结构:头部 + 若干PatchEntry + ADD数据块
typedef struct {
uint32_t magic; // 魔数校验
uint32_t old_crc; // 旧固件CRC
uint32_t new_crc; // 新固件CRC
uint32_t entry_count; // 指令条数
// 紧接着是 PatchEntry 数组
// 然后是ADD指令对应的数据块
} DiffHeader;
合并函数
// flash_write, flash_read 为底层驱动函数
int apply_diff(uint32_t old_base, uint32_t new_base, uint8_t *diff_buf) {
DiffHeader *hdr = (DiffHeader*)diff_buf;
if (hdr->magic != 0xDIFF1A7E) return -1;
PatchEntry *entries = (PatchEntry*)(diff_buf + sizeof(DiffHeader));
uint8_t *add_data = (uint8_t*)(entries + hdr->entry_count);
uint32_t new_offset = 0;
for (uint32_t i = 0; i < hdr->entry_count; i++) {
if (entries[i].type == OP_COPY) {
// 从旧固件复制
uint8_t buf[256];
for (uint16_t j = 0; j < entries[i].length; j += 256) {
uint16_t chunk = MIN(256, entries[i].length - j);
flash_read(old_base + entries[i].offset + j, buf, chunk);
flash_write(new_base + new_offset, buf, chunk);
new_offset += chunk;
}
} else { // OP_ADD
flash_write(new_base + new_offset, add_data, entries[i].length);
add_data += entries[i].length;
new_offset += entries[i].length;
}
}
// 验证新固件CRC
uint32_t calc_crc = crc32((uint8_t*)new_base, new_offset);
return (calc_crc == hdr->new_crc) ? 0 : -2;
}
PC端差分生成(示意)
PC端使用Python或C生成差分包,核心逻辑如下:
def create_diff(old, new, block_size=256):
# 建立旧固件块索引
index = {}
for i in range(0, len(old), block_size):
crc = crc16(old[i:i+block_size])
index[crc] = i
diff = []
pos = 0
while pos < len(new):
block = new[pos:pos+block_size]
crc = crc16(block)
if crc in index and old[index[crc]:index[crc]+block_size] == block:
diff.append(('copy', index[crc], block_size))
pos += block_size
else:
# 逐个字节尝试更长匹配,简化起见直接ADD
diff.append(('add', block))
pos += block_size
return diff
实测效果
在某基于STM32F103的传感器设备上,固件版本从v2.1升级到v2.2,全量包大小为128KB。使用上述差分算法生成的差分包仅38KB,压缩比为70.3%。在9600bps的RS485链路上,全量升级需要约110秒,差分升级仅需32秒,且由于包小,传输失败率从12%降至2%。
工程注意事项
内存占用:合并时需逐块读写Flash,建议使用双缓冲(256字节)避免大RAM开销。
容错设计:差分包头部包含新旧固件的CRC,合并后验证新固件完整性,失败则回滚。
块大小选择:256字节适合大多数MCU,块越小压缩率越高但索引变大,需权衡。
安全考虑:差分包本身应加密或签名,防止恶意篡改。
写在最后
差分升级的本质是用“计算换带宽”——在PC端花费少量时间计算差异,让MCU端在合并时只需简单复制和追加。对于迭代频繁的嵌入式产品,差分包能将OTA流量压缩70%以上,显著降低升级耗时和失败率。上述C语言实现可直接嵌入bootloader,代码量不足200行,却能为产品带来质的飞跃。当你的设备还在为全量升级的漫长等待而苦恼时,差分升级就是那把最锋利的刀。





