跳转至

LuatOS运行框架

一、LuatOS 运行框架概述

1.1 LuatOS 简介

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

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

1.2 LuatOS 软件包介绍

LuatOS 软件由两部分组成:内核固件和应用脚本。点击此处下载 LuatOS 软件包,如图:

  • 内核固件

下载底层 “CORE_V4030” 解压后,里面有一个 readme.txt 文件,详细介绍了不同的内核固件功能,本教程用的内核固件是:LuatOS-Air_V4030_RDA8910_TTS_NOLVGL_FLOAT.pac,如图:

  • 应用脚本 demo

下载 “上层软件 LuaTask_V2.4.4(demo+lib)”解压后,在 demo 文件夹有各种功能例程可以参考:

1.3 LuatOS 软件架构

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

1.内核固件(core)

内核固件是 LuatOS 的基石,它提供了系统启动、硬件驱动、中断处理等功能。在启动过程中,内核固件会创建一系列的 task,包括音频、协议栈、串口等。其中,有一个特别的任务用于加载 Lua 虚拟机,解析并运行用户编写的 Lua 脚本。这个任务被称为 Lua virtual task。

2.上层软件

库脚本(lib):库脚本包含了丰富的 API 接口和组件库,这些 API 接口和组件库封装了底层的硬件访问和通信协议,使得开发者可以通过简单的 Lua 代码实现复杂的物联网功能。例如,开发者可以通过调用 API 轻松实现 SOCKET、MQTT、OTA、GPIO、I2C、SPI 等功能。

业务代码(application script):业务代码是开发者根据自己的需求编写的 Lua 脚本,用于实现具体的物联网应用功能。这些脚本运行在 Lua 虚拟机中,通过调用库脚本提供的 API 接口和组件库来完成各项任务。

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 Air724UG-NFM 开发板

使用 Air724UG-NFM 开发板,如下图所示:

淘宝购买链接:Air724UG-NFM 开发板淘宝购买链接

此开发板的详细使用说明参考:Air724UG 产品手册 中的 《EVB_Air724UG_AXX开发板使用说明》,写这篇文章时最新版本的使用说明为:《EVB_Air724UG_A14开发板使用说明》;开发板使用过程中遇到任何问题,可以直接参考这份使用说明文档。

3.2 SIM 卡

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

3.3 PC 电脑

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

3.4 数据通信线

安卓的USB数据线。

四、准备软件环境

4.1 Luatools 工具

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

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

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

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

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

4.2 准备需要烧录的代码

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

4.2.1 烧录的内核固件文件

本教程选择这个内核固件:LuatOS-Air_V4030_RDA8910_TTS_NOLVGL_FLOAT.pac

4.2.2 烧录的脚本代码

右键点我,另存为,下载完整压缩文件包

五、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 使用的消息队列的应用理解:

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.init(0, 0)
sys.run()

八、回调的概念

基于以下两种场景介绍:

1、内部消息驱动管理

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

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.init(0, 0)
--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 收尾。

可以看下面的例子,简单了解整个 LuatOS 运行框架:

--必须在这个位置定义PROJECT和VERSION变量
--PROJECT:ascii string类型,可以随便定义,只要不使用,就行
--VERSION:ascii string类型,如果使用Luat物联云平台固件升级的功能,必须按照"X.X.X"定义,X表示1位数字;否则可随便定义
PROJECT = "ADC"
VERSION = "2.0.0"
--请求必要的库
require "sys"
--创建任务,用sys.taskInit函数可以创建多个任务
sys.taskInit(function()
    sys.wait(10000)
    log.info("协程创建")
end)

--加载ADC功能测试模块
require "testAdc"--运行到这一行,会调用testAdc.lua脚本文件
--运行完testAdc.lua脚本里的函数后继续逐行运行
--启动系统框架
sys.init(0, 0)
--sys.run()收尾
sys.run()

9.2.2 函数的调用

函数的调用和定义可以参考以下例程:

-----------------假如这个文件是test.lua脚本文件--------------------------
module(...,package.seeall)
--定义read函数
local function read()
    log.info("定义read函数")
end
--直接调用read()函数,其他的脚本文件require"test"时,会运行这个函数
read()

--定义write函数,在其他的脚本文件可以require"test",然后test.write()调用此函数
function write()
    log.info("定义read函数")
end

-- 定时每秒调用read函数
sys.timerLoopStart(read,1000)

