OTA 升级实践(二):差分还原、平台抽象层与可靠性设计
鸿蒙设备 OTA 升级实践(二):差分还原、平台抽象层与可靠性设计
第一章建立了"多通道 → 状态机 → staging → 写分区"的整体框架,本章深入两个核心技术点:差分包如何还原、如何用平台抽象层隔离硬件差异,并给出可靠性设计的完整清单。
一、差分还原:hdiffpatch 在设备端怎么跑
1.1 全量包 vs 差分包的判定
包下载到 staging 区后,第一件事是"这是什么包"。通用判定逻辑是读文件头魔数:
enum pkg_type {
PKG_FULL_CPIO, /* 全量包:cpio 归档 */
PKG_DIFF, /* 裸差分包 */
PKG_DIFF_ZIP, /* zip 封装的差分包(差分数据+manifest) */
PKG_NEED_MORE, /* 头还没收全,等等再说 */
};
/* 魔数判定(伪代码):cpio 头为 "070701",zip 为 "PK\x03\x04",
HDiffPatch 头为 "HDR\x1F" 类似自定义标记 */
enum pkg_type pkg_detect(const uint8_t *head, size_t len)
{
if (len < 6) return PKG_NEED_MORE;
if (memcmp(head, "070701", 6) == 0) return PKG_FULL_CPIO;
if (memcmp(head, "PK\x03\x04", 4) == 0) return PKG_DIFF_ZIP;
if (is_hdiff_magic(head)) return PKG_DIFF;
return PKG_UNKNOWN; /* 未知包直接拒绝 */
}
PKG_NEED_MORE 是个容易被忽略的状态:BLE/串口场景分包到达时,可能第一个包只有 10 字节,连魔数都凑不齐。接收侧必须容忍"数据不足"并等待,而不是立刻判失败。
1.2 还原流程:从 zip 到分区镜像
差分 zip 包的完整处理链:
staging/ota_diff.zip
│ ① 解压(zip → manifest.json + 各分区 patch 文件)
▼
manifest.json ──> 校验 from_version == 当前版本、product 匹配
│
▼ ② 逐分区处理
for each partition:
switch (mode):
case PATCH: 旧分区镜像(Flash) + patch文件 → 新镜像 → 写回
case UNCHANGED: 校验设备上现有镜像 checksum,通过则跳过
│
▼ ③ 全部成功
写 ota_info(新版本号、待确认标志)→ 清 staging → 重启
1.3 还原的核心约束:内存
hdiffpatch 还原的内存开销主要来自覆盖缓冲区(cover buffer),规模与差分数据的覆盖块数量相关。轻量设备 RAM 通常只有几十 MB 甚至几 MB,通用应对策略:
| 策略 | 做法 | 代价 |
|---|---|---|
| 限制覆盖块 | 打包时用 --patch-step 类参数控制块数 | 包体略增 |
| 流式还原 | 边读 patch 边写目标,不整包进内存 | 实现复杂 |
| 多线程还原 | hdiffpatch 提供多线程 patch | 占 RAM 更多,需权衡 |
经验法则:先在小内存设备上实测还原峰值内存,再决定打包参数。打包侧"能出包"不等于"设备端能还原"。
1.4 还原写回:NAND 的坑
还原出的新镜像要写回分区,NAND 上有几个经典坑(均为公开通用知识):
- 坏块管理:写分区不能裸写,必须走 FTL/UBI 层,否则写到坏块直接数据损坏
- 位反转:NAND 位反转要求 ECC 校验,CRC 通过≠数据可靠,关键元数据建议双写
- 写入前必擦:擦除粒度是块(128KB 级),写 4KB 的 manifest 前要先擦整块
- 关中断写:擦写期间若被高优先级任务抢占,可能超出 NAND 控制器时序约束(视平台而定,通用建议:擦写放独立线程+互斥锁,不长时间关全局中断)
二、平台抽象层:让 OTA 代码不绑死硬件
2.1 为什么需要抽象
同一套 OTA 中间件可能要跑在:NAND 方案、NOR 方案、eMMC 方案。分区表不同、擦写粒度不同、回滚方式不同。把"怎么读旧镜像、怎么开新分区"抽象成操作函数集,上层还原逻辑就完全复用:
/* 平台操作集(伪代码,模式来自通用 hdiffpatch 集成实践) */
struct ota_platform_ops {
/* 开始升级:平台做准备工作(如备份旧固件、确认分区表) */
int (*begin_upgrade)(const struct manifest *m);
/* 中止:清理半成品 */
void (*abort_upgrade)(void);
/* 打开"旧镜像"流:差分还原的基准输入。
可以是 Flash 直接读,也可以是 staging 里的旧包 */
int (*open_old)(const char *component, uint64_t old_size,
struct ota_stream *s);
/* 打开"新镜像"目标流:写到哪里、附带什么校验 */
int (*open_new)(const char *component, uint64_t new_size,
uint32_t checksum, struct ota_stream *s);
/* 单分区完成回调(平台可在此做分区级校验) */
int (*finish_component)(const char *component);
/* 整体完成:写升级标志、设置引导切换 */
int (*finish_upgrade)(void);
};
2.2 旧镜像从哪来:差分还原的输入选择
open_old 的实现是个关键设计决策:
- 从 Flash 分区直接读:要求"设备上当前分区的镜像"与差分基准完全一致(由 from_version 校验保证)
- 从 staging 读旧包:某些场景(如双分区)旧镜像另有存放处
注意点:还原过程中旧镜像会被随机读、新镜像顺序写,若两者在同一物理分区上操作,必须确保读旧数据先于写覆盖的位置——hdiffpatch 的 patch 算法天然满足"从前往后",但平台层实现时不可依赖这一假设,稳妥做法是先把旧分区镜像拷到 staging 再还原。
2.3 全量包的写入:Sink 模式
全量包(cpio)的处理是纯粹的"解包写分区",用 Sink 抽象最干净:
struct full_sink_ops {
int (*begin)(void *ctx, uint64_t total_size);
int (*write)(void *ctx, const uint8_t *data, size_t size);
int (*finish)(void *ctx);
void (*abort)(void *ctx);
};
cpio 解析器只管把成员文件按顺序喂给 write,至于 write 后面接的是"NAND 写"还是"eMMC 写",由平台装配决定。同一段解包代码,不同硬件零修改复用——这就是抽象层的价值。
三、可靠性设计清单
3.1 升级前
- 版本一致性:差分包 from_version 严格匹配当前版本,不匹配立即失败
- product/机型校验:manifest 里 product 字段与设备机型比对,防串包
- 电量门槛:电池设备升级前检查电量(通用建议 ≥30%),不足则拒绝或提示接电
- 空间检查:staging 区剩余空间 > 包大小 × 1.5(解压余量)
3.2 升级中
- 分片校验:BLE/串口每个分片带 CRC,收错的分片立即 NACK 重传
- 整包校验:下载完成后先验 md5/SHA-256,校验不通过绝不写分区
- 进度落盘:关键节点(下载完成/校验通过/开始写分区)写 ota_info,掉电后知道死在哪一步
- 看门狗:APPLYING 阶段循环内主动喂狗,或用独立任务监控还原线程活性
- 写分区前备份:破坏性写入前,旧固件必须已有完整备份(预留区或双分区)
3.3 升级后
- 自检确认机制:新固件首次启动进"确认等待期",跑通自检(关键外设初始化成功、通信链路建立、UI 渲染正常)后主动调用"确认"接口,bootloader 才固化本次升级
- 超时回滚:确认等待期内未收到确认,bootloader 自动回滚旧固件
- 失败上报:失败原因码持久化,重启后上报云端/APP,排查有据可依
3.4 安全(公开通用建议)
- 传输加密:HTTPS/DTLS 防窃听
- 固件验签:升级包厂商私钥签名,设备内置公钥验签,验签在写入分区前完成
- 防降级:设备记录"最低允许版本",低于它的包拒绝(防回滚攻击)
四、本章小结
- 包类型判定用魔数,且必须容忍"数据不足"状态
- 差分还原的核心约束是内存,打包参数要按"设备端实测"校准
- 平台抽象层(platform_ops + stream/sink)让 OTA 中间件与硬件解耦
- 可靠性 = 升级前校验 + 升级中断点与看门狗 + 升级后自检回滚,三段闭环
更多推荐
所有评论(0)