嵌入式 IAP 升级功能(#05)增量升级
全量升级由于要传输新版程序的完整镜像,因此升级时间通常较长,升级失败的概率也更大。那么能不能只传送差异数据呢?答案是可以。这种技术被称作增量升级/差量升级/差分升级。
常见的方案有:
- bsdiff/bspatch + quicklz
- hdifflite/hpatchlite + tinyuz
全量升级 & 增量升级
增量升级确实降低了传输过程中的数据量,但也带来了版本管理复杂的问题,所以说不能因为有了增量升级,全量升级就不用了。
以往我们做全量升级的时候没有引入压缩技术,在移植 hdiff/hpatchlite 的时候我发现,hdiff 生成的差异文件不比原文件小多少,但是其可压缩性非常高,这样就得把解压算法也移植进来。既然解压算法都已经有了,不把增量升级也压缩一下,那岂不是很浪费?
全量升级 | 未经压缩的新版程序 | (✘) |
经过压缩的新版程序 | (✔) | |
增量升级 | 未经压缩的差异文件 | (✘) |
经过压缩的差异文件 | (✔) |
升级包头
在线升级无非就是把新程序或者更新补丁发送给设备,设备收到后进行升级的过程。
为了保证升级能够顺利进行,除了新程序或者更新补丁外,我们还要向设备发送一些附加信息,这些附加信息通常被添加至升级文件的头部。
魔术数字 | 04B | 全量升级 ('Q','L','S','J') 增量升级 ('Z','L','S','J') |
包头长度 | 04B | 支持变长包头 * |
文件摘要 | 04B | 从「文件长度」之后开始计算 |
文件长度 | 04B | 从「文件长度」之后开始计算 |
产品型号 | 08B | 产品一型 ('P','N','-','A','0','0','0','1') 产品二型 ('P','N','-','A','0','0','0','2') |
设备地址 | 08B | 通配地址 (0xFFFFFFFFFFFFFFFF) 单点地址 (0x1111111111111111) |
新程序 LEN 值 | 04B | 对旧程序进行摘要值校验 或者 对旧程序进行差分还原时 某些可变字段必须以默认值进行处理 |
旧程序 LEN 值 | 04B | |
新程序 CRC 值 | 04B | |
旧程序 CRC 值 | 04B | |
新程序 MD5 值 | 16B | |
旧程序 MD5 值 | 16B | |
...... | ||
可以按需增加 |
变长包头的优势
升级文件
升级方案
未经压缩的全量升级 + 经过压缩的增量升级
经过压缩的全量升级 + 经过压缩的增量升级
升级流程
接收升级数据
flowchart TB %%{init: { "flowchart": { "curve": "basis" } } }%% Start(接收升级数据<br>开始)-->DoRecv[接收数据]-->IsHeadRecvDone{文件头接收完毕} IsHeadRecvDone--否/继续接收-->DoRecv IsHeadRecvDone--是-->IsHeadParsed{文件头已被处理} IsHeadParsed--是---->DoWrite[将接收到的数据写入外存<br>如果为「压缩增量升级」则将数据写入〈升级数据存储区〉<br>如果为「压缩全量升级」则将数据写入〈升级数据存储区〉<br>如果为「原版全量升级」则将数据写入〈新版程序存储区〉<br>(如果小程序为旧版本则写入时偏移75字节)]-->IsFileRecvOver{文件传输完毕} IsFileRecvOver--否/继续接收-->DoRecv2[继续接收数据] IsFileRecvOver--是-->DoCheck[校验接收到的升级文件<br>如果为「压缩增量升级」则从〈升级数据存储区〉中读出数据并计算CRC值<br>如果为「压缩全量升级」则从〈升级数据存储区〉中读出数据并计算CRC值<br>如果为「原版全量升级」则从〈新版程序存储区〉中读出数据并计算CRC值<br>(如果小程序为旧版本则读取时偏移75字节)]-->IsCheckOK{校验是否通过} IsCheckOK--否-->Over3(结束) IsCheckOK--是-->DoWriteHead[将文件头中的某些数据写入外存的第一个扇区<br>(如果小程序为旧版本则将“新程序大小”加75字节)]-->DoWriteHeadFlag[将该扇区中的升级标识从0xFFFFFFFF改成0xAABBFFFF]-->IsA{增量升级} IsA--是-->DoSetA[发起增量升级任务]-->Over4(结束) IsA--否-->IsB{有压缩的<br>全量升级} IsB--是-->DoSetB[发起全量升级任务]-->Over4(结束) IsB--否-->IsC{无压缩的<br>全量升级} IsC--是-->DoSetC[发起全量升级任务]-->Over4(结束) IsC--否-->Over5(结束) IsHeadParsed--否-->IsHeadOK{文件头格式正确} IsHeadOK--否-->Over1(结束) IsHeadOK--是-->IsDiffUpdate{是否为增量升级} IsDiffUpdate--否/继续接收-->DoRecv IsDiffUpdate--是-->IsCRCOK{文件头中的旧程序CRC与<br>当前程序的CRC是否一致} IsCRCOK--(当前程序中的可变字段以默认值进行处理)<br><br>不一致-->Over2(结束) IsCRCOK--是/继续接收-->DoRecv
执行升级任务
flowchart TB %%{init: { "flowchart": { "curve": "basis" } } }%% Start(执行升级任务<br>开始)-->IsUpdateFlag{升级文件接收完毕} IsUpdateFlag--否-->IsUpdateFlag IsUpdateFlag--是-->DoReadHead[从外存的第一个扇区中读取升级信息]-->IsReadSucc{读取成功} IsReadSucc--否-->Over1(结束) IsReadSucc--是-->IsUpdateFlagIsDownload{升级标识==0xAABBFFFF} IsUpdateFlagIsDownload--否-->Over2(结束) IsUpdateFlagIsDownload--是-->IsUpdateTypeIsZipDiff{是否为压缩增量升级} IsUpdateTypeIsZipDiff--是-->DoUnDiff[执行差分还原操作]--如果小程序为旧版本<br>则写入时偏移75字节<br>当前程序中的可变字段以默认值进行处理--->DoCheckNewAppCRC IsUpdateTypeIsZipDiff--否-->IsUpdateTypeIsZipFull{是否为压缩全量升级} IsUpdateTypeIsZipFull--是-->DoUnZip[执行文件解压操作]--如果小程序为旧版本<br>则写入时偏移75字节--->DoCheckNewAppCRC IsUpdateTypeIsZipFull--否-->IsUpdateTypeIsRawFull{是否为原版全量升级} IsUpdateTypeIsRawFull--是-->DoNothing[无需执行任何操作]--->DoCheckNewAppCRC[校验新程序<br>对比文件头中的新程序CRC与<br>新程序存储区中的数据的CRC] IsUpdateTypeIsRawFull--否-->Over3(结束) DoCheckNewAppCRC--如果小程序为旧版本<br>则读取时偏移75字节--->IsCRCCheckOK{CRC是否一致} IsCRCCheckOK--否-->Over4(结束) IsCRCCheckOK--是-->DoChangeUpdateFlag[将外存中的升级标识从0xAABBFFFF改为0xAABBCCDD]-->IsChangeSucc{修改是否成功} IsChangeSucc--否-->Over5(结束) IsChangeSucc--是-->DoChangeInternalFlashUpdateFlag[将内部flash中的0xAABBCCDD改成0x0000CCDD]-->DoReset[一秒后重启进入引导程序]-->OverX(结束)