跳转至

03 fota核心库+文件升级/串口升级

作者:孟伟 | 最后修改:2026-04-14

一、FOTA 功能概述

FOTA 是固件远程升级的简称,用于设备固件的远程更新和维护。LuatOS 提供了灵活的 FOTA 升级方案,支持多种升级方式以适应不同的应用场景。

1.1 FOTA 有什么用?

FOTA 功能为物联网设备提供安全可靠的固件更新能力,主要优势包括:

  • 远程维护:无需现场操作即可完成设备固件更新
  • 故障修复:快速修复已部署设备的软件缺陷
  • 功能升级:为设备增加新功能,提升产品价值
  • 成本节约:大幅降低设备维护和升级成本
  • 安全保障:支持完整性校验,确保升级过程安全可靠

1.2 FOTA 与 FOTA2 选择指南

核心区别总结

fota(底层核心库)

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

核心能力:

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

支持内部存储和外部 SPI Flash

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

代码特点:

_-- 需要手动控制每个步骤_
fota.init()
while not fota.wait() do sys.wait(100) end
fota.run(buf)  _-- 或 fota.file("/update.bin")-- 自行检查状态和重启_
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)

适用场景推荐

选择 fota 的情况:

需要自定义升级数据源

通过串口接收升级包

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

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

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

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

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

需要自定义进度监控逻辑

资源极度受限环境

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

开发测试阶段

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

需要验证自定义升级方案

选择 fota2 的情况:

标准的 HTTP 远程升级

从服务器下载升级包

使用合宙 IoT 平台服务

需要 HTTPS 安全下载

希望快速实现升级功能

不想处理网络下载细节

需要自动版本检查

希望简单的错误处理

生产环境部署

需要稳定的远程升级方案

需要详细的升级状态反馈

支持定时自动检查更新

实际选择建议

新手用户 → 直接选择 fota2

接口简单,学习成本低

内置完整错误处理

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

高级用户 → 根据需求选择

标准网络升级 → fota2

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

一句话总结:用 fota2 省心省力,用 fota 自由灵活

二、演示功能概述

本教程将使用 Air780EGH 核心板演示两种 FOTA 升级方式,主要包括:

(1)文件系统直接升级:通过 luatools 将升级包文件写入到文件系统中,模组读取的升级包文件直接升级;

(2)分段升级:通过分片段接收升级包数据并升级,demo 演示通过虚拟串口分段写入然后升级。

三、准备硬件环境

硬件清单:

  • Air780EGH 核心板一块
  • TYPE-C USB 数据线一根

硬件连接:

  • Air780EGH 核心板通过 TYPE-C USB 口供电
  • 外部供电/USB 供电拨动开关拨到 USB 供电一端
  • TYPE-C USB 数据线连接开发板和电脑 USB 口

四、软件环境

在开始实践本示例之前,先筹备一下软件环境:

  1. 烧录工具: Luatools 工具
  2. 内核固件:本demo开发测试时使用的固件为Air780EGH V2016 版本固件,本demo对固件版本没有什么特殊要求,所以你如果要测试本demo时,可以直接使用最新版本的内核固件;如果发现最新版本的内核固件测试有问题,可以使用我们开发本demo时使用的内核固件版本来对比测试;
  3. Python 3 环境:用于运行串口升级的 Python 脚本
  4. LuatOS 需要的脚本和资源文件

脚本和资源文件: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)

准备好软件环境之后,接下来查看如何烧录项目文件到 Air780EGH 核心板,将本篇文章中演示使用的项目文件烧录到 Air780EGH 核心板中。

五、API 接口说明

详细 FOTA API 文档请参考:https://docs.openluat.com/osapi/core/fota/

六、代码示例介绍

6.1 升级包制作

LuatOS 开发模式下,固件分为两部分:core(底层固件) 和 script(脚本)

远程升级时:可以仅升级 script;也可以同时升级 core+script

仅 script 脚本升级时对于 Air780EXX 系列、Air8000 系列、Air8101 系列是全量升级。

core+script 都升级时对于 Air780EXX 系列、Air8000 系列是差分升级,对于 Air8101 系列是全量升级

Air780EXX 系列、Air8000 系列仅 script 升级时升级包制作:

如果用户只是新增一些自己的脚本逻辑,没有更新底层,可以选择仅脚本升级

