LuatOS 课程-009 讲:fota 升级教程
作者:孟伟 | 最后修改:2026-01-22
Hello,大家好,我是孟伟。欢迎大家来到合宙 LuatOS 直播课堂,继续我们的 LuatOS 系列课程学习。
第一部分:课程背景与内容概览
今天是我们 LuatOS 系列课程的第 009 讲,同时也是 LuatOS FOTA(无线固件升级)专题课程的第一讲。
FOTA 是物联网设备保持生命力和可维护性的关键技术。本次课程将带大家深入理解 LuatOS FOTA 的底层原理,并掌握其应用开发方法。如果您还不清楚 LuatOS 课程背景,可以访问:LuatOS 课程背景 这个链接,进行了解;
本课程主要包含以下六个部分:
- Flash 分区详解,剖析 Flash 分区布局。
- 揭秘
.soc固件包的内部结构与秘密。 - 详解差分升级原理。
- 对比两套核心 API(
fota核心库vslibfota2扩展库)的如何选择。 - Demo 讲解:在 Air780EHM 开发板上进行多场景升级演示。
- 总结关键注意事项与常见问题。
第二部分: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 的“大楼”。但这里有个精妙的设计:这栋楼有两个门牌号系统。
- 物理地址(0x0 – 8MB):这是 Flash 芯片真实的“砖块地址”,用于存储所有数据。
- 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)分区是一个小型的键值对存储区域,用于保存设备的配置信息和状态数据。
特点:
- 持久化存储:断电后数据不丢失
- 高效访问:支持快速的键值对读写操作
- 固定大小:通常为 64KB
- 轻量级:适合存储少量关键配置,如网络参数、设备 ID、运行状态等
KV 分区的典型用途:
- 存储设备唯一标识符
- 保存网络连接参数(APN、服务器地址等)
- 记录设备运行状态和统计信息
- 存储用户自定义配置
- 保存升级状态和版本信息
2.4 FOTA 分区“系统升级专用间”:
2.4.1 分区介绍
作用 :这是今天的主角,用于临时存放固件升级包,实现安全、隔离的远程升级。
fota 分区具有以下特性:
- 独立空间 :与其他分区严格分离,确保升级安全,从网络或其他渠道下载的完整固件包或差分包先存储到 FOTA 分区
- 安全机制 :支持升级包完整性校验和签名验证,升级包写入 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、升级包写入流程:
- 初始化 FOTA 模块: 创建升级上下文结构体,分配必要的缓冲区和资源,为升级流程做准备。
- 下载升级包:
通过 HTTP、MQTT 或 UART 等通信协议从服务器或本地下载升级包数据。支持下载到文件系统,或者直接分包写入 fota 分区。
- 写入 FOTA 分区:
如果是下载到文件系统中暂存,待整个升级包在文件系统中下载完成后,再一次性从文件系统读取并写入到专用的 FOTA 分区。
也可以实时分包接收升级包数据并写入到文件系统中。
- 完成写入,验证升级包:
执行升级包的完整性校验(MD5)
若校验通过,则在特定位置中设置升级标志,并重启系统。Bootloader 下次启动时执行升级。
若校验失败,则报告错误并终止升级流程。
2、重启后升级流程:
- 设备上电,执行 Bootloader
设备重启后,Bootloader 首先检查是否存在有效的升级标志。
- 验证与执行升级 若无升级标志: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 流程中,它扮演着三个关键角色:
- 它是差分的蓝本:我们下一章要讲的‘差分升级’,其核心就是对比新旧两个版本的.soc 文件,生成差异部分。
- 它携带了分区地图:.soc 包内的
info.json配置文件,明确写明了本固件的编号、分区地址等信息,这是 FOTA 进行安全校验(防止变砖)的核心依据。 - 它统一了操作对象:无论底层是移芯还是展锐芯片,最终都封装成统一的.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 解压后都有哪些文件:

