跳转至

LuatOS 课程-009 讲:fota 升级教程

作者:孟伟 | 最后修改:2026-01-22

Hello,大家好,我是孟伟。欢迎大家来到合宙 LuatOS 直播课堂,继续我们的 LuatOS 系列课程学习。

第一部分:课程背景与内容概览

今天是我们 LuatOS 系列课程的第 009 讲,同时也是 LuatOS FOTA(无线固件升级)专题课程的第一讲。

FOTA 是物联网设备保持生命力和可维护性的关键技术。本次课程将带大家深入理解 LuatOS FOTA 的底层原理,并掌握其应用开发方法。如果您还不清楚 LuatOS 课程背景,可以访问:LuatOS 课程背景 这个链接,进行了解;

本课程主要包含以下六个部分:

  1. Flash 分区详解,剖析 Flash 分区布局。
  2. 揭秘 .soc 固件包的内部结构与秘密。
  3. 详解差分升级原理。
  4. 对比两套核心 API(fota核心库 vs libfota2扩展库)的如何选择。
  5. Demo 讲解:在 Air780EHM 开发板上进行多场景升级演示。
  6. 总结关键注意事项与常见问题。

第二部分:Flash 分区与 FOTA 分区简介

大家好,在开始动手操作 FOTA 之前,我们必须先了解它的“工作场地”——模组内部的 Flash 存储器。这就像装修房子前,一定要先看懂户型图。

不同型号的模组,Flash 分区就像不同的“户型”,格局各异,但都有一个核心设计:必须有一个独立的“系统升级专用间”,也就是 FOTA 分区。

今天,我们就以 Air780EHM 为例,来详细解读这份关键的“Flash 户型图”。

/*
flash layout, toatl 8MB
flash raw address: 0x00000000---0x00800000
flash xip address(from both ap/cp view): 0x00800000---0x01000000

0x00000000          |---------------------------------|
                    |      header1 4KB                |
0x00001000          |---------------------------------|
                    |      header2 4KB                |
0x00002000          |---------------------------------|
                    |      fuse mirror 4KB            |
0x00003000          |---------------------------------|
                    |      bl 116KB + 4KB             |
0x00021000          |---------------------------------|
                    |      factory data 8KB           |
0x00023000          |---------------------------------|
                    |      rel data(factory)40KB      |
0x0002D000          |---------------------------------|
                    |-ap+cp image 6276KB(包含脚本代码区)|
0x0064E000          |---------------------------------|
                    |      fota  1048KB(可用932KB)      |
0x0073C000          |---------------------------------|
                    |      hib backup 96KB(in fota)   |
0x00750000          |---------------------------------|
                    |      flashdump 16KB(in fota)    |
0x00754000          |---------------------------------|
                    |      lfs  512KB                 |
0x007E4000          |---------------------------------|
                    |      kv   64KB                  |
0x007E4000          |---------------------------------|
                    |      rel data 104KB             |
0x007fe000          |---------------------------------|
                    |      plat config 8KB            |
0x00800000          |---------------------------------|

*/

首先,我们看全局。Air780EHM 的 Flash 是一栋总容量 8MB 的“大楼”。但这里有个精妙的设计:这栋楼有两个门牌号系统。

  1. 物理地址(0x0 – 8MB):这是 Flash 芯片真实的“砖块地址”,用于存储所有数据。
  2. XIP 映射地址(8MB – 16MB):CPU 执行代码时“看到”的地址。系统固件被“映射”到这个区域,CPU 可以直接读取执行,这叫 XIP(就地执行)。

简单来说,固件实体存放在物理地址的低 8MB,而当 CPU 要运行时,它“看到”的是从 8MB 开始的那段地址。

大家可以看到,flash 中分区很多,但作为 LuatOS 开发者,我们主要关注其中四个“房间”:ap+cp image 分区(包含脚本代码区)、LFS 分区、KV 分区和 FOTA 分区

2.1 ap+cp image 分区– “主系统套房”

作用 :这是设备的操作系统和核心应用所在,相当于电脑的 C 盘。它包含了底层 C 固件、Lua 虚拟机,以及最重要的——脚本区。

脚本区:这是我们 Lua 代码的“家”。代码在这里拥有持久化特性,只有固件升级时才会被整体更新。它的大小在编译时就已经固定,不同固件版本会有差异,

脚本区大小会根据模组型号不同,固件编号不同也会有差别,大小在编译时固定,具体可以查看选型手册中固件版本说明中脚本区实际大小:780Exx 系列多固件功能说明8000 系列多固件功能说明

2.2 LFS 分区– “用户资料室” :

作用:提供一个小型文件系统,用于存储用户数据和配置。

特点:具有持久化存储,断电不丢失 、可动态写入和读取、支持文件系统 API 访问、可随时创建和删除的特性。可以用来储存用户配置文件、设备运行日志、网络配置信息、临时数据和缓存、用户生成的文件。

文件系统空间大小会根据模组型号不同,固件编号不同也会有差别,实际大小= 基础 lfs 空间 + 附件空间。

附加空间通常是去掉某些功能所节省出来的空间,所以有了不同编号的固件,来实现不同功能 + 大内存的需求。

具体可以查看选型手册中固件版本说明中文件系统实际大小:780Exx 系列多固件功能说明8000 系列多固件功能说明

2.3 KV 分区– “配置存储间”:

作用:KV(Key-Value)分区是一个小型的键值对存储区域,用于保存设备的配置信息和状态数据。

特点:

  1. 持久化存储:断电后数据不丢失
  2. 高效访问:支持快速的键值对读写操作
  3. 固定大小:通常为 64KB
  4. 轻量级:适合存储少量关键配置,如网络参数、设备 ID、运行状态等

KV 分区的典型用途:

  • 存储设备唯一标识符
  • 保存网络连接参数(APN、服务器地址等)
  • 记录设备运行状态和统计信息
  • 存储用户自定义配置
  • 保存升级状态和版本信息

2.4 FOTA 分区“系统升级专用间”

2.4.1 分区介绍

作用 :这是今天的主角,用于临时存放固件升级包,实现安全、隔离的远程升级。

fota 分区具有以下特性:

  1. 独立空间 :与其他分区严格分离,确保升级安全,从网络或其他渠道下载的完整固件包或差分包先存储到 FOTA 分区
  2. 安全机制 :支持升级包完整性校验和签名验证,升级包写入 FOTA 分区后会进行 MD5 校验,确保传输未损坏

FOTA 升级的核心安全规则:为什么“户型”要对齐?

这里有一个至关重要的安全限制,可以通过下面这个表格来了解:

**分区标志**
**固件1(FS=768KB)**
**固件2(FS=640KB)**
**固件3(FS=512KB)**
**说明**
**ap+cp起始**
0x0002D000
0x0002D000
0x0002D000
固定不变
**ap+cp结束/FOTA起始**
0x0060E000
0x0062E000
0x0064E000
核心差异!偏移量分别为-256KB和-128KB
**FOTA结束/LFS起始**
0x00714000
0x00734000
0x00754000
随FOTA起始地址偏移
**LFS结束/KV起始**
0x007D4000
0x007D4000
0x007D4000
LFS结束地址固定不变
**KV结束**
0x007E4000
0x007E4000
0x007E4000
KV结束地址固定不变
**FOTA分区大小**
1048KB
1048KB
1048KB
大小不变
**LFS分区大小**
768KB
640KB
512KB
随固件编号变化
**KV分区大小**
64KB
64KB
64KB
大小不变

通过上面表格可以看出,虽然两个固件的 FOTA 分区大小完全一样,但它们的 fota 分区、ap+cp 分区和 LFS 分区的起始、结束地址发生了偏移。

结论:使用错误编号的固件包升级时,其设计的分区地址与设备当前分区布局不匹配。FOTA 机制会在升级包写入前校验升级包,将会检查出完整性异常,并返回升级失败。

所以会有一个铁律:不同编号的固件之间不能进行 fota 升级,FOTA 差分升级只能在相同编号的固件版本之间进行。 既编号 1 固件只能差分升级为新版本的编号 1 固件,编号 2 固件只能差分升级为新版本的编号 2 固件。在制作升级包时,务必首先确认这个编号匹配关系。

如果不同编号固件间升级 :需要使用完整包重新烧录,不能使用 fota 升级

虽然这个房间总大小 1048KB,但是 fota 升级包大小并不等于 fota 分区空间大小。

