鸿蒙设备 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 中间件与硬件解耦
  • 可靠性 = 升级前校验 + 升级中断点与看门狗 + 升级后自检回滚,三段闭环
Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