LuatOS框架的使用(1)

LuatOS作为专为物联网设备设计的轻量级嵌入式操作系统框架,凭借其基于Lua脚本语言的高效开发模式,正被越来越多的开发者所青睐。本文将系统性地介绍LuatOS框架的核心架构与运行机制,帮助开发者快速掌握其基本使用方法,并通过实际案例演示如何在真实项目中高效集成与开发。

LuatOS 框架是整个 LuatOS 开发中最基础也是最核心的内容,无论使用 LuatOS 开发什么功能,都会用到它;

LuatOS 框架主要包含以下几部分:

1、LuatOS 软件的构成

2、LuatOS 开发环境的说明

3、LuatOS 用户脚本程序的运行机制

4、LuatOS 的 sys 核心库中任务、消息、定时器和调度器功能的使用

一、初步认识 LuatOS 开发

现在我们开始正式进入 LuatOS 开发的世界。

1.1 Lua 语言介绍

用户基于 LuatOS 进行软件二次开发,使用的编程语言为 Lua 语言,Lua 语言相对来说还是比较简单的,大家只要有其他编程语言的开发经验,就不用在 Lua 语言学习上花费太多时间,花个一两天大概看一遍就够了;后续可以边看 LuatOS demo,边学习 Lua 语言,这样学习效率更高。

1.2 LuatOS 软件的构成

1.2.1 LuatOS 软件架构

LuatOS 软件的总体架构参考上图,从二次开发的角度来看,主要理解几个概念:内核固件,标准库,核心库,扩展库,demo,project,用户开发的 LuatOS 项目应用软件;

接下来我结合架构图逐一分析下这几个概念(为了演示方便,在这里我打开两个浏览器窗口,左边是框架图,右边是文字介绍)。

1.2.2 LuatOS 内核固件

1、内核固件(又叫固件,底层固件,或者 core),我们会针对每种硬件编译好并且对外发布内核固件

2、内核固件文件以 soc 做为后缀,例如:

(1) LuatOS-SoC_V2008_Air8000_1.soc 表示Air8000 硬件的V2008版本的1 号LuatOS 固件文件;

(2) LuatOS-SoC_V2008_Air780EHM.soc 表示Air780EHM 硬件的V2008版本的 LuatOS 固件文件;

(3) LuatOS-SoC_V1004_Air8101.soc 表示Air8101 硬件的V1004版本的 LuatOS 固件文件;

3、内核固件包含主芯片系统平台层(这里有一个嵌入式操作系统,目前比较流行的是 FreeRTOS,接下来本文用到的嵌入式操作系统都会以 FreeRTOS 为例来说明),LuatOS 适配层,Lua 虚拟机,Lua 标准库,LuatOS 核心库几部分,除了主芯片系统平台层的源码没有开放外,其余四部分的源码全部开放;

4、内核固件中和用户软件二次开发息息相关的两部分是 Lua 标准库和 LuatOS 核心库,接下来我们看下这两部分内容;

1.2.3 Lua 标准库

Lua 标准库是 Lua 语言内置的一组核心功能模块,它们为 Lua 提供了基础编程能力。标准库的设计遵循 Lua 的"小而精"哲学,仅包含最必要的功能。

Lua 标准库已经编译到了 LuatOS 内核固件中,用户无法修改,可以直接使用。

主要包含以下几种库:

基础库(Basic Library):支持 collectgarbage(垃圾回收)、_G(全局变量表)、ipairs(迭代数组元素)、pairs(迭代键值对)、tostring(转换为字符串)、tonumber(转换为数字)、type(获取类型名)等功能函数;

协程库(Coroutine Library):支持 coroutine.create(创建协程)、coroutine.yield(挂起协程)、coroutine.resume(恢复协程)、coroutine.status(获取协程状态)、coroutine.running(获取正在运行的协程)等功能函数;

字符串处理库(String Library):支持 string.len(获取字符串长度)、string.match(字符串模式匹配)、string.byte(获取字符的 ascii 码)、string.char(获取 ascii 码对应的字符)等功能函数;

表处理库(Table Library):支持 table.insert(插入元素)、table.remove(移除元素)、table.concat(将表中的元素连接为一个字符串)、table.unpack(解包表中的元素为多个返回值)等功能函数;

此外还有数学库,输入输出库,操作系统库,调试库等功能模块,在这里我就不逐一列举了;

LuatOS 中使用的 Lua 是 5.3 版本

1.2.4 LuatOS 核心库

LuatOS 核心库是 专为嵌入式设备设计的 Lua 运行时扩展库,针对物联网(IoT)和资源受限环境(如 MCU)进行了深度优化和功能扩展。

LuatOS 核心库已经编译到了 LuatOS 内核固件中,用户无法修改,可以直接使用。

目前支持 74 个核心库,核心库中的所有功能模块,我们在后续文章中都会逐一讲解;

在核心库中有一个 sys 库,在本章中会重点介绍。

1.2.5 LuatOS 扩展库

LuatOS 扩展库是 在核心库基础上针对特定场景或硬件功能提供的附加功能模块,用于增强 LuatOS 在物联网(IoT)和嵌入式系统中的能力。

LuatOS 扩展库是用 Lua 语言实现的功能模块

用户开发项目软件时,需要主动加载扩展库文件,才能使用。

目前支持 19 个扩展库

扩展库中的部分功能模块,我们在后续文章中都会进行讲解。

1.2.6 LuatOS demo

LuatOS demo 是在 Lua 标准库、LuatOS 核心库、LuatOS 扩展库的基础上,我们针对独立的应用场景,开发的示例代码合集。