在不同编号的固件版本规划中,为了容纳不同的功能,其整个 Flash 的分区表布局(包括 FOTA、AP、CP、LFS 等分区的起始地址和大小)是预先定义好且固定的。因此,不同编号的固件,其 FOTA 分区大小可能相同也可能不同,但更关键的是其在整个 Flash 中的"位置"(地址)不同。所以具体 fota 分区大小可以看 2.5 章节具体表格。

2.4.2 升级流程

  • 初始化 FOTA → 下载升级包 → 写入 FOTA 分区 → MD5 校验 → 设置升级标志 → 重启
  • 重启后:Bootloader 检查升级标志 → 验证升级包 → 写入目标分区 → 启动新固件 → 验证新固件

1、升级包写入流程:
  1. 初始化 FOTA 模块: 创建升级上下文结构体,分配必要的缓冲区和资源,为升级流程做准备。
  2. 下载升级包:

通过 HTTP、MQTT 或 UART 等通信协议从服务器或本地下载升级包数据。支持下载到文件系统,或者直接分包写入 fota 分区。

  1. 写入 FOTA 分区:

如果是下载到文件系统中暂存,待整个升级包在文件系统中下载完成后,再一次性从文件系统读取并写入到专用的 FOTA 分区。

也可以实时分包接收升级包数据并写入到文件系统中。

  1. 完成写入,验证升级包:

执行升级包的完整性校验(MD5)

若校验通过,则在特定位置中设置升级标志,并重启系统。Bootloader 下次启动时执行升级。

若校验失败,则报告错误并终止升级流程。

2、重启后升级流程:
  1. 设备上电,执行 Bootloader

设备重启后,Bootloader 首先检查是否存在有效的升级标志。

  1. 验证与执行升级 若无升级标志:Bootloader 跳过升级流程,直接启动现有固件。 若存在升级标志:

2.5 主推型号各模组 fota 分区空间

在实际使用中,不同的固件版本或产品型号为了支持不同的功能,会调整 FOTA 分区内的大小,因此用户实际可用的升级空间可查看下表。

Air7xx 系列模组 LuatOS 多固件版本

型号与固件版本对应关系
Air700ECP/Air780EPM/Air780EGP(1-99号是32位固件,101-199号是64位固件)
Air700ECH/Air780EHN/EHU/EHM/EHV/EGH/EGG(1-99号是32位固件,101-199号是64位固件)
分区名称
简介
1号
2号




1号
2号
3号
4号
5号
6号
7号
9号
10号
11号
12号
13号


103号
104号
105号
106号
101号
102号
103号
104号
105号
106号
107号
109号
110号
111号
112号
113号
脚本区
脚本代码空间
**256KB**
**288KB**
**384KB**
**368KB**
**256KB**
**176KB**
**512KB**
**512KB**
**512KB**
**512KB**
**512KB**
**512KB**
**512KB**
**512KB**
**512KB**
**256KB**
**1024KB**
**512KB**
fs
文件系统
**168KB**
**168KB**
**168KB**
**168KB**
**192KB**
**168KB**
**768KB**
**640KB**
**512KB**
**1280KB**
**1408KB**
**1408KB**
**1536KB**
**2304KB**
**2432KB**
**3584KB**
**2304KB**
**512KB**
FOTA区
存放fota包空间
**192KB**
**216KB**
**288KB**
**276KB**
**192KB**
**132KB**
**384KB**
**384KB**
**384KB**
**384KB**
**384KB**
**384KB**
**384KB**
**384KB**
**384KB**
**192KB**
**768KB**
**384KB**

Air8000 系列固件版本

型号与固件版本对应关系
Air8000全系列所有型号(1-99号是32位固件,101-199号是64位固件)
分区名称
简介
1号
2号
3号
4号
5号
6号
7号
9号
10号
11号
12号
13号
101号
102号
103号
104号
105号
106号
107号
109号
110号
111号
112号
113号
脚本区
脚本代码空间
**512KB**
**512KB**
**512KB**
**512KB**
**512KB**
**512KB**
**512KB**
**512KB**
**512KB**
**256KB**
**1024KB**
**512KB**
fs
文件系统
**768KB**
**640KB**
**512KB**
**1280KB**
**1408KB**
**1408KB**
**1536KB**
**2304KB**
**2432KB**
**3584KB**
**2304KB**
**512KB**
FOTA区
存放fota包空间
**384KB**
**384KB**
**384KB**
**384KB**
**384KB**
**384KB**
**384KB**
**384KB**
**384KB**
**192KB**
**768KB**
**384KB**

第三部分:soc 软件包文件分析

“理解了 FOTA 的‘工作场地’(Flash 分区),接下来一个很自然的问题是:我们要通过网络下载并写入这个场地的‘升级包’,它到底是什么?

这个核心的升级包就是 .soc 文件。它不是一个普通的固件,而是合宙定义的标准化容器。在 FOTA 流程中,它扮演着三个关键角色:

  1. 它是差分的蓝本:我们下一章要讲的‘差分升级’,其核心就是对比新旧两个版本的.soc 文件,生成差异部分。
  2. 它携带了分区地图:.soc 包内的 info.json 配置文件,明确写明了本固件的编号、分区地址等信息,这是 FOTA 进行安全校验(防止变砖)的核心依据。
  3. 它统一了操作对象:无论底层是移芯还是展锐芯片,最终都封装成统一的.soc 格式,让升级工具和流程得以标准化。

所以,拆解.soc 文件,就是理解 FOTA 操作对象和实现原理的基础。明白了它里面有什么,你才能彻底搞懂后续的差分生成和升级执行究竟在操作什么。”

下面讲解一下合宙模组所用固件包.soc 文件的组成以及格式相关内容。

3.1 soc 简介

.soc 文件,是合宙在 2021 年自定义的一个文件,该文件, 用于用一种通用的格式,保存不同型号模组的固件。

该文件格式的优点是可以屏蔽模组差异, 对用户通用, 对Luatools 通用;

缺点是,当前的 合宙 IOT 升级后台尚且无法支持这个格式。

对于远程升级来说, 必须用 Luatools 内置的差分包制作工具, 输入两个不同版本的 soc文件, 生成一个.bin 结尾的差分包,才能上传到 合宙IOT 后台进行远程升级。

LuatOS 将来会适配非常多的 SoC/MCU 芯片来设计模组, 而各种模组的刷机格式各不相同,有必要定义一个统一的对外格式。

这里称之为 soc 格式, 后缀选定为 soc, 实际内容为 info.json 及多个固件文件的压缩包。

3.2 SoC 组成部分

  • [必选]包含的文件 info.json
  • [可选]脚本数据存储文件 script.bin, 使用 LuaDB v2 格式存储
  • [必选]原始固件,以不同 SoC 芯片为准

3.2.1 info.json 的内容

info.json 是固件配置文件,包含固件的详细参数和配置信息。以 780EHM 的 2018 版本 1 号固件解压后打开 info.json,具体包含如下参数:

{
    "rom": {        // ROM固件信息
        "build": {  // 构建信息
            "build_system": "",  // 构建系统
            "build_at": "",      // 构建时间
            "build_host": "",    // 构建主机
            "build_by": ""       // 构建者
        },
        "version-core": "v0001",  // 核心库版本
        "file": "luatos.binpkg",  // ROM固件文件名
        "mark": "default",        // 标记(默认配置)
        "fs": {     // 文件系统配置
            "filesystem": {  // 主文件系统
                "size": "C0000",    // 文件系统大小
                "type": "lfs",      // 文件系统类型(LittleFS)
                "offset": "714000"  // 文件系统在固件中的偏移地址
            },
            "script": {      // 脚本存储区
                "size": 512,        // 脚本存储区大小
                "type": "luadb",    // 存储类型(Lua数据库)
                "offset": "0"       // 脚本存储区偏移地址
            }
        },
        "version-bsp": "v0001"  // BSP(板级支持包)版本
    },
    "user": {       // 用户配置
        "project": "",       // 项目名称
        "log_br": "2000000", // 日志波特率
        "version": ""        // 用户版本
    },
    "download": {   // 下载配置
        "partition_addr": "00008000",  // 分区表地址
        "force_br": "1152000",         // 强制下载波特率
        "script_addr": "52E000",       // 脚本存储地址
        "app_addr": "00000000",        // 应用程序地址
        "core_addr": "0007e000",       // 核心固件地址
        "fs_addr": "714000",           // 文件系统地址
        "extra_param": "002f0200",     // 额外参数
        "nvm_addr": "00000000",        // NVM(非易失性存储器)地址
        "bl_addr": "00004000"          // Bootloader地址
    },
    "chip": {       // 芯片信息
        "ram": {    // 内存配置
            "total": 384,  // 总RAM大小(KB)
            "lua": 128,    // Lua虚拟机可用内存(KB)
            "sys": 256     // 系统内存(KB)
        },
        "type": "ec7xx"  // 芯片类型(移芯EC7XX系列)
    },
    "fota": {       // FOTA(无线固件升级)配置
        "fota_len": "106000",     // FOTA数据长度
        "cp_type": "diff",        // CP(协处理器)升级类型(差分升级)
        "magic_num": "eac37218",  // 魔数(用于固件验证)
        "core_type": "diff",      // 核心升级类型(差分升级)
        "fw_num": 1,              // 固件数量
        "full_addr": "5AE000",    // 完整固件存储地址
        "block_len": "40000",     // 块大小
        "ap_type": "diff"         // AP(应用处理器)升级类型(差分升级)
    },
    "version": 1,   // 配置文件版本
    "fs": {         // 文件系统信息
        "total_len": 288,      // 文件系统总长度(KB)
        "format_len": "1000"   // 格式化长度
    },
    "script": {     // Lua脚本配置
        "use-debug": true,  // 是否启用调试
        "lua": "5.3",       // Lua版本
        "file": "script.bin",  // 脚本文件名
        "use-luac": true,   // 是否使用Luac编译
        "bitw": 32          // 位宽(32位)
    }
}

3.2.2 脚本数据文件 script.bin

在给出的底层固件中并不包含此文件,此文件是在 luatools 工具点击生成量产文件后所生成的。是 Lua 脚本的二进制文件,包含了用户编写的应用程序代码。文件格式为 LuaDB 格式存储。

LuaDB 并非数据库, 而是一种用于 LuatOS 固件的文件打包格式.

其作用相当于一个只读文件系统.

3.2.3 原始固件

原始固件是构成 .soc 文件的核心数据部分,指由芯片原厂(如展锐、移芯)提供的底层系统映像文件。其格式依芯片平台而定,常见后缀包括 .pac、.binpkg、.fls、.img 等。该固件包含了操作系统内核、基础驱动、通信协议栈等核心代码,是设备功能运行的基石。

在 .soc 文件体系中,info.json 配置文件内的 "rom" -> "file" 字段即指明了所包含的原始固件文件名(例如 "luatos.binpkg")。该文件与 info.json、可选的 script.bin 一同被打包压缩,最终形成统一的 .soc 分发文件。

展锐平台:

展锐平台的 4G 模组,当前合宙用的是 8910 平台,典型模组是Air724UG,编译之后的文件后缀为 PAC, 可以用展锐提供的upgrade 固件烧录工具烧录,也可以用 Luatools 烧录。

移芯平台:

移芯平台的 4G 模组,合宙用的有 EC718、EC618、716 等。典型模组是 Air780EXX 系列、Air8000 系列。

移芯平台编译出来的固件,都是 binpkg 后缀结尾的固件,可以用移芯提供的 flashtool 工具烧录,也可以用 Luatools 烧录。

对于最终用户和开发者而言,无论底层原始固件格式如何,均推荐使用 Luatools 配合 .soc 文件进行下载和升级。

所以,.soc 文件的核心就在于,用一个外层的标准化包装,封装了内部不同平台的原生固件,让用户和工具都能用同一种方式处理它们。

3.3 soc 实际文件分析

soc 解压后实际文件在不同的模组中也不尽相同,但是都是 info.json+ 原始固件 + 辅助文件 + 脚本数据文件组成。

下面以移芯系列固件为例:将 .soc 压后会得到 7 个核心文件: comdb.txt 、 info.json 、 luat_conf_bsp.h 、 luatos.binpkg 、 luatos.elf 、 luatos_debug.map 、 mem_map.txt 。而使用 Luatools 生成量产文件后,还会多出两个文件: core.binpkg 和 script.bin 。

这些文件各司其职,共同构成了一个完整的固件系统。接下来,我们逐个分析它们的作用。

还是以 780ehm 为例,看下它的固件 soc 解压后都有哪些文件:

  1. info.json

第一个要介绍的是 info.json ,这是固件的核心配置文件,相当于固件的"身份证"。