更新完自己脚本后,修改版本号,点击生成量产文件,生成的量产文件中以.bin 结尾的就是仅脚本升级的升级包。

Air780EXX 系列、Air8000 系列 core+script 都升级时升级包制作:

每一次 core 的升级都会带来一些网络上的优化(例如信号差时的网络稳定性)以及一些 bug 修复,所以在发布新版本以后,用户可以先测试下 core 对自己脚本有无明显影响或性能提升,然后进行远程 FOTA。

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

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

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

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

Air8101 系列升级包制作:

因为此系列都是全量升级,所以在 luatools 中生成一次量产文件就行,生成的量产包如下:

其中 full_fota 文件夹下为 core+script 的全量升级包,script_ota 目录下是单脚本升级的升级包

6.2 文件系统直接升级

6.2.1 核心代码实现 (fota_file.lua)

local function fileUpgradeTask()
    -- 等待系统稳定后再开始升级
    sys.wait(10000)

    log.info("FOTA_FILE", "=== 开始文件系统升级 ===")

    -- 步骤1: 初始化FOTA流程
    log.info("FOTA_FILE", "初始化FOTA...")
    if not fota.init() then
        log.error("FOTA_FILE", "FOTA初始化失败")
        return
    end

    -- 步骤2: 等待底层准备就绪
    log.info("FOTA_FILE", "等待底层准备...")
    while not fota.wait() do
        sys.wait(100)
    end
    log.info("FOTA_FILE", "底层准备就绪")

    -- 步骤3: 从文件系统读取升级包并启动升级
    local filePath = "/update.bin"
    log.info("FOTA_FILE", "开始读取升级文件:", filePath)
    local result, isDone, cache = fota.file(filePath)
    log.info("FOTA_FILE", "升级文件写入flash中的fota分区结果", result, isDone, cache)

    -- 步骤4: 结束写入fota分区
    log.info("FOTA_FILE", "结束写入fota分区...")
    local result, isDone = fota.isDone()
    log.info("FOTA_FILE", "写入fota分区状态", "结果:", result, "完成:", isDone)
    if result then
        -- 步骤5: 处理写入结果
        if isDone then
            -- 升级文件成功写入flash中的fota分区,准备重启设备;
            -- 设备重启后,在初始化阶段的运行过程中会自动应用fota分区中的数据完成升级,最终升级结果可以通过观察日志中的版本号来区分。
            log.info("FOTA_FILE", "升级成功,准备重启设备")
            -- 调用fota.finish(true)结束升级流程,参数true表示正确走完流程。
            fota.finish(true)
            -- 可选:删除升级包文件
            -- os.remove("/update.bin")
            sys.wait(2000)
            rtos.reboot()
        else
            log.error("FOTA_FILE", "升级失败")
            -- -- 调用fota.finish(false)结束升级流程,参数false表示升级流程失败。
            fota.finish(false)
        end
    else
        log.error("FOTA_FILE", "升级失败:检查写入状态失败")
        -- -- 调用fota.finish(false)结束升级流程,参数false表示升级流程失败。
        fota.finish(false)
    end
end

-- 启动文件升级任务
sys.taskInit(fileUpgradeTask)

6.2.2 文件系统升级操作步骤

  1. 修改配置:在 main.lua 中取消 require("fota_file") 的注释,注释掉 require("fota_uart")
  2. 制作升级包:按照 6.1 章节流程来制作升级包
  3. 烧录文件:

  4. 使用 Luatools 烧录内核固件和脚本代码

  5. 通过"烧录文件系统"功能将升级包文件烧录到设备中
  6. 自动升级:设备启动后会自动检测并执行升级流程

注意:"烧录文件系统"功能使用:

将制作好的升级包修改名字为 update.bin,然后放到一个空文件夹中,在 luatools 的"烧录文件系统"功能栏中选中这个文件夹,点击下载即可;烧录成功后,update.bin 会在文件系统根目录下,既路径为"/update.bin"

6.2.3 运行日志

6.3 串口分段升级

6.3.1 核心代码实现 (fota_uart.lua)

-- 定义所需要的UART编号
-- uart_id = 1    -- UART1, 通常也是MAIN_UART
local uart_id = uart.VUART_0 -- 虚拟USB串口

-- 全局变量
local uart_zbuff = nil
local uart_fota_state = 0
local uart_rx_counter = 0
local uart_fota_writed = 0
local upgrade_active = false  -- 升级是否激活标志