--创建任务,用sys.taskInit函数可以创建多个任务
sys.taskInit(function()
    sys.wait(10000)
    log.info("协程创建")
end)
--请求misc库,用于调用misc.getVbatt()函数
require"misc"
sys.timerLoopStart(function ()
    log.info("vbatt.read",misc.getVbatt())
end,1000)

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

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

  • 多任务
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.init(0, 0)
sys.run()
  • 多任务之间互相等待
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.init(0, 0)
sys.run()
  • 多任务之间互相等待并传递数据
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.init(0, 0)
sys.run()
  • 传统定时器
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.init(0, 0)
sys.run()

9.2.4 功能验证

  • 准备例程

例程自己准备了 main.lua 和 test.lua 两个脚本文件。

以下 main.lua 脚本文件:

------------------------------------main.lua------------------------------------------------
--必须在这个位置定义PROJECT和VERSION变量
--PROJECT:ascii string类型,可以随便定义,只要不使用,就行
--VERSION:ascii string类型,如果使用Luat物联云平台固件升级的功能,必须按照"X.X.X"定义,X表示1位数字;否则可随便定义
PROJECT = "TEST"
VERSION = "2.0.0"

--加载日志功能模块,并且设置日志输出等级
--如果关闭调用log模块接口输出的日志,等级设置为log.LOG_SILENT即可
require "log"
LOG_LEVEL = log.LOGLEVEL_TRACE
--加载sys库
require "sys"

--加载test.lua脚本文件
require "test"
--调用test.lua脚本文件的函数
test.text()
--启动系统框架
sys.init(0, 0)
--sys.run()用来内部和外部消息处理循环,遍历消息队列的消息,找到对应的函数来执行。
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!

以下 test.lua 脚本文件:

------------------------------------test.lua---------------------------------------
module(...,package.seeall)

require "misc"

local a = 1
local b = 2

local function read()
    log.info("vbatt.read",misc.getVbatt())
end

local function write()
    log.info("write")
end
--内部调用函数
write()

function text()
    log.info("这是test.lua 的文本")
end
--循环定时器:,调用此函数会在core里面创建运行定时器,定时器时间到会把“定时器消息”,放到外部消息队列里
--sys.run()启动消息处理循环,从外部消息队列取到定时器消息后运行read函数
sys.timerLoopStart(read,1000)

--创建协程1
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)
--创建协程2
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)--单独订阅,可以当回调来用
  • 正确连接电脑和 4G 模组电路板

开发板的接线方式

首先将开发板放置好,接上 USB 并连接到电脑,同时,记得将天线也连接好,保证信号环境比较良好,比如可以看看手机信号来判断一下所在环境的信号状况。USB 的连接如上图所示。

在上图中,连接 USB 的插口旁边有一个 USB 字样,在进行脚本下载时,须连接此端口。

  • 识别 4G 模组的 BOOT 引脚

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

具体到 Air724UG-NFM 开发板:

1、当我们模块没开机时,按着 “下载模式” 键然后长按 “开机” 开机。

2、当我们模块开机时,按着“下载模式”键然后点按 “重启” 键即可。

  • 识别电脑的正确端口

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

  • 用 Luatools 工具烧录

1、 新建项目

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

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

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

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

2、 开始烧录

选择开发板对应的底层 core 和脚本文件。下载到板子中。

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

  • 对应 log