这个文件主要包含有:

  • 芯片类型(
  • ROM 文件信息及下载地址
  • 脚本文件信息及下载地址
  • 分区表配置
  • 下载参数(波特率、强制波特率等)

  • luatos.binpkg

LuatOS 的核心二进制固件包,包含了系统运行所需的所有核心代码。

.binpkg 为移芯系列芯片所生成的原始固件内容,是移芯芯片特有的格式,不同的芯片会有不同的原始固件格式

  1. 其他辅助文件

  2. mem_map.txt:这是设备内存的"地图",内存映射配置文件,定义了设备内存的分区结构和地址范围。

  3. luat_conf_bsp.h :支持功能配置头文件,定义了一些功能相关的配置宏,比如引脚定义、外设配置等。
  4. luatos.elf :包含完整调试信息的可执行文件,开发通常用不到,出现 ramdump 死机的时候用于解析死机日志时使用。
  5. luatos_debug.map :调试映射文件,记录了函数和变量在内存中的地址。
  6. comdb.txt :组件数据库文件,记录了固件中包含的各种组件信息,使用 EPAT 抓取日志时需要。

  7. 在 luatools 上点击生成量产文件后,量产文件也是一个 soc 文件,解压后除了上述内容外还会多出两个文件:core.binpkg、script.bin

core.binpkg :核心固件包,包含了系统的核心功能。

script.bin :Lua 脚本的二进制文件,包含了用户编写的应用程序代码。这是一个非常重要的文件,它支持单独更新脚本,无需重新下载完整固件。

总的来说,.soc 文件就是一个集配置文件、核心固件、用户脚本于一体的标准化容器。它解决了多平台统一管理的问题,并通过 info.json 实现精细控制,实现固件烧录、差分升级等功能。为后续的差分升级打下了坚实的基础。那么,如何利用两个不同版本的.soc 文件,生成一个体积小巧的升级包呢?这就是我们接下来要揭秘的——差分升级的奥秘

第四部分:差分包生成原理以及差分升级原理

4.1 差分升级的基本概念

在 FOTA(无线固件升级)中,升级方式主要分为两种:整包升级和差分升级。

  • 整包升级:指将完整的新版本固件包全部下载至设备,并完全覆盖设备上现有的旧版本固件。其过程直接,但数据量大。
  • 差分升级:是一种增量更新技术。其核心在于,设备无需下载完整的新固件包,而是仅下载新旧两个固件版本之间的差异部分(即“差分包”)。设备在本地利用此差分包与自身已有的旧版本固件进行合并,从而生成完整的新版本固件。

一个生动的比喻是:你手中有一本旧版的书籍,出版社发布了修订版,但实际内容只更改了其中的 30 页。此时,你无需购买整本新书,只需获取这 30 页的修订页,并替换旧书中对应的页面即可。差分升级正是这一高效思路在固件更新上的体现。

传统整包升级的痛点:

  • 升级包体积大:消耗大量网络带宽与设备存储空间。
  • 升级耗时长:下载时间长,升级过程慢,影响用户体验。
  • 流量成本高:对于部署量庞大的物联网设备群,升级产生的总流量成本非常显著。

差分升级的显著优势:

  • 体积极小:差分包通常仅为完整新固件包的 10%-30%,甚至更低。
  • 速度极快:下载时间大幅缩短,升级效率显著提升。
  • 可靠性更高:传输数据量小,在弱网环境下传输失败或出错的概率降低。
  • 成本大幅节省:在海量设备升级场景下,能节省可观的流量费用与服务器带宽成本。

4.2 差分升级原理:只传"差异",不传"全部"

差分升级的技术本质可概括为 “计算差异、传输差异、应用差异”。其核心在于通过算法比对,仅处理和传输发生变化的数据块,而非整个文件。

具体来说,在升级前,会使用专门的算法工具(luatools),对旧固件(V1)和新固件(V2)的二进制内容进行深度比对,精确找出所有被修改、新增或删除的数据块。然后,只将这些“差异”信息,打包成一个结构化的差分包(Δ)。设备获取这个小包后,在本地执行反向操作,根据包内的指引,将差异应用到自身的 V1 版本上,从而重构出 V2。

1. 差分包生成原理

差分升级的核心是 差异比较算法 ,常用的有:

  • BSDiff :基于后缀排序的高效差异算法
  • HDiffPatch :高性能的差异比较库
  • Rsync 算法 :用于网络传输的差异算法

合宙的 Luatools 等工具就集成了这类算法,能够智能地比较两个.soc 文件或原始固件,生成最优的差分包。

2. 差分包的组成

一个标准的差分包(通常为.bin 文件)是一个精心设计的数据包,通常包含::

  • 差异数据 :新旧固件的二进制差异
  • 元信息 :版本号、校验值、生成时间
  • 合并指令 :指导设备如何合并生成新固件
  • 校验机制 :确保差分包完整性和安全性

3. 差分升级的工作流程

下面看一下差分升级的工作流程,具体流程如下:

流程详解:

  1. 生成阶段:在 luatools 上,指定新旧版本 V1 和 V2,生成一个包含合并指令和差异数据的差分包(Δ)。
  2. 传输阶段:这个极小的差分包通过蜂窝网络、以太网、蓝牙、Wi-Fi 或本地串口等渠道,高效地下发到设备。
  3. 应用阶段:设备端在设备的固件升级功能模块控制下,完成差分包校验、新固件合并写入与最终验证后,重启切换至新版本固件完成升级的过程。

第五部分:单脚本升级以及 core+ 脚本升级 升级包制作

5.1 LuatOS 开发结构

LuatOS 二次开发由两部分组成:

  1. Core 部分:既底层固件,底层 C 代码编译的二进制固件,包含操作系统内核、驱动、基础库,文件较大,更新频率较低。
  2. Script 部分:上层 Lua 应用脚本,包含业务逻辑、基础配置、应用功能等,文件较小,更新频率较高。。

在设备使用过程中,升级的时候通常会碰到三种情况,一种是单脚本需要升级,一种是脚本 + 固件都需要升级。

在 4G 相关的模组中,比如 Air780Exx 系列、Air8000 系列模组中

单脚本升级时为全量升级,含 core 升级时为差分升级。

在 wifi 模组 Air8101 和 Air8101A 中

单脚本升级和含 core 升级都为全量升级

5.2 单脚本升级

5.2.1 为什么脚本升级使用全量模式?

单脚本升级采用全量升级模式,原因有三:

  1. 大小因素:脚本文件本身就很小,一般几十到几百 KB,即使全量传输消耗的流量和带宽也很有限,差分计算带来的收益不明显。
  2. 变更模式:脚本更新频繁,且可能完全重写逻辑,相邻版本之间可能没有明显的"差异",而是完全不同的实现,这种情况下差分效果差。
  3. 实现复杂度:全量升级实现简单,直接覆盖文件即可,无需复杂的差分生成和合并算法,开发成本低。

5.2.2 单脚本升级升级包制作

在 luatools 中点击生成量产文件,在生成的量产文件夹中,对应的.bin 后缀的就是单脚本升级的升级包。

5.2.3 实际应用场景

场景 1:快速迭代开发

在开发阶段,业务逻辑频繁调整,每次修改后直接全量更新脚本,简单快捷。

场景 2:配置文件更新

当需要修改服务器地址、端口号、超时时间等配置参数时,直接替换整个配置文件即可。

5.2.4 工作流程

开发者编写新脚本 → 打包为升级文件 → 通过 FOTA 平台下发 → 设备接收并覆盖旧脚本 → 重启后生效。

整个过程简单直接,适合高频次的业务逻辑更新。

5.3 含 core 升级

5.3.1 为什么必须用差分升级?

含 core 升级必须使用差分模式,主要原因如下:

  1. 文件大小因素:Core 固件通常很大(512KB~2MB+),全量传输消耗大量流量和时间。差分可以显著减少传输数据量,通常减少 90% 以上。
  2. 变更模式特点:Core 固件更新频率低,相邻版本变化小,主要是 bug 修复和功能增强,大部分代码不变,适合差分算法。
  3. 技术必要性:嵌入式设备存储空间有限,无法同时存储两个完整固件;fota 分区通常比较小。

5.3.2 含 core 升级升级包制作

对于含 core 升级的话需要制作差分包,原始版本生成一次量产文件,新版本生成一次量产文件。

针对这两个量产文件,制作一个差分文件,点击到 luatools 的主界面,依次点击图中蓝框所示意的地方(注:必须使用 luatools_3.0.9 及其以上版本,要不差分包升级的时候可能会出问题)

按下图所示选择低版本以及高版本的固件,然后点击开始执行即可,如果不想输出的差分包在 luatools 根目录下,可以自行选择一个输出路径

在你选择的目录下看到如下所示,.bin 文件就是升级差分包。

5.3.3 Core 固件的变更特点

Core 固件的更新通常是增量式的:修复一个 bug、优化某个驱动、增加一个小功能。比如 V1.0.0 到 V1.0.1,可能只是修复了网络连接中的一个空指针异常,99% 的代码都没有变化。这种场景下,差分升级只需传输那 1% 的变化部分,效率极高。

5.3.4 含 core 升级各芯片差异

4G 模组:

移芯系列模组:780Exx 系列、8000 系列等用移芯芯片的模组,含 core 升级为差分升级,需要手动差分

展锐系列模组:724UG 系列、722UG 系列、795UG 等用展锐芯片的模组,含 core 升级为差分升级,不过如果使用的是合宙 iot 平台的话,可以上传新版本的量产文件,差分过程可在服务器后台自动进行,但是如果是第三方服务器升级需要手动差分,把差分包上传到自己服务器中。

特殊情况:wifi 模组 8101 和 8101A,由于芯片不支持差分升级,单脚本升级或 core+ 脚本升级时都为全量升级。在 luatools 生成全量文件的时候,在指定目录下会有两个文件 full_fota 和 script_ota。full_fota 中为 core+ 脚本 升级的升级包,script_ota 中为单脚本的升级包。

5.4 升级类型对 Flash 分区影响与二次开发关系总结

5.4.1 升级类型对 Flash 分区的影响

**升级类型**
**升级内容**
**影响的分区**
**不影响的分区**
**升级模式**
单脚本升级
仅升级Lua应用脚本
ap+cp image分区中的脚本区
LFS分区、KV分区、其他系统分区
全量升级
含core升级
升级底层固件和Lua应用脚本
ap+cp image分区的全部(Core部分和脚本区)
LFS分区、KV分区、其他系统分区
4G模组:差分升级;WiFi模组:全量升级

5.4.2 各分区与二次开发的关系

**分区名称**
**功能描述**
**与二次开发的关系**
LFS分区
提供小型文件系统
用于存储用户数据、配置文件、日志等,升级时数据保留,支持动态读写
KV分区
键值对存储区域
用于保存设备配置、网络参数、运行状态等,升级时数据保留,高效轻量
FOTA分区
临时存放升级包
系统自动管理,用户无需直接操作,升级完成后升级包会被清除
ap+cp image分区(Core部分)
包含操作系统内核、驱动、基础库
由官方提供,决定设备底层功能和API接口,用户无法直接修改
ap+cp image分区(Script部分)
存放用户Lua应用脚本
用户二次开发的主要工作区域,包含业务逻辑、配置参数等,可完全自定义

5.4.3 升级包特点

  • 单脚本升级:文件小,更新频繁,直接生成.bin 文件
  • 含 core 升级:文件大,更新频率低,4G 模组需制作差分包,WiFi 模组为全量包

第六部分:fota 升级相关 api 简介(libfota2 扩展库和 fota 核心库)

在 luatos 中,升级一般是有两组接口都能实现 fota 功能,分别是 libfota2 扩展库和 fota 核心库,对应的 api 连接如下:

libfota2:https://docs.openluat.com/osapi/ext/libfota2/

fota:https://docs.openluat.com/osapi/core/fota/

6.1 libfota2 扩展库与 fota 核心库 如何选择

6.1.1 核心区别总结

fota(底层核心库)

定位: 基础升级,提供最核心的固件写入能力

核心能力:

支持两种写入方式:fota.run() 分段写入 和 fota.file() 文件直接升级

支持内部存储和外部 SPI Flash

提供完整的升级流程控制:init → run/file → isDone → finish

fota2(libfota2 扩展库)

定位: 完整的远程升级解决方案,开箱即用

核心能力

自动处理 HTTP/HTTPS 网络下载

支持合宙 IoT 平台和自建服务器

内置版本检查、下载、验证全流程

提供详细错误码和回调函数

代码特点:

_-- 一行代码完成升级_
local function fota_cb(ret)
    if ret == 0 then
        log.info("升级包下载成功,重启模块")
        rtos.reboot()
    end
end
libfota2.request(fota_cb, opts)

6.1.2 适用场景推荐

选择 fota 的情况

需要自定义升级数据源

通过串口接收升级包

通过 MQTT、TCP 等自定义协议传输

从 SD 卡、U 盘等外部存储读取

  1. 对升级流程有特殊控制需求:

需要在升级前后执行特定操作

需要精细控制数据写入时机

需要自定义进度监控逻辑

  1. 资源极度受限环境

设备存储空间极小,内存紧张,无法加载额外库

  1. 开发测试阶段

需要调试升级过程的每个环节

需要验证自定义升级方案

选择 libfota2 的情况
  • 标准的 HTTP 远程升级

从服务器下载升级包

使用合宙 IoT 平台服务

需要 HTTPS 安全下载

  • 希望快速实现升级功能

不想处理网络下载细节

需要自动版本检查

希望简单的错误处理

  • 生产环境部署

需要稳定的远程升级方案

需要详细的升级状态反馈

支持定时自动检查更新

6.1.3 实际选择建议

  1. 新手用户 → 直接选择 libfota2

接口简单,学习成本低

内置完整错误处理

适合大多数物联网应用场景

  1. 高级用户 → 根据需求选择

标准网络升级 → libfota2

自定义数据传输 → fota + 自定义逻辑

一句话总结:

  • libfota2 扩展库:适合绝大多数标准远程升级场景。你只要给它一个服务器地址(合宙 iot 平台甚至不用给),它自己就帮你完成版本检查、HTTP 下载、校验所有流程。一行代码 libfota2.request(cb) 就能发起升级,省心省力。
  • fota 核心库:给你最大的控制权。适合非标准升级渠道,比如通过串口、MQTT、TCP 自定义协议,或者从 SD 卡、U 盘读取升级包。你需要自己控制数据流的接收和写入过程。

6.2 libfota2 扩展库 api 介绍

6.2.1 libfota2.request(cbFnc, opts)

功能

发起远程升级

libfota2.request 是 LuatOS 为物联网设备提供的一个强大、灵活且安全的远程固件升级接口,它能极大简化通过合宙平台或私有服务器实现设备 FOTA 功能的开发流程。

参数

cbFnc

参数含义:升级包下载结果回调函数,用于返回升级包下载结果。回调函数的调用形式为:cbFnc(result)
         result: number类型
                 0表示成功;
                 1表示连接失败;
                 2表示url错误
                 3表示服务器断开;
                 4表示接收报文错误;
                 5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
数据类型:function类型
取值范围:任意有效的函数名都行;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:如下方所示,定义了一个函数fota_cb就可以做为此参数传入
         local function fota_cb(result)
             log.info("fota", result)
             if result == 0 then
                 log.info("升级包下载成功,重启模块")
                 rtos.reboot()
             elseif result == 1 then
                 log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
             elseif result == 2 then
                 log.info("url错误", "检查url拼写")
             elseif result == 3 then
                 log.info("服务器断开", "检查服务器白名单配置")
             elseif result == 4 then
                 log.error("FOTA 失败",
                "原因可能有:\n" ..
                "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
                "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n"..
                "3) 已经是最新版本,无需升级" )
             elseif result == 5 then
                 log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
             else
                 log.info("不是上面几种情况 result为", result)
             end
         end

opts

参数含义:fota升级参数配置;参数为table类型table内容格式说明如下
        {
            -- 参数含义:指定固件升级服务器的URL。默认值是合宙iot平台的升级地址。所以若使用合宙iot平台,则不 需要填
            -- 数据类型:string类型
            -- 取值范围:支持HTTP、HTTPS,支持域名、IP地址,支持自定义端口,标准的HTTP URL格式都支持;
            -- 是否必选:可选传入此参数
            -- 注意事项:-- 如果是使用合宙IOT平台,不需要填写URL, 因为默认值是合宙iot平台的升级地址
                        -- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
                        -- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ###
                        -- 如果不加###,则默认会上传如下参数
                            --1. opts.version string 版本号, 默认是 固件版本号.xxx.zzz格式。注:固件版本号是 rtos.version()返回的版本号,xxx.zzzz是_G.VERSION参数中x和z
                            --2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
                            --3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY,自建服务器不用填
                            --4. opts.imei string 设备识别码, Cat.1模块默认取IMEI,wifi模块默认取WLAN的STA MAC地 址,mcu默认取 mcu.unique_id() 返回的唯一ID
                            --5. opts.firmware_name string 底层版本号;默认是 _G.PROJECT..  "_LuatOS-SoC_" .. rtos.bsp();其中rtos.bsp()返回的是模组型号
            -- 参数示例:-- 合宙iot服务器:local opts ={}
                        -- 自建服务器:库会自动附加默认参数(imei, version等):local opts = { url = "http://192.168.1.5:8000/update%simei=xxxxxxx&project_key=xxxxxxxx&firmware_name=xxxxxxxxx&version=xxxxxxxx" }
                        -- 自建服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加###:local opts = { url = "###http://192.168.1.5:8000/update?device_id=12345&fw_ver=1.2.3"}
            url = ,

            -- 参数含义:请求的版本号
            -- 数据类型:string类型
            -- 取值范围:合宙IOT有一套版本号体系,不传就是合宙规则(默认是 BSP版本号.xxxx.zzz格式), 自建服务器的话当然是自行约定版本号了;注:BSP版本号是通过rtos.version()返回的版本号,xxx.zzz是_G.VERSION参数中x和z
            -- 是否必选:可选传入此参数
            -- 注意事项:暂无
            -- 参数示例:默认BSP版本号.xxx.zzz是"2012.001.000"或者自定义"1.0.0"
            version = ,

            -- 参数含义:上网使用的网卡ID;
            -- 数据类型:number或者nil;
            -- 取值范围:number类型时,取值范围参考socket api中的常量详解;
            -- 是否必选:可选传入此参数;
            -- 注意事项:如果没有传入此参数,内核固件会自动选择当前时间点其他功能模块设置的默认网卡;
                     -- 除非你HTTP请求时,一定要使用某一种网卡,才设置此参数;
                     -- 如果没什么特别要求,不要设置此参数,使用系统中设置的默认网卡即可 ;
                     -- 一般来说,LuatOS的网络应用demo中都会有netdrv_device功能模块设置默认网卡;
                     -- 所以建议使用http.request接口时,不要设置此参数,直接使用netdrv_device设置的默认网卡就行;
            -- 参数示例:socket.LWIP_GP表示使用4G网卡;
            adapter = ,

            -- 参数含义:设置整个 FOTA HTTP 请求过程的超时时间
            -- 数据类型:int类型,
            -- 取值范围:number类型时,取值范围为大于等于0的整数,0表示永久等待;
            -- 是否必选:可选传入此参数
            -- 注意事项:暂无
            -- 参数示例:300000表示300s
            timeout = ,

            -- 参数含义:合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
            -- 数据类型:string类型
            -- 取值范围:无特别限制;
            -- 是否必选:可选传入此参数
            -- 注意事项:仅用于合宙 IoT 平台。自建服务器不需要此参数。
                    --如果未设置,库会尝试从全局变量 PRODUCT_KEY 中获取。
            -- 参数示例:"user123" 或者 nil
            project_key = ,

            -- 参数含义:设备识别码,用于服务器识别具体设备
            -- 数据类型:string类型
            -- 取值范围:默认取IMEI(Cat.1模块)或WLAN的STA MAC地址  (wifi模块)或 mcu.unique_id()获取MCU唯一ID
            -- 是否必选:可选传入此参数
            -- 注意事项:暂无
            -- 参数示例:imei = mobile.imei(),
            imei = ,

            -- 参数含义:固件名称
            -- 数据类型:string类型
            -- 取值范围:默认是 _G.PROJECT.. "_LuatOS-SoC_" .. rtos.bsp()
            -- 是否必选:可选传入此参数
            -- 注意事项:暂无
            -- 参数示例:FOTA2_DEMO_LuatOS-SoC_Air780EPM
            firmware_name = ,

            -- 参数含义:服务器ca证书数据;
            -- 数据类型:string或者nil;
            -- 取值范围:无特别限制;
            -- 是否必选:可选传入此参数;
            -- 注意事项:当客户端需要验证服务器证书时,需要此参数,如果证书数据在一个文件中,要把文件内容读出来,赋值给server_ca_cert;
            -- 参数示例:例如通过Luatools烧录了server_ca.crt文件,就可以通过io.readFile("/luadb/server_ca.crt")读出文件内容赋值给赋值给server_ca_cert;
            server_cert = ,



            -- 参数含义:客户端证书数据;
            -- 数据类型:string或者nil;
            -- 取值范围:无特别限制;
            -- 是否必选:可选传入此参数;
            -- 注意事项:当服务器需要验证客户端证书时,需要此参数,如果证书数据在一个文件中,要把文件内容读出来,赋值给client_cert;
            -- 参数示例:例如通过Luatools烧录了clinet.crt文件,就可以通过io.readFile("/luadb/clinet.crt")读出文件内容赋值给赋值给client_cert;
            client_cert = ,


            -- 参数含义:加密后的客户端私钥数据;
            -- 数据类型:string或者nil;
            -- 取值范围:无特别限制;
            -- 是否必选:可选传入此参数;
            -- 注意事项:当服务器需要验证客户端证书时,需要此参数,如果加密后的私钥数据在一个文件中,要把文件内容读出来,赋值给client_key;
            -- 参数示例:例如通过Luatools烧录了clinet.key文件,就可以通过io.readFile("/luadb/clinet.key")读出文件内容赋值给client.key;
            client_key = ,


            -- 参数含义:客户端私钥口令数据;
            -- 数据类型:string或者nil;
            -- 取值范围:无特别限制;
            -- 是否必选:可选传入此参数;
            -- 注意事项:当服务器需要验证客户端证书时,需要此参数,如果加密后的私钥数据在一个文件中,要把文件内容读出来,赋值给client_password;
            -- 参数示例:例如通过Luatools烧录了clinet.password文件,就可以通过io.readFile("/luadb/clinet.password")读出文件内容赋值给client_password;
            client_password = ,


            -- 参数含义:HTTP请求方法;
             -- 数据类型:string;
             -- 取值范围:支持"GET"、"POST"、"HEAD"等所有HTTP请求方法,请求方法用大写字母表示;
             -- 是否必选:可选传入此参数;如果没有传入此参数或者传入了nil类型,则使用默认值,默认值分为以下两种情况:
             --          如果没有设置files,forms,body,bodyfile参数,则默认为"GET"
             --          如果至少设置了files,forms,body,bodyfile中的一种参数,则默认为"POST"
             -- 注意事项:暂无;
             -- 参数示例:GET请求时填"GET",POST请求时填"POST";
            method = ,



            -- 参数含义:HTTP请求头列表,键值对的形式;
            -- 数据类型:table或者nil;
            -- 取值范围:当为table数据类型时,请求头列表中支持一个或者多个请求头;
            -- 是否必选:可选传入此参数;
            -- 注意事项:暂无;
            -- 参数示例:{
            --               ["Content-Type"] = "application/x-www-form-urlencoded",
            --               ["self_defined_key"] = "self_defined_value"
            --           }
            headers = ,


            -- 参数含义:HTTP请求体;
            -- 数据类型:string或者zbuff或者nil;
            -- 取值范围:无特别限制;
            -- 是否必选:可选传入此参数;
            -- 注意事项:如果请求体是一个文件中的内容,需要把文件内容读出来,赋值给body使用;
            -- 参数示例:"123456" 或者 一个zbuff对象 或者 nil
            body = ,
        }

数据类型:table或者nil
取值范围:参考参数含义内各字段说明
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:如下方所示,如果url是"http://192.168.1.5:8000/update"version是"1.0.0"
        --local opts = {
        --    url = "http://192.168.1.5:8000/update",
        --    version = "1.0.0"
        --}

返回值

示例

本示例章节仅列举一些常用功能的核心代码片段

更加完整和详细的 demo,请参考 https://gitee.com/openLuat/LuatOS/tree/master/module 各个产品目录下的 demo/fota2 文件夹下内容

--用法实例
local libfota2 = require("libfota2")
-- 升级结果的回调函数
-- 功能:获取fota的回调函数
-- 参数:
-- result:number类型
--         0表示成功
--         1表示连接失败
--         2表示url错误
--         3表示服务器断开
--         4表示接收报文错误
--         5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
local function fota_cb(ret)
    log.info("fota", ret)
    if ret == 0 then
        log.info("升级包下载成功,重启模块")
        rtos.reboot()
    elseif ret == 1 then
        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
    elseif ret == 2 then
        log.info("url错误", "检查url拼写")
    elseif ret == 3 then
        log.info("服务器断开", "检查服务器白名单配置")
    elseif ret == 4 then
        log.error("FOTA 失败",
            "原因可能有:\n" ..
            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n"..
            "3) 已经是最新版本,无需升级" )
    elseif ret == 5 then
        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
    else
        log.info("不是上面几种情况 ret为", ret)
    end
end

libfota2.request(fota_cb)

6.3 fota 核心库 api 介绍

6.3.1 fota.init()

功能

初始化 fota 流程。

参数

返回值

local result = fota.init()

result

参数含义:初始化是否成功;成功返回true,失败返回false
数据类型:boolean
取值范围:true/false

例子

-- 示例
local result = fota.init()
if result then
    log.info("fota", "初始化成功")
else
    log.error("fota", "初始化失败")
end

6.3.2 fota.wait()

功能

等待底层 fota 流程准备好,等待底层固件升级流程初始化完成,包括存储设备就绪、升级上下文准备等。

参数

返回值

local isDone = fota.wait()

isDone

参数含义:是否准备好;准备好返回true,否则返回false
数据类型:boolean
取值范围:true/false

例子

-- 在开始写入数据前等待准备
fota.init()
while not fota.wait() do
    sys.wait(100)
end

log.info("fota", "底层准备就绪,开始升级")

6.3.3 fota.run(buff, offset, len)

功能

写入 fota 数据,支持逐段写入升级包。

注意事项:如果传入的是 zbuff,写入成功后,请自行清空 zbuff 内的数据

参数

buff

参数含义:fota数据,尽量用zbuff以提高性能
数据类型:zbuff或string
取值范围:有效的二进制数据
是否必选:是

offset

参数含义:起始偏移量,仅在buff参数传入时有效
数据类型:number
取值范围:非负整数,不超过zbuff大小
是否必选:可选,默认是0

len

参数含义:写入长度,仅在传入zbuff时有效
数据类型:number
取值范围:非负整数,不超过zbuff剩余空间
是否必选:可选,默认是zbuff:used()

返回值

local result, isDone, cache = fota.run(buff, offset, len)

result

参数含义:写入是否成功;成功返回true,失败返回false
数据类型:boolean
取值范围:true/false

isDone

参数含义:是否写入到最后一块;写入到最后一块返回true,否则返回false
数据类型:boolean
取值范围:true/false

cache

参数含义:已接收但尚未写入Flash的缓存数据量(单位:字节)
数据类型:number
取值范围:非负整数,超过64K必须做等待

例子

fota.init()
    while not fota.wait() do
        sys.wait(100)
    end
    local zb = zbuff.create(4096)  -- 创建4KB的zbuff

    -- 从某种数据源填充zbuff,data_source可以是http下载的,可以是串口接收的,可以是mqtt接收的升级包分段数据
    while true do
        local zb = data_source:read(4096)
        if zb:len() == 0 then break end
        _-- 提示:如果传入的是zbuff,写入成功后,请自行清空zbuff内的数据_
        local result, isDone, cache = fota.run(zb, 0, read_count)
        zb:del()  -- 清空zbuff内容

        if not result then
            log.error("fota", "写入失败")
            break
        end

        if isDone then
            log.info("fota", "升级包接收完成")
            check_upgrade_result()
            break
        end
        sys.wait(50)
    end

6.3.4 fota.file(path)

功能

从指定文件读取 fota 数据并写入

参数

path

参数含义:升级文件的文件路径
数据类型:string
取值范围:有效的文件系统路径
是否必选:是

返回值

local result, isDone, cache = fota.file("/xxx.bin")

result

参数含义:写入是否成功;有异常返回false,无异常返回true
数据类型:boolean
取值范围:true/false

isDone

参数含义:接收到最后一块返回true,否则返回false
数据类型:boolean
取值范围:true/false

cache

参数含义:已接收但尚未写入Flash的缓存数据量(单位:字节)
数据类型:number
取值范围:非负整数,超过64K必须做等待

例子

-- 从文件系统直接升级
local result, isDone, cache = fota.file("/luadb/update.bin")
if result then
    log.info("fota", "文件升级启动成功")

    -- 等待升级完成
    while true do
        local succ, fotaDone = fota.isDone()
        if not succ then
            log.error("fota", "升级过程出错")
            break
        end
        if fotaDone then
            log.info("fota", "文件升级完成")
            fota.finish(true)
            sys.wait(1000)
            rtos.reboot()  -- 重启设备
            break
        end
        sys.wait(500)
    end
else
    log.error("fota", "文件升级启动失败")
end



-- 2.先下载升级包到文件,然后升级
function monitor_upgrade_progress()
    while true do
        local succ, done = fota.isDone()
        if not succ then
            log.error("fota", "升级出错")
            break
        end
        if done then
            log.info("fota", "升级完成,准备重启")
            fota.finish(true)
            sys.wait(2000)
            rtos.reboot()
            break
        end
        log.info("fota", "升级进行中...")
        sys.wait(1000)
    end
end
function http_download_fota()
    -- 下载升级包
        http.request("GET", "https://www.air32.cn/", nil, nil, {dst=xxx.bin}).wait()
        log.info("fota", "下载完成")

        -- 初始化FOTA
        fota.init()
        while not fota.wait() do
            sys.wait(100)
        end
        -- 从文件升级
        local result, isDone, cache = fota.file("xxx.bin")
        if result then
            monitor_upgrade_progress()
        end

end
sys.taskInit(http_download_fota)

6.3.5 fota.isDone()

功能

等待底层 fota 流程完成

参数

返回值

local result, isDone = fota.isDone()

result

参数含义:有异常返回false,无异常返回true
数据类型:boolean
取值范围:true/false

isDone

参数含义:写入到最后一块返回true,否则返回false
数据类型:boolean
取值范围:true/false

例子

local result, isDone = fota.isDone()
log.info("升级状态", result, isDone)
if result then
    if isDone then
        log.info("升级成功")
        rtos.reboot()
    else
        log.info("升级失败")
    end
end

6.3.6 fota.finish(is_ok)

功能

结束 fota 流程

参数

is_ok

参数含义:是否完整走完流程,
         true表示正确走完流程了,
         false是用户层告诉底层本次升级用户取消了,除非需要中断当前的fota过程,一般不需要使用;
         除了用户取消,也可能因为升级过程中出错而调用fota.finish(false)
数据类型:boolean
取值范围:true或false
是否必选:是

返回值

local result = fota.finish(is_ok)

result

参数含义:成功返回true, 失败返回false
数据类型:boolean
取值范围:true或false

例子

--升级成功
succ,fotaDone  = fota.isDone()
if succ then
    if fotaDone then
        fota.finish(true)
        log.info("FOTA完成")
        done = true
        rtos.reboot()   --如果还有其他事情要做,就不要立刻reboot
        break
    end
end

--升级失败
succ,fotaDone,nCache = fota.run(rbuff)
if succ then
   total = total + rbuff:used()
else
  log.error("fota写入异常,结束本次流程")
  fota.finish(false)
  done = true
end

第七部分:LuatOS 上 FOTA 功能实际应用示例

本部分将深入两项具体实践:使用 FOTA 核心库进行固件升级和使用 libfota2 扩展库进行固件升级。

7.1 使用 FOTA 核心库的固件升级

FOTA 核心库应用 demo 代码:https://gitee.com/openLuat/LuatOS/tree/master/module/Air780EHM_Air780EHV_Air780EGH/demo/fota/fota(%E4%BD%BF%E7%94%A8fota%E6%A0%B8%E5%BF%83%E5%BA%93)

7.1.1 分析项目代码

文件说明
  1. main.lua:主程序入口文件。
  2. fota_file.lua:文件系统 FOTA 升级功能实现,从文件系统直接读取升级包进行固件升级。
  3. fota_uart.lua:串口分段升级功能实现,通过串口接收升级包数据进行固件升级。
演示功能
  1. 文件系统直接升级:

  2. 通过模组文件系统中的文件直接升级

  3. 代码演示通过 luatools 的烧录文件系统功能将升级包文件直接烧录到文件系统然后升级
  4. 适用于本地升级、批量生产等场景
  5. 串口分段升级:

  6. 通过串口将升级包文件分多个片段发送,每个片段接收并写入

  7. 代码演示使用 USB 虚拟串口分段写入升级包升级
  8. 适用于非标准数据传输(串口、TCP、MQTT 等自定义通道升级)
  9. 支持流程精细控制,可自定义升级前后处理逻辑
注意事项
  1. 升级包必须是针对当前硬件平台的正确固件
  2. 升级过程中请勿断电或中断升级流程
  3. 串口升级时,需确保串口连接稳定
  4. 升级成功后,设备会自动重启,无需手动操作。
  5. 升级失败时,可通过日志查看具体错误信息

7.2 使用 libfota2 扩展库的固件升级

libfota2 扩展库应用 demo 代码:https://gitee.com/openLuat/LuatOS/tree/master/module/Air780EHM_Air780EHV_Air780EGH/demo/fota/fota2(%E4%BD%BF%E7%94%A8libfota2%E6%89%A9%E5%B1%95%E5%BA%93)

7.2.1 分析项目代码

目录结构
fota2(使用libfota2扩展库)/
├── iot_server/          # 合宙IoT服务器升级方案
│   ├── main.lua         # 主程序入口文件
│   ├── update.lua
│   ├── air_srv_fota.lua
│   ├── netdrv_device.lua
│   ├── psm_power_fota.lua
│   ├── tcp_iot/
│   │   ├── tcp_iot_main.lua
│   │   ├── tcp_iot_sender.lua
│   │   └── tcp_iot_receiver.lua
│   ├── netdrv/
│   │   ├── netdrv_4g.lua
│   │   ├── netdrv_eth_spi.lua
│   │   └── netdrv_multiple.lua
│   └── readme.md        # 使用说明文档
│
└── self_server/         # 自定义服务器升级方案
    ├── main.lua         # 主程序入口文件
    ├── update.lua
    ├── customer_srv_fota.lua
    ├── psm_power_fota.lua
    ├── netdrv_device.lua
    ├── tcp_self_server/
    │   ├── tcp_self_main.lua
    │   ├── tcp_self_sender.lua
    │   └── tcp_self_receiver.lua
    ├── netdrv/
    │   ├── netdrv_4g.lua
    │   ├── netdrv_eth_spi.lua
    │   └── netdrv_multiple.lua
    └── readme.md        # 说明文档
文件说明
  1. iot_server 目录

main.lua :主程序入口,

update.lua :使用合宙 IoT 服务器简单远程升级模块。

air_srv_fota.lua :使用合宙 IoT 平台远程通过 tcp 下发指令控制升级功能模块。

netdrv_device.lua :网络驱动设备配置,支持 4G、WIFI、以太网等多种网络连接方式

psm_power_fota.lua :PSM 低功耗模式下的 FOTA 升级实现,解决 PSM 状态下升级中断问题 TCP 客户端 IoT 模块

tcp_iot/tcp_iot_main.lua :TCP 客户端主应用,处理 TCP 连接和升级指令接收

tcp_iot/tcp_iot_sender.lua :TCP 数据发送功能,负责向服务器发送设备状态和升级反馈

tcp_iot/tcp_iot_receiver.lua :TCP 数据接收功能,解析服务器下发的升级指令 网络驱动

netdrv/netdrv_4g.lua :4G 网卡驱动实现,负责 4G 网络连接管理

netdrv/netdrv_eth_spi.lua :SPI 外挂 CH390H 芯片的以太网卡驱动

netdrv/netdrv_multiple.lua :多网卡优先级管理,支持配置多种网卡的连接优先级

  1. self_server 目录

main.lua :主程序入口。

update.lua :自定义服务器远程简单升级功能模块。

customer_srv_fota.lua :自定义服务器通过 tcp 下发指令控制升级功能模块。

psm_power_fota.lua :PSM 低功耗模式下的 FOTA 升级实现,解决低功耗场景下的升级问题

netdrv_device.lua :网络驱动设备配置,支持多种网络连接方式 TCP 自定义服务器模块

tcp_self_server/tcp_self_main.lua :TCP 客户端主应用,处理 TCP 连接和升级指令接收

tcp_self_server/tcp_self_sender.lua :TCP 数据发送功能,负责向服务器发送设备状态和升级反馈

tcp_self_server/tcp_self_receiver.lua :TCP 数据接收功能,解析服务器下发的升级指令 网络驱动

netdrv/netdrv_4g.lua :4G 网卡驱动实现

netdrv/netdrv_eth_spi.lua :SPI 以太网卡驱动

netdrv/netdrv_multiple.lua :多网卡优先级管理

演示功能
  1. 场景一:合宙 IoT 服务器简单升级

  2. 使用合宙 iot.openluat.com 服务器进行远程升级

  3. 上电就检查升级,支持版本号自动检测和升级包下载
  4. 适用于快速部署和管理大量设备
  5. 场景二:TCP 服务器下发升级指令

  6. 通过 TCP 服务器下发 JSON 格式升级指令

  7. 通控制设备使用 FOTA 功能模块
  8. 场景三:PSM 低功耗 FOTA

  9. 针对 PSM 状态下升级未完成就进入休眠导致升级失败的情况

  10. 支持低功耗设备的可靠升级
注意事项
  1. 使用合宙 iot.openluat.com 进行远程升级时,版本号必须按照"XXX.YYY.ZZZ"三段格式定义
  2. 不同场景的功能模块不要同时加载,否则会导致功能冲突
  3. 升级过程中需保持网络连接稳定
  4. 建议在升级前检查设备电量,确保有足够电量完成升级。

7.3 Air780EHM 开发板上演示项目功能

准备硬件环境

  1. Air780EHM 开发板一块
  2. TYPE-C USB 数据线一根
  3. 可联网的 SIM 卡(用于远程升级场景)
  4. ttl 小板(可选,用于物理串口升级)

准备软件环境

  1. 烧录工具:Luatools 下载调试工具
  2. 内核固件:Air780EHM V2018 版本(或最新版本)
  3. 脚本文件:对应场景的脚本文件
  4. 模拟工具:

  5. 串口升级:Python 环境和 main.py 脚本

  6. 远程升级:合宙 iot.openluat.com 平台账号

演示流程

  1. 文件系统升级演示

  2. 使用 Luatools 烧录升级包到文件系统

  3. 烧录 fota_file.lua 相关脚本
  4. 观察日志输出,验证升级是否成功
  5. 串口升级演示

  6. 烧录 fota_uart.lua 相关脚本

  7. 按下 Power 键启动升级模式
  8. 运行 Python 脚本发送升级包
  9. 观察串口输出和升级进度
  10. 远程升级演示

  11. 在合宙 IoT 平台创建设备和升级任务

  12. 烧录对应场景的脚本
  13. 设备自动连接平台并检测升级
  14. 观察升级进度和结果

7.4 升级扩展

支持蓝牙功能的模块比如 Air8000A 或者 Air8101 等模组,可以通过蓝牙升级,具体升级可参考对应教程文档。

8000 模组使用蓝牙升级教程文档:https://docs.openluat.com/air8000/luatos/app/ota/ble_fota/

第八部分:注意事项与常见问题

8.1 注意事项

1、 版本号格式:

使用合宙 IoT 平台时,项目的 VERSION 必须为 xxx.yyy.zzz 的三段数字格式(如 "001.000.001"),否则平台版本比对可能出错。

2、PRODUCT_KEY:

使用合宙 IoT 平台时,必须在 main.lua 中正确定义全局变量 PRODUCT_KEY,其值需从 IoT 平台的项目中获取。

3、重启时机:

下载升级包成功(result 为 0)后,通常需要调用 rtos.reboot() 重启设备以更新。你可以根据需要延迟重启。

4、自建服务器规则:

使用 libfota2 扩展库的时候,填写自建服务器 url 时候记得 url 前面拼上 ###

需要升级时,服务器应返回 HTTP 200,消息体为升级文件内容。

无需升级时,服务器应返回 HTTP 300 或以上的状态码。

5、使用 iot 平台需要注意:

设备在自己名下;

代码中项目 key(PRODUCT_KEY)要填写正确;

配置好升级包文件后需要指定升级设备,配置需要升级设备的 imei;

升级失败可以在 iot 平台中打开 固件升级 -> 升级日志 页面,输入 iemi 来查看下升级失败的原因是什么。

8.2 为什么升级后我的模块没有任何反应了,像是变砖一样

有多种可能,

8.2.1 检查脚本

首先先检查下用户自己的脚本,有可能是引起重启/死机的代码写在了最前面,例如新加的某个值或者函数为 nil 但是还是去做了些加减乘除或者判断大小的逻辑。可以直接本地烧录下新版本的 core+ 脚本验证,如果有 fskv 等用到 flash 的代码,可能需要仔细检查才能排除问题,比如下载的时候勾选如下图所示的两个选项。

8.2.2 检查 core

如果是仅脚本升级,但是没注意使用了新 core 中才有的接口,就有可能引起循环重启,如果重启在代码最开头,模块可能来不及打印任何日志就重启了,可以直接本地烧录下新版本的 core+ 脚本验证,如果有 fskv 等用到 flash 的代码,可能需要仔细检查。

8.3 检查过脚本和 core,没问题,为什么会循环升级 6 次以后禁止升级

检查下升级包是否正常,有时候因为人员误操作,经常会出现旧脚本 + 新 core 或者新脚本 + 旧 core 的意外组合,

例如:

本来应该如下表描述的一样


脚本版本号
core 版本号
旧版本
001.000.000
V2003
新版本
001.000.005
V2004

操作人员失误后变成了如下


脚本版本号
core 版本号
误操作旧版本(1)
001.000.005
V2003
误操作旧版本(2)
001.000.000
V2004
误操作新版本(1)
001.000.000
V2004
误操作新版本(2)
001.000.005
V2003

然后误操作旧版本(1) 和误操作新版本(1)进行差分,这样虽然脚本版本号旧版本大于了新版本,但是 core 的旧版本小于新版本,所以升级平台依旧认为是依次有效的升级,下发了升级包。

升级完成后,模块内部脚本版本号变成了 001.000.000 core 版本号为 V2004 ,下次模块请求升级的时候,当前固件上报的脚本版本号(001.000.000)依旧小于云平台存储的脚本版本号(001.000.005),然后继续下发升级包,就这么循环升级,直到流量耗尽,建议可以做一个类似合宙 iot 平台的禁止升级规则

在正确生成差分包,并且上传成功后,可以在 iot 平台里解除禁止升级的限制

在"我的设备"中选择升级 imei 所在的项目,然后点击右边的"解除禁止升级",

确定“导致设备循环升级的异常”已经处理完成后,点击确定解除,即可解除限制升级

8.4 如何处理同个项目外面有多个版本设备的升级情况

8.4.1 场景 1:多种不同内核固件版本都要升级为最新版本内核固件 + 最新脚本

需要对每个版本都生成对应的差分包

操作步骤:

8.4.2 场景 2:多种不同内核固件版本 + 不同版本脚本都要升级为最新版本脚本,既只升级脚本。

操作步骤:

8.4.3 升级规则说明

  1. 内核固件:需分情况对待,4G 模组系列比如 Air780EXX 系列、Air724UG、Air8000 系列等仅支持差分升级,wifi 模组 Air8101/Air8101A 是全量升级
  2. 脚本:支持全量升级,可一次性完成

8.5 fota 升级对 fskv 或文件系统的影响

8.5.1 远程升级时,会清除 FSKV 中的数据吗?

默认不会,FSKV 数据存储在独立分区,远程升级主要操作 FOTA 专用分区、ap/cp 分区、用户脚本分区,不会直接修改 FSKV 分区。

8.5.2 远程升级时,自己创建的文件会被删除吗?

不会,用户文件系统中自行创建的文件会保留,不会影响用户文件。

第九部分 fota 错误总结

9.1 差分包过大

9.2 iot 平台升级没有配置 imei

9.3 制作差分包时的旧固件不是模组中实际的固件

下面例子模组中实际固件是 2016_2 号固件

制作差分时用的 2020_1 号固件对 2016_1 号固件制作的差分包

平台校验版本通过,正常下发升级包

9.4 不同编号固件制作差分包

不同编号的固件制作差分包的时候通常制作的差分包过大,升级失败,日志见 9.1

问题持续更新......