展示了 Lua 标准库、LuatOS 核心库、LuatOS 扩展库的实际应用方法,可以帮助开发者快速验证硬件功能、理解各种库的 API 使用场景,开发者可以参考这些 demo 快速开发自己的项目。

1.2.7 LuaOS project

LuatOS project 是基于功能相对完整的硬件(例如整机开发板,硬件上集成了模组或者工业引擎,lcd,tp,摄像头,485 接口,矢量字库芯片,tf 卡,以太网等),开发的完整项目软件。开发者可以参考这种硬件和项目软件,更加快速地开发自己的项目。

1.2.8 用户开发的 LuaOS 项目应用软件

用户开发的 LuatOS 项目应用软件,是指开发者基于自己的实际项目需求(可以是演示 demo 需求,也可以是真实的项目需求),使用 Lua 脚本语言,调用 Lua 标准库、LuatOS 核心库、LuatOS 扩展库编写的项目应用脚本代码。

用户开发的 LuatOS 项目应用软件,我们开发的 LuatOS demo, LuatOS project,这三种软件都属于 LuatOS 项目应用软件,区别在于 demo 和 project 是我们开发的,另外一种是我们的用户开发的。

1.3 LuatOS 项目应用软件(hello_luatos)开发调试过程

了解了 LuatOS 软件构成之后,我们接下来以一个 hello_luatos 项目为例,先总体看下用户如何开发调试自己的项目应用软件。

再来回顾一下 LuatOS 软件架构图:

用户开发的项目应用软件,位于架构图中的最上层(也就是黄色背景的这一层);

开发用户项目应用软件时,需要调用 Lua 标准库、LuatOS 核心库、LuatOS 扩展库来实现。

hello_luatos 项目应用软件的需求为:每隔一秒钟通过日志输出一次 Hello, LuatOS。

1.3.1 根据项目需求编写项目应用软件代码

开发项目软件使用的编程语言为 Lua 脚本语言,编写的脚本文件后缀为.lua;

代码编写工具推荐使用 Visual Studio Code;

hello_luatos 的项目软件代码我已经编写好了,源码已经提交到:https://gitee.com/openLuat/LuatOS/tree/master/module/Air8000/demo/luatos_framework/hello_luatos

打开这个路径,可以看到,一共包含如下三个文件:

在这里我们先简单的看下这三个文件的核心内容,详细内容在后续章节会细细讲解。

1、main.lua:这个文件的作用大家可以看文件头注释,是一个入口文件,类似于 C 语言的 main 函数,LuatOS 项目软件代码是从 main.lua 开始执行的,所以 main.lua 文件必须存在,并且文件名也必须是 main.lua

在 main.lua 中还有一行代码,如下图所示,加载 hello_luatos 应用功能模块,也就是说执行到这行代码时,就会加载并且运行 hello_luatos.lua 这个文件;和 main.lua 不同的是,hello_luatos.lua 的文件名可以根据自己的应用功能模块含义自定义(只要定义的文件名和 Lua 标准库、LuatOS 核心库、LuatOS 扩展库不要重复就行),文件名修改后,在 main.lua 中 require 新的文件名即可;例如文件名修改为 hello_air8000.lua,在 main.lua 中 require "hello_air8000"即可。

2、hello_luatos.lua:还是先看下图中的这个文件头注释,重点看下选中的几行文字描述:

为什么单独创建一个 hello_luatos.lua 去实现 每隔一秒打印一次Hello, LuatOS 的业务功能呢? 这个是为了应用功能设计模块化,不同功能模块之间解耦,逻辑清晰,代码阅读成本低;这个是一个很好的设计思想,希望大家以后开发项目软件都要遵循。

3、readme.md:这个文件是对当前 demo 项目软件的使用说明,从功能概述、硬件环境、软件环境,操作步骤几方面说明了当前这个 demo 项目如何使用。

1.3.2 LuatOS 软件的运行载体

开发好 hello_luatos 的项目应用软件代码后,这个项目完整的 LuatOS 软件也就完成了;

完整的 LuatOS 软件 = LuatOS 内核固件 + LuatOS 扩展库 + hello_luatos 的项目应用软件;

完整的 LuatOS 软件需要一个运行载体,才能看到软件的运行效果;

目前提供了两类运行载体:

1、硬件核心板或者开发板:例如 Air8000 的核心板和开发板,Air780EHM/EHV/EGH/EPM 的核心板和开发板,Air8101 的核心板和开发板; 本篇中我们将使用 Air8000 的核心板来运行 LuatOS 软件进行演示;

2、PC 模拟器 :直接可以在电脑上使用 LuatOS 模拟器来运行,使用模拟器运行有一些限制,模拟器支持的功能并不完整,例如和外设有关的功能,lcd,字库等可能并不支持,但是一些纯软件的功能,例如 LuatOS 运行框架,网络应用等还是可以模拟运行的。所以本讲的 LuatOS 运行框架,我们也会使用 PC 模拟器来进行演示,这样可以节省硬件载体烧录软件所花费的时间。

1.3.3 烧录 完整的 hello_luatos 项目 LuatOS 软件 到 Air8000 核心板 运行

我们先来看下,完整的 hello_luatos 项目的 LuatOS 软件第一种运行载体(以 Air8000 核心板为例)上的运行效果。

要将完整的 LuatOS 软件烧录到 Air8000 核心板中,并且观察软件的运行效果,需要用到 Luatools 下载调试工具;

在这里我就不详细说明 Luatools 的用法了,只重点说明一下烧录 hello_luatos 这个完整的项目软件过程中涉及到的一些关键操作:

