一、鸿蒙轻量设备的 OTA 有什么不一样

1.1 与 Linux / 手机 OTA 的三大差异

维度Linux/手机 OTA鸿蒙轻量设备
固件形态整机镜像 + 动态分区多分区静态镜像(root/system/app 各自成包)
应用与系统应用可独立热更新应用打包进固件,跟随整机升级
包体大小GB 级几 MB,且要塞进几十 MB 的 NAND
通信方式强网(Wi-Fi/蜂窝)弱网常态:BLE、Cat.1、串口中继

核心结论:鸿蒙轻量设备的 OTA 不是"下载一个 APK",而是"通过任意通道搬一份新固件进 Flash,且搬的过程中设备必须随时可活"。

1.2 典型分区布局:A/B 双分区是主力方案

以 SPI-NAND 方案的常见轻量设备为例(这也是多数鸿蒙面板类产品的实际选择),分区表长这样:

┌─────────────────────────────┐
│  spl(一级引导)            │  极少升级,出问题就是砖
├─────────────────────────────┤
│  env / env_r(环境变量)    │  互为冗余:版本号、升级标志、引导决策
├─────────────────────────────┤
│  os(当前系统分区)         │  内核+应用打包成一个 itb 镜像
├─────────────────────────────┤
│  os_r(备份系统分区)       │  os 的 A/B 冗余,回滚的物理基础
├─────────────────────────────┤
│  rodata / rodata_r(只读资源)│  字体/语音等资源镜像,同样 A/B 成对
├─────────────────────────────┤
│  data(NFTL 卷,可写)      │  用户数据 + OTA 中转区(差分包/全量包落这里)
└─────────────────────────────┘

三个关键设计:

  • A/B 成对出现:os/os_r、rodata/rodata_r都是双份,升级时写 _r 侧,bootloader 决定引导哪一侧——这是"升级失败可回滚"的物理基础,比单分区+备份搬运简单可靠得多
  • env 双写冗余:env/env_r 互为备份,环境变量(含版本号、升级标志)掉电写坏一半还能从另一份恢复
  • 中转区放 data 分区:固件包先落到 NFTL 可写文件系统,整包校验通过后才允许写 os_r/rodata_r,任何阶段掉电都不伤及正在运行的系统

1.3 差分包:为什么是"两分区"模型

鸿蒙轻量设备差分包通常把固件拆成若干分区镜像,每个分区独立决策:

  • PATCH 模式:该分区有变化,包内携带旧镜像 → 新镜像 的 hdiffpatch 差分数据
  • UNCHANGED 模式:该分区没变,包内只记"沿用设备上现有内容",省流量

一个典型差分包的 manifest 逻辑结构(伪代码):

/* 差分包清单:描述"这个包是谁、从哪到哪、每个分区怎么处理" */
struct manifest {
    uint32_t format_version;              /* 清单格式版本 */
    char     product[64];                 /* 产品标识,防串包 */
    char     from_version[32];            /* 基准版本:必须等于设备当前版本 */
    char     to_version[32];              /* 目标版本 */
    struct {
        char     name[16];                /* 分区名,如 "system"、"app" */
        char     file[32];                /* 全量镜像文件名(兜底用) */
        char     patch_file[32];          /* 差分文件名 */
        uint8_t  mode;                    /* PATCH / UNCHANGED */
        uint64_t old_size;                /* 基准镜像大小,防基准不匹配 */
        uint64_t new_size;                /* 新镜像大小 */
        uint64_t patch_offset;            /* 差分数据在包内偏移 */
        uint64_t patch_size;
        uint32_t checksum;                /* 分区数据校验值 */
    } partitions[2];                      /* 轻量设备常见 1~2 个分区 */
};

from_version 是差分包的命门:差分数据是针对"某个确定的旧版本"算出来的。设备当前版本 ≠ from_version 时,必须直接判失败,否则 patch 出来的是"四不像"固件。from_version 从哪来:打包工具从当前镜像的版本配置文件(如 image_cfg.json一类的描述文件)读取版本号写入 manifest,因此改版本号必须改版本描述文件并重新出包,只改打包配置是不生效的——这是差分包最常见的翻车点之一。


