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

📅 2026/8/17 21:10:50
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 的原理对旧固件和新固件做二进制对比生成一个差分文件.patch包含添加的数据 删除的标记 额外数据设备端用 bspatch 把旧固件 差分文件合成新固件bspatch 依赖 bzip2 压缩差分文件通常比原始差异大一点但远小于全量。二、生成差分包在服务器端生成差分包# 安装bsdiffsudoaptinstallbzip2libbz2-devgitclone https://github.com/mendsley/bsdiff.gitcdbsdiffmake# 生成差分包bsdiff firmware_v1.bin firmware_v2.bin firmware_v2.patchPython 版生成差分importbsdiffwithopen(firmware_v1.bin,rb)asf:old_dataf.read()withopen(firmware_v2.bin,rb)asf:new_dataf.read()patchbsdiff.diff(old_data,new_data)withopen(firmware_v2.patch,wb)asf:f.write(patch)print(fOld:{len(old_data)}bytes)print(fNew:{len(new_data)}bytes)print(fPatch:{len(patch)}bytes)print(fRatio:{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写入分区)差分升级流程设备从服务器下载 patch 文件从 ota_0 读旧固件 patch → 生成新固件 → 写入 ota_1切换到 ota_1 启动#includeesp_ota_ops.h#includebzlib.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(patchsize32)return-1;int64_tbzip2_diff_size*(int64_t*)(patch8);int64_tbzip2_extra_size*(int64_t*)(patch16);int64_tctrl_size*(int64_t*)(patch24);// 解压diff数据uint8_t*diff_bufmalloc(bzip2_diff_size*3);unsignedintdiff_outbzip2_diff_size*3;BZ2_bzBuffToBuffDecompress((char*)diff_buf,diff_out,(char*)(patch32),bzip2_diff_size,0,0);// 解压extra数据uint8_t*extra_bufmalloc(bzip2_extra_size*3);unsignedintextra_outbzip2_extra_size*3;BZ2_bzBuffToBuffDecompress((char*)extra_buf,extra_out,(char*)(patch32bzip2_diff_size),bzip2_extra_size,0,0);// 合成新固件int64_toldpos0,newpos0;int64_tdiff_pos0,extra_pos0;while(newposnewsize){// 1. 从old拷贝diff修正int64_tcopy_len...;// 从ctrl数据读取for(inti0;icopy_lennewposnewsize;i){if(oldposioldsize){new[newpos]old[oldposi]diff_buf[diff_pos];}else{new[newpos]diff_buf[diff_pos];}newpos;diff_pos;}oldposcopy_len;// 2. 从extra拷贝copy_len...;// 从ctrl数据读取memcpy(newnewpos,extra_bufextra_pos,copy_len);newposcopy_len;extra_poscopy_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_dataNULL;size_tpatch_size0;esp_err_terrhttp_download(patch_url,patch_data,patch_size);if(err!ESP_OK)returnerr;// 2. 读取当前固件constesp_partition_t*runningesp_ota_get_running_partition();constesp_partition_t*updateesp_ota_get_next_update_partition(NULL);uint8_t*old_firmwaremalloc(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_data8);// 4. 分配新固件缓冲区uint8_t*new_firmwaremalloc(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_handle0;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_chunkmalloc(CHUNK_SIZE);uint8_t*new_chunkmalloc(CHUNK_SIZE);uint8_t*diff_bufmalloc(CHUNK_SIZE);int64_told_pos0,new_pos0;while(new_posnew_size){// 分块读取旧固件intread_sizemin(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(inti0;iread_size;i){new_chunk[i]old_chunk[i]diff_buf[i];}// 写入新固件分区fwrite(new_chunk,1,read_size,new_fp);old_posread_size;new_posread_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_dataf.read()withopen(new_path,rb)asf:new_dataf.read()patchbsdiff.diff(old_data,new_data)# 用版本号命名差分包old_verextract_version(old_path)new_verextract_version(new_path)patch_pathf{patch_dir}/patch_{old_ver}_to_{new_ver}.binwithopen(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:fhttps://ota.example.com/patches/{patch_path},}returnmanifest# 生成当前版本到最近3个版本的差分包forold_verin[1.0.0,1.0.1,1.0.2]:manifestgenerate_patch(ffirmware_v{old_ver}.bin,ffirmware_v1.0.3.bin,patches/)print(fGenerated:{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秒七、踩坑清单内存不足ESP32 做 bspatch 需要同时加载旧固件 差分 新固件到内存。1MB 固件做差分要 2MB 内存必须用 PSRAM 或分块处理MD5 校验差分合成后必须校验 MD5。任何一字节错误都会导致固件跑飞版本跳跃差分包是版本 A 到版本 B 的。如果设备运行版本 A要升到版本 C需要先生成 A→C 的差分或先升到 B 再升到 CFlash 对齐ESP32 Flash 写入以 4KB 为单位。差分合成的新固件如果不是 4KB 对齐末尾补 0xFF回滚机制差分升级失败后ota_0 分区没被覆盖重启还能跑旧版。但如果写到一半 OTA 分区坏了下次启动会进 ota_1 的无效分区。建议双区切换 回滚标志bzip2 库ESP32 上用 bzip2 需要移植。用make menuconfig→ Component config → bzip2 启用。也可以改成不压缩的差分格式体积大些但省 CPU生成效率服务器端生成差分包比较慢几十秒。建议在 CI/CD 中预生成不要实时生成