(1)首先我们要准备好一块 Air8000 核心板、一根带有数据传输功能的 type-c 接口的 usb 数据线、一个 WINDOWS10 以及以上版本操作系统的电脑;

(2)Air8000 核心板正面的 供电/充电 拨动开关 拨到供电一端,背面的 USB ON/USB OFF 拨动开关 拨到 USB ON 一端;

(3)type-c 接口的 usb 数据线连接 Air8000 核心板和电脑;

(4)打开 Luatools,勾选 4G 模块 USB 打印,点击项目管理测试,创建一个项目,选择好 LuatOS 内核固件,hello_luatos 的应用软件脚本代码,勾选 USB BOOT 下载,然后点击 下载底层和脚本 按钮;

此时如果 Air8000 核心板已经处于开机运行状态,则等一段时间,会自动开始烧录软件; 如果处于开机运行状态下,没有自动烧录软件,则用手按住核心板正面的下载按钮不放开,然后再按一下核心板的复位按钮就可以开始烧录软件; 如果没有处于开机运行状态,则用手按住核心板正面的下载按钮不放开,然后再长按开机按钮 2 秒,就可以开始烧录软件; 出现如下图所示的下载完成状态就表示烧录成功:

下载成功后,Air8000 核心板自动开机运行,在 Luatools 主界面的日志窗口就可以看到运行日志,我们可以看到大约每隔 1 秒钟,日志输出一次 Hello, LuatOS

下面我来实际演示这个过程。

1.3.4 使用 PC 模拟器 运行 完整的 hello_luatos 项目 LuatOS 软件

打开 Luatools,点击 账户-> 打开资源下载 的菜单

会弹出 Luatools 资源管理窗口

勾选 公共资源->LuatOS 的 PC 模拟器->VXXXX 版本下的默认资源 ,然后点击右上角的 开始下载(非刷机) 按钮;

下载成功后,点击右上角的 打开本地资源目录 按钮,resource 目录下的 LuatOS_PC 就是 LuatOS 的 PC 模拟器,找到最新版本的压缩包,解压缩之后,打开解压缩后的目录,有如下文件:

其中 luatos-pc.exe 就是模拟器的可执行文件,luatos.bat 就是运行模拟器的批处理文件。

在这个目录下,创建一个 cmd 命令行窗口的快捷方式,如下图所示:

双击 cmd 命令行窗口,然后输入下面一行命令,运行 luatos 批处理文件,同时输入要运行的 luatos 项目配置文件

luatos --llt=H:\Luatools\project\luatos_framework_hello_luatos_Air8000.ini

然后按回车键,就可以运行 hello_luatos 项目软件;

这行命令的前半部分固定为 luatos --llt=,表示要加载 Luatools 创建的项目配置文件,根据配置文件找到所有的应用脚本文件来运行;

这行命令的后半部分为使用 Luatools 创建的 hello_luatos 项目软件的配置文件的绝对路径,根据你自己的实际情况进行填写;配置文件都存储在 Luatools 目录下的 project 目录;

模拟器运行后的效果如下图所示:

我们可以看到:每隔 1 秒钟,日志输出一次 Hello, LuatOS

下面我来实际演示下这个过程。

1.4 LuatOS 项目应用软件(hello_luatos)的运行逻辑详解

在使用 Air8000 核心板和模拟器实际运行 hello_luatos 项目软件之后,大家对这个项目实现的功能应该是比较清楚了;

接下来,我用 Visual Studio Code,打开 hello_luatos 的项目应用脚本 main.lua 和 hello_luatos.lua,逐行分析应用逻辑的运行过程。

在分析脚本代码之前,先来看看简化后的下面这张图,这张图说明了 LuatOS 项目应用软件的总体运行过程

从这张图可以看出,在 LuatOS 内核固件中有一个 FreeRTOS,FreeRTOS 运行起来之后,创建了很多任务,有软件定时器任务,TCP/IP 协议栈任务,文件系统任务,Lua 虚拟机任务等。

其中 Lua 虚拟机任务和 LuatOS 项目的应用软件关系最为密切;

Lua 虚拟机任务运行起来之后,经过必要的初始化动作,就会去寻找 main.lua 脚本文件,找到之后,从 main.lua 的第一行代码开始解析执行,main.lua 会执行必要的初始化动作并且加载运行其他的 Lua 脚本应用功能模块,main.lua 的最后一行代码为 sys.run(),sys.run()是一个 while true 的循环函数,实际上也是 Lua 虚拟机任务的处理函数;

在这个 while 循环里面,不断的分发处理各种消息,调度 LuatOS 项目应用软件的正常运行。

在理解了 LuatOS 项目应用软件的基本运行逻辑之后,接下来,我们一起来看下 hello_luatos 的项目应用脚本 main.lua 和 hello_luatos.lua 的运行过程。

hello_luatos 的应用逻辑比较简单,并且代码中的注释也比较详细,我们就边看代码边讲解。

二、全面认识 LuatOS 运行框架如何使用

我们在上一小节中分析了 hello_luatos 的应用脚本代码,虽然 hello_luatos 这个项目很小,但是我们已经接触到了:

1、LuatOS 中的两个核心概念:任务和定时器;

2、LuatOS 的调度器:sys.run()函数

3、LuatOS 的一个核心库 sys

从本章开始,我们一起系统性地学习 LuatOS 运行框架如何使用,主要包含以下几项内容:

1、LuatOS 的三个核心概念:任务(task),消息(message),定时器(timer);

2、LuatOS 的一个调度器:sys.run()函数

3、LuatOS 的一个核心库:sys 核心库

4、基于一个相对完整的 LuatOS 项目,分析本节课学习到的核心概念,调度器和核心库知识,系统理解本节课的知识在实际项目中的应用方法;

