跳转至

LuatOS运行框架

一、LuatOS 运行框架概述

1.1 LuatOS 简介

LuatOS 是合宙历时十余年开发并不断完善的嵌入式物联网开发操作系统,将蜂窝通信模组和 MCU 的共性高度抽象为统一接口,秉承与硬件无关、与操作系统种类无关的软件设计理念,用户可以轻松实现“一次编程、跨平台通用”的效果。

LuatOS 从 2014 年推出至今,一直深受广大用户好评,在多行业多应用领域得到充分验证,功能非常齐全,性能非常稳定。搭载 LuatOS 的合宙硬件模组已经出货数亿只,涵盖 2G、4G、MCU、Wi-Fi 类产品,稳定可靠。

1.2 LuatOS 软件包介绍

LuatOS 软件由两部分组成:内核固件和应用脚本。

  • 内核固件

固件文件均以 .soc 为后缀

点击此处可以获取 Air780E 内核固件,如图:

Air780E 的内核固件 core_V1112 下载 ,有三种固件:

1.数传版( LuatOS-SoC_V1112_EC618.soc),不含 UI 类(U8G2/LCD/EINK/DISP/LVGL)/TTS,仅包含少量外设驱动库

2.TTS 版(LuatOS-SoC_V1112_EC618_TTS.soc), 在数传版上添加 TTS 支持, 但 TTS 资源需要外置在额外的 SPI Flash, 且不支持 MP3/AMR 播放

3.全功能版(LuatOS-SoC_V1112_EC618_FULL.soc), 会开启大部分库, 但需要特别指出的是,不包含 TTS 库的内置 TTS 资源模式

我们一般使用全功能版,如图:

  • 应用脚本 demo

点击此处可以获取应用脚本 demo,下载流程可以参考下图:

下载解压后文件里包含各种功能的脚本例程,可以参考例程进行开发,如图:

1.3 LuatOS 软件架构

LuatOS 软件架构主要包括内核固件(core)和上层软件(自己项目的业务代码 application script)两大部分。内核固件是 LuatOS 的核心,负责处理硬件级别的任务,如驱动、中断等。而上层软件(application script),用于实现具体的物联网应用功能。

图中分成四个部分:分别是平台层,接口层,用户层还有箭头:

平台层:代表内核固件,是 LuatOS 系统的核心部分,它负责系统的基本功能和硬件抽象层的实现。内核固件可以通过官方的内核固件下载地址进行下载,或者通过定制固件系统定制自己需要的固件。

用户层:代表上层软件,包括 demo 示例以及用户自己的业务代码(application script)。

接口层:代表 API 接口,是连接内核固件和上层软件的桥梁,底层 core API 接口提供了对底层硬件的直接访问和控制功能,如 GPIO、I2C、SPI 等。这些接口通常用于实现底层硬件的初始化和配置。

图中的箭头:代表消息处理机制,用于处理内部和外部的消息。

1.4 LuatOS 软件基本运行逻辑

LuatOS 是合宙的一款基于 SoC 和 Lua 脚本的嵌入式操作系统,其软件基本运行逻辑可以归纳为以下几点:

1. 系统启动与初始化

系统启动:当设备加电或复位后,LuatOS 系统会开始启动。在启动过程中,系统会进行一系列的初始化操作,包括硬件初始化、内存分配、文件系统挂载等。

加载 Lua 虚拟机:作为基于 Lua 脚本的操作系统,LuatOS 在启动时会加载 Lua 虚拟机,以便后续执行 Lua 脚本。

2. 任务调度与执行

多任务机制:LuatOS 实现了一套多任务机制,允许开发者在系统中创建多个并行运行的任务。这些任务通过 Lua 语言的协程机制来实现,使得每个任务都可以在自己的执行上下文中运行,而不会相互干扰。

任务创建与启动:开发者可以通过调用 sys 模块中的 taskInit 函数来创建任务。在创建任务时,需要指定一个任务函数,该函数包含了任务要执行的代码。任务创建后,需要在代码的最后一行调用 sys.run()函数来启动主程序,使得框架内的任务代码会在 sys.run()中运行。

任务调度:LuatOS 的任务调度器会根据任务的优先级和运行状态来调度任务的执行。当某个任务处于可执行状态时,调度器会将其放入运行队列中,并等待 CPU 资源来执行它。同时,调度器还会处理任务的等待、延时、同步等操作。

3. 网络通信与数据处理

网络协议栈:LuatOS 内置了丰富的网络协议栈,使得设备能够轻松接入互联网。开发者可以利用这些协议栈来实现设备的网络通信功能,如 TCP/IP、UDP、HTTP 等。

数据处理:设备在接收到网络数据后,LuatOS 会将其传递给相应的任务进行处理。任务可以根据数据的类型和内容来执行相应的操作,如解析数据、存储数据、发送响应等。

4. 日志输出与调试

