news 2026/8/17 21:10:45

IoT OTA差分升级:bspatch减小固件体积

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
IoT OTA差分升级:bspatch减小固件体积

IoT OTA 差分升级:bspatch 减小固件体积

先说结论

两个版本间改动不大(改 bug、调参数),差分包通常是全量的 5-15%。全量改了 50% 以上,差分就没意义了。做差分升级推荐 bspatch,算法成熟、实现简单。

做 OTA 升级时最头疼的是固件体积。ESP32 OTA 分区通常划 512KB-1MB,一次全量升级要传 1MB 数据。如果是 NB-IoT 或 4G Cat.1 模块,带宽就几十 kbps,传 1MB 要 2-3 分钟。

差分升级只传变化的部分,通常几十 KB 就够了。


一、bsdiff/bspatch 原理

bsdiff 的原理:

  1. 对旧固件和新固件做二进制对比
  2. 生成一个差分文件(.patch),包含:添加的数据 + 删除的标记 + 额外数据
  3. 设备端用 bspatch 把旧固件 + 差分文件合成新固件

bspatch 依赖 bzip2 压缩,差分文件通常比原始差异大一点,但远小于全量。


二、生成差分包

在服务器端生成差分包:

# 安装bsdiffsudoaptinstallbzip2libbz2-devgitclone https://github.com/mendsley/bsdiff.gitcdbsdiff&&make# 生成差分包bsdiff firmware_v1.bin firmware_v2.bin firmware_v2.patch

Python 版生成差分:

importbsdiffwithopen('firmware_v1.bin','rb')asf:old_data=f.read()withopen('firmware_v2.bin','rb')asf:new_data=f.read()patch=bsdiff.diff(old_data,new_data)withopen('firmware_v2.patch','wb')asf:f.write(patch)print(f"Old:{len(old_data)}bytes")print(f"New:{len(new_data)}bytes")print(f"Patch:{len(patch)}bytes")print(f"Ratio:{len(patch)/len(new_data)*100:.1f}%")

三、ESP32 差分升级实现

ESP32 的 OTA 分区方案:

分区表: nvs 0x9000 24KB otadata 0x16000 8KB ota_0 0x20000 1MB (当前运行分区) ota_1 0x120000 1MB (OTA写入分区)

差分升级流程:

  1. 设备从服务器下载 patch 文件
  2. 从 ota_0 读旧固件 + patch → 生成新固件 → 写入 ota_1
  3. 切换到 ota_1 启动