二、多通道接入:一套 OTA,四种来源

2.1 为什么要抽象"来源"

同一台设备,升级包可能来自四个方向:

Wi-Fi   —— 直连云端 HTTPS 下载(强网,速度快)
BLE     —— 手机 App 蓝牙分包推送(无网现场)
Cat.1   —— 蜂窝模组透传下载(Wi-Fi 不可用场景)
本地    —— U 盘/串口直接提交一个升级包文件

如果每个通道各写一套升级逻辑,状态、进度、错误处理会四头不统一。正确做法是:通道只负责"把字节搬进来",进入统一入口后走同一条状态机。

2.2 统一入口:Controller 模式

伪代码示例(模式通用,命名泛化):

/* 对外统一入口:任何通道最终都汇入这里 */
enum ota_source { SRC_WIFI, SRC_BLE, SRC_CAT1, SRC_LOCAL };

rt_err_t ota_start_check_via_wifi(void);            /* Wi-Fi 云端检查+下载 */
rt_err_t ota_prepare_local_receive(enum ota_source); /* 通知控制器"包要来了" */
rt_err_t ota_submit_local_file(const char *path,     /* 包已就位,开始应用 */
                               enum ota_source);
void     ota_cancel(void);
void     ota_register_finish_cb(ota_finish_cb_t cb);
int      ota_get_snapshot(struct ota_snapshot *out); /* 状态+进度+错误码快照 */

设计要点:

  • 来源只是标签:状态机内部不关心包从哪来,只记录 source 用于日志和上报
  • 快照接口:UI、云端、手机 App 都通过同一个 get_snapshot 查询,天然单一路径
  • 互斥:控制器内部持一把互斥锁,保证同一时刻只有一条升级会话,后到的请求返回 BUSY

2.3 BLE 分包接收的状态设计(以通用 FOTA 为例)

BLE/串口这类"分包流"通道,接收侧是一个独立的收包状态机:

/* 通用 FOTA 分包接收状态(伪代码,模式通用) */
enum fota_phase {
    FOTA_IDLE,          /* 空闲 */
    FOTA_RECEIVING,     /* 分包接收中 */
    FOTA_APPLYING,      /* 校验+应用包 */
    FOTA_FINISH_REPORTED, /* 已上报完成 */
    FOTA_LOCAL_CANCELED,  /* 本地取消 */
};

/* 接收上下文:记录断点续传与校验所需的一切 */
struct fota_recv_state {
    uint32_t total_recv_len;        /* 已收总字节数 */
    uint32_t expected_total_len;    /* 包总长 */
    uint32_t expected_seq;          /* 下一个期望分片号 */
    uint32_t packet_count;          /* 收包计数 */
    uint32_t duplicate_count;       /* 重复分片计数(诊断用) */
    bool     use_crc32;             /* CRC16 或 CRC32 */
    union  { uint16_t crc16; uint32_t crc32; } expect_crc;
    FILE    *fp;                    /* 写入 staging 区的文件句柄 */
    /* 失速检测:最近一次收到数据的 tick */
    rt_tick_t last_receive_tick;
    bool     timeout_armed;
};

几条血泪经验(通用现象,非特定项目):

  • 分片号从 0 还是从 1 计:收发两端必须书面约定,"差一"错误会让最后一个分片永远对不上
  • 重复分片不要当错误:BLE 环境下手机重发是常态,正确处理是"序号 < 期望值 → 静默丢弃并计数"
  • 失速检测必须有:now - last_receive_tick > 3s 即触发恢复流程,否则一次蓝牙抖动就把整个会话挂死
  • flush 节奏:每收 64KB 强制 fflush 一次,把"文件系统缓存"与"已收字节"对齐,掉电后断点续传才准确

三、状态机:OTA 的脊梁

3.1 状态全集

一个成熟的多通道 OTA 状态机大致长这样:

enum ota_state {
    IDLE,        /* 空闲 */
    CHECKING,    /* 云端检查更新 */
    PROBING,     /* 探测包类型(全量 cpio / 差分 zip / 数据不足) */
    DOWNLOADING, /* 下载中(或分包接收中) */
    VERIFYING,   /* 校验(哈希/验签/manifest 检查) */
    APPLYING,    /* 应用升级(差分还原/全量解包/写分区) */
    FINISHING,   /* 收尾(清理 staging、写 ota_info) */
    REBOOTING,   /* 重启切换 */
    SUCCESS,     /* 成功终态 */
    FAILED,      /* 失败终态 */
    CANCELED,    /* 取消终态 */
};

3.2 进度映射:让 UI 看到合理的进度条

下载+应用+校验的时间分布极不均匀(差分还原占大头),直接把"字节进度"喂给 UI 会卡在 5% 十分钟然后瞬间跳到 100%。通用解法是分段进度映射表:

┌──────────────────────────────────────────────────┐
│ 0~5%    下载(快,强网几秒)                      │
│ 5~10%   解压/探测(中)                           │
│ 10~95%  应用(差分还原/解包写分区,慢,占大头)   │
│ 95~99%  收尾与自检                                │
│ 99~100% 重启切换                                  │
└──────────────────────────────────────────────────┘

不同通道用不同映射参数(BLE 接收慢,其"下载"段就应占更多进度区间)。

3.3 状态持久化:掉电恢复的依据

状态机的关键状态必须落盘(写入 ota_info/env 区),保证掉电重启后能恢复现场:

/* 掉电恢复伪代码 */
void ota_boot_recovery(void)
{
    struct ota_persisted p;
    if (read_ota_info(&p) != OK)
        return;                       /* 无升级标志,正常启动 */

    switch (p.phase) {
    case PHASE_DOWNLOADING:
        /* 下载中断:清理半包,回 IDLE(或报告断点) */
        clear_staging();
        break;
    case PHASE_APPLYING:
        /* 应用中断:这是最危险的状态。
           全量包:分区已写了一半 → 走 bootloader 回滚;
           差分包:还原中途掉电 → 目标分区已损坏,必须回滚或重下 */
        rollback_or_redownload();
        break;
    case PHASE_REBOOTING:
        /* 已切分区待确认:bootloader 引导新固件,等待自检确认 */
        wait_new_firmware_confirm();
        break;
    }
}

APPLYING 阶段掉电是最危险的窗口,所以通用设计原则是:写关键分区前,先把旧固件备份到预留区(或依赖双分区方案),任何破坏性写入之前必须有退路。

3.4 一条升级会话的完整时序

把前面所有组件串起来,一次 Wi-Fi 差分升级的完整时序:

UI/云端          Controller          下载器        应用器(staging+diff)     Flash
  │  "检查更新"     │                  │                │                   │
  ├───────────────>│ CHECKING         │                │                   │
  │                ├── query ────────>│                │                   │
  │                │<─── url+md5 ─────┤                │                   │
  │                ├── DOWNLOADING ──>│ 流式写 staging │                   │
  │                │                  ├───────────────>│                   │
  │                ├── VERIFYING ─────┼───────────────>│ md5+manifest 校验 │
  │                ├── APPLYING ──────┼───────────────>│ 还原+写分区 ─────>│
  │                ├── FINISHING ─────┼───────────────>│ 清 staging+写info │
  │                ├── REBOOTING      │                │                   │
  │                ▼                  │                │                   │
  │             [重启 → bootloader → 新固件自检 → 确认]                        │
  │<──────────────── SUCCESS(新版本自检通过后上报)                          │

四、本章小结

  • 鸿蒙轻量设备 OTA 的本质:任意通道 → 统一状态机 → staging 中转 → 校验后写分区 → 回滚兜底
  • 通道只搬字节,状态只走一条:多源接入用 Controller 模式收口
  • 差分包的三要素:manifest 清单、from_version 严格匹配、分区级 PATCH/UNCHANGED 决策
  • 进度是"映射"出来的,不是真实字节数;状态必须持久化,APPLYING 掉电必须有退路

Logo

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

更多推荐