日志输出:LuatOS 提供了日志输出功能,允许开发者在代码中输出不同等级的日志信息。这些日志信息可以帮助开发者了解系统的运行状态和调试问题。

调试工具:LuatOS 还提供了调试工具,如 Luatools 等,这些工具可以帮助开发者进行代码调试、固件烧录、日志查看等操作。

5. 资源管理与优化

内存管理:LuatOS 提供了内存管理功能,包括内存的分配、释放、垃圾回收等。这些功能可以帮助开发者有效地管理设备的内存资源,避免内存泄漏和溢出等问题。

低功耗优化:为了满足物联网设备对低功耗的需求,LuatOS 还提供了一系列的低功耗优化措施。这些措施包括休眠模式、定时唤醒、电源管理等,可以帮助设备在不影响性能的前提下降低功耗。

1.5 LuatOS 核心功能概述

1.5.1 task

task 分为两种:

1. 内核固件中的 task(真 task)

定义:在内核固件中,task 通常指的是由操作系统调度和管理的执行单元。它是操作系统进行并发处理和资源分配的基本单位。

实现:内核固件中的 task 通常由操作系统内核提供支持和管理,包括任务的创建、调度、执行、同步和通信等。

特性:内核固件中的 task 具有独立的执行上下文,包括程序计数器、寄存器、堆栈等。它们可以并发执行,以提高系统的吞吐量和响应速度。

示例:在 FreeRTOS 等实时操作系统中,task(比如:core 中的 Lua Virtual machine)是系统的基本调度单位。每个 task 都有自己的优先级、堆栈大小和任务函数等属性。操作系统根据任务的优先级和状态来调度任务的执行。

2.Lua 脚本中的 task(假 task)

定义:在 Lua 脚本中,task 通常指的是由 Lua 虚拟机调度和管理的协程(coroutine)。虽然 Lua 脚本中的 task 也使用了“task”这个词,但它与内核固件中的 task 在概念和实现上存在显著差异。

实现:Lua 脚本中的 task 是通过 Lua 语言的协程机制来实现的。协程是一种比线程更轻量级的并发执行单元,它允许在多个执行路径之间切换,而不会引发线程切换的开销。

特性:Lua 脚本中的 task 具有非抢占式的调度特性,即一个协程在执行过程中不会被其他协程抢占。它们通过显式调用 yield 函数来让出控制权,以便其他协程可以执行。此外,Lua 脚本中的 task 还支持通过消息机制进行同步和通信。

示例:在 LuatOS 系统中,开发者可以通过调用 sys 模块中的 taskInit 函数来创建任务(实际上是协程)。这些任务可以在自己的执行上下文中运行 Lua 代码,并通过消息机制与其他任务进行通信和同步。

1.5.2 消息队列

在 LuatOS 系统中,有“外部”消息队列和“内部”消息队列两种消息队列。

1.“外部”消息队列

内核固件中的 task 使用的消息队列,core 中各 task 间数据通信的一种手段,是由 RTOS 内核提供的服务,具有高效、实时和可靠的特点,适用于需要实时响应的物联网应用。

2.“内部”消息队列

Lua 脚本中的 task 使用的消息队列,在 Lua 脚本中合宙自定义的一套内部消息驱动机制,具有灵活性和易用性的特点,适用于 Lua 脚本内部的数据传递和同步。

1.5.3 消息驱动管理

1.内部消息处理

LuatOS 的内部消息处理是指在 Lua 脚本内部,通过 “内部” 消息队列和相应的处理函数来管理和处理各种内部事件或消息。通过以下方式来处理:

内部消息首先通过生产者 sys.publish() 发布,放在 “内部消息队列” 里面。

通过 sys.subscribe()sys.waitUntil() 调用消费者,通过消息处理函数表,把 “内部” 消息队列的消息和消费者的回调函数或协程 ID 绑定。

sys.run() 启动消息处理循环,遍历“内部” 消息队列的消息,然后在消息处理表找到对应的函数来执行。

2.外部消息处理

LuatOS 使用外部消息队列来存储和管理从外部源接收到的消息。这些消息可能来自硬件接口(如传感器、按键等),也可能来自网络通信(如 MQTT、HTTP 等)。

当外部消息到达时,它们会被放入 “外部” 消息队列中。Lua 脚本中的主循环或特定模块会定期检查这个队列,并根据消息类型和数据执行相应的处理逻辑。

与内部消息处理类似,外部消息处理也使用 sys.run() 来启动消息处理循环。但是,处理外部消息时可能需要使用特定的 API 或库来读取和处理这些消息,如 net 库用于网络通信、pio 库用于硬件接口等。

以下是两个例子:

lua 版本定时器消息处理:

在协程调用 sys.timerStart(),结果会在 core 中的定时器 task 创建并启动定时器,等 core 中的定时器时间到,会把定时器消息放到 “外部” 消息队列,sys.run() 启动消息处理循环,从而调用相关函数。