2.1 LuatOS 的任务(task)

2.1.1 基本概念

2.1.1.1 FreeRTOS task 和 LuatOS task

先来看一下这张图,和上一张 LuatOS项目应用软件的总体运行过程图 相比,新增了一段说明文字以及几个 LuatOS task 示意图:

在 LuatOS 项目应用软件脚本运行过程中,只要代码可以被执行到,就可以调用 sys.taskInit 和 sys.taskInitEx 两个核心库的 API,创建 LuatOS 的 task。

从这张图中我们可以看到,有两种任务:

第一种是 LuatOS 内核固件中的任务,也就是 FreeRTOS 创建的任务;这种任务我们把它命名为 FreeRTOS task;

第二种是 LuatOS 项目应用脚本中的任务,也就是用户脚本代码中调用 sys.taskInit 和 sys.taskInitEx 两个 API 创建的任务,这种任务我们把它命名为 LuatOS task;严格意义上说,LuatOS task 并不是真正的 task,而是利用了 Lua 中的协程(coroutine)概念,来等价实现的一种 task 效果,因为 task 的受众面比协程的受众面要广的很多,所以我们在 LuatOS 中,将协程(coroutine)包装成了 task 的概念,这样更容易理解和使用;关于协程(coroutine)的知识不属于本课程讲解的范畴,大家有兴趣的可以借助网络资源自行学习。

在这里我使用一个形象的比喻,争取可以让大家更加直观的理解这两种任务的联系和区别:

1、有一个森林,这个森林就是 FreeRTOS;

2、森林里生长了很多棵大树,每一棵大树都是 FreeRTOS 创建的一个任务,就是 FreeRTOS task;

3、这些大树分成了两种:

一种是长出了树干,树干上还有很多树枝,这一种大树只有一棵,就是 Lua 虚拟机任务

一种是只长了光秃秃的树干,树干上没有树枝,这种大树有很多,除 Lua 虚拟机之外,其余所有任务都是这种大树

4、Lua 虚拟机任务这棵大树的树干上长出的所有树枝,就是一个个的 LuatOS task

根据以上描述画一张简图如下:

第一次接触 LuatOS 开发的用户,对 LuatOS task 的功能特性可能会有误区,所以在这里我们先对比下 FreeRTOS task 和 LuatOS task 的一些重要的功能特性区别;

LuatOS 内核固件中的任务(FreeRTOS task)和 LuatOS 项目应用脚本中的任务(LuatOS task)的重要区别如下:

通过上面这个表格中的文字描述,可能理解的不是很直观,接下来举两个例子,来实际说明一下 LuatOS task 的特性。

2.1.1.2 LuatOS 的多任务调度机制

第一个例子用来说明 LuatOS task 的协作式的任务调度机制;

核心代码片段如下,我们首先分析下这段代码的业务逻辑

我们使用 PC 模拟器来实际运行一下这个例子:

我已经在 Luatools 工具上创建了一个 luatos_framework_luatos_task_Air8000 项目,并且已经把应用脚本添加到这个项目下,为了节省时间,使用模拟器来运行一下这个例子来看看实际的效果:

打开 cmd 命令行窗口,输入命令

luatos --llt=H:\Luatools\project\luatos_framework_luatos_task_Air8000.ini

运行日志如下

下面我们结合代码来分析一下关键步骤的运行日志;

通过以上代码和日志的分析,可以知道,在这个程序中有 task1 和 task2 两个 LuatOS task;

当 task1 内部的代码运行到 sys.wait(500)时,task1 会挂起自己,task1 挂起之后,下一个恢复运行的 task 就是 task2,为什么呢?因为 task2 挂起时长是 300ms,task1 的挂起时长是 500ms,task1 要等 500ms 之后才能恢复运行,task2 最长只需要等待 300ms 就可以恢复运行,所以接下来肯定是 task2 先恢复运行;

当 task2 内部的代码运行到 sys.wait(300)时,task2 会挂起自己,task2 挂起之后,下一个恢复运行的 task 有可能是 task1,也有可能是 task2,为什么呢?因为 task2 挂起时长是 300ms,task1 的挂起时长是 500ms;

如果 task1 之前已经被挂起的时长超过了 200ms,接下来不到 300ms,task1 就应该重新恢复运行,这种情况下,因为 task2 在 300ms 之后就能恢复运行,所以 task1 就应该先恢复运行;

如果 task1 之前已经被挂起的时长小于 200ms,接下来超过 300ms,task1 才能重新恢复运行,这种情况下,因为 task2 在 300ms 之后就能恢复运行,所以 task2 就应该先恢复运行;

从这个例子,我们可以看出,task1 和 task2 都会通过 sys.wait(timeout)函数将自己挂起来实现不同 task 之间的任务调度;

如果我们简单地方修改一下代码,如下图所示,注释掉黄色背景的一行代码-- sys.wait(500) :

会发生什么事情呢?在模拟器上实际运行一下看看:

可以看到,task1 一直在运行,task2 永远得不到运行;

因为 task 没有优先级的概念,task1 首先运行,并且 task1 没有挂起自己,所以 task1 就会一直运行,task2 就没机会运行;

2.1.1.3 LuatOS 的多任务访问共享资源方式

第二个例子用来说明 LuatOS task 的多任务如何使用共享资源;

核心代码片段如下,我们首先分析下这段代码的业务逻辑

我们使用 PC 模拟器来实际运行一下这个例子:

我已经在 Luatools 工具上创建了一个 luatos_framework_luatos_task_Air8000 项目,并且已经把应用脚本添加到这个项目下,为了节省时间,使用模拟器来运行一下这个例子来看看实际的效果:

