OTA升级(1)
一、鸿蒙轻量设备的 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 掉电必须有退路
更多推荐
所有评论(0)