lua 版本音频播放消息处理:

在协程调用 audio.play (),结果会在 core 中的音频控制 task 播放音频,音频播放结束,会把播放结束消息放到 “外部” 消息队列,sys.run() 启动消息处理循环,从而调用相关函数。

1.5.4 Lua 脚本中写的 task 是伪并发应用

在 Lua 脚本中,默认情况下,Lua 是单线程的,这意味着它不具备真正的并发执行能力。然而,可以通过一些技巧和库来实现伪并发(或者说模拟并发)的效果。

Lua 支持协程,协程允许在多个任务之间切换执行,从而模拟并发。虽然协程不是真正的并行执行,但它们可以在单个线程中提供类似并发的行为。

core 在启动过程中,会创建一系列的 task,例如音频、协议栈、串口等。

其中有一个 task,用来加载 Lua 虚拟机,解析运行用户编写的 Lua 脚本,这个 task,我们称之为 Lua virtual task(另外在 Lua 脚本中,利用 Lua 的协程特性封装了一个 sys.taskInit 接口,借用了 task 的概念,实际上并不是真正的 task)

脚本中的所有应用,都运行在 Lua virtual machine task 中,属于单 task 应用。

1.5.5 Lua 脚本运行流程

在 LuatOS 环境中,从 core 运行到 Lua 虚拟机运行,再到 main.lua 脚本的一行一行嵌套执行,直到最终回归到 sys.run()的过程,是一个复杂但有序的系统启动和任务调度过程。以下是对这一过程的详细解析:

1. 系统启动与 core 运行

系统初始化:设备上电后,LuatOS 系统开始初始化。这个过程中,系统会加载内核固件(core),并初始化必要的硬件接口、内存管理、任务调度器等核心组件。

创建任务:在 core 的启动过程中,系统会创建一个特殊的任务——Lua virtual task。这个任务负责加载和运行 Lua 虚拟机,以及解析和执行用户编写的 Lua 脚本。

2.Lua 虚拟机运行

加载 Lua 虚拟机:Lua virtual task 在创建后,会加载 Lua 虚拟机。这个虚拟机是 Lua 脚本的执行环境,它提供了对 Lua 语言的支持,包括变量的声明、函数的调用、控制结构的执行等。

准备执行环境:在加载 Lua 虚拟机后,系统会准备执行环境,包括初始化全局变量、加载系统库等。这些操作确保了 Lua 脚本在执行时能够访问到必要的资源和函数。

3.main.lua 脚本执行

定位 main.lua:Lua 虚拟机在准备好执行环境后,会开始寻找并执行 main.lua 脚本。这个脚本是用户编写的 Lua 程序的主入口,它包含了程序的主要逻辑和函数调用。

逐行解析执行:Lua 虚拟机在找到 main.lua 后,会逐行解析并执行脚本中的代码。这个过程中,脚本中的函数调用、变量赋值、控制结构等都会按照 Lua 语言的语法规则被解析和执行。

调用系统库和 API:在 main.lua 的执行过程中,脚本可能会调用系统库中的函数或 API 来实现特定的功能。例如,调用网络库函数来发送 HTTP 请求,或调用硬件接口库函数来控制 GPIO 引脚的状态。

4.sys.run()的调用与任务调度

执行到 sys.run():在 main.lua 脚本的末尾,会调用 sys.run()函数。这个函数是 LuatOS 任务调度器的入口点,它标志着用户编写的 Lua 脚本的执行结束,并将控制权交还给系统。

任务调度:在 sys.run()被调用后,LuatOS 的任务调度器开始工作。它会根据任务的优先级和状态,决定何时运行哪个任务。这个过程中,任务调度器会不断地在任务之间切换,确保每个任务都有机会运行。

事件处理与消息队列:在任务执行过程中,系统可能会接收到各种事件(如网络数据到达、按键按下等)。这些事件会被放入消息队列中,并由任务调度器在适当的时候分发给相应的任务进行处理。

二、演示功能概述

LuatOS 核心功能包括 task 管理,消息驱动管理和回调机制,脚本运行的基本逻辑。

本篇文章将逐一介绍这些内容。

三、准备硬件环境

3.1 Air780E 核心板

使用 Air780E 核心板,如下图所示:

淘宝购买链接:Air780E 核心板淘宝购买链接

此核心板的详细使用说明参考:Air780E 产品手册 中的 《开发板Core_Air780E使用说明VX.X.X.pdf》,写这篇文章时最新版本的使用说明为:《开发板Core_Air780E使用说明V1.0.5.pdf》;核心板使用过程中遇到任何问题,可以直接参考这份使用说明 pdf 文档。

3.2 SIM 卡

中国大陆环境下,可以上网的 sim 卡,一般来说,使用移动,电信,联通的物联网卡或者手机卡都行。

3.3 PC 电脑

WIN10以及以上版本的WINDOWS系统。

3.4 数据通信线