打开 cmd 命令行窗口,输入命令

luatos --llt=H:\Luatools\project\luatos_framework_luatos_task_Air8000.ini

运行日志如下

可以看到:

1、task1 中 for 循环前后,全局共享变量的值增加了 100;

2、task2 中 for 循环前后,全局共享变量的值增加了 100;

非常有规律,说明:

1、task1 在 for 循环执行过程中,没有被其他 task(这个 demo 中就是 task2)打断;

2、task2 在 for 循环执行过程中,没有被其他 task(这个 demo 中就是 task1)打断;

3、如果被打断了,循环前后的值就不会增加 100,要比 100 要大;

如果我们简单地修改一下代码,如下图所示,打开黄色背景的两行代码 sys.wait(5) :

会发生什么事情呢?在模拟器上实际运行一下看看

可以看到:

无论是 task1 还是 task2,都不是增加了 100

1、task1 中 for 循环前后,全局共享变量的值增加了 200;

2、task2 中 for 循环前后,全局共享变量的值增加了 199;

说明:

1、task1 在 for 循环执行过程中,task2 插入执行了;

2、task2 在 for 循环执行过程中,task1 插入执行了;

看到这里,就出现了共享资源冲突的问题,但这个冲突完全是我们自己写 LuatOS 脚本代码故意让他冲突的;

task1 和 task2 在各自的 for 循环执行了 sys.wait(5)语句,执行这个语句,就是把自己挂起,让其他 task 运行;

只要我们在编码时,在一个 task 内部操作共享资源过程中,不要主动挂起这个 task,就不会存在共享资源竞争和冲突的问题;

讲到这里,大家对 LuatOS task 的概念应该有了一个基本的认识,接下来我们详细看下 LuatOS task 如何使用。

注意事项:

若无特别指定是 FreeRTOS task 还是 LuatOS task,后续本文中的任务(或者 task)都是指 LuatOS task;

2.1.2 作用

我们首先想一个问题,为什么要有 task 这个概念,task 有什么用呢?

2.1.2.1 编程设计更简单

带着这个问题,我先来回顾一下 LuatOS 的最初的一次版本演变过程;

在 LuatOS 诞生的前一两年,应该是 2012 年到 2013 年,具体的时间记不清楚了;

当时在 LuatOS 中是没有 task 这个概念的,没有 task 的话,写代码以及逻辑跳转太繁琐了;

例如有个简单的指示灯闪烁功能需求:一个指示灯亮 500 毫秒,然后灭 500 毫秒,一直这样循环;

如果没有 task,核心代码片段如下:

在这段代码中,需要通过定时器不断地进行异步处理,根据异步处理逻辑代码运行会不断地发生跳转,设计或者阅读代码时,用户的思路也要不断地跳来跳去,使用起来比较繁琐;

后来 LuatOS 支持了 task,这个功能需求,使用 task 来实现,核心代码片段如下:

在一个 task 的处理函数中线性的直来直去的控制指示灯闪烁,比在非 task 中绕来绕去的控制指示灯闪烁,思路更清晰,逻辑也更简单。

通过刚才的这个例子,我们基本可以明白,LuatOS 最初设计 task 时,很重要的一个原因就是为了让用户编程设计更简单。

2.1.2.2 其他作用

到后来,随着 LuatOS task 的应用越来越广泛,LuatOS task 在以下几方面所起的作用也越来越明显:

1、实现多任务协作式的并发执行;

2、更简捷地支持了模块化解耦设计;

3、更简捷的处理异步事件,可以很方便地将异步处理逻辑封装成同步处理逻辑;

这些作用,在接下来的内容中都会讲解到,我们继续往下看;

2.1.3 创建

2.1.3.1 创建 API

怎么创建一个 task,在 sys 核心库中提供了两个 api:

sys.taskInit(task_func, ...)

sys.taskInitEx(task_func, task_name, non_targeted_msg_cbfunc, ...)

这两个 api 分别创建两种 task,首先我们给这两种 task 起个名字,一种叫基础 task,一种叫高级 task;

sys.taskInit(task_func, ...)创建的 task 是基础 task;

sys.taskInitEx(task_func, task_name, non_targeted_msg_cbfunc, ...)创建的 task 是高级 task;

从设计原理的角度来看,基础 task 和高级 task 的区别是:

(1) 所有的基础 task 共享一个全局消息队列;

(2) 每个高级 task 都有自己独立的消息队列,同时又能使用全局消息队列;

从用户使用的角度来看,基础 task 和高级 task 的区别是:

(1) 基础 task 如果阻塞功能使用不当,可能会丢失自己应该处理的消息;

(2) 高级 task 如果阻塞功能使用不当,不会丢失自己应该处理的消息;

虽然从设计原理来看,高级 task 比基础 task 使用起来不容易犯错;

但是由于基础 task 使用起来简洁,基础 task 还是需要掌握,一旦掌握之后,也不容易犯错;

接下来我们先看下这两个 api 的说明,结合说明再运行一些实际的例子,来理解如何创建 task;

sys.taskInit(task_func, ...)

**功能:**创建并且启动运行一个基础 task;这里表述的是基础 task,既然有基础 task,肯定还会有另外一种 task,在这里我们先不展开说明,后续的小节内容会讲到。

注意事项:

1、可以在能够执行到的任意代码位置使用此函数;关于这一点,我们在这里先不展开讲,等到下一小节再展开;

2、在 LuatOS 中,对创建的 task 数量没有特别限制,只要 ram 够用,可以一直创建;不同的硬件,用户可用 RAM 资源也不同,例如:

(1) Air8000 系列的用户可用 RAM 为 4MB

(2) Air780 系列中 Air780EPM 用户可用 RAM 为 1MB,Air780EHM/EHV/EGH 用户可用 RAM 为 4MB

(3) Air8101 系列的用户可用 RAM 为 2MB

(4) PC 模拟器用户可用 RAM 为 2MB

下面这个例子用来说明如何查看用户可用 ram 信息;

我们首先分析下这段代码的业务逻辑

我们在模拟器上实际运行一下看看,输入命令 luatos --llt=H:\Luatools\project\luatos_framework_luatos_task_Air8000.ini

运行日志如下

通过上图的这份日志,可以看出,Lua虚拟机中的用户可用ram信息:

  • 第一个数值为分配给Lua虚拟机的总ram为2097144字节,将近2MB字节;这个值在运行过程中一直保持不变;

  • 第二个数值为LuatOS应用程序运行过程中实时使用的ram,根据业务逻辑会发生动态变化,我们演示的这个demo业务逻辑比较简单,所有变化幅度比较小,一直在35KB左右波动;如果业务逻辑复杂,波动幅度就会比较大;

  • 第三个数值为LuatOS应用程序运行过程中历史最高使用的ram,我们演示的这个demo中,为37040字节,即历史最高水位; 平时开发过程中,大家可以加上这段代码,实时观察下第二个数值和第三个数值,如果这两个数值频繁的接近第一个数值,那就说明你的程序占用的内存就比较多了,此时就需要重点分析解决问题,否则很容易就会造成内存不足而导致重启;(一般来说,不会出现这样的问题;关于如何分析解决内存使用接近上限的问题,后续会有文章专门来讲,今天在这里就不说了)

知道了怎么查看用户可用ram信息后,我们再来看一个问题,每创建一个基础task需要占用多少ram资源? 下面这个例子用来说明如何分析一个基础task占用的ram资源; 这个例子的核心代码片段如下,我们首先分析下这段代码的业务逻辑

在模拟器上运行下面一段代码实际看下日志

从日志可以看出: led task创建前,Lua ram使用35296字节,led task创建后,Lua ram使用36112字节,led这个task创建并且运行需要816字节的ram; 因为led task的逻辑很简单,所以占用的ram较小,如果创建并且运行一个逻辑复杂的task,占用的ram就会变大; 因为task的ram消耗主要包括两部分: - 一部分是创建开销,这个相对较小,每个task的创建开销基本一样; - 一部分是运行开销,这个和task内部创建的所有局部变量、表、字符串等都有关系,不同task的消耗不一样,差别就比较大;

刚才这个例子演示的task很简单,创建并且运行消耗了816字节的ram; 大家可能会有疑问,这个demo只有这个task是我自己写的,仅消耗了816字节的ram,为什么日志中打印的总消耗ram是36112字节呢?剩余的35KB左右的ram被谁用去了? Lua虚拟机初始化运行时,会创建Lua状态机,加载标准库/核心库中的函数名和常量,加载运行main.lua以及main.lua中require的其余lua文件,这些都需要消耗ram,可以说,这些都是LuatOS应用程序运行的基本开销,不同的LuatOS应用项目,这个基本开销也不一样,主要取决于初始化过程中用户main.lua以及其他应用脚本的复杂度;

了解了用户可用ram以及每个基础task大概占用的ram之后,我们再回到原始的问题:在LuatOS中,对创建的task数量没有特别限制,只要ram够用,可以一直创建。 带着这个问题,我们再回到刚才的日志截图,以刚才的简单demo为例,初始化之后,创建用户的第一个task之前,剩余的可用ram是2097144 - 35296 = 2061848字节,假设创建并且运行每个task需要消耗816字节,则应该可以创建 2053120/816 = 2526个task。

下面这个例子用来说明可以创建多少个task; 这个例子的核心代码片段如下,我们首先分析下这段代码的业务逻辑

在模拟器上实际运行一下看看,输入命令 luatos --llt=H:\Luatools\project\luatos_framework_luatos_task_Air8000.ini

运行日志如下

可以看到,创建了2366个task之后,就提示memory不足,报错了; 和计算的理论值2526个有一定误差,这个在可以接受的范围内,一方面是因为存在内存碎片(上图的日志还有2097144-2042032=55112字节的ram无法使用,如果这部分ram可以利用,还能再多创建55112/816=67个task),另一方面可能还会存在一些我们没考虑到的其他一些地方需要消耗比较少的ram; 至此,大家应该理解了在LuatOS中,对创建的task数量没有特别限制,只要ram够用,可以一直创建所表达的意思。

参数

task_func

下面这个例子用来说明_task_func 参数的一种典型的错误使用方式_;

核心代码片段如下,我们首先分析下这段代码的业务逻辑

在模拟器上实际运行一下看看,输入命令

luatos --llt=H:\Luatools\project\luatos_framework_luatos_task_Air8000.ini

运行日志如下

黄色背景提示出现的错误是指:创建 task 的时候,传入的第一个参数期望是 function 类型,但是实际却传入了 nil 类型;

为什么会出现这个错误呢,和 Lua 语言的解析执行顺序有关;

Lua 解析器是按照从上到下的顺序解析执行代码的,首先执行第 1 行的 sys.taskInit(led_task_func),执行到这里发现 led_task_func 变量不存在,所以就报错;因为此时还没有执行过第 3 行;犯的错误是:先使用后定义。

怎么解决这个问题呢?有两种方式:

一种方式是还是在同一个 lua 文件中,参考以下代码,调整一下 led_task_func 函数定义和使用的顺序,先定义,再使用就没问题

