ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

OTA 实战三:差分升级(Delta OTA)—— 4G 流量太贵怎么办

OTA 实战三:差分升级(Delta OTA)—— 4G 流量太贵怎么办 适用人群跑通全量 OTAESP32 / STM32 自写 Bootloader、想省流量省钱的同学读完你能得到① 搞懂为什么全量升级在 4G/NB-IoT 上烧钱② 理解差分升级原理③ 拿到一份能在 MCU 上跑的块级差分 C 代码 配套 PC 端生成脚本。一、先算笔账全量升级有多烧钱上两篇我们都是把整个新固件.bin下到设备。这在 Wi-Fi 下无所谓但换成4G / NB-IoT就肉疼了假设你的固件app.bin编译出来800KB。你卖了 1 万台设备每个月推送一次小更新其实只改了几十行代码。全量升级800KB × 10000 台 8GB/月流量。4G 物联网卡按 ~0.01 元/KB 算 → 单次推送8 万元。一年推送 12 次 →近 100 万。而你实际改动可能就30KB。这就是差分升级要解决的问题。差分升级Delta OTA服务器只下发新旧固件的差异包设备用手上的旧固件 差异包在本地还原出新固件再走和以前一样的刷写/切换流程。流量通常能砍掉80%~95%。二、前置概念3 个词名词白话解释全量升级直接下发完整new.bin上两篇的做法差分/增量升级下发patch.bin只含变化部分设备本地合成new.binbsdiff / block diff两种主流差分算法。bsdiff 压缩率高但吃内存block diff 简单省内存、适合 MCU 关键认知差分升级不是替换 A/B 或 Bootloader而是叠在它们之上的传输优化层。底层怎么刷、怎么回滚上两篇的逻辑原样复用只是下载的东西从new.bin变成了patch.bin。三、原理差异包到底长啥样3.1 整体流程对比全量全量 服务器 ──new.bin(800KB)──▶ 设备 ──直接写 ota_1──▶ 切换/回滚 差分 服务器 ──patch.bin(35KB)─▶ 设备 ──用 old(ota_0)patch 合成 new──▶ 写 ota_1 ──▶ 切换/回滚 ▲ ▲ CI 上 diff(old,new) 本地 apply(patch, old)3.2 两种差分算法怎么选算法原理压缩率内存需求适合场景bsdiff/bspatch找新旧文件的相同/近似片段编码复制 新增高改动小几乎只发差异高需 oldnew索引常 1×文件大小服务器、Linux 网关、RAM 充足的芯片block diff块级固件按固定块如 256B切片块没变就引用旧块变了就发新块中依赖链接布局稳定极低一次处理一个块STM32 等 RAM 紧张的 MCU⚠️STM32 上别直接搬 bsdiffbsdiff 解压时内存占用常是文件本身的 12 倍。1MB 固件要 12MB RAM而 F4 一般只有 128~192KB。所以 MCU 上几乎都用block diff 或流式变体。四、块级差分实战教学版但原理真实可用下面给一套端到端可跑的块级差分PC 端 Python 生成 patch设备端 C 应用 patch。简单、内存友好一次只处理一个块足以讲透原理。4.1 PC 端生成差异包服务器/CI 上跑# gen_patch.py —— 在服务器/电脑上运行输入 old.bin new.bin输出 patch.binBLOCK256oldopen(old.bin,rb).read()newopen(new.bin,rb).read()# 补齐到 BLOCK 整数倍Flash 按字/双字擦写末尾不满一块必须补齐# 否则设备端末块会读越界、数据错乱真实固件长度通常不整除 256pad(-len(old))%BLOCK oldoldb\xff*pad newnewb\xff*padassertlen(old)len(new),补齐后新旧固件长度必须一致outbytearray()outlen(new).to_bytes(4,little)# 总长度outBLOCK.to_bytes(4,little)# 块大小foriinrange(0,len(new),BLOCK):ob,nbold[i:iBLOCK],new[i:iBLOCK]ifobnb:outbytes([0])# opcode 0 从旧固件复制(同偏移 i)outi.to_bytes(4,little)else:outbytes([1])# opcode 1 原始新数据outnbopen(patch.bin,wb).write(out)print(fnew.bin{len(new)}B patch.bin{len(out)}B 节省{1-len(out)/len(new):.1%})跑一下你就知道省多少改动 30KB 的 800KB 固件patch 通常只有35~60KB省下 90%。4.2 设备端应用差异包MCU 上跑流式、低内存#includestm32f4xx.h// 以 F4 为例flash_write 见 OTA_02#defineBLOCK256// 把 len 字节从 src 写到 flash_dst4 字节对齐循环调用 HAL_FLASH_Program// 用 memcpy 读避免对 uint8_t 缓冲做非对齐的 uint32_t 强转M0 会 HardFaultstaticvoidflash_write_buf(uint32_tdst,constuint8_t*src,uint32_tlen){for(uint32_ti0;ilen;i4){uint32_tw;memcpy(w,srci,4);flash_write_word(dsti,w);}}// old_base: 当前运行固件地址(如 ota_0)// patch : 收到的差异包// new_base: 合成后写入地址(如 ota_1)voidapply_patch(constuint8_t*old_base,constuint8_t*patch,uint32_tnew_base){uint32_ttotal,bsz;memcpy(total,patch,4);// 总长度memcpy(bsz,patch4,4);// 块大小constuint8_t*ppatch8;for(uint32_toff0;offtotal;offbsz){uint32_tn(total-offbsz)?(total-off):bsz;// 末块可能不足 bszuint8_top*p;if(op0){// 复制旧块同偏移uint32_told_off;memcpy(old_off,p,4);p4;flash_write_buf(new_baseoff,old_baseold_off,n);}else{// 写入新块原始数据flash_write_buf(new_baseoff,p,n);pbsz;// patch 里此块按 bsz 存已 padding}}}为什么这版适合 MCU全程只用一个块大小(256B)的临时缓冲不把整个固件载入 RAM。逐块读旧→合成→写新和 OTA_02 的烧写逻辑无缝衔接。生产环境建议直接移植成熟的bsdiff 嵌入式端口或Google Courgette / xdelta的精简版压缩率更高但内存模型和上面这段代码完全一致——理解了块级再看 bsdiff 只多一层索引。差分算法和 ESP-IDF 压缩 OTA 的建议都对照了 bsdiff/bspatch、xdelta3 的官方资料和 Espressif 文档。五、怎么接回上两篇的 OTA关键衔接差分不动底层机制只改下载什么 多一步合成接 ESP32OTA_01原http 下载 new.bin → 写 ota_1 → esp_ota_end → set_boot_partition 改http 下载 patch.bin → apply_patch(ota_0, patch, ota_1) → esp_ota_end → set_boot_partitionesp_ota_ops那套校验/回滚原样用只是喂给 ota_1 的内容由 patch 合成而来。接 STM32 BootloaderOTA_02原串口收 new.bin → 写 APP_ADDR → app_is_valid → jump 改串口收 patch.bin → apply_patch(当前app, patch, APP_ADDR) → app_is_valid → jumpapp_is_valid()的栈顶/复位向量校验照旧合成出的新固件不合法就拒绝跳转。六、安全差分更要校验差分包也是别人能伪造/传输出错的东西规矩不变patch 下载完先算CRC / SHA256 / 签名不对就丢弃重下合成出的新固件必须用app_is_validSTM32或esp_ota_endESP32整包校验通过才切换合成失败 → 旧固件还在app_is_valid失败 → 自动回滚/停留重传不变砖。七、新手必踩的 5 个坑#坑后果正确做法1在 STM32 上硬跑 bsdiffRAM 爆掉解压直接崩用 block diff / 流式或换 RAM 充足的芯片跑 bsdiff2链接地址被打乱改了代码顺序导致函数重排block diff 几乎找不到相同块patch 和全量一样大白省用可复现构建固定链接布局让未改函数地址不变3patch 不校验就应用损坏 patch 合成出坏固件 → 变砖应用前必做 CRC/签名校验4新旧固件长度不等块级等长假设失效合成错位保证总长度一致跨大版本用支持增删的算法(bsdiff)5首版/跨大版本也用 delta设备上根本没有旧固件可 diff首次部署、架构大改时回退全量升级第 2 条最隐蔽你只改了 30 行但因为链接器把整个镜像重排了块级差分几乎全部命中变了patch 瞬间涨到 700KB——和省流量背道而驰。可复现构建reproducible build是差分升级的前提。八、动手练一练编译两份只差几十行的固件old.bin/new.bin或拿 OTA_01 改版本号的两个 bin跑gen_patch.py看 patch 比 new 小多少。在 PC 写个 Python/C 的apply_patch把 patch old 还原成new_recovered.bin和原new.bin逐字节比对一致才算对。故意调整代码顺序让链接地址变动再生成一次 patch观察它变大多少——亲手验证可复现构建的重要性。把块级差分接到 OTA_01 的 ESP32 流程下载 patch 而非 full应用后set_boot_partition验证能正常升级。把 patch 随机翻转一个字节看app_is_valid/esp_ota_end是否能拦住、设备是否不砖。这 5 步跑通你就掌握了大批量 4G 设备低成本远程升级的核心本领——这是很多物联网公司的真实痛点。小结差分升级 传输优化层底层刷写/回滚完全复用前两篇。核心就一句服务器发差异设备用旧固件本地合成新固件。MCU 上别硬上 bsdiff用块级差分一次一个块RAM 友好但前提是可复现构建让未改代码地址不动否则白省。算清那笔8 万元 vs 几千元的账你就知道为什么量产物联网项目离不开它。下一篇预告《安全启动与固件签名Secure Boot—— 拒绝非法固件》
返回列表