带Type-C口的USB数据线。

四、准备软件环境

4.1 Luatools 工具

要想烧录 LuatOS 固件到 4G 模组中,需要用到合宙的强大的调试工具:Luatools

详细使用说明参考:Luatools 工具使用说明

Luatools 工具集具备以下几大核心功能:

  • 一键获取最新固件:自动连接合宙服务器,轻松下载最新的合宙模组固件。
  • 固件与脚本烧录:便捷地将固件及脚本文件烧录至目标模组中。
  • 串口日志管理:实时查看模组通过串口输出的日志信息,并支持保存功能。
  • 串口调试助手:提供简洁的串口调试界面,满足基本的串口通信测试需求。

Luatools 下载之后,新建一个命名为 "Luatools" 的文件夹,将下载的Luatools_v3.exe拷贝或移动到新建的Luatools文件夹内,点击 Luatools_v3.exe 即可运行。

4.2 准备需要烧录的代码

首先要说明一点: 脚本代码, 要和固件的 soc 文件一起烧录。

4.2.1 烧录的内核固件文件

底层 core 下载地址:LuatOS 固件版本下载地址

Air780E 的内核固件在 Luatools 解压后目录的 LuatOS-SoC_V1112_EC618_FULL.soc

4.2.2 烧录的脚本代码

首先要下载 Air780 的 LuatOS 示例代码到一个合适的项目目录,示例代码网站: https://gitee.com/openLuat/LuatOS-Air780E

下载流程参考下图:

下载的文件解压,找到 LuatOS-Air780E-master\demo\hmeta\main.lua,如图:

五、LuatOS 运行框架软硬件资料

六、task 概念

  • 内核固件中的 task

core 在启动过程中,会创建一系列的 task,例如音频、协议栈、串口等,这些 task 都是内核固件中的 task,例如以下代码,系统启动中,会初始化串口,从而创建了一个串口的 task:

local uartid = 1 -- 根据实际设备选取不同的uartid

--串口初始化,core在启动过程中会调用,并创建一个串口task
uart.setup(
    uartid,--串口id
    115200,--波特率
    8,--数据位
    1--停止位
)

另外,Lua virtual task 也是内核中的 task,Lua 写的所有脚本的运业务逻辑全在这个 task 中运行,从内核操作系统的视角来看,是单 task 的伪并发应用。

  • lua 脚本中的 task

调用 sys 模块中的 taskInit 函数来创建的 task,属于 Lua 脚本中的 task。

--创建协程1
sys.taskInit(function()
        while true do
         log.info("lua脚本中的task1")
         sys.wait(2000) --延时2秒,这段时间里可以运行其他代码
        end
end)
--创建协程2
sys.taskInit(function()
        while true do
         log.info("lua脚本中的task2")
         sys.wait(2000) --延时2秒,这段时间里可以运行其他代码
        end
end)

两个协程之间切换执行,从而模拟并发,并通过消息机制与其他任务进行通信和同步,从而实现 “假 task” 的功能。

七、消息和消息队列的概念

LuatOS 消息是指在 LuatOS 操作系统中,不同任务(Task)或模块之间传递的数据单元。消息可以包含各种类型的数据,如数字、字符串、结构体等,用于在任务或模块之间传递信息。

LuatOS 消息队列是一种数据结构,用于存储和管理在 LuatOS 中传递的消息。消息队列按照先进先出的原则工作,即先发送的消息先被处理。

在 LuatOS 系统中,有两种消息队列:

  • 内核固件中的 task 使用的消息队列

“外部”消息队列:core 中各 task 间数据通信的一种手段。

以下函数都运用了内核固件中的消息队列:

--循环发数据,调用此函数会在core里面创建运行定时器,定时器时间到会把“定时器消息”,放到外部消息队列里
--sys.run()启动消息处理循环,从外部消息队列取到定时器消息后运行uart.write函数
sys.timerLoopStart(uart.write,1000, uartid, "test")
  • lua 脚本中的 task 使用的消息队列

“内部”消息队列:在 Lua 脚本中合宙自定义的一套内部消息驱动机制。

以下是 Lua 脚本中的 task 使用的消息队列的应用理解:

sys = require("sys")
--第一个任务
sys.taskInit(function()
    while true do
        log.info("task1","wow")
        sys.publish("TASK1_DONE")--发布这个消息,并把消息放到内部消息队列
    end
end)

--第二个任务
sys.taskInit(function()
    while true do
        sys.waitUntil("TASK1_DONE")--等待内部消息
        log.info("task2","wow")
    end
end)

--单独订阅内部消息
sys.subscribe("TASK1_DONE",function()
    log.info("subscribe","wow")
end)

sys.run()

八、回调的概念

基于以下两种场景介绍:

1、内部消息驱动管理

发布和订阅以及消息回调处理,结合以下代码片段讲解:

sys = require("sys")
--第一个任务
sys.taskInit(function()
    while true do
        log.info("task1","wow")
        --生产者:发布这个消息,并把消息"TASK1_DONE"放到内部消息队列
        sys.publish("TASK1_DONE")
        --延时1秒
        sys.wait(1000)
    end
end)

--第二个任务
sys.taskInit(function()
    while true do
        --消费者:在消息处理函数表中把协程ID和"TASK1_DONE"绑定
        --此处阻塞等待TASK1_DONE消息,只有收到这个消息时,才会退出阻塞等待状态
        sys.waitUntil("TASK1_DONE")
        log.info("task2","wow")
    end
end)

--消费者:在消息处理函数表中把回调函数和"TASK1_DONE"绑定
--当收到TASK1_DONE消息时,会执行function() [log.info](http://log.info/)("subscribe","wow")end 函数
sys.subscribe("TASK1_DONE",function()
    log.info("subscribe","wow")
end)
--sys.run()用来内部消息处理循环,遍历“内部” 消息队列的消息,然后在消息处理表找到对应的函数来执行。
sys.run()

2、外部消息驱动管理

例如 sys.timer....的回调处理,uart.on,mqtt.on 的回调处理,都是外部消息的应用,结合以下代码片段讲 uart.on 的回调处理:

sys = require("sys")

local uartid = 1

--core启动初始化,会创建一个串口task
uart.setup(
    uartid,--串口id
    115200,--波特率
    8,--数据位
    1--停止位
)
--注册串口接收回调函数,
--当硬件串口收到数据,会把消息"receive"放到外部消息队列
uart.on(uartid, "receive", function(id, len)
    local s = ""
    repeat
        s = uart.read(id, 128)
        if #s > 0 then
            log.info("uart", "receive", id, #s, s)
        end
    until s == ""
end)
--sys.run()启动外部消息处理循环,取到外部消息队列中的"receive"消息,从而调用串口接收回调函数。
sys.run()

九、完整示例功能讲解

9.1 sys 库接口文档

9.1.1 sys.wait(ms)

Task 任务延时函数,只能用于任务函数中

参数

传入值类型
释义
number
ms 整数,最大等待 126322567 毫秒

返回值

定时结束返回 nil,被其他线程唤起返回调用线程传入的参数

例子

sys.wait(30)--等待30ms

9.1.2 sys.waitUntil(id, ms)

Task 任务的条件等待函数(包括事件消息和定时器消息等条件),只能用于任务函数中。

参数

传入值类型
释义
param
id 消息 ID
number
ms 等待超时时间,单位 ms,最大等待 126322567 毫秒

返回值

result 接收到消息返回 true,超时返回 false

data 接收到消息返回消息参数

例子

local result, data = sys.waitUntil("SIM_IND", 120000)

9.1.3 sys.waitUntilExt(id, ms)

Task 任务的条件等待函数扩展(包括事件消息和定时器消息等条件),只能用于任务函数中。

参数

传入值类型
释义
param
id 消息 ID
number
ms 等待超时时间,单位 ms,最大等待 126322567 毫秒

返回值

message 接收到消息返回 message,超时返回 false

data 接收到消息返回消息参数

例子

local result, data = sys.waitUntilExt("SIM_IND", 120000)

9.1.4 sys.taskInit(fun, …)

创建一个任务线程,在模块最末行调用该函数并注册模块中的任务函数,main.lua 导入该模块即可

参数

传入值类型
释义
param
fun 任务函数名,用于 resume 唤醒时调用
param
… 任务函数 fun 的可变参数

返回值

返回该任务的线程号

例子

local id = sys.taskInit(task1,'a','b')
sys.taskInit(task2,'a','b')

9.1.5 sys.timerStop(val, …)

关闭定时器

参数

传入值类型
释义
param
val 值为 number 时,识别为定时器 ID,值为回调函数时,需要传参数
param
… val 值为函数时,函数的可变参数

返回值

例子

--每秒执行,永久循环,返回定时器编号
local loopId = sys.timerLoopStart(log.info,1000,"1s loop timer")
--10秒后手动停止上面的无限循环定时器
sys.timerStart(function()
    sys.timerStop(loopId)
    log.info("stop 1s loop timer")
end,10000)

9.1.6 sys.timerStopAll(fnc)

关闭同一回调函数的所有定时器

参数

传入值类型
释义
param
fnc 定时器回调函数

返回值

例子

sys.timerStopAll(cbFnc)

9.1.7 sys.timerStart(fnc, ms, …)

开启一个定时器

参数

传入值类型
释义
param
fnc 定时器回调函数
number
ms 整数,最大定时 126322567 毫秒
param
… 可变参数 fnc 的参数

返回值

number 定时器 ID,如果失败,返回 nil

例子

--开启一个10秒的定时器,10s后运行并结束
sys.timerStart(function()
    log.info("stop 1s loop timer")
end,10000)

9.1.8 sys.timerLoopStart(fnc, ms, …)

开启一个循环定时器

参数

传入值类型
释义
param
fnc 定时器回调函数
number
ms 整数,最大定时 126322567 毫秒
param
… 可变参数 fnc 的参数

返回值

number 定时器 ID,如果失败,返回 nil

例子

--每秒执行,永久循环,返回定时器编号
local loopId = sys.timerLoopStart(log.info,1000,"1s loop timer")

9.1.9 sys.timerIsActive(val, …)

判断某个定时器是否处于开启状态

参数

传入值类型
释义
param
val 有两种形式一种是开启定时器时返回的定时器 id,此形式时不需要再传入可变参数…就能唯一标记一个定时器另一种是开启定时器时的回调函数,此形式时必须再传入可变参数…才能唯一标记一个定时器
param
… 可变参数

返回值

number 开启状态返回 true,否则 nil

例子

--每秒执行,永久循环,返回定时器编号
local loopId = sys.timerLoopStart(log.info,1000,"1s loop timer")
local number = sys.timerIsActive(loopId)

9.1.10 sys.subscribe(id, callback)

订阅消息

参数

传入值类型
释义
param
id 消息 id
param
callback 消息回调处理

返回值

例子

sys.taskInit(function()
    while true do
        sys.publish("TASK1_DONE")--发布这个消息,此时所有在等的都会收到这条消息
        sys.wait(1000)
    end
end)
--单独订阅,可以当回调来用
sys.subscribe("TASK1_DONE",function()
    log.info("subscribe","wow")
end)

9.1.11 sys.unsubscribe(id, callback)

取消订阅消息

参数

传入值类型
释义
param
id 消息 id
param
callback 消息回调处理

返回值

例子

sys.unsubscribe("NET_STATUS_IND", callback)

9.1.12 sys.publish(…)

发布内部消息,存储在内部消息队列中

参数

传入值类型
释义
string
topic的值
any
附带的参数1
any
附带的参数2
any
附带的参数N
返回值

例子

sys.publish("TASK1_DONE")--发布这个消息,此时所有在等的都会收到这条消息

9.1.13 sys.run()

run()从底层获取 core 消息并及时处理相关消息,查询定时器并调度各注册成功的任务线程运行和挂起

参数

返回值

例子

sys.run()

9.2 功能讲解

9.2.1 LuatOS 运行框架

由于没有 main 函数,一些习惯常规单片机开发者一时不知怎么运行程序。

模块运行从 main.lua 开始运行,先调用必须的库(例如:sys 库),然后运行代码,创建任务,最后 sys.run 收尾。

首先用 vscode 打开 adc 的例程文件,包含两个脚本文件,先打开 main.lua 脚本文件,简单了解整个 LuatOS 运行框架,如图:

9.2.2 函数的调用

用 vscode 打开 adc 的例程文件中的 testAdc.lua 脚本文件,调用规则如图:

可以看到暴露外部的函数定义在申明时会带有本脚本的文件名称,比如testAdc.lua,在 testAdc.lua 中定义的函数,在 main.lua 中调用。 而内部函数则没有文件名称,直接调用即可。

9.2.3 任务的创建和任务间通信

通过 api 应用示例来理解任务的创建和任务间通信:

  • 多任务
sys = require("sys")
--第一个任务
sys.taskInit(function()
    while true do
        log.info("task1","wow")
        sys.wait(1000) --延时1秒,这段时间里可以运行其他代码
    end
end)

--第二个任务
sys.taskInit(function()
    while true do
        log.info("task2","wow")
        sys.wait(500) --延时0.5秒,这段时间里可以运行其他代码
    end
end)

sys.run()
  • 多任务之间互相等待
sys = require("sys")
--第一个任务
sys.taskInit(function()
    while true do
        log.info("task1","wow")
        sys.wait(1000) --延时1秒,这段时间里可以运行其他代码
        sys.publish("TASK1_DONE")--发布这个消息,此时所有在等的都会收到这条消息
    end
end)

--第二个任务
sys.taskInit(function()
    while true do
        sys.waitUntil("TASK1_DONE")--等待这个消息,这个任务阻塞在这里了
        log.info("task2","wow")
    end
end)

--第三个任务
sys.taskInit(function()
    while true do
        local result = sys.waitUntil("TASK1_DONE",500)--等待超时时间500ms,超过就返回false而且不等了
        log.info("task3","wait result",result)
    end
end)

--单独订阅,可以当回调来用
sys.subscribe("TASK1_DONE",function()
    log.info("subscribe","wow")
end)

sys.run()
  • 多任务之间互相等待并传递数据
sys = require("sys")
--第一个任务
sys.taskInit(function()
    while true do
        log.info("task1","wow")
        sys.wait(1000) --延时1秒,这段时间里可以运行其他代码
        sys.publish("TASK1_DONE","balabala")--发布这个消息,并且带上一个数据
    end
end)

--第二个任务
sys.taskInit(function()
    while true do
        local _,data = sys.waitUntil("TASK1_DONE")--等待这个消息,这个任务阻塞在这里了
        log.info("task2","wow receive",data)
    end
end)

--第三个任务
sys.taskInit(function()
    while true do
        local result,data = sys.waitUntil("TASK1_DONE",500)--等待超时时间500ms,超过就返回false而且不等了
        log.info("task3","wait result",result,data)
    end
end)

--单独订阅,可以当回调来用
sys.subscribe("TASK1_DONE",function(data)
    log.info("subscribe","wow receive",data)
end)

sys.run()
  • 传统定时器
sys = require("sys")

--一秒后执行某函数,可以在后面传递参数
sys.timerStart(log.info,1000,"1s timer")
--直接写个function也行
sys.timerStart(function()
    log.info("1s timer function")
end,1000)

--每秒执行,永久循环,返回定时器编号
local loopId = sys.timerLoopStart(log.info,1000,"1s loop timer")
--10秒后手动停止上面的无限循环定时器
sys.timerStart(function()
    sys.timerStop(loopId)
    log.info("stop 1s loop timer")
end,10000)

sys.run()

9.2.4 功能验证

  • 修改例程

在 ADC 的 demo 基础上(点此链接查看 adc 的 demo 例子),只修改了 main.lua 脚本文件,

用 vscode 打开 adc 的例程文件中的 main.lua 脚本文件,增加了一部分待定代码:

-- LuaTools需要PROJECT和VERSION这两个信息
PROJECT = "adcdemo"
VERSION = "1.0.0"

log.info("main", PROJECT, VERSION)

-- 一定要添加sys.lua !!!!
sys = require("sys")

-- 添加硬狗防止程序卡死
if wdt then
    wdt.init(9000) -- 初始化watchdog设置为9s
    --循环定时器:,调用此函数会在core里面创建运行定时器,定时器时间到会把“定时器消息”,放到外部消息队列里
    --sys.run()启动消息处理循环,从外部消息队列取到定时器消息后运行wdt.feed函数
    sys.timerLoopStart(wdt.feed, 3000) -- 3s喂一次狗
end

local testAdc = require "testAdc"--运行到这一行,切换到tesaAdc.lua脚本逐行运行
--创建内部task,调用testAdc.dotest
sys.taskInit(testAdc.dotest)

------------自己添加部分代码开始------------------------------------
local a,b=1,2
sys.taskInit(function()
        while true do
        --生产者:发布这个消息,并把消息"TEST_wait"放到内部消息队列
        sys.publish("TEST_wait",a)--可以发布多个变量sys.publish("TEST",1,2,3)
         --生产者:发布这个消息,并把消息"TEST_subscribe"放到内部消息队列
        sys.publish("TEST_subscribe",b)--发布这个消息,此时所有在等的都会收到这条消息
       --调用此函数会在core里面创建运行定时器,定时器时间到会把“定时器消息”,放到外部消息队列里
        sys.wait(2000) --延时2秒,这段时间里可以运行其他代码
        end
end)
sys.taskInit(function()
        while true do
                 --消费者:在消息处理函数表中把协程ID和"TEST_wait"绑定
                 --此处阻塞等待TEST_wait消息,收到这个消息或者超过10秒,才会退出阻塞等待状态
                result, data = sys.waitUntil("TEST_wait", 10000)--等待超时时间10000ms,超过就返回false而且不等了
                if result == true then
                        print("rev")
                        print(data)--输出a的值
                end
                --sys.wait(2000)
        end
end)
local function callBackTest(x,y,...)
        print("callBack",x)--输出b的值
end
--消费者:在消息处理函数表中把回调函数和"TEST_subscribe"绑定
--当收到TEST_subscribe消息时,会执行callBackTest 函数
sys.subscribe("TEST_subscribe",callBackTest)--单独订阅,可以当回调来用
----------自己添加部分代码结束-------------------------------

-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
--sys.run()用来内部和外部消息处理循环,遍历消息队列的消息,找到对应的函数来执行。
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!

新增代码主要是用 sys.taskInit(fun, …)函数创建了两个线程,第一个线程用来发布消息,第二个线程用来阻塞等待消息,并用 sys.subscribe(id, callback)函数订阅消息,实现回调函数。

  • 正确连接电脑和 4G 模组电路板

使用带有数据通信功能的数据线,不要使用仅有充电功能的数据线。

  • 识别 4G 模组的 BOOT 引脚

在下载之前,要用模组的 BOOT 引脚触发下载, 也就是说,要把 4G 模组的 BOOT 引脚拉到 1.8v,或者直接把 BOOT 引脚和 VDD_EXT 引脚相连。我们要在按下 BOOT 按键时让模块开机,就可以进入下载模式了。

具体到 Air780E 开发板:

1、当我们模块没开机时,按着 BOOT 键然后长按 POW 开机。

2、当我们模块开机时,按着 BOOT 键然后点按重启键即可。

  • 识别电脑的正确端口

判断是否进入 BOOT 模式:模块上电,此时在电脑的设备管理器中,查看串口设备, 会出现一个端口表示进入了 BOOT 下载模式,如下图所示:

  • 用 Luatools 工具烧录

1、新建项目

首先,确保你的 Luatools 的版本,大于等于 3.0.6 版本的。

在 Luatools 的左上角上有版本显示的,如图所示:

Luatools 版本没问题的话, 就点击 Luatools 右上角的“项目管理测试”按钮,如下图所示:

这时会弹出项目管理和烧录管理的对话框,可以新建一个项目,如下图:

2、 开始烧录

选择 Air780E 板子对应的底层 core 和刚改的 main.lua 脚本文件。下载到板子中。

点击下载后,我们需要进入 BOOT 模式才能正常下载。

如果没进入 BOOT 模式会出现下图情况:

进入 BOOT 模式下载,如图:

  • 对应 log
[2024-10-21 16:29:25.733][000000000.009] am_service_init 851:Air780E_A17
[2024-10-21 16:29:25.736][000000000.023] Uart_BaseInitEx 1049:uart 0 rx cache 256 dma 256
[2024-10-21 16:29:25.737][000000000.224] self_info 122:model Air780E_A17 imei 868327075469021
[2024-10-21 16:29:25.738][000000000.226] I/pm poweron: Power/Reset
[2024-10-21 16:29:25.739][000000000.226] I/main LuatOS@EC618 base 23.11 bsp V1112 32bit
[2024-10-21 16:29:25.739][000000000.226] I/main ROM Build: Sep  3 2024 15:55:27
[2024-10-21 16:29:25.740][000000000.234] D/main loadlibs luavm 262136 14360 14392
[2024-10-21 16:29:25.740][000000000.234] D/main loadlibs sys   277872 54024 81264
[2024-10-21 16:29:25.741][000000000.250] I/user.main        adcdemo        1.0.0
[2024-10-21 16:29:25.742][000000000.293] rev
[2024-10-21 16:29:25.743][000000000.294] 1
[2024-10-21 16:29:25.743][000000000.295] callBack        2
[2024-10-21 16:29:26.583][000000001.291] I/user.开始读取ADC
[2024-10-21 16:29:26.614][000000001.321] I/user.结束读取ADC
[2024-10-21 16:29:26.616][000000001.322] I/user.adc        读取耗时        100次        29        ms        单次        0        ms
[2024-10-21 16:29:26.618][000000001.324] D/user.adc        adc0        11
[2024-10-21 16:29:26.619][000000001.325] D/user.adc        adc1        9
[2024-10-21 16:29:26.622][000000001.326] D/user.adc        CPU TEMP        41000
[2024-10-21 16:29:26.623][000000001.328] D/user.adc        VBAT        4076
[2024-10-21 16:29:27.581][000000002.292] rev
[2024-10-21 16:29:27.586][000000002.293] 1
[2024-10-21 16:29:27.589][000000002.295] callBack        2
[2024-10-21 16:29:27.627][000000002.329] D/user.adc        adc0        14
[2024-10-21 16:29:27.632][000000002.331] D/user.adc        adc1        9
[2024-10-21 16:29:27.635][000000002.332] D/user.adc        CPU TEMP        41000
[2024-10-21 16:29:27.637][000000002.333] D/user.adc        VBAT        4077

扩展

常见问题

1.为什么LuatOS固件运行某些功能会有延时

脚本中的应用属于单 task 应用,都运行在一个 Lua virtual machine task 中,Lua 主 task 一直在循环处理内部消息队列和外部消息队列中的消息,消息是先进先出先处理。

即使有新消息到达,插入到内部消息队列或者外部消息队列中,必须等前面所有的消息处理结束,才能处理新消息

例如:首先产生了一个定时器消息,定时器消息的处理动作(新建一个文件,写 500K 数据)比较耗时,在写文件的过程中,core 中的串口 task 产生了一个串口接收数据通知消息,虽然可以插入到 Lua 主 task 的外部消息队列中,但是由于 Lua 主 task 还在执行写文件的动作,所以串口消息只能等待,问题就表现为“我明明已经向模块串口发送了数据,为什么脚本在 3 秒后才开始处理串口数据呢?”

给读者的话

本篇文章由公帅开发;

本篇文章描述的内容,如果有错误、细节缺失、细节不清晰或者其他任何问题,总之就是无法解决您遇到的问题;

请登录合宙技术交流论坛,点击文档找错赢奖金-Air780E-LuatOS-软件指南-基础服务-LuatOS运行框架

用截图标注+文字描述的方式跟帖回复,记录清楚您发现的问题;

我们会迅速核实并且修改文档;

同时也会为您累计找错积分,您还可能赢取月度找错奖金!