另一种方式是以模块化的方式创建多个 lua 文件,函数定义和声明放到一个单独的 lua 库文件中,函数使用放到另一个 lua 应用文件中,在应用文件中,加载库文件,就可以使用库文件的函数;这种方式我们在这里就不介绍了,后续有完整的项目 demo,大家可以参考;

这两种解决方式,本质上,采用的都是:先定义后使用的思路。

...

**参数含义:**task的处理函数携带的可变参数;

**数据类型:**任意数据类型;

**取值范围:**无特别限制;

**是否必选:**可选传入此参数;

**注意事项:**是Lua语言的一种语法,表示可变参数,参数数量可以是0个,1个,2个,...,多个;

参数示例:

不传入,或者bool类型的true,或者number类型的2,或者string类型的"led",或者table类型的{name="LuatOS", password="123456"}等等等等;

总之,任何数据类型的任何自定义内容都行;

下面这个例子用来说明_可变参数...的使用方式_;

核心代码片段如下,我们首先分析下这段代码的业务逻辑

在模拟器上实际运行一下看看,输入命令

luatos --llt=H:\Luatools\project\luatos_framework_luatos_task_Air8000.ini

运行日志如下

从日志可以看出,sys.taskInit(led_task_func, "arg1", 3, nil, true, led_task_func) 的可变参数,"arg1", 3, nil, true, led_task_func 按照顺序传递给了任务处理函数 led_task_func;

返回值

local task_object = sys.taskInit(task_func, ...)

有一个返回值 task_object

task_object

含义说明:创建的task对象;如果为thread类型,表示创建成功;如果为nil类型,表示创建失败;

数据类型:thread或者nil;

取值范围:无特别限制;

注意事项:

虽然这个函数有返回值,但是这个返回值在整个LuatOS系统中,没有应用场景;

所以不用深入了解这个返回值的用途;

sys.taskInitEx(task_func, task_name, non_targeted_msg_cbfunc, ...)

功能

创建并且启动运行一个高级 task;刚才我们看过的 api,sys.taskInit 是创建一个基础 task;在这里,sys.taskInitEx 是创建一个高级 task,看到这里,我们接触到了两种 task:基础 task 和高级 task,等我们把这个 api 看完,再总结下基础 task 和高级 task 的区别;

sys.taskInitEx 这个 api 和sys.taskInit 这个 api 的区别主要体现在task_name, non_targeted_msg_cbfunc这两个参数上,其余内容完全一样;

所以我们接下来我们重点看一下这两个参数,其余内容就简单的过一遍;

注意事项

1、可以在能够执行到的任意代码位置使用此函数;关于这一点,我们在这里先不展开讲,等到下一小节再展开;

2、在 LuatOS 中,对创建的 task 数量没有特别限制,只要 ram 够用,可以一直创建;这一点在介绍 sys.taskInit 时已经讲过,内容完全一样,这里就不重复介绍了;

参数

task_func

task_name

**参数含义:**task的名称;

**数据类型:**推荐string类型(虽然number类型也行,但是不好理解,不要使用);

**取值范围:**任意string类型的字符串都行,无特别限制;

**是否必选:**必须传入此参数;

注意事项:

在一个的LuatOS项目中,创建的所有高级task的task_name不能重复;

目前在核心库中没有检查是否重复,需要用户自行保证;

参数示例:"LED_TASK"、"GPIO_TASK"等任意自定义的字符串;

task_name 这个参数,字面意思来看,表示 task 的名称;实际上,在 sys 核心库内,会根据这个 task_name 创建一个资源表,资源表中有一个消息队列,这个消息队列就可以看做是这个高级 task 专有的消息队列;所以说,当一个高级 task 有了自己的消息队列,谁都可以发送定向消息到这个 task 的消息队列中,提高了消息处理的效率;

我们再看一下创建基础 task 的 api,sys.taskInit(task_func, ...),没有 task_name,在 sys 核心库内,不会为基础 task 创建独立的消息队列。虽然如此,但是会创建一个全局的消息队列,所有的基础 task 和高级 task 都会共享使用这一个全局消息队列。

现在大家对基础 task,高级 task,独立消息队列,全局消息队列先有一个基本的认识,后续我们还会通过示例以及文字描述来做进一步总结。

non_targeted_msg_cbfunc

这个参数的意思是:当一个高级 task 在读取自己的独立消息队列中的某一种消息时,发现读出来的消息不是自己期望的消息,则直接把这个消息丢给_non_targeted_msg_cbfunc_处理。

下面这个例子用来说明如何使用;

这个例子的核心代码片段如下,我们首先分析下这段代码的业务逻辑

我们在模拟器上实际运行一下看看,输入命令

luatos --llt=H:\Luatools\project\luatos_framework_luatos_task_Air8000.ini

运行日志如下:

红色背景的日志为非目标消息回调函数的运行逻辑;

绿色背景的日志为目标消息的运行逻辑;

从日志可以看出,在高级 task 内部使用 sys.waitMsg 等待目标消息(消息名为"MQTT_EVENT")时,如果收到了非目标消息,都给非目标消息回调函数 mqtt_client_main_cbfunc 去处理了。

**参数含义:**task的处理函数携带的可变参数;

**数据类型:**任意数据类型;

**取值范围:**无特别限制;

**是否必选:**可选传入此参数;

注意事项:...是Lua语言的一种语法,表示可变参数,参数数量可以是0个,1个,2个,...,多个;

参数示例:

不传入,或者boolean类型的true,或者number类型的2,或者string类型的"led",或者table类型的{name="LuatOS", password="123456"}等等等等;