#include"esp_ota_ops.h"#include"bzlib.h"// bzip2库// bspatch核心函数intbspatch(constuint8_t*old,int64_toldsize,constuint8_t*patch,int64_tpatchsize,uint8_t*new,int64_tnewsize){// 差分文件头: 8字节magic + 8字节newsize + 8字节diffsize + 8字节extrasizeif(patchsize<32)return-1;int64_tbzip2_diff_size=*(int64_t*)(patch+8);int64_tbzip2_extra_size=*(int64_t*)(patch+16);int64_tctrl_size=*(int64_t*)(patch+24);// 解压diff数据uint8_t*diff_buf=malloc(bzip2_diff_size*3);unsignedintdiff_out=bzip2_diff_size*3;BZ2_bzBuffToBuffDecompress((char*)diff_buf,&diff_out,(char*)(patch+32),bzip2_diff_size,0,0);// 解压extra数据uint8_t*extra_buf=malloc(bzip2_extra_size*3);unsignedintextra_out=bzip2_extra_size*3;BZ2_bzBuffToBuffDecompress((char*)extra_buf,&extra_out,(char*)(patch+32+bzip2_diff_size),bzip2_extra_size,0,0);// 合成新固件int64_toldpos=0,newpos=0;int64_tdiff_pos=0,extra_pos=0;while(newpos<newsize){// 1. 从old拷贝+diff修正int64_tcopy_len=...;// 从ctrl数据读取for(inti=0;i<copy_len&&newpos<newsize;i++){if(oldpos+i<oldsize){new[newpos]=old[oldpos+i]+diff_buf[diff_pos];}else{new[newpos]=diff_buf[diff_pos];}newpos++;diff_pos++;}oldpos+=copy_len;// 2. 从extra拷贝copy_len=...;// 从ctrl数据读取memcpy(new+newpos,extra_buf+extra_pos,copy_len);newpos+=copy_len;extra_pos+=copy_len;// 3. 调整old位置oldpos+=...;// 从ctrl数据读取的偏移}free(diff_buf);free(extra_buf);return0;}// OTA差分升级主函数esp_err_tota_delta_update(constchar*patch_url){// 1. 下载patch文件uint8_t*patch_data=NULL;size_tpatch_size=0;esp_err_terr=http_download(patch_url,&patch_data,&patch_size);if(err!=ESP_OK)returnerr;// 2. 读取当前固件constesp_partition_t*running=esp_ota_get_running_partition();constesp_partition_t*update=esp_ota_get_next_update_partition(NULL);uint8_t*old_firmware=malloc(running->size);spi_flash_mmap_handle_tmap_handle;esp_partition_mmap(running,0,running->size,SPI_FLASH_MMAP_DATA,(constvoid**)&old_firmware,&map_handle);// 3. 解析patch头获取新固件大小int64_tnew_size=*(int64_t*)(patch_data+8);// 4. 分配新固件缓冲区uint8_t*new_firmware=malloc(new_size);if(!new_firmware){printf("Not enough memory for new firmware\n");returnESP_ERR_NO_MEM;}// 5. 合成新固件bspatch(old_firmware,running->size,patch_data,patch_size,new_firmware,new_size);// 6. 写入OTA分区esp_ota_handle_tupdate_handle=0;esp_ota_begin(update,OTA_SIZE_WITH_SECTOR,&update_handle);esp_ota_write(update_handle,new_firmware,new_size);esp_ota_end(update_handle);esp_ota_set_boot_partition(update);// 7. 清理free(new_firmware);free(patch_data);esp_partition_munmap(map_handle);printf("Delta OTA done, restarting...\n");esp_restart();returnESP_OK;}

四、内存优化

ESP32 内部 SRAM 只有 520KB,放不下 1MB 固件。必须分块处理:

#defineCHUNK_SIZE(4*1024)// 4KB分块intbspatch_streaming(FILE*old_fp,FILE*patch_fp,FILE*new_fp,int64_told_size,int64_tnew_size){uint8_t*old_chunk=malloc(CHUNK_SIZE);uint8_t*new_chunk=malloc(CHUNK_SIZE);uint8_t*diff_buf=malloc(CHUNK_SIZE);int64_told_pos=0,new_pos=0;while(new_pos<new_size){// 分块读取旧固件intread_size=min(CHUNK_SIZE,old_size-old_pos);fseek(old_fp,old_pos,SEEK_SET);fread(old_chunk,1,read_size,old_fp);// 分块读差分数据fread(diff_buf,1,read_size,patch_fp);// 合成新数据for(inti=0;i<read_size;i++){new_chunk[i]=old_chunk[i]+diff_buf[i];}// 写入新固件分区fwrite(new_chunk,1,read_size,new_fp);old_pos+=read_size;new_pos+=read_size;}free(old_chunk);free(new_chunk);free(diff_buf);return0;}

五、服务器端差分生成

自动化生成差分包:

#!/usr/bin/env python3importbsdiffimportos,sys,hashlibdefgenerate_patch(old_path,new_path,patch_dir):withopen(old_path,'rb')asf:old_data=f.read()withopen(new_path,'rb')asf:new_data=f.read()patch=bsdiff.diff(old_data,new_data)# 用版本号命名差分包old_ver=extract_version(old_path)new_ver=extract_version(new_path)patch_path=f"{patch_dir}/patch_{old_ver}_to_{new_ver}.bin"withopen(patch_path,'wb')asf:f.write(patch)# 生成manifestmanifest={"old_version":old_ver,"new_version":new_ver,"old_md5":hashlib.md5(old_data).hexdigest(),"new_md5":hashlib.md5(new_data).hexdigest(),"patch_size":len(patch),"full_size":len(new_data),"patch_url":f"https://ota.example.com/patches/{patch_path}",}returnmanifest# 生成当前版本到最近3个版本的差分包forold_verin["1.0.0","1.0.1","1.0.2"]:manifest=generate_patch(f"firmware_v{old_ver}.bin",f"firmware_v1.0.3.bin","patches/")print(f"Generated:{manifest}")

六、实测数据

ESP32 固件,1MB:

版本差异全量大小差分包大小压缩比
改了3个函数1024KB8KB0.8%
加了1个模块1024KB45KB4.4%
修改了20%代码1024KB180KB17.6%
大改(50%不同)1024KB520KB50.8%
完全不同1024KB1050KB102.5%

下载时间对比(NB-IoT 20kbps 带宽)

方案下载数据量下载时间
全量OTA1024KB~7分钟
差分OTA(小改)8KB3.2秒
差分OTA(中改)45KB18秒
差分OTA(大改)180KB73秒

七、踩坑清单

  1. 内存不足:ESP32 做 bspatch 需要同时加载旧固件 + 差分 + 新固件到内存。1MB 固件做差分要 2MB+ 内存,必须用 PSRAM 或分块处理
  2. MD5 校验:差分合成后必须校验 MD5。任何一字节错误都会导致固件跑飞
  3. 版本跳跃:差分包是版本 A 到版本 B 的。如果设备运行版本 A,要升到版本 C,需要先生成 A→C 的差分,或先升到 B 再升到 C
  4. Flash 对齐:ESP32 Flash 写入以 4KB 为单位。差分合成的新固件如果不是 4KB 对齐,末尾补 0xFF
  5. 回滚机制:差分升级失败后,ota_0 分区没被覆盖,重启还能跑旧版。但如果写到一半 OTA 分区坏了,下次启动会进 ota_1 的无效分区。建议双区切换 + 回滚标志
  6. bzip2 库:ESP32 上用 bzip2 需要移植。用make menuconfig→ Component config → bzip2 启用。也可以改成不压缩的差分格式(体积大些但省 CPU)
  7. 生成效率:服务器端生成差分包比较慢(几十秒)。建议在 CI/CD 中预生成,不要实时生成
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/17 21:10:04

每天省下一小时重复劳动的炉石传说插件:HsMod到底能帮你做什么

每天省下一小时重复劳动的炉石传说插件&#xff1a;HsMod到底能帮你做什么 【免费下载链接】HsMod Hearthstone Modification Based on BepInEx 项目地址: https://gitcode.com/GitHub_Trending/hs/HsMod 你是否有过这样的经历&#xff1a;清晨爬起来只想把炉石传说日常…

作者头像 李华
网站建设 2026/8/17 21:08:49

Ryujinx模拟器终极调优攻略:从装不上到丝滑畅玩的完整实操

Ryujinx模拟器终极调优攻略&#xff1a;从装不上到丝滑畅玩的完整实操 【免费下载链接】Ryujinx 用 C# 编写的实验性 Nintendo Switch 模拟器 项目地址: https://gitcode.com/GitHub_Trending/ry/Ryujinx Ryujinx 是一款用 C# 编写的开源 Nintendo Switch 模拟器&#x…

作者头像 李华
网站建设 2026/8/17 21:06:50

系统架构设计师考试模拟题库(下):案例分析与论文指导

系统架构设计师考试模拟题库(下):案例分析与论文指导 上期我们做了选择题和案例分析,今天我们来完成下篇,包括更多案例分析和最重要的论文写作指导。 下午卷案例分析题(续) 案例二:分布式缓存设计 背景:某社交平台需要设计缓存系统,支撑日活5000万用户的信息流查…

作者头像 李华