- info.json
第一个要介绍的是 info.json ,这是固件的核心配置文件,相当于固件的"身份证"。
这个文件主要包含有:
- 芯片类型(
- ROM 文件信息及下载地址
- 脚本文件信息及下载地址
- 分区表配置
-
下载参数(波特率、强制波特率等)
-
luatos.binpkg
LuatOS 的核心二进制固件包,包含了系统运行所需的所有核心代码。
.binpkg 为移芯系列芯片所生成的原始固件内容,是移芯芯片特有的格式,不同的芯片会有不同的原始固件格式
-
其他辅助文件
-
mem_map.txt:这是设备内存的"地图",内存映射配置文件,定义了设备内存的分区结构和地址范围。
- luat_conf_bsp.h :支持功能配置头文件,定义了一些功能相关的配置宏,比如引脚定义、外设配置等。
- luatos.elf :包含完整调试信息的可执行文件,开发通常用不到,出现 ramdump 死机的时候用于解析死机日志时使用。
- luatos_debug.map :调试映射文件,记录了函数和变量在内存中的地址。
-
comdb.txt :组件数据库文件,记录了固件中包含的各种组件信息,使用 EPAT 抓取日志时需要。
-
在 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. 差分升级的工作流程
下面看一下差分升级的工作流程,具体流程如下:

流程详解:
- 生成阶段:在 luatools 上,指定新旧版本 V1 和 V2,生成一个包含合并指令和差异数据的差分包(Δ)。
- 传输阶段:这个极小的差分包通过蜂窝网络、以太网、蓝牙、Wi-Fi 或本地串口等渠道,高效地下发到设备。
- 应用阶段:设备端在设备的固件升级功能模块控制下,完成差分包校验、新固件合并写入与最终验证后,重启切换至新版本固件完成升级的过程。
第五部分:单脚本升级以及 core+ 脚本升级 升级包制作
5.1 LuatOS 开发结构
LuatOS 二次开发由两部分组成:
- Core 部分:既底层固件,底层 C 代码编译的二进制固件,包含操作系统内核、驱动、基础库,文件较大,更新频率较低。
- Script 部分:上层 Lua 应用脚本,包含业务逻辑、基础配置、应用功能等,文件较小,更新频率较高。。
在设备使用过程中,升级的时候通常会碰到三种情况,一种是单脚本需要升级,一种是脚本 + 固件都需要升级。
在 4G 相关的模组中,比如 Air780Exx 系列、Air8000 系列模组中
单脚本升级时为全量升级,含 core 升级时为差分升级。
在 wifi 模组 Air8101 和 Air8101A 中
单脚本升级和含 core 升级都为全量升级
5.2 单脚本升级
5.2.1 为什么脚本升级使用全量模式?
单脚本升级采用全量升级模式,原因有三:
- 大小因素:脚本文件本身就很小,一般几十到几百 KB,即使全量传输消耗的流量和带宽也很有限,差分计算带来的收益不明显。
- 变更模式:脚本更新频繁,且可能完全重写逻辑,相邻版本之间可能没有明显的"差异",而是完全不同的实现,这种情况下差分效果差。
- 实现复杂度:全量升级实现简单,直接覆盖文件即可,无需复杂的差分生成和合并算法,开发成本低。
5.2.2 单脚本升级升级包制作
在 luatools 中点击生成量产文件,在生成的量产文件夹中,对应的.bin 后缀的就是单脚本升级的升级包。
5.2.3 实际应用场景
场景 1:快速迭代开发
在开发阶段,业务逻辑频繁调整,每次修改后直接全量更新脚本,简单快捷。
场景 2:配置文件更新
当需要修改服务器地址、端口号、超时时间等配置参数时,直接替换整个配置文件即可。
5.2.4 工作流程
开发者编写新脚本 → 打包为升级文件 → 通过 FOTA 平台下发 → 设备接收并覆盖旧脚本 → 重启后生效。
整个过程简单直接,适合高频次的业务逻辑更新。
5.3 含 core 升级
5.3.1 为什么必须用差分升级?
含 core 升级必须使用差分模式,主要原因如下:
- 文件大小因素:Core 固件通常很大(512KB~2MB+),全量传输消耗大量流量和时间。差分可以显著减少传输数据量,通常减少 90% 以上。
- 变更模式特点:Core 固件更新频率低,相邻版本变化小,主要是 bug 修复和功能增强,大部分代码不变,适合差分算法。
- 技术必要性:嵌入式设备存储空间有限,无法同时存储两个完整固件;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 盘等外部存储读取
- 对升级流程有特殊控制需求:
需要在升级前后执行特定操作
需要精细控制数据写入时机
需要自定义进度监控逻辑
- 资源极度受限环境
设备存储空间极小,内存紧张,无法加载额外库
- 开发测试阶段
需要调试升级过程的每个环节
需要验证自定义升级方案
选择 libfota2 的情况:
- 标准的 HTTP 远程升级
从服务器下载升级包
使用合宙 IoT 平台服务
需要 HTTPS 安全下载
- 希望快速实现升级功能
不想处理网络下载细节
需要自动版本检查
希望简单的错误处理
- 生产环境部署
需要稳定的远程升级方案
需要详细的升级状态反馈
支持定时自动检查更新
6.1.3 实际选择建议
- 新手用户 → 直接选择 libfota2
接口简单,学习成本低
内置完整错误处理
适合大多数物联网应用场景
- 高级用户 → 根据需求选择
标准网络升级 → 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 分析项目代码
文件说明
- main.lua:主程序入口文件。
- fota_file.lua:文件系统 FOTA 升级功能实现,从文件系统直接读取升级包进行固件升级。
- fota_uart.lua:串口分段升级功能实现,通过串口接收升级包数据进行固件升级。
演示功能
-
文件系统直接升级:
-
通过模组文件系统中的文件直接升级
- 代码演示通过 luatools 的烧录文件系统功能将升级包文件直接烧录到文件系统然后升级
- 适用于本地升级、批量生产等场景
-
串口分段升级:
-
通过串口将升级包文件分多个片段发送,每个片段接收并写入
- 代码演示使用 USB 虚拟串口分段写入升级包升级
- 适用于非标准数据传输(串口、TCP、MQTT 等自定义通道升级)
- 支持流程精细控制,可自定义升级前后处理逻辑
注意事项
- 升级包必须是针对当前硬件平台的正确固件
- 升级过程中请勿断电或中断升级流程
- 串口升级时,需确保串口连接稳定
- 升级成功后,设备会自动重启,无需手动操作。
- 升级失败时,可通过日志查看具体错误信息
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 # 说明文档
文件说明
- 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 :多网卡优先级管理,支持配置多种网卡的连接优先级
- 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 :多网卡优先级管理
演示功能
-
场景一:合宙 IoT 服务器简单升级
-
使用合宙 iot.openluat.com 服务器进行远程升级
- 上电就检查升级,支持版本号自动检测和升级包下载
- 适用于快速部署和管理大量设备
-
场景二:TCP 服务器下发升级指令
-
通过 TCP 服务器下发 JSON 格式升级指令
- 通控制设备使用 FOTA 功能模块
-
场景三:PSM 低功耗 FOTA
-
针对 PSM 状态下升级未完成就进入休眠导致升级失败的情况
- 支持低功耗设备的可靠升级
注意事项
- 使用合宙 iot.openluat.com 进行远程升级时,版本号必须按照"XXX.YYY.ZZZ"三段格式定义
- 不同场景的功能模块不要同时加载,否则会导致功能冲突
- 升级过程中需保持网络连接稳定
- 建议在升级前检查设备电量,确保有足够电量完成升级。
7.3 Air780EHM 开发板上演示项目功能
准备硬件环境
- Air780EHM 开发板一块
- TYPE-C USB 数据线一根
- 可联网的 SIM 卡(用于远程升级场景)
- ttl 小板(可选,用于物理串口升级)
准备软件环境
- 烧录工具:Luatools 下载调试工具
- 内核固件:Air780EHM V2018 版本(或最新版本)
- 脚本文件:对应场景的脚本文件
-
模拟工具:
-
串口升级:Python 环境和 main.py 脚本
- 远程升级:合宙 iot.openluat.com 平台账号
演示流程
-
文件系统升级演示:
-
使用 Luatools 烧录升级包到文件系统
- 烧录 fota_file.lua 相关脚本
- 观察日志输出,验证升级是否成功
-
串口升级演示:
-
烧录 fota_uart.lua 相关脚本
- 按下 Power 键启动升级模式
- 运行 Python 脚本发送升级包
- 观察串口输出和升级进度
-
远程升级演示:
-
在合宙 IoT 平台创建设备和升级任务
- 烧录对应场景的脚本
- 设备自动连接平台并检测升级
- 观察升级进度和结果
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 升级规则说明
- 内核固件:需分情况对待,4G 模组系列比如 Air780EXX 系列、Air724UG、Air8000 系列等仅支持差分升级,wifi 模组 Air8101/Air8101A 是全量升级
- 脚本:支持全量升级,可一次性完成
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

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