-- 按键回调函数 - Power键
local function power_key_callback()
    if not upgrade_active then
        log.info("FOTA_UART", "Power键按下,启动串口升级模式")
        -- 初始化串口和缓冲区
        uart_zbuff = zbuff.create(1024)
        uart.setup(uart_id, 115200)
        uart.on(uart_id, "receive", uart_cbfun)
        upgrade_active = true
        uart_fota_state = 0
        uart_rx_counter = 0
        uart_fota_writed = 0
        -- 发布事件,唤醒升级任务
        sys.publish("UART_UPGRADE_START")
    else
        log.info("FOTA_UART", "升级模式已激活,请等待当前升级完成")
    end
end

-- 配置Power键
gpio.setup(gpio.PWR_KEY, power_key_callback, gpio.PULLUP, gpio.FALLING)
gpio.debounce(gpio.PWR_KEY, 200, 1)  -- 200ms去抖

-- 清理资源的函数
local function cleanup_resources()
    if uart_zbuff then
        uart_zbuff:del()
        uart_zbuff = nil
    end
    uart.close(uart_id)  -- 关闭串口
    upgrade_active = false
    uart_fota_state = 0
    log.info("FOTA_UART", "资源已清理,串口已关闭")
end

-- 串口接收回调函数
function uart_cbfun(id, len)
    if not upgrade_active or not uart_zbuff then
        return
    end

    -- 防御缓冲区超标的情况
    if uart_zbuff:used() > 8192 then
        log.warn("fota", "uart_zbuff待处理的数据太多了,强制清空")
        uart_zbuff:del()
    end

    while true do
        local len = uart.rx(id, uart_zbuff)
        if len <= 0 then
            break
        end
        uart_rx_counter = uart_rx_counter + len
        log.info("uart", "收到数据", len, "累计", uart_rx_counter)
        -- 首次收到数据即发布事件,唤醒升级任务
        if uart_fota_state == 0 then
            sys.publish("UART_FOTA")
        end
    end
end

-- 串口升级任务
local function uartUpgradeTask()
    local fota_state = 0 -- 0还没开始, 1进行中

    while true do
        -- 等待升级启动信号
        sys.waitUntil("UART_UPGRADE_START")
        log.info("FOTA_UART", "升级任务已启动,等待数据...")

        while upgrade_active do
            -- 等待升级数据到来
            sys.waitUntil("UART_FOTA", 1000)
            if not upgrade_active then break end

            local used = uart_zbuff and uart_zbuff:used() or 0
            if used > 0 then
                if fota_state == 0 then
                    -- 等待FOTA的状态
                    if used > 5 then
                        local data = uart_zbuff:query()
                        uart_zbuff:del()
                        -- 如果接受到 #FOTA\n 代表数据要来了
                        if data:startsWith("#FOTA") and data:endsWith("\n") then
                            fota_state = 1
                            log.info("fota", "检测到fota起始标记,进入FOTA状态", data)
                            if fota.init() then
                                -- 固件数据发送端应该在收到#FOTA RDY\n之后才开始发送数据
                                uart.write(uart_id, "#FOTA RDY\n")
                            else
                                log.error("FOTA_UART", "FOTA初始化失败")
                                cleanup_resources()
                                break
                            end
                        end
                    end
                else
                    -- 已进入升级状态:把收到的数据喂给fota.run
                    uart_fota_writed = uart_fota_writed + used
                    log.info("准备写入fota包", used, "累计写入", uart_fota_writed)
                    local result, isDone, cache = fota.run(uart_zbuff)
                    log.debug("fota.run", result, isDone, cache)
                    uart_zbuff:del() -- 清空缓冲区

                    if not result then
                        -- 写入失败,退出升级状态并通知上位机
                        log.error("fota", "出错了", result, isDone, cache)
                        uart.write(uart_id, "#FOTA ERR\n")
                        -- 调用fota.finish(false)结束升级流程,参数false表示升级流程失败。
                        fota.finish(false)
                        cleanup_resources()
                        fota_state = 0
                        break
                    elseif isDone then
                        -- 全部数据写入完成,等待底层校验结束
                        local success = false
                        for i = 1, 30 do  -- 最多等待3秒
                            sys.wait(100)
                            local succ, fotaDone = fota.isDone()
                            if not succ then
                                log.error("fota", "校验过程出错")
                                uart.write(uart_id, "#FOTA ERR\n")
                                fota.finish(false)
                                cleanup_resources()
                                fota_state = 0
                                break
                            end
                            if fotaDone then
                                uart_fota_state = 1
                                -- 升级文件成功写入flash中的fota分区,准备重启设备;
                                log.info("fota", "已完成,1s后重启")
                                -- 调用fota.finish(true)结束升级流程,参数true表示正确走完流程。
                                fota.finish(true)
                                -- 反馈给上位机
                                uart.write(uart_id, "#FOTA OK\n")
                                sys.wait(1000)
                                success = true
                                rtos.reboot()
                                break
                            end
                        end
                        if not success then
                            log.error("fota", "校验超时")
                            uart.write(uart_id, "#FOTA ERR\n")
                            fota.finish(false)
                            cleanup_resources()
                            fota_state = 0
                        end
                        break
                    else
                        -- 单包写入成功,通知上位机继续下发
                        log.info("fota", "单包写入完成", used, "等待下一个包")
                        uart.write(uart_id, "#FOTA NEXT\n")
                    end
                end
            end
        end

        -- 重置状态,等待下次升级
        fota_state = 0
        uart_fota_state = 0
        uart_rx_counter = 0
        uart_fota_writed = 0
    end