总之,任何数据类型的任何自定义内容都行;

返回值

local task_object = sys.taskInitEx(task_func, task_name, non_targeted_msg_cbfunc, ...)

有一个返回值 task_object

task_object

**含义说明:**表示创建的task对象;如果为thread类型,表示创建成功;如果为nil类型,表示创建失败;

**数据类型:**thread或者nil;

**取值范围:**无特别限制;

**注意事项:**虽然这个函数有返回值,但是这个返回值在整个LuatOS系统中,没有应用场景,所以不用深入了解这个返回值的用途;

基础 task 和高级 task 的区别

现在我们回顾一下刚才讲的两个 api,一个是创建基础 task,一个是创建高级 task;

在这里我们对基础 task 和高级 task 先做一个简单的总结:

task分为基础task和高级task两种;

从设计原理的角度来看,基础task和高级task的区别是:

(1) 所有的基础task共享一个全局消息队列;

(2) 每个高级task都有自己独立的消息队列,同时又能使用全局消息队列;

从用户使用的角度来看,基础task和高级task的区别是:

(1) 基础task如果阻塞功能使用不当,可能会丢失自己应该处理的消息;

(2) 高级task如果阻塞功能使用不当,不会丢失自己应该处理的消息;

虽然从设计原理来看,高级task比基础task使用起来不容易犯错;

但是由于基础task使用起来简洁,基础task还是需要掌握,一旦掌握之后,也不容易犯错;

sys核心库提供的task管理功能有以下几种:

(1) 基础task的创建和启动运行:sys.taskInit(task_func, ...)

(2) 高级task的创建和启动运行:sys.taskInitEx(task_func, task_name, non_targeted_msg_cbfunc, ...)

在这里,大家对这两种 task 的区别有一个基本的认识就行,在后续消息章节,我们还会进一步深入讲解;

2.1.3.2 什么时间创建

理解了怎么创建 task 之后,我们来看下一个问题;

在什么时间点创建 task,在代码的什么位置创建 task?

task 的创建时间点非常灵活,例如:

1、每个应用功能模块初始化时,可以创建 task;

2、检测到某个事件发生时,例如 4G 模组检测到插入了一张 sim 卡时,可以创建 task;

3、在一个 task 的处理函数中运行时,可以创建另外一个 task;

4、......等等各种场景

总结下来只有一句话:只要写的一段代码能被执行到,在这段代码中就可以使用 api 创建 task。

那是不是意味着,在项目应用软件代码的任何位置都能创建 task,也并不是,只有一个例外;

在 main.lua 中 sys.run()之后不能写代码创建 task;

为什么有这个限制,我们在 1.4 章节,基于 hello_luatos 讲解 LuatOS 应用软件的运行逻辑时已经讲到,我们再根据下面这张图以及 hello_luatos 代码来简单的回顾一下:

2.1.3.3 应用脚本代码运行位置

刚才在学习 sys.taskInit 和 sys.taskInitEx 这两个 api 时,有以下两段话:

只要写的一段代码能被执行到,在这段代码中就可以使用这两个 api 创建 task;

回调函数是在 task 之外的业务逻辑中被执行的; 在回调函数内部无法使用 sys.wait(timeout)、sys.waitUntil(msg, timeout)、sys.waitMsg(task_name, msg, timeout)等必须用在 task 中的函数;

从这两段话中,引出一个问题:在 LuatOS 应用脚本开发过程中,我们所编写的应用脚本代码,存在两种业务运行的逻辑环境:

1、一种是在 task 的任务处理函数内部的业务逻辑环境中运行,我们简称为:在 task 内部运行;

2、一种是在 task 的任务处理函数外部的业务逻辑环境中运行,我们简称为:在 task 外部运行;

怎么理解这两种业务逻辑运行环境?我们看下面这张图

看右边生长出分支的这棵大树,这棵大树就是 FreeRTOS 创建的 Lua 虚拟机 task,是一个 FreeRTOS task;

在这个 Lua 虚拟机 FreeRTOS task 上,这棵大树再分为两部分:

1、树干部分:树干部分运行的业务逻辑环境就是 LuatOS task 外部运行环境;

2、树枝部分:每个树枝都是一个独立的 LuatOS task,树枝部分运行的业务逻辑环境就是 LuatOS task 内部运行环境;

在这里,大家只要知道有 task 内部运行和 task 外部运行两种环境即可,后续我们讲解其他功能时,会用到这两种概念,并且也会结合示例来说明二者的差别;

由于篇幅过长,更多精彩内容,请看下篇~

相关推荐
康小庄1 小时前
List线程不安全解决办法和适用场景
java·数据结构·spring boot·spring·list·intellij-idea
会算数的⑨1 小时前
Spring AI Alibaba学习(一)—— RAG
java·人工智能·后端·学习·spring·saa
IT 行者1 小时前
Spring Security 7 响应头配置完全指南
java·后端·spring·security
bug-0071 小时前
springboot 自定义消息处理
java·spring boot·后端
我真的是大笨蛋1 小时前
MySQL临时表深度解析
java·数据库·sql·mysql·缓存·性能优化
九皇叔叔1 小时前
【02】微服务系列 之 初始化工程
java·数据库·微服务
lxl13071 小时前
学习C++(4)构造函数+析构函数+拷贝构造函数
开发语言·c++·学习
季明洵1 小时前
两数之和、四数相加II、三数之和、四数之和
java·数据结构·算法·leetcode·蓝桥杯·哈希算法
人道领域1 小时前
javaWeb从入门到进阶(SpringBoot基础案例3)
java·spring boot·后端