7.2.6 OTA
概述
OTA :( Over-the-Air Technology,空中下载技术)是指通过无线网络实现远程软件升级的技术。最早由安卓系统引入到手机设备中, OTA 技术大幅简化了传统软件升级过程,无需通过计算机连接设备,用户可直接在设备上下载并安装更新。这一技术极大地方便了用户,提高了设备维护的效率。
- 在 OTA 的广义应用中,可以划分为云端和设备端两个主要组成部分。云端部分负责处理设备的升级请求,包括执行升级校验、下发升级包以及收集升级结果等任务。而设备端则主要依赖云端下发的升级包,完成系统软件( FOTA, Firmware Over-the-Air)或应用程序( SOTA, Software Over-the-Air)的更新与升级。
- 本文旨在提供底层设备端 OTA 的用户手册,详细阐述 OTA 在底层系统软件和应用程序升级中的机制及其实现方法,同时提供相关的开发指导。需要特别指出的是,通过 OTA 升级的系统软件与应用程序,主要是指更新存储在外部存储器(如 eMMC )中的数据。
- OTA 的对外交付物主要为一套 API 及其相应的实现库(如 libupdate.so),该库实现了底层烧写校验等关键功能。上层的 OTA 服务架构则由客户方实现,用以对接客户的云端服务。在成功从云端下载升级包后, OTA 服务会通过调用 libupdate.so 中的接口,实现版本的升级和校验等操作,从而确保设备能够顺利、安全地完成软件更新。
缩略语
缩略语 | 英文全称 | 中文解释 |
---|---|---|
SoC | System on Chip | 片上系统 |
BL[x] | Boot Loader Stage [x] | 启动的阶段 x |
SPL | Secondary Program Loader | 二级程序加载器 |
GPT | GUID Partition Table | GUID 磁盘分区表 |
GUID | Globally Unique IDentifier | 全局唯一标识符 |
RSA | RSA Algorithm | RSA 公开密钥密码体制 |
eMMC | embedded MultiMedia Card | 嵌入式非易失性存储器 |
系统分区表
OTA 时,将以分区为单位对目标分区进行更新,这些分区按照不同的类型做如下区分:
分区类型 | 属性 | 升级方式 | 举例 |
---|---|---|---|
持久化分区 | 参数分区:一般存储系统运行时需要加载的一些配置文件和参数等数据。如分区表中的 ubootenv 等 用户分区:指与系统启动无关的分区。一般在系统启动后才会被挂载,如 userdata 分区 | 单分区一般没有镜像, 分区数据需要长期保存, 不支持 OTA 升级。 | ubootenv, veeprom, userdata |
AB 分区 | 前缀同名且尾缀带 _a 和 _b 的分区称做 AB 分区。 | AB 分区交替升级。 | boot_a, boot_b |
BAK 分区 | 前缀同名且尾缀带bak 的分区称作 BAK 分区,主要构成为 1 个主分区和若干个备份分区。 | BAK 分区只升级主分区,主分区升级并验证成功后,将主分区内容同步到备份分区。 | SBL, SBL_bak |
开启OTA
RDK默认不开启OTA功能,如需开启请按如下流程操作:
-
开启OTA前,若未编译过工程,没有生成out目录则执行下列命令,必须先编译工程或者建立编译环境。
# 编译工程,生成镜像
sudo ./pack_image.sh
# 仅建立编译环境
sudo ./pack_image.sh -p -
将build_params目录下的
ubuntu-22.04_desktop_rdk-s100_beta.conf
和ubuntu-22.04_desktop_rdk-s100_release.conf
中的中的PARTITION_FILE配置为OTA版本(PARTITION_FILE="s100-ota-gpt.json); -
将source/bootloader/device/rdk/s100目录下的
board_s100_debug.mk
和board_s100_release.mk
文件中的RDK_OTA
变量配置为开启(export RDK_OTA="yes") -
编译
- 制作新的miniboot的deb包
./mk_debs.sh hobot-miniboot
- 重新编译本地镜像
sudo ./pack_image.sh -l
- 制作新的miniboot的deb包
OTA 打包工具
OTA 打包工具介绍
OTA打包工具位于ota_tools/
路径下,该文件夹包含的内容如下:
tree
.
├── hdiffz // 差分工具,用于生成文件差异
├── hpatchz // 差分工具,用于应用文件差异
├── mk_otapackage.py // OTA 打包工具,用于生成 OTA 升级包
├── ota_pack_tool.sh // OTA 打包脚本,封装了 mk_otapackage.py 的功能
├── ota_process // OTA 升级过程中使用的二进制工具,包含在 OTA 包中
├── out // OTA 打包的输出文件夹
│ ├── deploy // 存放 OTA 包制作过程的中间文件
│ └── ota_packages // 存放制作好的 OTA 升级包
└── private_key.pem // 私钥文件,用于 OTA 包的签名
一般使用位于ota_tools/
路径下的ota_pack_tool.sh制作所需的OTA升级包,支持OTA升级包解包,解包后重打包,制作升级包以及制作差分包等。
使用方法如下:
Usage: ./ota_pack_tool.sh [OPTIONS]...
Options:
-x, -unpack <ota_package> Unpack the given OTA package file.
-r, -repack Repack the files into a new OTA package.
-c, -create <pack_type> -d <source_dir>
Generate OTA package by source dir or img_packages.
<pack_type>: sys, sys_signed ... etc.
<source_dir>: sys and sys_signed require img dir.
(e.g.)run "./ota_pack_tool.sh -c sys -d ../out/product/img_packages/"
-i, -inc <pack_type> -old <old_pkg> -new <new_pkg>
Create an incremental OTA package.
<pack_type>: sys, sys_signed.
<old_pkg>, <new_pkg>: OTA pack, xxx.zip.
(e.g.)run "./ota_pack_tool.sh -i sys -old out/ota_packages/all_in_one_old.zip -new out/ota_packages/all_in_one_new.zip".
-h, -help Display this help message.
OTA 升级包解包与重打包
升级包解包指令为:
./ota_pack_tool.sh -x out/ota_packages/all_in_one.zip
- 升级包解包后,可以更新
ota_tools/out/ota_unpack
下的镜像,重新制作OTA包,使用的OTA配置文件与ota_process均位于ota_tools/out/ota_unpack
目录,该方法无法修改OTA配置文件gpt.conf。
升级包重打包指令为:
./ota_pack_tool.sh -r
- 打包的源文件夹路径为:ota_tools/out/ota_unpack
- 打包后的目标文件路径为:ota_tools/out/ota_repack
OTA 制作普通升级包
通过如下命令可以制作系统升级包,使用的分区配置文件通过ota_pack_tool.sh脚本中的GPT_CONFIG配置,默认使用/out/product/img_packages/s100-ota-gpt.json
,可根据实际需求修改。
# sys代表打包nonsecure版本升级包
./ota_pack_tool.sh -c sys -d ~/s100/out/product/img_packages/
# sys_signed代表打包secure版本升级包
./ota_pack_tool.sh -c sys_signed -d ~/s100/out/product/img_packages/
生成的 OTA 升级包将输出到ota_tools/out/ota_packages
目录,在该目录下,您将看到zip和signature两种后缀的文件,其中zip后缀文件是 OTA 升级包,signature后缀文件是对同名升级包的签名文件:
all_in_one.signature #nonsecure 升级包签名文件
all_in_one.zip #nonsecure 升级包文件
all_in_one_signed.signature #secure 升级包签名文件
all_in_one_signed.zip #secure 升级包文件
OTA 制作差分升级包
可以通过ota_pack_tool制作all_in_one_signed_inc.zip系统差分包,制作时使用的OTA配置文件与ota_process均为new包解压所得。
-
差分升级作用
节省流量,并不节省升级时间。
-
差分升级原理
利用差分算法,得出新老镜像差分镜像,升级时 差分还原到对向分区。(flash介质的镜像不差分,其体积仅数M,且flash读取慢)。差分库:hpatchz。支持的镜像:OTA镜像包中的所有非flash介质的镜像。
-
差分升级的限制
- flash上的分区不支持差分升级;
- boot分区有写入行为,不支持差分升级;
- 只有镜像包的大小大于10M时才支持差分升级;
- 需要差分升级的分区如需挂载时,必须以只读方式挂载,且挂载时应添加noload选项,否则会出现md5校验失败的问题;
-
制作差分升级包
- 差分镜像升级时需要依赖已经烧录的旧镜像包,因此,在计划使用差分升级时,请务必妥善保存旧镜像包避免丢失或损坏。
# 基于out/ota_packages/all_in_one_signed_old.zip制作out/ota_packages/all_in_one_signed_new.zip的差分包all_in_one_signed_inc.zip
./ota_pack_tool.sh -i sys_signed -old out/ota_packages/all_in_one_signed_old.zip -new out/ota_packages/all_in_one_signed_new.zip
签名密钥
签名所用的私钥private_key.pem
放置在工程的:ota_tools
目录。签名所用的公钥public_key.pem
放置在工程的:source/bootloader/miniboot/ota_flash_tools/
目录,在设备端该公钥的路径为 /usr/hobot/share/ota/public_key.pem。
如果需要替换为自己的密钥,步骤如下:
-
私钥生成:
openssl genrsa -out private_key.pem 4096
-
公钥生成:
openssl rsa -RSAPublicKey_out -in private_key.pem -out public_key.pem
-
替换路径
source/bootloader/miniboot/ota_flash_tools/
下的public_key.pem
以及替换路径ota_tools/
下的private_key.pem
. -
执行下列命令重新编译
#回到项目的顶层目录,制作miniboot的deb包
./mk_debs.sh hobot-miniboot
#编译本地项目
sudo ./pack_image.sh -l
注意:
- 打包生成的 OTA 包默认命名为 all_in_one_xxx.zip。升级程序会对包名进行校验,包名中必须包含 "all_in_one" 关键字,且后缀必须为 .zip。此外,包名中不得包含以下关键字:"app"、"APP"、"middleware"、"param"。
OTA 升级包介绍
升级包结构
Archive: all_in_one_signed.zip
Length Date Time Name
--------- ---------- ----- ----
1047 2025-05-14 12:11 gpt.conf
5326 2025-05-14 12:11 data.json
526336 2025-05-13 20:37 HSM_FW_signed.img
264192 2025-05-13 20:37 HSM_RCA_signed.img
264192 2025-05-13 20:37 keyimage_signed.img
264192 2025-05-13 20:37 keyimage_ohp_signed.img
526336 2025-05-13 20:37 scp_signed.img
526336 2025-05-13 20:37 spl_signed.img
1312768 2025-05-13 20:37 MCU_S100_V1.0_signed.img
256288 2025-05-13 20:37 acore_cfg.img
348928 2025-05-13 20:37 bl31.img
986080 2025-05-13 20:37 optee.img
1076928 2025-05-13 20:37 uboot.img
36388864 2025-05-13 20:37 boot.img
8589934592 2025-05-13 20:37 system.img
310144 2025-05-13 19:47 ota_process
--------- -------
8632992549 16 files
上述内容展示了当前 OTA 升级包中的文件结构,主要包括以下四类文件。镜像文件的数量会根据实际配置有所增减。
文件 | 描述 |
---|---|
gpt.conf | 分区表文件 |
data.json | OTA 配置文件 |
*.img | 各分区镜像 |
ota_process | OTA 烧写程序 |
OTA 配置文件
OTA 升级包中包含一个名为 data.json 的配置文件。该文件在编译时生成,其中包含了升级包的分区信息和镜像信息。
通用配置
配置 | 类型 | 功能 |
---|---|---|
backup_dir | arr[obj] | HSM备份目录 |
ab_sync | str | reserved字段,默认固定false |
nor_sign | bool | NOR Flash镜像签名校验开关 |
update_partition | arr[str] | 升级分区 |
partition_info | arr[str] | 各分区配置 |
各分区配置( partition_info)
配置 | 类型 | 功能 |
---|---|---|
md5sum | arr[obj] | 各镜像 MD5 值 |
md5_scope | arr[obj] | 各镜像 MD5 校验长度 |
medium | str | 所在外存介质 (NOR/eMMC/NAND) |
part_type | str | 分区类型 (AB/BAK/GOLDEN) |
upgrade_method | str | 升级方式 (image) |
imgname | str | 镜像名,仅支持以 .img/.bin/.ubifs 为后缀的文件 |
以下是一个 data.json 文件的示例:
{
"antirollbackUpdate_host": false,
"antirollbackUpdate_hsm": false,
"backup_dir": "/tmp/ota/backup",
"ab_sync": false,
"update_partition": [
"HSM_FW",
"HSM_RCA",
"keyimage",
"scp",
"spl",
"MCU",
"acore_cfg",
"bl31",
"optee",
"uboot",
"boot",
"system"
],
"nor_sign": true,
"partition_info": {
"HSM_FW": {
"md5sum": {
"HSM_FW_signed.img": "3e19e04f97e0cb4e37899958e3aec34f"
},
"md5_scope": {
"HSM_FW_signed.img": 524288
},
"medium": "nor",
"part_type": "BAK",
"have_anti_ver": null,
"upgrade_method": "image",
"imgname": "HSM_FW_signed.img"
},
"HSM_RCA": {
"md5sum": {
"HSM_RCA_signed.img": "9b2f9cb4d00586dd49112f50fdb90952"
},
"md5_scope": {
"HSM_RCA_signed.img": 262144
},
"medium": "nor",
"part_type": "BAK",
"have_anti_ver": null,
"upgrade_method": "image",
"imgname": "HSM_RCA_signed.img"
},
"keyimage": {
"md5sum": {
"keyimage_signed.img": "df28a9900fa504a90a4c85368cb7879d",
"keyimage_ohp_signed.img": "af81665e36a9a274084e2dc02b7a3830"
},
"md5_scope": {
"keyimage_signed.img": 262144,
"keyimage_ohp_signed.img": 262144
},
"medium": "nor",
"part_type": "BAK",
"have_anti_ver": null,
"upgrade_method": "image",
"imgname": "keyimage%s_signed.img"
},
"scp": {
"md5sum": {
"scp_signed.img": "6ea63e573ae3bb50dc8cb0052c29fddb"
},
"md5_scope": {
"scp_signed.img": 524288
},
"medium": "nor",
"part_type": "AB",
"have_anti_ver": null,
"upgrade_method": "image",
"imgname": "scp_signed.img"
},
"spl": {
"md5sum": {
"spl_signed.img": "ba48f24f989de1ddbc6eb3aedd139b4f"
},
"md5_scope": {
"spl_signed.img": 524288
},
"medium": "nor",
"part_type": "AB",
"have_anti_ver": null,
"upgrade_method": "image",
"imgname": "spl_signed.img"
},
"MCU": {
"md5sum": {
"MCU_S100_V1.0_signed.img": "d44784323de5f5d57fa606ea178fd854"
},
"md5_scope": {
"MCU_S100_V1.0_signed.img": 1310720
},
"medium": "nor",
"part_type": "AB",
"have_anti_ver": null,
"upgrade_method": "image",
"imgname": "MCU_S100_V1.0_signed.img"
},
"acore_cfg": {
"md5sum": {
"acore_cfg.img": "fe36a8ad522a2ca5ee811da8208828ef"
},
"md5_scope": {
"acore_cfg.img": 256288
},
"medium": "emmc",
"part_type": "AB",
"have_anti_ver": null,
"upgrade_method": "image",
"imgname": "acore_cfg.img"
},
"bl31": {
"md5sum": {
"bl31.img": "5be617cd08128e89318f61964696509a"
},
"md5_scope": {
"bl31.img": 348928
},
"medium": "emmc",
"part_type": "AB",
"have_anti_ver": null,
"upgrade_method": "image",
"imgname": "bl31.img"
},
"optee": {
"md5sum": {
"optee.img": "f0f663462f523f9e8722e0c26d29209e"
},
"md5_scope": {
"optee.img": 986080
},
"medium": "emmc",
"part_type": "AB",
"have_anti_ver": null,
"upgrade_method": "image",
"imgname": "optee.img"
},
"uboot": {
"md5sum": {
"uboot.img": "78b07c6a4264bd757c4d992e2dc0ee0b"
},
"md5_scope": {
"uboot.img": 1076928
},
"medium": "emmc",
"part_type": "AB",
"have_anti_ver": null,
"upgrade_method": "image",
"imgname": "uboot.img"
},
"boot": {
"md5sum": {
"boot.img": "aca98db8136ad4f1d55cd3e7bdb4c856"
},
"md5_scope": {
"boot.img": 36388864
},
"medium": "emmc",
"part_type": "AB",
"have_anti_ver": null,
"upgrade_method": "image",
"imgname": "boot.img"
},
"system": {
"md5sum": {
"system.img": "a6f61c86ae0045ad3167b3daf7c33b3a"
},
"md5_scope": {
"system.img": 8589934592
},
"medium": "emmc",
"part_type": "AB",
"have_anti_ver": null,
"upgrade_method": "image",
"imgname": "system.img"
}
}
}
通过以上配置, OTA 升级包能够确保每个分区的镜像在升级过程中被正确校验和更新。
OTA 实现详解
OTA流程
以下流程以ota_tool中实现为例(开发者实现可参考此工具)。
-
准备阶段:
-
升级阶段:
-
验证阶段:
OTA 状态机
升级包的状态
状态存储在OTA进程空间,以下是流程说明:
/**
* @enum otahl_update_result
* @brief ota upgrade result
* @NO{S21E03C02}
*/
typedef enum otahl_update_result {
OTA_UPGRADE_NOT_START = 0, /**< OTA upgrade not start */
OTA_UPGRADE_IN_PROGRESS, /**< OTA upgrading */
OTA_UPGRADE_SUCCESS, /**< OTA upgrade success*/
OTA_UPGRADE_FAILED, /**< OTA upgrade failed*/
} otahl_update_result_e;
OTA升级流程的状态
状态存储在veeprom,以下是流程说明:
typedef enum ota_update_flag {
OTA_FLAG_NORMAL = 0, // normal状态
OTA_FLAG_BURN, // 烧写状态
OTA_FLAG_VERIFY, // 待验证状态
OTA_FLAG_VERIFIED, // 已验证状态
} ota_update_flag_e;
misc (AB状态机)
-
区域分配
地瓜使用andriod AB机制来应用AB系统,下面的信息都是对于该机制的部分原理介绍,详细原理请参考:https://source.android.google.cn/docs/core/ota?hl=zh-cn
bootloader_message_ab 总共占4K: 其中AB使用的bootloader_control 位于struct bootloader_message_ab->slot_suffix处,占32个字节。
struct bootloader_message_ab {
struct bootloader_message message;
char slot_suffix[32];
char update_channel[128];
// Round up the entire struct to 4096-byte.
char reserved[1888];
};
/**
* Be cautious about the struct size change, in case we put anything post
* bootloader_message_ab struct (b/29159185).
*/
#if (__STDC_VERSION__ >= 201112L) || defined(__cplusplus)
static_assert(sizeof(struct bootloader_message_ab) == 4096,
"struct bootloader_message_ab size changes");
#endif -
AB结构体数据
#define ARRAY_32 (32U)
struct slot_metadata {
// Slot priority with 15 meaning highest priority, 1 lowest
// priority and 0 the slot is unbootable.
uint8_t priority : 4;
// Number of times left attempting to boot this slot.
uint8_t tries_remaining : 3;
// 1 if this slot has booted successfully, 0 otherwise.
uint8_t successful_boot : 1;
// 1 if this slot is corrupted from a dm-verity corruption, 0 otherwise.
uint8_t verity_corrupted : 1;
// Reserved for further use.
uint8_t reserved : 7;
} __attribute__((packed));
struct bootloader_control {
// NUL terminated active slot suffix.
char slot_suffix[4];
// Bootloader Control AB magic number (see BOOT_CTRL_MAGIC).
uint32_t magic;
// Version of struct being used (see BOOT_CTRL_VERSION).
uint8_t version;
// Number of slots being managed.
uint8_t nb_slot : 3;
// Number of times left attempting to boot recovery.
uint8_t recovery_tries_remaining : 3;
// Ensure 4-bytes alignment for slot_info field.
uint8_t reserved0[2];
// Per-slot information. Up to 4 slots.
struct slot_metadata slot_info[4];
// Reserved for further use.
uint8_t reserved1[8];
// CRC32 of all 28 bytes preceding this field (little endian
// format).
uint32_t crc32_le;
} __attribute__((packed)); -
状态机说明
-
状态1:默认状态,AB slot都可以启动。b的优先级高于a,默认从b启动。
-
状态2:升级状态(烧写状态),a slot不可启动且boot success为0。
-
状态3:烧写成功,还未重启,这个时候将要升级的slot设置为active状态,将当前slot设置为非active状态(调整优先级)。
-
状态4:重启验证成功,将a设置为success boot状态。
veeprom区域
OTA升级的状态机存储在此区域,以下是OTA中对此区域的应用:
#define VEEPROM_OTA_STAT_OFFSET \
(1024) /**< offset 0f OTA status in the veeprom partition */
#define VEEPROM_OTA_STAT_SIZE \
(2048) /**< size of OTA status in the veeprom partition */
#define VEEPROM_RECOVERY_STAT_OFFSET \
(VEEPROM_OTA_STAT_OFFSET + \
VEEPROM_OTA_STAT_SIZE) /**< offset 0f recovery information in the veeprom partition */
#define VEEPROM_RECOVERY_STAT_SIZE \
(128) /**< size 0f recovery information in the veeprom partition */
-
OTA区域
起始位置:1024
大小:2048
作用:OTA状态存储
存储数据如下:
/**
* @ota_status_t
* @brief ota status
* @NO{S21E03C04U}
*/
typedef struct ota_status_s {
uint32_t magic; /**< magic number */
ota_update_flag_e up;
ota_update_flag_e up_system; /**< system partition update flag */
ota_update_flag_e up_backup; /**< system partition update flag */
ota_update_flag_e up_app; /**< system partition update flag */
ota_update_flag_e up_middleware; /**< system partition update flag */
ota_update_flag_e up_param; /**< system partition update flag */
ota_update_owner_e owner;
uint32_t next_slot; /**< expect slot for next boot */
update_part_t update_part;
uint8_t reserved[ARRAY_32];
uint32_t crc32_le; /**< crc verify value */
} ota_status_t;
启动状态切换
下图说明正常启动时A/B slot是如何切换的(有没有OTA都是这个流程)
-
启动时,ROM boot count自动累加(该标记在aon域,下电时复位)
-
OTA升级时设置要更新的slot优先级为15,另一个slot为14。设置要升级的slot重试次数为1,slot损坏为0
-
OTA启动验证成功时该slot标记为successboot,启动失败时该slot标记为已损坏
-
从未标记successboot的slot启动时将会消耗一次重试次数,重试次数为0或被标记损坏会跳过此slot