[2024-11-01 00:40:36.301] 工具提示: soc log port COM28打开成功
[2024-11-01 00:40:36.333] 工具提示: ap log port COM29打开成功
[2024-11-01 00:40:36.336] 工具提示: 用户虚拟串口 COM27
软件重启开机,如果是意外重启,请去http://wiki.openluat.com/doc/luatApi/#rtospoweron,查看
[2024-11-01 00:40:36.366] [I]-[write]
[2024-11-01 00:40:36.366] [I]-[这是test.lua 的文本]
[2024-11-01 00:40:36.366] [I]-[poweron reason:] 3 TEST 2.0.0 2.4.4 LuatOS-Air_V4030_RDA8910_TTS_NOLVGL_FLOAT
[2024-11-01 00:40:36.366] [I]-[core build time] Jul 24 2024 17:58:47
[2024-11-01 00:40:36.366] rev
[2024-11-01 00:40:36.366] 1
[2024-11-01 00:40:36.366] callBack 2
[2024-11-01 00:40:36.366] [I]-[ril.proatc] RDY
[2024-11-01 00:40:36.366] [I]-[ril.sendat] ATE0
[2024-11-01 00:40:36.366] [I]-[ril.proatc] RDY
[2024-11-01 00:40:36.366]
[2024-11-01 00:40:36.366] RDY
[2024-11-01 00:40:36.429] [I]-[ril.proatc] ATE0
[2024-11-01 00:40:36.429] [I]-[ril.defurc] ATE0
[2024-11-01 00:40:36.429] AT^tracectrl=0,1,2
[2024-11-01 00:40:36.429]
[2024-11-01 00:40:36.429] OK
[2024-11-01 00:40:36.429] [I]-[ril.proatc] OK
[2024-11-01 00:40:36.429] [I]-[ril.defrsp] ATE0 true OK nil
[2024-11-01 00:40:36.429] [I]-[ril.sendat] AT+CMEE=0
[2024-11-01 00:40:36.429] [I]-[ril.proatc] OK
[2024-11-01 00:40:36.429] [I]-[ril.defrsp] AT+CMEE=0 true OK nil
[2024-11-01 00:40:36.429] [I]-[ril.sendat] AT+WISN?
[2024-11-01 00:40:36.494] 工具提示: USB 断开连接 COM28 write wait error(0),[WinError 433] 指定不存在的设备。
[2024-11-01 00:40:38.142] 工具提示: soc log port COM28打开成功
[2024-11-01 00:40:38.192] 工具提示: ap log port COM29打开成功
[2024-11-01 00:40:38.203] 工具提示: cp log port COM30打开成功
[2024-11-01 00:40:38.209] 工具提示: 用户虚拟串口 COM27
[2024-11-01 00:40:38.253] [I]-[ril.proatc] ^CARDMODE: 255
[2024-11-01 00:40:38.253] [I]-[ril.defurc] ^CARDMODE: 255
[2024-11-01 00:40:38.253]
[2024-11-01 00:40:38.253] ^CARDMODE: 255
[2024-11-01 00:40:38.253] [I]-[ril.proatc] +CPIN: SIM REMOVED
[2024-11-01 00:40:38.253] [I]-[ril.defurc] +CPIN: SIM REMOVED
[2024-11-01 00:40:38.253]
[2024-11-01 00:40:38.253] +CPIN: SIM REMOVED
[2024-11-01 00:40:38.274] AT^tracectrl=0,1,2
[2024-11-01 00:40:38.274]
[2024-11-01 00:40:38.274] OK
[2024-11-01 00:40:38.322] ATI
[2024-11-01 00:40:38.322]
[2024-11-01 00:40:38.322] LuatOS-Air_V4030_RDA8910_TTS_NOLVGL_FLOAT
[2024-11-01 00:40:38.322]
[2024-11-01 00:40:38.322] OK
[2024-11-01 00:40:38.369] rev
[2024-11-01 00:40:38.369] 1
[2024-11-01 00:40:38.369] callBack 2
[2024-11-01 00:40:38.369] [I]-[vbatt.read] 3758
[2024-11-01 00:40:38.417] AT+CGSN
[2024-11-01 00:40:38.417]
[2024-11-01 00:40:38.417] 863482063029199
[2024-11-01 00:40:38.417]
[2024-11-01 00:40:38.417] OK
[2024-11-01 00:40:38.431] 工具提示: AP LOG保存在D:\Desktop\LuaTools\log\4gdiag\2024-11-01_004038_LuatOS-Air_V4030_RDA8910_TTS_NOLVGL_FLOAT_863482063029199_COM29.bin 如果有问题交给FAE
[2024-11-01 00:40:38.438] 工具提示: CP LOG保存在D:\Desktop\LuaTools\log\4gdiag\01-00-40-38-438.Sn(1556bd).tra 如果有问题交给FAE
[2024-11-01 00:40:38.495] AT+CPIN?
[2024-11-01 00:40:38.526]
[2024-11-01 00:40:38.526] +CME ERROR: 10
[2024-11-01 00:40:39.397] [I]-[vbatt.read] 3758
[2024-11-01 00:40:40.407] rev
[2024-11-01 00:40:40.407] 1
[2024-11-01 00:40:40.407] callBack 2
[2024-11-01 00:40:40.407] [I]-[vbatt.read] 3758

常见问题

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

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

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

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

2.代码在 sys.taskInit 里面报错,task 会死掉,怎么让模块重启

默认当前 task 消亡,不影响其他 task 和整个 Luat 应用程序的正常运行,如果需要模块重启,把_G.COROUTINE_ERROR_RESTART 这个变量设置为 true,在 task 报错的时候就可以重启了

给读者的话

本篇文章由公帅开发;

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

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

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

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

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