end

-- 启动串口升级任务
sys.taskInit(uartUpgradeTask)

6.3.2 串口升级操作步骤

1、搭建好演示硬件环境

2、修改 demo 脚本代码,确保 main.lua 中已注释 require("fota_file"),取消 require("fota_uart") 的注释

3、使用 Luatools 制作升级包,先把新旧版本分别生成量产文件,然后再制作升级包,工具上栏 luatOS-> 固件工具-> 差分包/整包升级包制作,将制作好的升级包放在 main.py 同级目录下

4、Luatools 烧录内核固件和修改前的 demo 脚本代码,烧录成功后,自动开机运行。

5、确认设备连接到电脑的串口(虚拟 USB 串口)

6、按一下核心板上的 Powerkey 键,然后运行 Python 脚本发送升级包:

python main.py

7、脚本会自动寻找设备虚拟串口,发送升级命令并传输 fota_uart.bin 文件,注意自己生成的升级包要改成这个名字。

8、设备接收并验证升级包,升级成功后会自动重启

9、可以看到如下日志:

注意:如果是使用虚拟串口直接使用 python main.py 来运行程序即可,如果是使用物理串口,就指定串口运行:

# 指定串口
python fota_script.py -p COM1

6.3.3 串口通讯过程说明

串口升级采用简单的文本协议进行握手和数据传输控制:

协议流程:

  1. 上位机发送:#FOTA\n
  2. 设备回复:#FOTA RDY\n
  3. 上位机发送:256 字节数据包
  4. 设备回复:#FOTA NEXT\n(请求下一包)
  5. 重复步骤 3-4 直到所有数据发送完成
  6. 设备回复:#FOTA OK\n(升级成功)
  7. 设备自动重启

6.3.4 运行日志

串口分段升级日志解读

  1. USB 虚拟串口连接,收到#FOTA 起始指令
  2. 开始分段接收升级包,每次 256 字节,累计 5751 字节
  3. 所有数据包写入成功,MD5 校验通过
  4. 升级完成,重启
  5. 重启后新版本 1.0.2 运行,新增日志确认升级成功

结果:串口 FOTA 升级完全成功,版本从 1.0.0 升级到 1.0.2。

Python 脚本输出:

D:\gitee_hz\LuatOS\module\Air780EHM_Air780EHV_Air780EGH\demo\fota\fota(使用fota核心库)>python main.py
使用串口: COM59
设备响应 b'#FOTA RDY\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 256
设备响应 b'#FOTA NEXT\n'
发送升级包数据 26
设备响应 b'#FOTA OK\n'
发送完毕,退出

七、总结

至此,本教程详细介绍了 LuatOS 的两种 FOTA 升级方式。通过文件系统直接升级和串口分段升级,可以满足不同场景下的固件更新需求。

FOTA 升级关键特性:

  • 支持完整的升级流程:初始化、数据传输、校验、重启
  • 提供多种升级方式适应不同应用场景
  • 具备完整性校验机制,确保升级安全可靠
  • 支持升级状态查询和错误处理