跳转至

作者:朱天华

Hello,大家好,我是朱天华。

欢迎大家来到合宙 LuatOS 直播课堂,一起学习 LuatOS 课程。

自我介绍

首先做一下自我介绍,大家可以简单看一下,我就不逐字阅读了。

  1. 2012 年加入合宙(当时的公司是合宙的前身,名字还不叫合宙,实际上合宙 2014 年才成立);
  2. 在合宙做过软件工程师,技术支持部负责人,软件部负责人,产品经理;
  3. 使用 LuatOS-Air 开发过很多量产项目,例如定位器,云喇叭,冰柜,血压计等等等等;

第一部分:LuatOS 课程背景

1.1 合宙介绍

先简单的介绍一下合宙,内容也不少,我还是不逐字阅读了,只挑重点的黄色内容看下。

上海合宙通信科技有限公司,注册地位于上海市黄浦区,公司总部位于上海浦东新区。

在深圳设立有分公司,从事销售和技术工作。

在开封设立有分公司,从事销售和技术工作,同时还设有仓库,用于电商平台发货;

在四川设有工厂,从事合宙模组和工业引擎硬件的制造和生产。

公司属于信息传输、软件和信息技术服务业,聚焦物联网无线通信领域。主营业务为自主设计、开发、制造无线通信模块、工业引擎等通信产品,同时配套相关服务。2014 年底,合宙推出 LuatOS 开源平台,将公司在模组行业多年的研发成果开放,便于开发者进行二次开发,构建起了物联网研发生态。

公司客户主要面向有物联网无线通信需求的企业。重点销售领域包括智能安防、金融支付终端、农业物联网、远程医疗监测、智能家居等多个领域。目前,已服务众多企业,使用合宙模块出货的企业有数万家。

1.2 合宙目前的主推硬件产品介绍

从上面的合宙介绍章节,可以看到,截止到目前(2025 年 9 月),合宙对外开放 LuatOS 软件系统,已经将近 11 年;再加上在对外开放前,合宙已经使用了 3 年的 LuatOS 来开发内部产品,所以总的来说,合宙已经投入 14 年的时间,来精心打磨 LuatOS。

在正式介绍 LuatOS 之前,我们先来大概看一下目前合宙主推的一系列硬件产品。

为什么要介绍硬件产品呢?

因为 LuatOS 是一个软件系统,需要运行在合宙的硬件产品上。只有打造一个“硬件搭台,软件唱戏”的完整生态系统,才能发挥出 LuatOS 的巨大价值,才能让 LuatOS 的易用性真正服务到每一个客户。

完整的合宙硬件产品选型手册,大家可以点击 合宙产品选型手册 自行阅读。

在这里我简单地对目前合宙主推的硬件产品做一下介绍,如下面几张图片所示

从这张图可以看出,目前合宙主推以下四种硬件产品:

  1. Air8000 工业引擎系列:Air8000 系列绝对可以称得上是合宙在 2025 年的明星产品,集成度高,功能全,支持 4G/WiFi/以太网/蓝牙/GNSS/G-sensor/VoLTE/TTS/内部充电/2.6V 到 4.35V 的宽压供电/音频/摄像头/LCD/UART/CAN/I2C/PWM/SPI/485/232 等众多功能,提供了一个 全能旗舰版 和多个 功能裁剪版 给客户自由选择,可以大大简化客户产品的硬件设计;提供了国内版、北美版和欧亚版,一套软件,全球可用,大大降低维护成本;
  2. Air7XX 模组系列: Air780 系列是合宙 4G 模组的经典产品,在支持基本 4G 数传/外挂以太网数传和基本外设的功能基础上,从 Flash/RAM 资源,UI,音频,GNSS,国内国外可用,几个维度细分为多种硬件版本,满足客户的多样化需求; Air700 系列是黄豆大小面积的 4G 模组经典产品,适用于穿戴等对模组尺寸要求较小的场景; Air7000 系列是多网融合的全球通模组,支持 64MB 的 DDR 和 128MB 的 PSRAM,支持 4G/WiFi/以太网/蓝牙/GNSS,支持 VoLTE/TTS;
  3. Air6101/Air6102 等 Air6XXX WiFi 模组系列从基本的数传应用,到多媒体应用,支持 WiFi/外挂以太网/外挂 4G/蓝牙,支持 720P 分辨率的 LCD 显示以及 100 万像素摄像头拍照,未来还会支持音频/视频录制/多种音视频协议;
  4. Air10XX 系列高性能 MCU合宙正在规划的 MCU 类产品,拥有 88 个管脚,16MB RAM,可以搭载 LuatOS 操作系统,高效开发,快人一步;

目前合宙主推的这四种系列的硬件产品,除了 Air7XX 模组系列中有几款 AT 开发专用的产品外,其余所有硬件产品都全面支持 LuatOS 二次开发。

1.3 LuatOS 的发展历程

在了解了合宙主推的可以运行 LuatOS 的硬件产品之后 ,接下来,我从时间线的角度,梳理一下 LuatOS 的发展历程。

1.3.1 历史和现状

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

历经十余年的发展,搭载 LuatOS 的合宙硬件产品已经出货数亿只,涵盖 2G、4G、MCU、Wi-Fi 类产品,稳定可靠。

1.3.2 为什么选择 Lua 脚本语言

LuatOS 二次开发使用的编程语言为 Lua 脚本语言。

2012 年,软件二次开发的主流语言是 C 语言,当时合宙内部使用 2G 蜂窝模组,搭建软件二次开发的框架时,为什么选择了 Lua 语言呢?

当时合宙开发了众多定位器项目,项目需求不同,如果使用 C 语言开发,开发效率低,对软件工程师的能力要求较高;经过调研,就选择了免费轻量级的嵌入式 Lua 脚本语言;

Lua 语言的优势如下:

和 C 语言相比

  • 语法简单,学习成本低;(一个大学生学习两周我们的 LuatOS,基本可以独立开发收款音箱项目)
  • 具有内存自动回收机制,不用担心 C 语言中由于开发能力不足,经常出现的内存泄漏问题;

和其他脚本语言相比

  • Lua 语言是运行速度最快的脚本语言;下面这张图是主流编程语言的运行效率对照图,从图中可以看出,所有编程语言中,C 语言运行效率最高,Lua 脚本语言的运行效率是 C 语言的 1/30,在所有脚本语言中的运行效率最高;另外一种非常流行的 Python 脚本语言的运行效率是 C 语言的 1/200;

  • 体积小巧,最小仅需 16KB RAM,、128KB Flash 就可以运行 Lua 虚拟机,非常适合运行在 Flash 和 RAM 资源有限的蜂窝模组硬件产品上;

Lua 语言的劣势如下:

尽管 Lua 是运行速度最快的脚本语言,但是比 C 语言还是慢不少,运行效率是 C 语言的 1/30;对于一些实时性要求比较高的场景,比如微秒级别,几十毫秒级别的高精度定时器,都会有一定误差;

虽然有这个劣势,但是 90% 以上的物联网应用场景对实时性要求并不高,所以最终我们选择了 Lua 语言来搭建 LuatOS 软件系统。

1.3.3 为什么命名为 LuatOS

2012 年,合宙基于 2G 蜂窝模组,使用 Lua 语言搭建软件二次开发框架时,2G 模组的软件主流开发方式仍然是传统的 AT 开发,2G 模组仅仅做为一种标准配件,通过 UART 连接一个 MCU,MCU 和 2G 模组之间的 UART 通信完全由 AT 命令实现。

以一个温湿度传感器上传到服务器为例,当时主流的 AT 开发方式如下图所示:

  1. MCU 硬件通过 I2C 连接温湿度传感器硬件,MCU 硬件通过 UART 连接 2G 蜂窝模组硬件;
  2. MCU 进行软件编程,通过 I2C 读取温湿度传感器中的温湿度数据;
  3. MCU 进行软件编程,通过 UART 发送 AT 命令 AT+CIPSTART 控制 2G 蜂窝模组硬件连接服务器,发送 AT 命令 AT+CIPSEND 给服务器发送温湿度数据;

在今天,AT 软件开发方式仍然占有一席之地,合宙 Air780 系列模组下也有支持 AT 开发的子产品型号。

还是以这个温湿度传感器上传到服务器为例,当时合宙使用 Lua 语言想要搭建的二次开发框架如下图所示:

和传统的 AT 软件开发方式相比,主要有以下几点区别

  1. 软件二次开发框架,替换掉了 MCU+AT 的模式,支持多任务 + 异步处理,使得软件更加稳定可靠
  2. 2G 蜂窝模组硬件通过 I2C 连接温湿度传感器硬件,运行在 2G 蜂窝模组之上的 软件二次开发框架 软件编程去读取温湿度传感器的数据;
  3. 软件二次开发框架 软件 和 其他业务软件 之间的业务逻辑控制,一部分仍然使用的是 AT 命令控制的方式,只不过这些 AT 命令是跑在一种纯软件实现的虚拟 UART 之上;

讲到这里,我们来对 LuatOS 进行拆分:

Luat = Lua + AT;

Lua 是指这种 软件二次开发框架 使用的是 Lua 语言进行编程;

AT 是指 软件二次开发框架 和 2G 蜂窝模组硬件中的其他业务软件之间的逻辑控制,相当一部分功能使用的还是 AT 命令去控制;

Lua + AT,因为有两个连续的字母 a,读起来不通顺,所有删掉其中一个大写字母 A;这就是 Luat 的由来。

虽然 LuatOS 发展到今天,LuatOS 软件系统和硬件上的其他业务软件之间的逻辑控制,已经完全不再使用 AT 命令,全部改成了 API 调用;但是之前的 Luat 命名存在了七、八年之久,已经深入人心,轻易改名影响太大,所以 Luat 就一直沿用了下来。

LuatOS = Luat + OS;

介绍清楚 Luat 之后,OS 比较好理解,就是操作系统的意思,因为 LuatOS 具备操作系统的一些核心特性和功能,包括任务调度与管理,内存管理,文件系统,网络与通信协议栈等。

介绍完 LuatOS 命名的由来,再来看一下 LuatOS 怎么读,既然 LuatOS = Lua + AT + OS,所以就读作 Lu a ti OS,用中英文来混合记忆就是 “噜啊替 OS”。

1.4 为什么要讲 LuatOS 课程

在了解了 LuatOS 的发展历程之后,我们再来看下一个问题,为什么要讲 LuatOS 课程?

LuatOS 是一款专为 资源受限的嵌入式设备 和 物联网(IoT)场景 设计的轻量级操作系统,其核心优势在于 开发非常高效。

截止到今天,虽然至少有上万家客户已经使用合宙的 LuatOS 成功开发并且量产项目;但是这个数量远远不够,合宙的目标是,让中国每个做物联网的企业和工程师都知道 LuatOS,如果能用 LuatOS 开发和量产项目,就更好了。

为了实现这个目标,合宙通过 docs 文档中心搭建,gitee 开源生态建设,企业微信群实时答疑,各种技术社区发布技术文章,公众号/视频号宣传等多种途径,一直在大力推广 LuatOS。

但是在技术直播渠道上一直没有动作,为了弥补技术直播的空白,所以从今天开始,合宙决定正式开设 LuatOS 线上直播课程。

1.5 LuatOS 课程包含哪些内容

目前初步规划的 LuatOS 课程包含以下内容:

  1. LuatOS 框架
  2. 网络协议:socket,mqtt,http,ftp,websocket,ntp
  3. 合宙提供的网络应用服务:fota,errDump
  4. 4G/WiFi/以太网 单网使用以及多网切换使用
  5. 4G/WiFi/以太网 多网融合使用;
  6. 外设驱动:adc,uart,modbus,gpio,i2c,spi,pwm,can,onewire
  7. 多媒体:audio,lcd/u8g2,camera,AirUI,AirTalk
  8. 数据永久存储:fs,fskv,tfcard,sfud,little_flash
  9. WiFi/蓝牙服务:ble,WiFi 配网
  10. 位置服务:GNSS,AirLBS,WiFiLocation,jt808
  11. 低功耗:lowpower
  12. 运营商服务:cc,sms

每隔两周或者每隔一周直播一个课程主题;

1.6 今天的 LuatOS 框架课程主要讲哪些内容

今天是 LuatOS 课程的第 001 讲,LuatOS 框架;

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

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

  1. LuatOS 软件的构成
  2. LuatOS 开发环境的说明
  3. LuatOS 用户脚本程序的运行机制
  4. LuatOS 的 sys 核心库中任务、消息、定时器和调度器功能的使用
  5. LuatOS 的 sys 核心库的源码分析

第二部分:初步认识 LuatOS 开发

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

2.1 Lua 语言介绍

用户基于 LuatOS 进行软件二次开发,使用的编程语言为 Lua 语言,关于 Lua 语言的知识,大家参考以下资料系统性地自学就行了:

  1. 合宙 docs 网站 Lua 精简版
  2. Lua 菜鸟教程
  3. Lua 官方英文原版
  4. 非官方中文翻译版
  5. 直接借助于 AI 工具来学习

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

2.2 LuatOS 软件的构成

2.2.1 LuatOS 软件架构

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

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

2.2.2 LuatOS 内核固件

  1. 内核固件(又叫固件,底层固件,或者 core),合宙会针对每种硬件产品编译好并且对外发布内核固件,例如现在合宙主推的几种硬件产品对应的 LuatOS 内核固件的发布地址分别为:

  2. Air8000 系列:https://docs.openluat.com/air8000/luatos/firmware/

  3. Air780EHM/EPM:https://docs.openluat.com/air780epm/luatos/firmware/version/
  4. Air780EHV:https://docs.openluat.com/air780ehv/luatos/firmware/version/
  5. Air780EGH:https://docs.openluat.com/air780egh/luatos/firmware/version/
  6. Air8101 系列:https://docs.openluat.com/air8101/luatos/firmware/
  7. 内核固件文件以 soc 做为后缀,例如

  8. LuatOS-SoC_V2008Air80001.soc 表示 Air8000 硬件 V2008 版本的 1 号LuatOS 固件文件;

  9. LuatOS-SoC_V2008_Air780EHM.soc 表示 Air780EHM 硬件 V2008版本的 LuatOS 固件文件;
  10. LuatOS-SoC_V1004_Air8101.soc 表示 Air8101 硬件 V1004版本的 LuatOS 固件文件;
  11. 内核固件包含主芯片系统平台层(这里有一个嵌入式操作系统,目前比较流行的是 FreeRTOS,接下来本文用到的嵌入式操作系统都会以 FreeRTOS 为例来说明),LuatOS 适配层,Lua 虚拟机,Lua 标准库,LuatOS 核心库几部分,除了主芯片系统平台层的源码没有开放外,其余四部分的源码全部开放,源码路径:https://gitee.com/openLuat/LuatOS,有兴趣深入学习的朋友可以自行阅读学习;
  12. 内核固件中和用户软件二次开发息息相关的两部分是 Lua 标准库和 LuatOS 核心库,接下来我们看下这两部分内容;

2.2.3 Lua 标准库

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

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

主要包含以下几种库

  1. 基础库(Basic Library):支持 collectgarbage(垃圾回收)、_G(全局变量表)、ipairs(迭代数组元素)、pairs(迭代键值对)、tostring(转换为字符串)、tonumber(转换为数字)、type(获取类型名)等功能函数;
  2. 协程库(Coroutine Library):支持 coroutine.create(创建协程)、coroutine.yield(挂起协程)、coroutine.resume(恢复协程)、coroutine.status(获取协程状态)、coroutine.running(获取正在运行的协程)等功能函数;
  3. 字符串处理库(String Library):支持 string.len(获取字符串长度)、string.match(字符串模式匹配)、string.byte(获取字符的 ascii 码)、string.char(获取 ascii 码对应的字符)等功能函数;
  4. 表处理库(Table Library):支持 table.insert(插入元素)、table.remove(移除元素)、table.concat(将表中的元素连接为一个字符串)、table.unpack(解包表中的元素为多个返回值)等功能函数;

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

LuatOS 中使用的 Lua 是 5.3 版本,大家可以通过以下链接自行学习 Lua 标准库的用法:

  1. Lua 官方英文版
  2. 非官方中文翻译版

2.2.4 LuatOS 核心库

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

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

目前支持 74 个核心库,详细介绍可以参考:LuatOS 核心库 ;我们打开这个核心库页面先总体浏览一遍;

核心库中的所有功能模块,我们在后续课程中都会逐一讲解;

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

2.2.5 LuatOS 扩展库

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

LuatOS 扩展库是用 Lua 语言实现的功能模块,源码开放:https://gitee.com/openLuat/LuatOS/tree/master/script/libs

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

目前支持 19 个扩展库,详细介绍可以参考:LuatOS 扩展库 ;我们打开这个扩展页面先总体浏览一遍;

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

2.2.6 LuatOS demo

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

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

现在合宙主推的几种硬件产品对应的 LuatOS demo 的发布地址分别为:

demo 中的大部分内容,我们在后续课程中都会进行讲解。

2.2.7 LuaOS project

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

现在合宙主推的几种硬件产品(主要是核心板和整机开发板)对应的 LuatOS project 的发布地址分别为:

project 中的大部分内容,我们在后续课程中都会进行讲解。

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

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

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

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

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

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

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

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

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

2.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 项目如何使用。

2.3.2 LuatOS 软件的运行载体

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

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

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

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

  1. 合宙硬件产品的核心板或者开发板:例如 Air8000 的核心板和开发板,Air780EHM/EHV/EGH/EPM 的核心板和开发板,Air8101 的核心板和开发板; 本讲课程中我们将使用 Air8000 的核心板来运行 LuatOS 软件进行演示,Air8000 核心板的外观如下图所示 Air8000 核心板的详细使用说明,大家可以访问:https://docs.openluat.com/air8000/product/shouce/#air8000_1 自行学习,在这里我就不讲解了。
  2. PC 模拟器:直接可以在电脑上使用 LuatOS 模拟器来运行,使用模拟器运行有一些限制,模拟器支持的功能并不完整,例如和外设有关的功能,lcd,字库等可能并不支持,但是一些纯软件的功能,例如 LuatOS 运行框架,网络应用等还是可以模拟运行的。所以本讲的 LuatOS 运行框架,我们也会使用 PC 模拟器来进行演示,这样可以节省硬件载体烧录软件所花费的时间。

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

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

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

关于 Air8000 上 Luatools 的详细说明,大家可以参考以下两篇文章:

https://docs.openluat.com/air8000/luatos/common/download/

https://docs.openluat.com/air8000/luatos/common/hello/

在这里我就不详细说明 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 下面我来实际演示这个过程。

2.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

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

2.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 核心库,这个核心库文档参考:https://docs.openluat.com/osapi/core/sys/
  4. 基于一个相对完整的 LuatOS 项目,分析本节课学习到的核心概念,调度器和核心库知识,系统理解本节课的知识在实际项目中的应用方法;

3.1 LuatOS 的任务(task)

3.1.1 基本概念

3.1.1.1 FreeRTOS task 和 LuatOS task

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

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

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

  1. 第一种是 LuatOS 内核固件中的任务,也就是 FreeRTOS 创建的任务;这种任务我们把它命名为 FreeRTOS task;
  2. 第二种是 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. 这些大树分成了两种:

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

  5. 一种是只长了光秃秃的树干,树干上没有树枝,这种大树有很多,除 Lua 虚拟机之外,其余所有任务都是这种大树
  6. Lua 虚拟机任务这棵大树的树干上长出的所有树枝,就是一个个的 LuatOS task

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

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

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

区别项
FreeRTOS task
LuatOS task
任务优先级
不同的task支持不同的优先级,也支持相同的优先级
task没有优先级的概念
任务调度
1、高优先级的task准备就绪后,可以抢占当前正在运行的低优先级task的执行权2、相同优先级的task,采取时间片轮转的方式获得执行权采用的是 抢占式+时间片轮转 的调度方式
1、当前正在执行的task,必须自己主动让出执行权,其他task才有可能执行;2、也就是说,我正在运行,其他task别想打断我,我只要自己不想停,其他task都没有机会得到运行采用的是 协作式 的调度方式
多任务访问共享资源
提供了临界区、互斥锁、信号量等方法实现了共享资源的安全访问,防止数据竞争或者资源冲突
不需要复杂的共享资源安全访问机制,完全靠协作式的任务调度来控制,使用起来非常简单

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

3.1.1.2 LuatOS 的多任务调度机制

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

这个例子的完整代码链接:scheduling.lua

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

--[[
@module  scheduling
@summary task调度演示 
@version 1.0
@date    2025.08.12
@author  朱天华
@usage
本文件为scheduling应用功能模块,用来演示task协作式的调度方式,核心业务逻辑为:
1、创建两个task,task1和task2;
2、在task1的任务处理函数中,每隔500毫秒,task1的计数器加1,并且通过日志打印task1计数器的值;
3、在task2的任务处理函数中,每隔300毫秒,task2的计数器加1,并且通过日志打印task2计数器的值;

本文件没有对外接口,直接在main.lua中require "scheduling"就可以加载运行;
]]

-- 第一个task的任务处理函数
local function task1_func()
    local count = 0
    while true do
        count = count + 1
        log.info("task1_func", "运行中,计数:", count)
        -- 等待500ms
        sys.wait(500)  
    end
end

-- 第二个task的任务处理函数
local function task2_func()
    local count = 0
    while true do
        count = count + 1
        log.info("task2_func", "运行中,计数:", count)
        -- 等待300ms
        sys.wait(300)  
    end
end

-- 创建并启动第一个task
-- 运行这个task的任务处理函数task1_func
sys.taskInit(task1_func)

log.info("task_scheduling", "after task1 and before task2")

-- 创建并启动第二个task
-- 运行这个task的任务处理函数task2_func
sys.taskInit(task2_func)

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

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

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

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

运行日志如下

[2025-08-28 22:18:58.169][00000000.021] I/user.task1_func 运行中,计数: 1
[2025-08-28 22:18:58.171][00000000.023] I/user.task_scheduling after task1 and before task2
[2025-08-28 22:18:58.175][00000000.027] I/user.task2_func 运行中,计数: 1
[2025-08-28 22:18:58.462][00000000.314] I/user.task2_func 运行中,计数: 2
[2025-08-28 22:18:58.661][00000000.514] I/user.task1_func 运行中,计数: 2
[2025-08-28 22:18:58.772][00000000.624] I/user.task2_func 运行中,计数: 3
[2025-08-28 22:18:59.087][00000000.939] I/user.task2_func 运行中,计数: 4
[2025-08-28 22:18:59.162][00000001.014] I/user.task1_func 运行中,计数: 3
[2025-08-28 22:18:59.393][00000001.245] I/user.task2_func 运行中,计数: 5
[2025-08-28 22:18:59.662][00000001.515] I/user.task1_func 运行中,计数: 4
[2025-08-28 22:18:59.703][00000001.556] I/user.task2_func 运行中,计数: 6
[2025-08-28 22:19:00.011][00000001.864] I/user.task2_func 运行中,计数: 7
[2025-08-28 22:19:00.171][00000002.024] I/user.task1_func 运行中,计数: 5
[2025-08-28 22:19:00.311][00000002.163] I/user.task2_func 运行中,计数: 8
[2025-08-28 22:19:00.617][00000002.470] I/user.task2_func 运行中,计数: 9
[2025-08-28 22:19:00.685][00000002.537] I/user.task1_func 运行中,计数: 6
[2025-08-28 22:19:00.927][00000002.779] I/user.task2_func 运行中,计数: 10
[2025-08-28 22:19:01.186][00000003.038] I/user.task1_func 运行中,计数: 7
[2025-08-28 22:19:01.241][00000003.094] I/user.task2_func 运行中,计数: 11
[2025-08-28 22:19:01.548][00000003.401] I/user.task2_func 运行中,计数: 12
[2025-08-28 22:19:01.694][00000003.546] I/user.task1_func 运行中,计数: 8
[2025-08-28 22:19:01.850][00000003.702] I/user.task2_func 运行中,计数: 13
[2025-08-28 22:19:02.164][00000004.017] I/user.task2_func 运行中,计数: 14
[2025-08-28 22:19:02.206][00000004.059] I/user.task1_func 运行中,计数: 9

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

通过以上代码和日志的分析,可以知道,在这个程序中有 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) :

--[[
@module  scheduling
@summary task调度演示 
@version 1.0
@date    2025.08.12
@author  朱天华
@usage
本文件为scheduling应用功能模块,用来演示task协作式的调度方式,核心业务逻辑为:
1、创建两个task,task1和task2;
2、在task1的任务处理函数中,每隔500毫秒,task1的计数器加1,并且通过日志打印task1计数器的值;
3、在task2的任务处理函数中,每隔300毫秒,task2的计数器加1,并且通过日志打印task2计数器的值;

本文件没有对外接口,直接在main.lua中require "scheduling"就可以加载运行;
]]

-- 第一个task的任务处理函数
local function task1_func()
    local count = 0
    while true do
        count = count + 1
        log.info("task1_func", "运行中,计数:", count)
        -- 等待500ms
        -- sys.wait(500)  
    end
end

-- 第二个task的任务处理函数
local function task2_func()
    local count = 0
    while true do
        count = count + 1
        log.info("task2_func", "运行中,计数:", count)
        -- 等待300ms
        sys.wait(300)  
    end
end

-- 创建并启动第一个task
-- 运行这个task的任务处理函数task1_func
sys.taskInit(task1_func)

log.info("task_scheduling", "after task1 and before task2")

-- 创建并启动第二个task
-- 运行这个task的任务处理函数task2_func
sys.taskInit(task2_func)

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

[2025-08-28 22:22:47.351][00000000.128] I/user.task1_func 运行中,计数: 1
[2025-08-28 22:22:47.355][00000000.132] I/user.task1_func 运行中,计数: 2
[2025-08-28 22:22:47.356][00000000.132] I/user.task1_func 运行中,计数: 3
[2025-08-28 22:22:47.365][00000000.142] I/user.task1_func 运行中,计数: 4
[2025-08-28 22:22:47.366][00000000.142] I/user.task1_func 运行中,计数: 5
[2025-08-28 22:22:47.368][00000000.145] I/user.task1_func 运行中,计数: 6
[2025-08-28 22:22:47.371][00000000.148] I/user.task1_func 运行中,计数: 7
[2025-08-28 22:22:47.381][00000000.158] I/user.task1_func 运行中,计数: 8
[2025-08-28 22:22:47.384][00000000.160] I/user.task1_func 运行中,计数: 9
[2025-08-28 22:22:47.385][00000000.162] I/user.task1_func 运行中,计数: 10
[2025-08-28 22:22:47.386][00000000.163] I/user.task1_func 运行中,计数: 11
[2025-08-28 22:22:47.394][00000000.171] I/user.task1_func 运行中,计数: 12
[2025-08-28 22:22:47.400][00000000.177] I/user.task1_func 运行中,计数: 13
[2025-08-28 22:22:47.401][00000000.178] I/user.task1_func 运行中,计数: 14
[2025-08-28 22:22:47.408][00000000.185] I/user.task1_func 运行中,计数: 15
[2025-08-28 22:22:47.410][00000000.186] I/user.task1_func 运行中,计数: 16
[2025-08-28 22:22:47.410][00000000.187] I/user.task1_func 运行中,计数: 17
[2025-08-28 22:22:47.413][00000000.189] I/user.task1_func 运行中,计数: 18
[2025-08-28 22:22:47.413][00000000.190] I/user.task1_func 运行中,计数: 19
[2025-08-28 22:22:47.414][00000000.190] I/user.task1_func 运行中,计数: 20
[2025-08-28 22:22:47.414][00000000.191] I/user.task1_func 运行中,计数: 21
[2025-08-28 22:22:47.415][00000000.191] I/user.task1_func 运行中,计数: 22
[2025-08-28 22:22:47.415][00000000.192] I/user.task1_func 运行中,计数: 23
[2025-08-28 22:22:47.416][00000000.192] I/user.task1_func 运行中,计数: 24
[2025-08-28 22:22:47.416][00000000.193] I/user.task1_func 运行中,计数: 25
[2025-08-28 22:22:47.417][00000000.193] I/user.task1_func 运行中,计数: 26
[2025-08-28 22:22:47.417][00000000.194] I/user.task1_func 运行中,计数: 27
[2025-08-28 22:22:47.417][00000000.194] I/user.task1_func 运行中,计数: 28
[2025-08-28 22:22:47.417][00000000.194] I/user.task1_func 运行中,计数: 29
[2025-08-28 22:22:47.418][00000000.194] I/user.task1_func 运行中,计数: 30
[2025-08-28 22:22:47.418][00000000.195] I/user.task1_func 运行中,计数: 31
[2025-08-28 22:22:47.419][00000000.195] I/user.task1_func 运行中,计数: 32
[2025-08-28 22:22:47.419][00000000.196] I/user.task1_func 运行中,计数: 33
[2025-08-28 22:22:47.419][00000000.196] I/user.task1_func 运行中,计数: 34
[2025-08-28 22:22:47.419][00000000.196] I/user.task1_func 运行中,计数: 35
[2025-08-28 22:22:47.420][00000000.196] I/user.task1_func 运行中,计数: 36
[2025-08-28 22:22:47.420][00000000.197] I/user.task1_func 运行中,计数: 37
[2025-08-28 22:22:47.420][00000000.197] I/user.task1_func 运行中,计数: 38
[2025-08-28 22:22:47.421][00000000.197] I/user.task1_func 运行中,计数: 39
[2025-08-28 22:22:47.421][00000000.198] I/user.task1_func 运行中,计数: 40
[2025-08-28 22:22:47.421][00000000.198] I/user.task1_func 运行中,计数: 41
[2025-08-28 22:22:47.422][00000000.198] I/user.task1_func 运行中,计数: 42
[2025-08-28 22:22:47.422][00000000.198] I/user.task1_func 运行中,计数: 43
[2025-08-28 22:22:47.422][00000000.199] I/user.task1_func 运行中,计数: 44
[2025-08-28 22:22:47.422][00000000.199] I/user.task1_func 运行中,计数: 45
[2025-08-28 22:22:47.422][00000000.199] I/user.task1_func 运行中,计数: 46
[2025-08-28 22:22:47.423][00000000.199] I/user.task1_func 运行中,计数: 47
[2025-08-28 22:22:47.430][00000000.207] I/user.task1_func 运行中,计数: 48
[2025-08-28 22:22:47.431][00000000.208] I/user.task1_func 运行中,计数: 49
[2025-08-28 22:22:47.433][00000000.209] I/user.task1_func 运行中,计数: 50

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

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

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

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

这个例子的完整代码链接:shared_resource.lua

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

--[[
@module  shared_resource
@summary 共享资源访问演示 
@version 1.0
@date    2025.08.15
@author  朱天华
@usage
本文件为shared_resource应用功能模块,用来演示多个task访问共享资源的功能,核心业务逻辑为:
1、创建一个全局共享变量global_shared_variable,变量值初始化为0;
2、创建两个task,task1和task2;
2、在task1的任务处理函数中:
   (1) 每隔1秒,执行一次for循环
   (2) 循环体内循环100次,每次将全局共享变量global_shared_variable的值加1
3、在task2的任务处理函数中,每隔300毫秒,task2的计数器加1,并且通过日志打印task2计数器的值;

本文件没有对外接口,直接在main.lua中require "task_scheduling"就可以加载运行;
]]

-- 全局共享变量,初始值为0
local global_shared_variable = 0

-- 第一个task的任务处理函数
local function task1_func()
    while true do
        log.info("task1_func", "for循环前,全局共享变量的值:", global_shared_variable)

        for i=1,100 do
            global_shared_variable = global_shared_variable + 1
            -- sys.wait(5)
        end

        log.info("task1_func", "for循环后,全局共享变量的值:", global_shared_variable)

        sys.wait(1000)
    end
end

-- 第二个task的任务处理函数
local function task2_func()
    while true do
        log.info("task2_func", "for循环前,全局共享变量的值:", global_shared_variable)

        for i=1,100 do
            global_shared_variable = global_shared_variable + 1
            -- sys.wait(5)
        end

        log.info("task2_func", "for循环后,全局共享变量的值:", global_shared_variable)

        sys.wait(1000)
    end
end

-- 创建并启动第一个task
-- 运行这个task的任务处理函数为task1_func
sys.taskInit(task1_func)

-- 创建并启动第二个task
-- 运行这个task的任务处理函数为task2_func
sys.taskInit(task2_func)

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

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

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

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

运行日志如下

[2025-08-28 22:25:11.744][00000000.045] I/user.task1_func for循环前,全局共享变量的值: 0
[2025-08-28 22:25:11.744][00000000.046] I/user.task1_func for循环后,全局共享变量的值: 100
[2025-08-28 22:25:11.745][00000000.046] I/user.task2_func for循环前,全局共享变量的值: 100
[2025-08-28 22:25:11.745][00000000.047] I/user.task2_func for循环后,全局共享变量的值: 200
[2025-08-28 22:25:12.699][00000001.001] I/user.task1_func for循环前,全局共享变量的值: 200
[2025-08-28 22:25:12.700][00000001.001] I/user.task1_func for循环后,全局共享变量的值: 300
[2025-08-28 22:25:12.702][00000001.004] I/user.task2_func for循环前,全局共享变量的值: 300
[2025-08-28 22:25:12.702][00000001.004] I/user.task2_func for循环后,全局共享变量的值: 400
[2025-08-28 22:25:13.712][00000002.014] I/user.task1_func for循环前,全局共享变量的值: 400
[2025-08-28 22:25:13.714][00000002.016] I/user.task1_func for循环后,全局共享变量的值: 500
[2025-08-28 22:25:13.719][00000002.021] I/user.task2_func for循环前,全局共享变量的值: 500
[2025-08-28 22:25:13.720][00000002.022] I/user.task2_func for循环后,全局共享变量的值: 600
[2025-08-28 22:25:14.714][00000003.016] I/user.task1_func for循环前,全局共享变量的值: 600
[2025-08-28 22:25:14.715][00000003.017] I/user.task1_func for循环后,全局共享变量的值: 700
[2025-08-28 22:25:14.719][00000003.020] I/user.task2_func for循环前,全局共享变量的值: 700
[2025-08-28 22:25:14.719][00000003.021] I/user.task2_func for循环后,全局共享变量的值: 800
[2025-08-28 22:25:15.725][00000004.027] I/user.task1_func for循环前,全局共享变量的值: 800
[2025-08-28 22:25:15.726][00000004.028] I/user.task1_func for循环后,全局共享变量的值: 900
[2025-08-28 22:25:15.729][00000004.031] I/user.task2_func for循环前,全局共享变量的值: 900
[2025-08-28 22:25:15.739][00000004.040] I/user.task2_func for循环后,全局共享变量的值: 1000
[2025-08-28 22:25:16.727][00000005.029] I/user.task1_func for循环前,全局共享变量的值: 1000
[2025-08-28 22:25:16.728][00000005.029] I/user.task1_func for循环后,全局共享变量的值: 1100
[2025-08-28 22:25:16.736][00000005.038] I/user.task2_func for循环前,全局共享变量的值: 1100
[2025-08-28 22:25:16.737][00000005.038] I/user.task2_func for循环后,全局共享变量的值: 1200
[2025-08-28 22:25:17.740][00000006.042] I/user.task1_func for循环前,全局共享变量的值: 1200
[2025-08-28 22:25:17.741][00000006.043] I/user.task1_func for循环后,全局共享变量的值: 1300
[2025-08-28 22:25:17.745][00000006.047] I/user.task2_func for循环前,全局共享变量的值: 1300
[2025-08-28 22:25:17.753][00000006.055] I/user.task2_func for循环后,全局共享变量的值: 1400
[2025-08-28 22:25:18.742][00000007.044] I/user.task1_func for循环前,全局共享变量的值: 1400
[2025-08-28 22:25:18.742][00000007.044] I/user.task1_func for循环后,全局共享变量的值: 1500
[2025-08-28 22:25:18.744][00000007.046] I/user.task2_func for循环前,全局共享变量的值: 1500
[2025-08-28 22:25:18.751][00000007.053] I/user.task2_func for循环后,全局共享变量的值: 1600
[2025-08-28 22:25:19.747][00000008.048] I/user.task1_func for循环前,全局共享变量的值: 1600
[2025-08-28 22:25:19.748][00000008.050] I/user.task1_func for循环后,全局共享变量的值: 1700
[2025-08-28 22:25:19.752][00000008.054] I/user.task2_func for循环前,全局共享变量的值: 1700
[2025-08-28 22:25:19.753][00000008.055] I/user.task2_func for循环后,全局共享变量的值: 1800
[2025-08-28 22:25:20.754][00000009.056] I/user.task1_func for循环前,全局共享变量的值: 1800
[2025-08-28 22:25:20.755][00000009.057] I/user.task1_func for循环后,全局共享变量的值: 1900
[2025-08-28 22:25:20.760][00000009.061] I/user.task2_func for循环前,全局共享变量的值: 1900
[2025-08-28 22:25:20.761][00000009.062] I/user.task2_func for循环后,全局共享变量的值: 2000
[2025-08-28 22:25:21.755][00000010.057] I/user.task1_func for循环前,全局共享变量的值: 2000
[2025-08-28 22:25:21.757][00000010.058] I/user.task1_func for循环后,全局共享变量的值: 2100
[2025-08-28 22:25:21.761][00000010.063] I/user.task2_func for循环前,全局共享变量的值: 2100
[2025-08-28 22:25:21.768][00000010.070] I/user.task2_func for循环后,全局共享变量的值: 2200
[2025-08-28 22:25:22.767][00000011.069] I/user.task1_func for循环前,全局共享变量的值: 2200
[2025-08-28 22:25:22.768][00000011.070] I/user.task1_func for循环后,全局共享变量的值: 2300
[2025-08-28 22:25:22.770][00000011.072] I/user.task2_func for循环前,全局共享变量的值: 2300
[2025-08-28 22:25:22.771][00000011.073] I/user.task2_func for循环后,全局共享变量的值: 2400
[2025-08-28 22:25:23.779][00000012.080] I/user.task1_func for循环前,全局共享变量的值: 2400
[2025-08-28 22:25:23.780][00000012.082] I/user.task1_func for循环后,全局共享变量的值: 2500

可以看到:

  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) :

--[[
@module  shared_resource
@summary 共享资源访问演示 
@version 1.0
@date    2025.08.15
@author  朱天华
@usage
本文件为shared_resource应用功能模块,用来演示多个task访问共享资源的功能,核心业务逻辑为:
1、创建一个全局共享变量global_shared_variable,变量值初始化为0;
2、创建两个task,task1和task2;
2、在task1的任务处理函数中:
   (1) 每隔1秒,执行一次for循环
   (2) 循环体内循环100次,每次将全局共享变量global_shared_variable的值加1
3、在task2的任务处理函数中,每隔300毫秒,task2的计数器加1,并且通过日志打印task2计数器的值;

本文件没有对外接口,直接在main.lua中require "task_scheduling"就可以加载运行;
]]

-- 全局共享变量,初始值为0
local global_shared_variable = 0

-- 第一个task的任务处理函数
local function task1_func()
    while true do
        log.info("task1_func", "for循环前,全局共享变量的值:", global_shared_variable)

        for i=1,100 do
            global_shared_variable = global_shared_variable + 1
            sys.wait(5)
        end

        log.info("task1_func", "for循环后,全局共享变量的值:", global_shared_variable)

        sys.wait(1000)
    end
end

-- 第二个task的任务处理函数
local function task2_func()
    while true do
        log.info("task2_func", "for循环前,全局共享变量的值:", global_shared_variable)

        for i=1,100 do
            global_shared_variable = global_shared_variable + 1
            sys.wait(5)
        end

        log.info("task2_func", "for循环后,全局共享变量的值:", global_shared_variable)

        sys.wait(1000)
    end
end

-- 创建并启动第一个task
-- 运行这个task的任务处理函数为task1_func
sys.taskInit(task1_func)

-- 创建并启动第二个task
-- 运行这个task的任务处理函数为task2_func
sys.taskInit(task2_func)

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

[2025-08-28 22:29:33.526][00000000.079] I/user.task1_func for循环前,全局共享变量的值: 0
[2025-08-28 22:29:33.527][00000000.079] I/user.task2_func for循环前,全局共享变量的值: 1
[2025-08-28 22:29:34.941][00000001.493] I/user.task1_func for循环后,全局共享变量的值: 200
[2025-08-28 22:29:34.942][00000001.494] I/user.task2_func for循环后,全局共享变量的值: 200
[2025-08-28 22:29:35.947][00000002.499] I/user.task1_func for循环前,全局共享变量的值: 200
[2025-08-28 22:29:35.949][00000002.501] I/user.task2_func for循环前,全局共享变量的值: 201
[2025-08-28 22:29:37.375][00000003.928] I/user.task1_func for循环后,全局共享变量的值: 400
[2025-08-28 22:29:37.377][00000003.929] I/user.task2_func for循环后,全局共享变量的值: 400
[2025-08-28 22:29:38.381][00000004.933] I/user.task1_func for循环前,全局共享变量的值: 400
[2025-08-28 22:29:38.381][00000004.933] I/user.task2_func for循环前,全局共享变量的值: 401
[2025-08-28 22:29:39.801][00000006.353] I/user.task1_func for循环后,全局共享变量的值: 600
[2025-08-28 22:29:39.802][00000006.354] I/user.task2_func for循环后,全局共享变量的值: 600
[2025-08-28 22:29:40.805][00000007.357] I/user.task1_func for循环前,全局共享变量的值: 600
[2025-08-28 22:29:40.806][00000007.358] I/user.task2_func for循环前,全局共享变量的值: 601
[2025-08-28 22:29:42.255][00000008.807] I/user.task1_func for循环后,全局共享变量的值: 800
[2025-08-28 22:29:42.256][00000008.808] I/user.task2_func for循环后,全局共享变量的值: 800
[2025-08-28 22:29:43.265][00000009.817] I/user.task1_func for循环前,全局共享变量的值: 800
[2025-08-28 22:29:43.266][00000009.818] I/user.task2_func for循环前,全局共享变量的值: 801
[2025-08-28 22:29:44.735][00000011.287] I/user.task1_func for循环后,全局共享变量的值: 1000
[2025-08-28 22:29:44.735][00000011.287] I/user.task2_func for循环后,全局共享变量的值: 1000

可以看到:

无论是 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;

3.1.2 作用

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

3.1.2.1 编程设计更简单

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

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

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

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

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

function led_off()
    -- 熄灭指示灯
    off()
    -- 启动一个定时器,500毫秒之后执行定时器的回调函数led_on
    sys.timerStart(led_on, 500)
end

function led_on()
    -- 点亮指示灯
    on()
    -- 启动一个定时器,500毫秒之后执行定时器的回调函数led_off
    sys.timerStart(led_off, 500)
end

-- 首先执行led_on()函数点亮指示灯
led_on()

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

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

local function led_blink_task_func()
    while true do
        -- 点亮指示灯
        on()
        -- 等待500毫秒
        sys.wait(500)
        -- 熄灭指示灯
        off()
        -- 等待500毫秒
        sys.wait(500)
    end
end

-- 创建一个task
-- 并且运行这个task的处理函数led_blink_task_func
sys.taskInit(led_blink_task_func)

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

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

3.1.2.2 其他作用

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

  1. 实现多任务协作式的并发执行;
  2. 更简捷地支持了模块化解耦设计;
  3. 更简捷的处理异步事件,可以很方便地将异步处理逻辑封装成同步处理逻辑;

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

3.1.3 创建

3.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 资源也不同,例如:

  3. Air8000 系列的用户可用 RAM 为 4MB

  4. Air780 系列中 Air780EPM 用户可用 RAM 为 1MB,Air780EHM/EHV/EGH 用户可用 RAM 为 4MB
  5. Air8101 系列的用户可用 RAM 为 2MB
  6. PC 模拟用户可用 RAM 为 2MB

下面这个例子用来说明如何查看用户可用 ram 信息; 这个例子的完整代码链接:memory_valid.lua 核心代码片段如下,我们首先分析下这段代码的业务逻辑


--[[ @module memory_valid @summary “查看用户可用 ram 信息”演示功能模块 @version 1.0 @date 2025.08.12 @author 朱天华 @usage 本文件为 memory_valid 应用功能模块,用来演示:如何查看用户可用 ram 信息,核心业务逻辑为: 1、创建一个 task; 2、在 task 的任务处理函数中,每隔 1 秒查询一次当前的 ram 信息;

本文件没有对外接口,直接在 main.lua 中 require "memory_valid"就可以加载运行; ]]

local function print_mem_info() -- 这个接口可以强制执行一次垃圾内存回收,方便我们分析内存信息 -- 在实际项目开发中中,用户不需要主动使用这个接口,LuatOS 内部的 Lua 虚拟机会自动进行垃圾回收 collectgarbage()

-- rtos.meminfo()有三个返回值:
-- 1、总ram大小(单位字节)
-- 2、运行过程中实时使用的ram大小(单位字节)
-- 3、运行过程中历史使用的最高ram大小(单位字节)

-- rtos.meminfo()是Lua虚拟机中的用户可用ram信息,用户开发的大部分LuatOS脚本程序都是自动从这里分配ram
log.info("mem.lua", rtos.meminfo())
-- rtos.meminfo("sys")是内核系统中的用户可用ram信息,用户使用zbuff核心库时,会用到这部分ram
-- 在这里我们就不展开讲这一部分了,等讲到zbuff的时候再做讨论
log.info("mem.sys", rtos.meminfo("sys"))

end

local function mem_task_func() while true do -- 打印当前的 ram 信息 print_mem_info()

    -- 延时1秒
    sys.wait(1000)
end

end

-- 创建并启动一个 task -- 运行这个 task 的任务处理函数 mem_task_func sys.taskInit(mem_task_func)

    我们在模拟器上实际运行一下看看,输入命令
    `luatos --llt=H:\Luatools\project\luatos_framework_luatos_task_Air8000.ini`
    运行日志如下
    ```lua
[2025-08-28 22:32:59.068][00000000.069] I/user.mem.lua 2097144 35816 36480
[2025-08-28 22:32:59.068][00000000.070] I/user.mem.sys 251904 131072 65536
[2025-08-28 22:33:00.011][00000001.013] I/user.mem.lua 2097144 35520 37040
[2025-08-28 22:33:00.012][00000001.014] I/user.mem.sys 251904 131072 65536
[2025-08-28 22:33:01.015][00000002.017] I/user.mem.lua 2097144 35520 37040
[2025-08-28 22:33:01.017][00000002.018] I/user.mem.sys 251904 131072 65536
[2025-08-28 22:33:02.024][00000003.025] I/user.mem.lua 2097144 35512 37040
[2025-08-28 22:33:02.026][00000003.027] I/user.mem.sys 251904 131072 65536
[2025-08-28 22:33:03.024][00000004.026] I/user.mem.lua 2097144 35504 37040
[2025-08-28 22:33:03.026][00000004.027] I/user.mem.sys 251904 131072 65536
[2025-08-28 22:33:04.032][00000005.033] I/user.mem.lua 2097144 35520 37040
[2025-08-28 22:33:04.033][00000005.034] I/user.mem.sys 251904 131072 65536
[2025-08-28 22:33:05.033][00000006.034] I/user.mem.lua 2097144 35512 37040
[2025-08-28 22:33:05.034][00000006.035] I/user.mem.sys 251904 131072 65536
[2025-08-28 22:33:06.041][00000007.042] I/user.mem.lua 2097144 35512 37040
[2025-08-28 22:33:06.043][00000007.044] I/user.mem.sys 251904 131072 65536
[2025-08-28 22:33:07.045][00000008.047] I/user.mem.lua 2097144 35504 37040
[2025-08-28 22:33:07.046][00000008.048] I/user.mem.sys 251904 131072 6553
通过上图的这份日志,可以看出,Lua虚拟机中的用户可用ram信息:
- 第一个数值为分配给Lua虚拟机的总ram为2097144字节,将近2MB字节;这个值在运行过程中一直保持不变;
- 第二个数值为LuatOS应用程序运行过程中实时使用的ram,根据业务逻辑会发生动态变化,我们演示的这个demo业务逻辑比较简单,所有变化幅度比较小,一直在35KB左右波动;如果业务逻辑复杂,波动幅度就会比较大;
- 第三个数值为LuatOS应用程序运行过程中历史最高使用的ram,我们演示的这个demo中,为37040字节,即历史最高水位;
平时开发过程中,大家可以加上这段代码,实时观察下第二个数值和第三个数值,如果这两个数值频繁的接近第一个数值,那就说明你的程序占用的内存就比较多了,此时就需要重点分析解决问题,否则很容易就会造成内存不足而导致重启;(一般来说,不会出现这样的问题;关于如何分析解决内存使用接近上限的问题,后续会有一节课程专门来讲,今天在这里就不说了)

知道了怎么查看用户可用ram信息后,我们再来看一个问题,每创建一个基础task需要占用多少ram资源?
下面这个例子用来说明如何分析一个基础task占用的ram资源;
这个例子的完整代码链接:[memory_task.lua](https://gitee.com/openLuat/LuatOS/tree/master/module/Air8000/demo/luatos_framework/luatos_task/memory_task.lua)
核心代码片段如下,我们首先分析下这段代码的业务逻辑
```lua

--[[ @module memory_task @summary “单个 task 占用的 ram 资源”演示功能模块 @version 1.0 @date 2025.08.12 @author 朱天华 @usage 本文件为 memory_task 应用功能模块,用来演示“单个 task 占用的 ram 资源”,核心业务逻辑为: 1、在创建一个 task 前,打印下当前的 ram 信息; 2、创建并且启动一个 task; 3、在创建并且启动这个 task 后,打印下当前的 ram 信息;

本文件没有对外接口,直接在 main.lua 中 require "memory_task"就可以加载运行; ]]

local function print_mem_info() -- 这个接口可以强制执行一次垃圾内存回收,方便我们分析内存信息 -- 在实际项目开发中中,用户不需要主动使用这个接口,LuatOS 内部的 Lua 虚拟机会自动进行垃圾回收 collectgarbage()

-- rtos.meminfo()有三个返回值:
-- 1、总ram大小(单位字节)
-- 2、运行过程中实时使用的ram大小(单位字节)
-- 3、运行过程中历史使用的最高ram大小(单位字节)

-- rtos.meminfo()是Lua虚拟机中的用户可用ram信息,用户开发的大部分LuatOS脚本程序都是自动从这里分配ram
log.info("mem.lua", rtos.meminfo())
-- rtos.meminfo("sys")是内核系统中的用户可用ram信息,用户使用zbuff核心库时,会用到这部分ram
-- 在这里我们就不展开讲这一部分了,等讲到zbuff的时候再做讨论
log.info("mem.sys", rtos.meminfo("sys"))

end

-- led task 的任务处理函数 local function led_task_func() while true do log.info("led_task_func")

    -- 永远等待一个不存在的消息
    sys.waitUntil("INVALID_MESSAGE")
end

end

log.info("before led task") -- 在创建一个 task 之前,打印下当前的 ram 信息 print_mem_info()

-- 创建并启动一个 led task -- 运行这个 task 的任务处理函数 led_task_func sys.taskInit(led_task_func)

log.info("after led task") -- 在创建一个 task 之后,打印下当前的 ram 信息 print_mem_info()

    在模拟器上运行下面一段代码实际看下日志
    ```lua
[2025-08-28 22:38:07.218][00000000.057] I/user.mem.lua 2097144 35296 36112
[2025-08-28 22:38:07.221][00000000.060] I/user.mem.sys 251904 131072 65536
[2025-08-28 22:38:07.224][00000000.063] I/user.led_task_func
[2025-08-28 22:38:07.232][00000000.071] I/user.after led task
[2025-08-28 22:38:07.234][00000000.073] I/user.mem.lua 2097144 36112 37824
[2025-08-28 22:38:07.234][00000000.074] I/user.mem.sys 251904 131072 65536
从日志可以看出:
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;
这个例子的完整代码链接:[task_count.lua](https://gitee.com/openLuat/LuatOS/tree/master/module/Air8000/demo/luatos_framework/luatos_task/task_count.lua)
核心代码片段如下,我们首先分析下这段代码的业务逻辑
```lua

--[[ @module task_count @summary “创建 task 的数量”演示功能模块 @version 1.0 @date 2025.08.12 @author 朱天华 @usage 本文件为 task_count 应用功能模块,用来演示“可以创建多少个 task”,核心业务逻辑为: 执行一个 while true 循环,每次执行到循环体内,执行以下两项动作: 1、创建并且启动一个 task,启动后,task 处于阻塞状态,永远不会死亡 2、task 数量的计数器加一,并且打印当前已经创建的 task 总数量

本文件没有对外接口,直接在 main.lua 中 require "task_count"就可以加载运行; ]]

local count = 0

-- task 的任务处理函数 local function led_task_func() while true do log.info("led_task_func") sys.waitUntil("INVALID_MESSAGE") end end

-- 不断地创建 task,直到 ram 资源耗尽 while true do sys.taskInit(led_task_func) count = count+1 log.info("create task count", count) end

    在模拟器上实际运行一下看看,输入命令
    `luatos --llt=H:\Luatools\project\luatos_framework_luatos_task_Air8000.ini`
    运行日志如下
    ```lua
[2025-08-28 22:41:43.487][00000005.870] I/user.create task count 2363
[2025-08-28 22:41:43.487][00000005.871] I/user.led_task_func
[2025-08-28 22:41:43.487][00000005.871] I/user.create task count 2364
[2025-08-28 22:41:43.488][00000005.872] I/user.led_task_func
[2025-08-28 22:41:43.488][00000005.872] I/user.create task count 2365
[2025-08-28 22:41:43.489][00000005.873] I/user.led_task_func
[2025-08-28 22:41:43.494][00000005.878] I/user.create task count 2366
[2025-08-28 22:41:43.501][00000005.885] I/user.led_task_func
[2025-08-28 22:41:43.510][00000005.893] E/mem memory allocation failed total 2097144, used 2042032, alloc 640
[2025-08-28 22:41:43.512][00000005.896] E/user.coroutine.resume not enough memory
stack traceback:
        /lua/sys.lua: in function 'sysplus.waitUntil'
        [string "task_count.lua"]:22: in function <[string "task_count.lua"]:19>
可以看到,创建了2366个task之后,就提示memory不足,报错了;
和计算的理论值2526个有一定误差,这个在可以接受的范围内,一方面是因为存在内存碎片(上图的日志还有2097144-2042032=55112字节的ram无法使用,如果这部分ram可以利用,还能再多创建55112/816=67个task),另一方面可能还会存在一些我们没考虑到的其他一些地方需要消耗比较少的ram;
至此,大家应该理解了在LuatOS中,对创建的task数量没有特别限制,只要ram够用,可以一直创建所表达的意思。

参数

task_func

参数含义:task的处理函数
数据类型:function
取值范围:任意有效的函数名都行;
是否必选:必须传入此参数;
注意事项:
    特别需要注意传入的函数名的作用域,如果使用不当,很可能会使用一个无效的函数名;
    如下代码就是一个典型的错误示例
    -- 此处sys.taskInit(led_task_func)中使用的led_task_func并不是一个function类型,而是nil类型;    
    -- 因为下文定义的函数led_task_func,没办法在其上部去使用;
    -- 任何函数都要先定义后使用,不允许先使用后定义
    sys.taskInit(led_task_func)

    local function led_task_func()
        -- 此处省略了代码
    end
参数示例:
    如下方所示,定义了一个函数led_task_funcled_task_func就可以做为此参数传入
    local function led_task_func()
        -- 此处省略了代码
    end

    sys.taskInit(led_task_func)

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

这个例子的完整代码链接:task_func.lua

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

--[[
@module  task_func
@summary “task任务处理函数”演示功能模块 
@version 1.0
@date    2025.08.12
@author  朱天华
@usage
本文件为task_func应用功能模块,用来演示“如何设置task任务处理函数”,核心业务逻辑为:
1、创建一个task时,需要设置task任务处理函数;
2、演示一种常见的错误设置方式;

本文件没有对外接口,直接在main.lua中require "task_func"就可以加载运行;
]]

-- 创建并启动一个led task
-- 运行这个task的任务处理函数led_task_func
-- 此处运行会报错,因为执行到这行代码时,找不到led_task_func函数的定义,犯了“先使用,后定义”的错误
sys.taskInit(led_task_func)

local function led_task_func()
    while true do
        log.info("led_task_func")
        sys.wait(1000)
    end
end

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

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

运行日志如下

[2025-08-28 22:46:11.903][00000000.096] E/main Luat:
[2025-08-28 22:46:11.904][00000000.097] E/main bad argument #1 to 'create' (function expected, got nil)
[2025-08-28 22:46:11.905][00000000.098] E/main Lua VM exit!! reboot in 15000ms

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

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

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

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

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

local function led_task_func()
    while true do
        log.info("led_task_func")
        sys.wait(1000)
    end
end

sys.taskInit(led_task_func)

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

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

...

参数含义:task的处理函数携带的可变参数
数据类型:任意数据类型;
取值范围:无特别限制;
是否必选:可选传入此参数;
注意事项:...Lua语言的一种语法,表示可变参数,参数数量可以是0个,1个,2个,...,多个;
参数示例:
    不传入,或者bool类型的true,或者number类型的2,或者string类型的"led",或者table类型的{name="LuatOS", password="123456"}等等等等;
    总之,任何数据类型的任何自定义内容都行;

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

这个例子的完整代码链接:variable_args.lua

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

--[[
@module  variable_args
@summary “task创建时的可变参数”演示功能模块 
@version 1.0
@date    2025.08.12
@author  朱天华
@usage
本文件为variable_args应用功能模块,用来演示“task创建时的可变参数”如何使用,核心业务逻辑为:
1、创建一个task,可变参数部分携带5个参数;
2、在task的任务处理函数中打印传入的5个参数的值;

本文件没有对外接口,直接在main.lua中require "variable_args"就可以加载运行;
]]

local function led_task_func(arg1, arg2, arg3, arg4, arg5)
    while true do
        log.info("led_task_func", arg1, arg2, arg3, arg4, arg5)
        sys.wait(1000)
    end
end

-- 创建并启动一个task
-- 这个task的任务处理函数为led_task_func
-- 携带5个参数,分别为"arg1", 3, nil, true, led_task_func
-- 运行这个task的任务处理函数led_task_func时,会将这5个参数传递给任务处理函数使用
sys.taskInit(led_task_func, "arg1", 3, nil, true, led_task_func)

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

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

运行日志如下

I/user.led_task_func arg1 3 nil true function: 0218C360
I/user.led_task_func arg1 3 nil true function: 0218C360
I/user.led_task_func arg1 3 nil true function: 0218C360
I/user.led_task_func arg1 3 nil true function: 0218C360

从日志可以看出,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的处理函数
数据类型:function
取值范围:任意有效的函数名都行;
是否必选:必须传入此参数;
注意事项:
    特别需要注意传入的函数名的作用域,如果使用不当,很可能会使用一个无效的函数名;
    如下代码就是一个典型的错误示例

    -- 此处sys.taskInitEx(led_task_func, "LED_TASK")中使用的led_task_func并不是一个function类型,而是nil类型;    
    -- 因为下文定义的函数led_task_func,没办法在其上部去使用;
    -- 任何函数都要先定义后使用,不允许先使用后定义
    sys.taskInitEx(led_task_func, "LED_TASK")

    local function led_task_func()
        -- 此处省略了代码
    end
参数示例:
    如下所示,定义了一个函数led_task_funcled_task_func就可以做为此参数传入
    local function led_task_func()
        -- 此处省略了代码
    end

    sys.taskInitEx(led_task_func, "LED_TASK")

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的处理函数中使用sys.waitMsg(task_name, msg, timeout)接口阻塞等待msg消息时
    接收到msg之外的其他消息时的回调函数;称之为非目标消息回调函数;
    回调函数的格式为:

    -- 处理sys.waitMsg(task_name, msg, timeout)接口阻塞等待时,接收到的msg之外的消息
    function non_targeted_msg_cbfunc(msg)
        -- msg[1]为消息名,msg[2]为消息携带的第一个参数,
        -- msg[3]为消息携带的第二个参数,msg[4]为消息携带的第三个参数
        -- msg[1], msg[2], msg[3], msg[4] 分别对应
        -- sys.sendMsg(task_name, msg, arg2, arg3, arg4)中的 msg, arg2, arg3, arg4
        log.info("non_targeted_msg_cbfunc", msg[1], msg[2], msg[3], msg[4])
    end

    回调函数是在task之外的业务逻辑中被执行的    
    在回调函数内部无法使用sys.wait(timeout)sys.waitUntil(msg, timeout)
    sys.waitMsg(task_name, msg, timeout)等必须用在task中的函数
数据类型:function或者nil
取值范围:无特别限制;
是否必选:可选传入此参数;
注意事项:
    特别需要注意传入的函数名的作用域,如果使用不当,很可能会使用一个无效的函数名;
    如下代码就是一个典型的错误示例

    -- 此处sys.taskInitEx(led_task_func, "LED_TASK", led_task_cbfunc)中使用的led_task_cbfunc
    -- 并不是一个function类型,而是nil类型;    
    -- 因为下文定义的函数led_task_cbfunc,没办法在其上部去使用;
    -- 任何函数都要先定义后使用,不允许先使用后定义
    local function led_task_func()
        -- 此处省略了代码
    end

    sys.taskInitEx(led_task_func, "LED_TASK", led_task_cbfunc)

    local function led_task_cbfunc(msg)
        -- 此处省略了代码
    end
参数示例:
    如下所示,定义了一个函数led_task_cbfuncled_task_cbfunc就可以做为此参数传入
    local function led_task_func()
        -- 此处省略了代码
    end       

    local function led_task_cbfunc(msg)
        -- 此处省略了代码
    end

    sys.taskInitEx(led_task_func, "LED_TASK", led_task_cbfunc)

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

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

这个例子的完整代码链接:non_targeted_msg.lua

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

--[[
@module  non_targeted_msg
@summary “非目标消息回调函数”演示功能模块 
@version 1.0
@date    2025.08.12
@author  朱天华
@usage
本文件为non_targeted_msg应用功能模块,用来演示“非目标消息回调函数”如何使用,核心业务逻辑为:
1、创建一个高级task,task名称为"MQTT_CLINET_MAIN",task的非目标消息回调函数为mqtt_client_main_cbfunc;
2、创建一个基础task,每隔一秒向名称为"MQTT_CLINET_MAIN"的高级task发送一条非目标消息;
3、创建一个高级task,task名称为"SEND_MSG_TASK",每隔一秒向名称为"MQTT_CLINET_MAIN"的高级task发送一条目标消息;

本文件没有对外接口,直接在main.lua中require "non_targeted_msg"就可以加载运行;
]]

-- mqtt_client_main的任务名
local TASK_NAME = "MQTT_CLINET_MAIN"

-- 非目标消息回调函数
local function mqtt_client_main_cbfunc(msg)
    log.info("mqtt_client_main_cbfunc", msg[1], msg[2], msg[3], msg[4])
end

-- mqtt main task 的任务处理函数
local function mqtt_client_main_task_func()
    -- 连接、断开连接、订阅、取消订阅、异常等各种事件的处理调度逻辑
    while true do
        -- 等待"MQTT_EVENT"消息
        msg = sys.waitMsg(TASK_NAME, "MQTT_EVENT")
        log.info("mqtt_client_main_task_func waitMsg", msg[2], msg[3], msg[4])

        -- connect连接结果
        -- msg[3]表示连接结果,true为连接成功,false为连接失败
        if msg[2] == "CONNECT" then
            -- mqtt连接成功
            if msg[3] then
                log.info("mqtt_client_main_task_func", "connect success")
            -- mqtt连接失败
            else
                log.info("mqtt_client_main_task_func", "connect error")
            end

        -- subscribe订阅结果
        -- msg[3]表示订阅结果,true为订阅成功,false为订阅失败
        elseif msg[2] == "SUBSCRIBE" then
            -- 订阅成功
            if msg[3] then
                log.info("mqtt_client_main_task_func", "subscribe success", "qos: "..(msg[4] or "nil"))
            -- 订阅失败
            else
                log.error("mqtt_client_main_task_func", "subscribe error", "code", msg[4])
            end

        -- 被动关闭了mqtt连接
        -- 被网络或者服务器断开了连接
        elseif msg[2] == "DISCONNECTED" then
            log.info("mqtt_client_main_task_func", "disconnected")
        end
    end
end

local function send_non_targeted_msg_task_func()
    local count = 0

    while true do
        count = count+1

        -- 向TASK_NAME这个任务发送一条消息
        -- 消息名称为"UNKNOWN_EVENT"
        -- 消息携带一个number类型的参数count
        sys.sendMsg(TASK_NAME, "UNKNOWN_EVENT", count)

        -- 延时等待1秒
        sys.wait(1000)
    end
end

local function send_targeted_msg_task_func()
    while true do
        -- 向TASK_NAME这个任务发送一条消息
        -- 消息名称为"MQTT_EVENT"
        -- 消息携带两个参数
        -- 第一个参数为"CONNECT"
        -- 第二个参数为true
        -- 这条消息的意思是MQTT连接成功
        sys.sendMsg(TASK_NAME, "MQTT_EVENT", "CONNECT", true)

        -- 延时等待1秒
        sys.wait(1000)

        -- 向TASK_NAME这个任务发送一条消息
        -- 消息名称为"MQTT_EVENT"
        -- 消息携带三个参数
        -- 第一个参数为"SUBSCRIBE"
        -- 第二个参数为true
        -- 第三个参数为0
        -- 这条消息的意思是MQTT订阅成功,qos为0
        sys.sendMsg(TASK_NAME, "MQTT_EVENT", "SUBSCRIBE", true, 0)

        -- 延时等待1秒
        sys.wait(1000)

        -- 向TASK_NAME这个任务发送一条消息
        -- 消息名称为"MQTT_EVENT"
        -- 消息携带一个参数"DISCONNECTED"
        -- 这条消息的意思是MQTT连接被动断开
        sys.sendMsg(TASK_NAME, "MQTT_EVENT", "DISCONNECTED")

        -- 延时等待1秒
        sys.wait(1000)
    end
end

-- 创建并且启动一个高级task
-- task的任务处理函数为mqtt_client_main_task_func
-- task的名称为TASK_NAME变量的值"MQTT_CLINET_MAIN"
-- task的非目标消息回调函数为mqtt_client_main_cbfunc
-- 运行这个task的任务处理函数mqtt_client_main_task_func
sys.taskInitEx(mqtt_client_main_task_func, TASK_NAME, mqtt_client_main_cbfunc)

-- 创建并且启动一个基础task
-- 运行这个task的任务处理函数send_targeted_msg_task_func
sys.taskInit(send_non_targeted_msg_task_func)

-- 创建并且启动一个高级task
-- task的任务处理函数为send_targeted_msg_task_func
-- task的名称为SEND_TASK_NAME
-- 运行这个task的任务处理函数send_targeted_msg_task_func
sys.taskInitEx(send_targeted_msg_task_func, "SEND_MSG_TASK")

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

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

运行日志如下:

I/user.mqtt_client_main_cbfunc UNKNOWN_EVENT 1 nil nil
I/user.mqtt_client_main_task_func waitMsg CONNECT true nil
I/user.mqtt_client_main_task_func connect success
I/user.mqtt_client_main_cbfunc UNKNOWN_EVENT 2 nil nil
I/user.mqtt_client_main_task_func waitMsg SUBSCRIBE true 0
I/user.mqtt_client_main_task_func subscribe success qos: 0
I/user.mqtt_client_main_cbfunc UNKNOWN_EVENT 3 nil nil
I/user.mqtt_client_main_task_func waitMsg DISCONNECTED nil nil
I/user.mqtt_client_main_task_func disconnected

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

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

从日志可以看出,在高级 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 的区别有一个基本的认识就行,在后续消息章节,我们还会进一步深入讲解;

3.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;

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

3.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 外部运行两种环境即可,后续我们讲解其他功能时,会用到这两种概念,并且也会结合示例来说明二者的差别;

3.1.4 状态

task 被创建之后,就会按照代码中设计的业务逻辑来来运行,task 的状态有三种:

运行状态,阻塞状态,死亡状态;

我们先来看下一个 task 的状态机

  1. 调用 sys.taskInit 或者 sys.taskInitEx 两个 api 后,创建的 task 就会进入运行状态;
  2. 当发生以下两种事件中的任意一种,运行状态切换为死亡状态,一旦进入死亡状态,这个 task 也就结束了

  3. task 的任务处理函数,按照正常的业务逻辑已经运行结束,正常退出函数;也就是状态机中的 normal exit

  4. task 的任务处理函数,在运行过程中,出现了异常,例如对非 number 类型的变量进行数据运算,把一个 number 类型当做 table 来用等等等等,一旦出现类似的异常,任务处理函数就会异常退出;也就是状态机中的 abnormal exit
  5. 当发生以下三种事件中的任意一种,运行状态切换为阻塞状态,一旦进入阻塞状态,就是 task 任务处理函数中的代码把自己给挂起了

  6. 任务处理函数中执行到 sys.wait(timeout),表示在这里阻塞等待 timeout 毫秒的时间;

  7. 任务处理函数中执行到 sys.waitUntil(msg, timeout),表示在这里阻塞等待全局的 msg 消息,或者阻塞等待 timeout 毫秒的时间;
  8. 任务处理函数中执行到 sys.waitMsg(task_name, msg, timeout),表示在这里阻塞等待定向发给 task_name 的 msg 消息,或者阻塞等待 timeout 毫秒的时间;
  9. 当发生以下三种事件中的任意一种,阻塞状态切换为运行状态

  10. 任务处理函数中执行到 sys.wait(timeout),sys.waitUntil(msg, timeout),sys.waitMsg(task_name, msg, timeout)创建的定时器超时时间到达,此时会产生一个定时器到达事件

  11. 任意代码处执行 sys.publish(msg, ...),发布了全局 msg 消息,在等待这个全局 msg 消息的所有处于阻塞状态的 task 都能依次接收到此消息,并且从阻塞状态切换为运行状态继续运行
  12. 任意代码处执行 sys.sendMsg(task_name, msg, arg2, arg3, arg4),向名称为 task_name 的高级 task 发送了定向 msg 消息,在等待这个定向 msg 消息的并且处于阻塞状态的 task 可以接收到此消息,并且从阻塞状态切换为运行状态继续运行

通过以上的 task 状态机介绍,我们可以看出,运行状态和阻塞状态之间的切换最为复杂;

这两种状态之间的切换,用到定时器和消息两种事件,接下来我们来重点看下消息和定时器两个概念。

3.2 LuatOS 的消息(message)

3.2.1 基本概念

LuatOS 的消息机制是 LuatOS task 协作和事件驱动编程的核心部分,消息机制包括消息的订阅、发送、接收处理,以及系统消息的定义和使用;

消息需要存储到消息队列中,消息队列中的消息遵循先进先出(FIFO)原则;

LuatOS 的消息队列有两种: 内核消息队列和用户消息队列。

先看下面这张图,黄色背景的就是内核消息队列;

FreeRTOS 创建的每一个 task 都有一个消息队列,这种消息队列就叫内核消息队列;

内核消息队列是 FreeRTOS 直接管理的,存储 FreeRTOS 不同 task 之间的消息的队列;

内核消息的发送,由内核固件自行处理,不开放给 LuatOS 用户使用;

内核消息的接收,对 LuatOS 用户来说,FreeRTOS 的其他 task(例如定时器 task)发给 Lua 虚拟机 task 的消息,在 sys 核心库中可以接收到这些内核消息,并且由 sys 核心库负责把接收到的消息,传递给对应的 LuatOS task 或者订阅的回调函数来处理;

对 LuatOS 用户来说,内核消息的一个常见应用就是定时器,定时器内容在后续章节会重点讲解,在这里就暂时跳过了;

用户消息队列由 Lua 虚拟机内部管理,用户消息队列中的消息订阅,发送,和接收处理,LuatOS 用户可以直接控制;

用户消息队列中的消息,和 LuatOS 应用脚本程序的关系最为密切,本章节,我们将重点学习用户消息队列中的消息如何使用;

用户消息队列又可以进一步划分为全局消息队列和定向消息队列:

  1. 全局消息队列中存储的全局消息有系统全局消息用户全局消息
  2. 定向消息队列中存储的定向消息有系统定向消息用户定向消息

3.2.2 系统全局消息

系统全局消息是内核固件的 C 代码中调用 sys.publish 接口发布的消息,例如"IP_READY";

LuatOS 用户在脚本程序中可以使用 sys.subscribe 和 sys.waitUntil 订阅和接收处理;

这部分消息牵涉到的功能模块比较多,大家可以参考:系统全局消息,有个简单的认识就行,这些系统全局消息,在后续的 LuatOS 课程中,每讲到一个主题,都会讲解对应的系统全局消息;

在本章节就不细讲了。

3.2.3 系统定向消息

系统定向消息是内核固件的 C 代码中调用 sys.sendMsg 接口发布的消息,例如 socket.EVENT;

LuatOS 用户在脚本程序中可以使用 sys.waitMsg 接收处理;

但是由于这部分内容和内核固件的耦合比较大,一般来说都是 LuatOS 的扩展库去使用,目前只有在 socket 功能有关的扩展库中使用过系统定向消息,用户编写的 Lua 应用脚本程序中很少用到,所以在这里也不细讲了;

在后续的 LuatOS 课程中,每讲到一个主题,如果有对应的系统定向消息,我们再逐一讲解;

接下来我们重点看下用户全局消息和用户定向消息:

从消息接收处理方的角度来划分,消息可以分为全局消息和定向消息;
全局消息是指消息可以被任意订阅方接收处理;
定向消息是指消息只能被指定的task接收处理


sys核心库提供的全局消息管理功能有以下几种
(1) 全局消息发布:sys.publish(msg, ...)
(2) 全局消息订阅:sys.subscribe(msg, msg_cbfunc)
(3) 全局消息取消订阅:sys.unsubscribe(msg, msg_cbfunc)
(4) 全局消息阻塞等待(只能在task中使用):sys.waitUntil(msg, timeout)


sys核心库提供的定向消息管理功能有以下几种
(1) 定向消息发布:sys.sendMsg(task_name, msg, arg2, arg3, arg4)
(2) 定向消息阻塞等待(只能在task中使用):sys.waitMsg(task_name, msg, timeout)
(3) 定向消息清除:sys.cleanMsg(task_name)

3.2.4 用户全局消息

用户全局消息处理的完整周期

用户全局消息处理的完整周期包括以下几部分:

  1. 消息订阅:有两个 api 可以订阅全局消息

  2. sys.subscribe(msg, cbfunc):为全局消息 msg 订阅一个回调函数 cbfunc;相当于在全局消息订阅表中增加一项[msg] = {cbfunc = true}

  3. sys.waitUntil(msg, timeout):为全局消息 msg 订阅一个 task,这个 api 只能在 task 任务处理函数的业务逻辑中使用;相当于在全局消息订阅表中增加一项[msg] = {task = true}
  4. 消息发布:有一个 api 可以发布全局消息

  5. sys.publish(msg, ...):发布一条全局消息;相当于在全局消息队列中的队尾位置增加一项{msg, ...}

  6. 消息调度处理:有一个 api 对全局消息进行调度处理

  7. sys.run():读取全局消息并且分发给订阅者去处理;

  8. 从全局消息队列中取出第一条消息{msg1, ...}
  9. 根据消息名 msg1,到全局消息订阅表中找 msg1 的订阅者
  10. 如果订阅者是回调函数 bfunc1,则执行 cbfunc1,执行完之后,并不会自动从全局消息订阅表中删除 msg1 对应的 cbfunc1 = true 标记,只有脚本程序中主动调用 sys.unsubscribe(msg1, cbfunc1),才会删除这个标记;
  11. 如果订阅者是一个 task1,task1 处于阻塞状态,则让 task1 退出阻塞状态,继续运行;然后自动从全局消息订阅表中删除 msg1 对应的 task1 = true 标记
  12. 在全局消息表中处理完 msg1 的所有订阅者;
  13. 从全局消息队列中删除{msg1, ...}这条消息
  14. 删除订阅者:订阅者有两种,删除的方式各不相同

  15. 回调函数类型的订阅者,在消息调度处理过程中,不会自动删除;只有脚本程序中主动调用 sys.unsubscribe(msg, cbfunc),才会删除

  16. task 类型的订阅者,在消息调度处理过程中,处理完之后,会自动删除;
  17. 删除消息:sys.run()消息调度处理过程中,自动删除;

有一个重要注意事项:

全局消息必须先订阅,然后再发布,这样才能保证发布的消息可以被订阅者处理;怎么理解呢?我们再看下上面这张图,在 sys.sun()调度时,首先从全局消息队列中取出队首的一条消息,然后从全局消息订阅表中找到这条消息对应的所有订阅者进行处理,处理完之后,就删除了这条消息,所以此时如果订阅者还没有准备好,则无法处理消息;

了解了用户全局消息的完整生命周期后,接下来我们:

1、先看下在整个生命周期中用到的几个 sys 核心库的 api

2、写一个完整的 demo 示例来实际运行演示一下如何使用;

sys.publish(msg, ...)

功能

发布一个全局消息;

注意事项

可以在能够执行到的任意代码位置使用此函数;

sys.publish(msg, ...)是全局消息的生产者,全局消息有生产就会有消费,不然消息就没有存在的意义了;

有两个接口可以注册全局消息的消费者:

1、一个是 sys.subscribe(msg, msg_cbfunc)中注册的 msg_cbfunc 消息回调函数;

2、一个是 sys.waitUntil(msg, timeout)所在的 task;

所以全局消息的生产者和消费者的使用组合,有以下两种:

1、sys.publish(msg, ...) 和 sys.subscribe(msg, msg_cbfunc)

在 sys.publish(msg, ...)之前,必须使用 sys.subscribe(msg, msg_cbfunc)注册消息回调函数;

这样才能保证发布的 msg 消息可以被 msg_cbfunc 消息回调函数处理;

2、sys.publish(msg, ...) 和 sys.waitUntil(msg, timeout)

在 sys.publish(msg, ...)之前,必须保证 task 正在 sys.waitUntil(msg, timeout)代码处,处于阻塞等待状态;

这样才能保证发布的 msg 消息可以被 task 处理;

参数

msg

参数含义:消息的名称;
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"SEND_DATA_REQ"等任意自定义的字符串;

...

参数含义:消息携带的可变参数;
数据类型:任意数据类型;
取值范围:无特别限制;
是否必选:可选传入此参数;
注意事项:...Lua语言的一种语法,表示可变参数,参数数量可以是0个,1个,2个,...,多个;
参数示例:
    不传入,或者boolean类型的true,或者number类型的2,或者string类型的"led",或者table类型的{name="LuatOS", password="123456"}等等等等;
    总之,任何数据类型的任何自定义内容都行;

返回值

nil

示例

-- 发布一条全局消息"SEND_DATA_REQ",携带一个参数"123456"
sys.publish("SEND_DATA_REQ", "123456")

sys.subscribe(msg, msg_cbfunc)

功能

订阅一个全局消息的回调函数;

注意事项

可以在能够执行到的任意代码位置使用此函数;

sys.publish(msg, ...) 和 sys.subscribe(msg, msg_cbfunc)配合使用时:

在 sys.publish(msg, ...)之前,必须使用 sys.subscribe(msg, msg_cbfunc)注册消息回调函数;

这样才能保证发布的 msg 消息可以被 msg_cbfunc 消息回调函数处理;

同一个全局消息 msg,可以多次调用 sys.subscribe(msg, msg_cbfunc)订阅多个不同的回调函数;

参数

msg

参数含义:全局消息的名称,和sys.publish(msg, ...)中的msg保持一致
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"SEND_DATA_REQ"等任意自定义的字符串;

msg_cbfunc

参数含义:
    全局消息msg的回调函数     
    回调函数的格式有两种:

    第一种:使用...来接收全局消息携带的参数
    function msg_cbfunc(...)
        -- ...是Lua语言的一种语法,表示可变参数,参数数量可以是0个,1个,2个,...,多个;
        -- 如下代码所示,args[1]表示可变参数的第一个参数,args[2]表示可变参数的第二个参数,...
        local args = {...}
        log.info("msg_cbfunc", args[1], args[2], args[3])
    end

    第二种:使用具体的形参名来接收全局消息携带的参数,
           形参的个数只要不少于sys.publish(msg, ...)...的参数个数就行
    function msg_cbfunc(arg1, arg2, arg3)
        log.info("msg_cbfunc", arg1, arg2, arg3)
    end

    回调函数是在task之外的业务逻辑中被执行的    
    在回调函数内部无法使用sys.wait(timeout)sys.waitUntil(msg, timeout)
    sys.waitMsg(task_name, msg, timeout)等必须用在task中的函数
数据类型:function
取值范围:无特别限制;
是否必选:必须传入此参数;
注意事项:
    特别需要注意传入的函数名的作用域,如果使用不当,很可能会使用一个无效的函数名;
    如下代码就是一个典型的错误示例

    -- 此处sys.subscribe("SEND_DATA_REQ", send_data_req_cbfunc)中使用的send_data_req_cbfunc
    -- 并不是一个function类型,而是nil类型;    
    -- 因为下文定义的函数send_data_req_cbfunc,没办法在其上部去使用;
    -- 任何函数都要先定义后使用,不允许先使用后定义    
    sys.subscribe("SEND_DATA_REQ", send_data_req_cbfunc)

    local function send_data_req_cbfunc()
        -- 此处省略了代码
    end
参数示例:
    如下所示,定义了一个函数send_data_req_cbfuncsend_data_req_cbfunc就可以做为此参数传入
    local function send_data_req_cbfunc()
        -- 此处省略了代码
    end

    sys.subscribe("SEND_DATA_REQ", send_data_req_cbfunc)

返回值

nil

示例

local function send_data_req_cbfunc1(data, tag)
    -- 此处的data为"123456",tag为"LuatOS"
    log.info("send_data_req_cbfunc1", data, tag)
end

local function send_data_req_cbfunc2(...)
    local args = {...}
    -- 此处的args[1]为"123456",args[2]为"LuatOS"
    log.info("send_data_req_cbfunc2", args[1] args[2])
end

sys.subscribe("SEND_DATA_REQ", send_data_req_cbfunc1)
sys.subscribe("SEND_DATA_REQ", send_data_req_cbfunc2)

-- 发布一条全局消息"SEND_DATA_REQ",携带的第一个参数为"123456",携带的第二个参数为"LuatOS"
sys.publish("SEND_DATA_REQ", "123456", "LuatOS")

sys.unsubscribe(msg, msg_cbfunc)

功能

取消订阅一个全局消息的回调函数;

注意事项

可以在能够执行到的任意代码位置使用此函数;

参数

msg

参数含义:全局消息的名称,和sys.subscribe(msg, msg_cbfunc)中的msg保持一致
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"SEND_DATA_REQ"等任意自定义的字符串;

msg_cbfunc

参数含义:
    全局消息msg的回调函数,和sys.subscribe(msg, msg_cbfunc)中的msg_cbfunc保持一致     
    回调函数的格式有两种:

    第一种:使用...来接收全局消息携带的参数
    function msg_cbfunc(...)
        -- ...是Lua语言的一种语法,表示可变参数,参数数量可以是0个,1个,2个,...,多个;
        -- 如下代码所示,args[1]表示可变参数的第一个参数,args[2]表示可变参数的第二个参数,...
        local args = {...}
        log.info("msg_cbfunc", args[1], args[2], args[3])
    end

    第二种:使用具体的形参名来接收全局消息携带的参数,
           形参的个数只要不少于sys.publish(msg, ...)...的参数个数就行
    function msg_cbfunc(arg1, arg2, arg3)
        log.info("msg_cbfunc", arg1, arg2, arg3)
    end
数据类型:function
取值范围:无特别限制;
是否必选:必须传入此参数;
注意事项:
    特别需要注意传入的函数名的作用域,如果使用不当,很可能会使用一个无效的函数名;
    如下代码就是一个典型的错误示例

    -- 此处sys.unsubscribe("SEND_DATA_REQ", send_data_req_cbfunc)中使用的send_data_req_cbfunc
    -- 并不是一个function类型,而是nil类型;    
    -- 因为下文定义的函数send_data_req_cbfunc,没办法在其上部去使用;
    -- 任何函数都要先定义后使用,不允许先使用后定义

    sys.unsubscribe("SEND_DATA_REQ", send_data_req_cbfunc)

    local function send_data_req_cbfunc()
        -- 此处省略了代码
    end
参数示例:
    如下所示,定义了一个函数send_data_req_cbfuncsend_data_req_cbfunc就可以做为此参数传入
    local function send_data_req_cbfunc()
        -- 此处省略了代码
    end

    sys.unsubscribe("SEND_DATA_REQ", send_data_req_cbfunc)

返回值

nil

示例

local function send_data_req_cbfunc1(data, tag)
    -- 此处的data为"123456",tag为"LuatOS"
    log.info("send_data_req_cbfunc1", data, tag)
end

local function send_data_req_cbfunc2(...)
    local args = {...}
    -- 此处的args[1]为"123456",args[2]为"LuatOS"
    log.info("send_data_req_cbfunc2", args[1] args[2])
end

sys.subscribe("SEND_DATA_REQ", send_data_req_cbfunc1)
sys.subscribe("SEND_DATA_REQ", send_data_req_cbfunc2)

-- 执行下面这行代码后
-- SEND_DATA_REQ"的回调函数send_data_req_cbfunc1会被清除
-- 本代码片段中"SEND_DATA_REQ"只有send_data_req_cbfunc2这一个回调函数了
sys.unsubscribe("SEND_DATA_REQ", send_data_req_cbfunc1)

-- 发布一条全局消息"SEND_DATA_REQ",携带的第一个参数为"123456",携带的第二个参数为"LuatOS"
sys.publish("SEND_DATA_REQ", "123456", "LuatOS")

sys.waitUntil(msg, timeout)

功能

在 task 中阻塞等待一个全局消息;

注意事项

基础 task 和高级 task 的定义:参考本文第一章节的描述;

只能在基础 task 和高级 task 处理函数的业务逻辑中使用此函数;

sys.publish(msg, ...) 和 sys.waitUntil(msg, timeout)配合使用时:

在 sys.publish(msg, ...)之前,必须保证 task 正在 sys.waitUntil(msg, timeout)代码处,处于阻塞等待状态;

这样才能保证发布的 msg 消息可以被 task 处理;

同一个全局消息 msg,可以被多个正在 sys.waitUntil(msg, timeout)代码处阻塞等待的 task 处理;

参数

msg

参数含义:全局消息的名称,和sys.publish(msg, ...)中的msg保持一致
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"SEND_DATA_REQ"等任意自定义的字符串;

timeout

参数含义:阻塞等待全局消息msg的超时时长,单位毫秒;
数据类型:number或者nil
取值范围:
    大于等于1,小于等于0x7FFFFFFF,之间的所有正整数;最大时长0x7FFFFFFF毫秒  596小时  24.85天;
    如果为nil,表示一直阻塞等待全局消息,不会超时;
是否必选:可选传入此参数;
注意事项:
    此处的超时机制基于软件定时器实现,受系统负载、任务数量、消息堆积、网络中断优先级最高等多因素影响,无法实现高精度;
    尤其是几个毫秒级别,几十毫秒级别,几百毫秒级别的超时时长,误差都比较大,秒级别以上的超时时长误差较小;
    例如:设置的1毫秒,可能要等几十毫秒;设置的几十毫秒,可能要等上百毫秒;设置的几百毫秒,可能要等几百几十毫秒;
    可以简单的认为会延迟几十毫秒左右,以此来评估超时时长精度是否可以满足业务需求;

    调用本接口时,如果传入了timeout参数,则sys核心库内部会自动创建并且运行一个软件定时器,超时时长到达后,会自动删除这个定时器;
    LuatOS应用程序中可用的软件定时器总数量为64个    
    注意控制自已应用程序中的同时运行的软件定时器总数量不要超过64个,否则创建新的定时器会返回失败;
参数示例:50100060000等;

返回值

local result, arg1, arg2, arg3, argN = sys.waitUntil(msg, timeout)

有数量不固定的返回值:

第一个返回值为 result

剩余的返回值 arg1, arg2, arg3, argN,表示可变数量的返回值,只有当第一个返回值 result 为 true 时,这些可变数量的返回值才有意义,和 sys.publish(msg, ...)中...表示的可变参数一一对应

result

含义说明:阻塞等待的结果;true表示收到了msg消息false表示超时没有收到msg消息
数据类型:boolean
取值范围:true或者false
注意事项:暂无;

arg1, arg2, arg3, argN

含义说明:
    result为true时arg1, arg2, arg3, argN表示sys.publish(msg, ...)...可变参数,从前到后一一对应;
    result为false时arg1, arg2, arg3, argN全部都为nil,没有任何意义;
数据类型:
    result为true时arg1, arg2, arg3, argN的数据类型和sys.publish(msg, ...)...可变参数的数据类型,从前到后一一对应;
    result为false时arg1, arg2, arg3, argN全部都为nil类型
取值范围:无特别限制;
注意事项:暂无;

正确示例

local function send_data_task_func()
    -- 此处可以等到"SEND_DATA_REQ"消息,result为true, data为"123456",tag为"LuatOS"
    local result, data, tag = sys.waitUntil("SEND_DATA_REQ", 5000)
    log.info("send_data_task_func", result, data, tag)
end

local function send_data_extask_func()
    -- 此处接收不到"UNKNOWN_SEND_DATA_REQ"消息,因为没有发布这个消息,所以超时5秒后退出阻塞等待状态
    -- result为false, data为nil,tag为nil
    local result, data, tag = sys.waitUntil("UNKNOWN_SEND_DATA_REQ", 5000)
    log.info("send_data_extask_func", result, data, tag)

    sys.cleanMsg("EXTASK2")
    sys.taskDel("EXTASK2")
end

sys.taskInit(send_data_task_func)
sys.taskInitEx(send_data_extask_func, "EXTASK2")

-- 发布一条全局消息"SEND_DATA_REQ",携带的第一个参数为"123456",携带的第二个参数为"LuatOS"
sys.publish("SEND_DATA_REQ", "123456", "LuatOS")

错误示例

local function send_data_task_func()
    -- 此处阻塞等待1秒钟是一种典型的错误写法;
    -- sys.publish("SEND_DATA_REQ", "123456", "LuatOS")时,task在sys.wait(1000)代码处阻塞等待
    -- 会导致后续的sys.waitUntil("SEND_DATA_REQ", 5000)接收不到"SEND_DATA_REQ"消息
    sys.wait(1000)

    -- 此处接收不到"SEND_DATA_REQ"消息
    -- 因为在sys.publish("SEND_DATA_REQ", "123456", "LuatOS")时,task没有在sys.waitUntil("SEND_DATA_REQ", 5000)代码处阻塞等待
    -- 所以超时5秒后退出阻塞等待状态
    -- result为false, data为nil,tag为nil
    local result, data, tag = sys.waitUntil("SEND_DATA_REQ", 5000)
    log.info("send_data_task_func", result, data, tag)
end

local function send_data_extask_func()
    -- 此处阻塞等待1秒钟是一种典型的错误写法;
    -- sys.publish("SEND_DATA_REQ", "123456", "LuatOS")时,task在sys.wait(1000)代码处阻塞等待
    -- 会导致后续的sys.waitUntil("SEND_DATA_REQ", 5000)接收不到"SEND_DATA_REQ"消息
    sys.wait(1000)

    -- 此处接收不到"SEND_DATA_REQ"消息
    -- 因为在sys.publish("SEND_DATA_REQ", "123456", "LuatOS")时,task没有在sys.waitUntil("SEND_DATA_REQ", 5000)代码处阻塞等待
    -- 所以超时5秒后退出阻塞等待状态
    -- result为false, data为nil,tag为nil
    local result, data, tag = sys.waitUntil("SEND_DATA_REQ", 5000)
    log.info("send_data_extask_func", result, data, tag)

    sys.cleanMsg("EXTASK2")
    sys.taskDel("EXTASK2")
end

sys.taskInit(send_data_task_func)
sys.taskInitEx(send_data_extask_func, "EXTASK2")

-- 发布一条全局消息"SEND_DATA_REQ",携带的第一个参数为"123456",携带的第二个参数为"LuatOS"
sys.publish("SEND_DATA_REQ", "123456", "LuatOS")

用户全局消息处理的代码示例

在了解了全局消息的几个 api 之后,我们再看下图回顾一下全局消息处理的完整周期

下面这个例子用来说明用户全局消息的完整处理过程;

这个例子的完整代码链接:global_msg_receiver1.lua global_msg_receiver2.lua global_msg_sender.lua

因为是三个文件,并且代码比较多,所以在这里我就不粘贴代码到这里了,直接打开 vscode 来分析代码的业务逻辑;

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

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

运行 15 秒钟的日志如下,为了方便分析,我把日志做了处理,每个订阅者订阅同一类型的消息日志汇总到了一起:

init_subscribe_cbfunc 订阅者

init_subscribe_cbfunc 订阅的 task 内部发布的消息,运行正常,符合预期,每秒接收到一次 task 内部发布的消息;

因为 10 秒钟之后,取消了订阅,所以只收到前 10 条消息

I/user.init_subscribe_cbfunc from task 1
I/user.init_subscribe_cbfunc from task 2
I/user.init_subscribe_cbfunc from task 3
I/user.init_subscribe_cbfunc from task 4
I/user.init_subscribe_cbfunc from task 5
I/user.init_subscribe_cbfunc from task 6
I/user.init_subscribe_cbfunc from task 7
I/user.init_subscribe_cbfunc from task 8
I/user.init_subscribe_cbfunc from task 9
I/user.init_subscribe_cbfunc from task 10

init_subscribe_cbfunc 订阅的 timer 发布的消息,运行正常,符合预期,每秒接收到一次 task 内部发布的消息;

因为 10 秒钟之后,取消了订阅,所以只收到前 10 条消息

I/user.init_subscribe_cbfunc from timer 1
I/user.init_subscribe_cbfunc from timer 2
I/user.init_subscribe_cbfunc from timer 3
I/user.init_subscribe_cbfunc from timer 4
I/user.init_subscribe_cbfunc from timer 5
I/user.init_subscribe_cbfunc from timer 6
I/user.init_subscribe_cbfunc from timer 7
I/user.init_subscribe_cbfunc from timer 8
I/user.init_subscribe_cbfunc from timer 9
I/user.init_subscribe_cbfunc from timer 10

delay_subscribe_cbfunc 订阅者

delay_subscribe_cbfunc 订阅的 task 内部发布的消息,我们可以看到是从第 6 条消息开始接收处理,前面的 5 条丢失了;这是因为 sys.timerStart(sys.subscribe, 5000, "SEND_DATA_REQ", delay_subscribe_cbfunc)这行代码,是开机之后延迟 5 秒,然后才订阅了"SEND_DATA_REQ"回调函数的 delay_subscribe_cbfunc,所以前 5 秒的消息都无法接收处理;这段运行日志就可以验证本文前面描述的这段话:

在 sys.publish(msg, ...)之前,必须使用 sys.subscribe(msg, msg_cbfunc)注册消息回调函数;这样才能保证发布的 msg 消息可以被 msg_cbfunc 消息回调函数处理;再简单点来说,订阅要在发布前,就像邮局定报纸一样,你只有购买了订阅服务,在邮局发报纸的时候才不会把你漏掉;

I/user.delay_subscribe_cbfunc from task 6
I/user.delay_subscribe_cbfunc from task 7
I/user.delay_subscribe_cbfunc from task 8
I/user.delay_subscribe_cbfunc from task 9
I/user.delay_subscribe_cbfunc from task 10

同理,delay_subscribe_cbfunc 订阅的 timer 内部发布的消息,表现也是一样

I/user.delay_subscribe_cbfunc from timer 6
I/user.delay_subscribe_cbfunc from timer 7
I/user.delay_subscribe_cbfunc from timer 8
I/user.delay_subscribe_cbfunc from timer 9
I/user.delay_subscribe_cbfunc from timer 10

task(success_wait_until_base_task_func)订阅者

task 任务处理函数 success_wait_until_base_task_func 中通过 sys.waitUntil("SEND_DATA_REQ")订阅的 task 内部发布的消息,运行正常,符合预期,每秒接收到一次 task 内部发布的消息,一共收到 15 条消息;

I/user.success_wait_until_base_task_func from task 1
I/user.success_wait_until_base_task_func from task 2
I/user.success_wait_until_base_task_func from task 3
I/user.success_wait_until_base_task_func from task 4
I/user.success_wait_until_base_task_func from task 5
I/user.success_wait_until_base_task_func from task 6
I/user.success_wait_until_base_task_func from task 7
I/user.success_wait_until_base_task_func from task 8
I/user.success_wait_until_base_task_func from task 9
I/user.success_wait_until_base_task_func from task 10
I/user.success_wait_until_base_task_func from task 11
I/user.success_wait_until_base_task_func from task 12
I/user.success_wait_until_base_task_func from task 13
I/user.success_wait_until_base_task_func from task 14
I/user.success_wait_until_base_task_func from task 15

task 任务处理函数 success_wait_until_base_task_func 中通过 sys.waitUntil("SEND_DATA_REQ")订阅的 timer 发布的消息,运行正常,符合预期,每秒接收到一次 task 内部发布的消息,一共收到 15 条消息;

I/user.success_wait_until_base_task_func from timer 1
I/user.success_wait_until_base_task_func from timer 2
I/user.success_wait_until_base_task_func from timer 3
I/user.success_wait_until_base_task_func from timer 4
I/user.success_wait_until_base_task_func from timer 5
I/user.success_wait_until_base_task_func from timer 6
I/user.success_wait_until_base_task_func from timer 7
I/user.success_wait_until_base_task_func from timer 8
I/user.success_wait_until_base_task_func from timer 9
I/user.success_wait_until_base_task_func from timer 10
I/user.success_wait_until_base_task_func from timer 11
I/user.success_wait_until_base_task_func from timer 12
I/user.success_wait_until_base_task_func from timer 13
I/user.success_wait_until_base_task_func from timer 14
I/user.success_wait_until_base_task_func from timer 15

task(lost_wait_until_base_task_func)订阅者

task 任务处理函数 lost_wait_until_base_task_func 中通过 sys.waitUntil("SEND_DATA_REQ")订阅的 task 内部发布的消息,运行异常,一共收到 0 条消息;15 条消息全部丢失

task 任务处理函数 lost_wait_until_base_task_func 中通过 sys.waitUntil("SEND_DATA_REQ")订阅的定时器发布的消息,运行异常,一共收到 4 条消息;11 条消息全部丢失

I/user.lost_wait_until_base_task_func from timer 4
I/user.lost_wait_until_base_task_func from timer 7
I/user.lost_wait_until_base_task_func from timer 10
I/user.lost_wait_until_base_task_func from timer 13

这段运行日志就可以验证本文前面描述的这段话:

在 sys.publish(msg, ...)之前,必须保证 task 正在 sys.waitUntil(msg, timeout)代码处,处于阻塞等待状态;

这样才能保证发布的 msg 消息可以被 task 处理;

我们再看下这个订阅者 task 的任务处理函数代码,在如下代码的第 4 行和第 5 行,都会使 task 阻塞,其中第 4 行会阻塞 3 秒,这 3 秒内,无论是 task 发布的消息还是 timer 发布的消息,都无法接收处理,直接丢失;3 秒后,在第 5 行阻塞等待"SEND_DATA_REQ"消息,等到一条消息后,重新又阻塞 3 秒,在本 demo 中的表现就是每隔 3 秒接收到一次 task 或者 timer 发布的消息;

local function lost_wait_until_base_task_func()
    local result, tag, count
    while true do
        -- 阻塞等待3秒钟
        -- 在这段时间内,本task无法及时处理全局消息发送模块发布的"SEND_DATA_REQ"消息,会造成消息丢失
        sys.wait(3000)

        result, tag, count = sys.waitUntil("SEND_DATA_REQ")
        if result then
            log.info("lost_wait_until_base_task_func", tag, count)
        end
    end
end

如何避免这种问题呢?

在接收消息的 task 任务处理函数业务逻辑中,最好只有 sys.waitUntil,并且也只有一处 sys.waitUntil 阻塞当前 task;上面这个代码块,只要把第 4 行去掉就没有问题了;

如果业务逻辑必须要多处阻塞代码,只要保证消息发布的时序和消息阻塞接收的时序不出问题就行;

3.2.5 用户定向消息

用户定向消息处理的完整周期

简化后用户定向消息处理的完整周期包括以下几部分:

  1. 消息发送:有一个 api 可以发送定向消息

  2. sys.sendMsg(task_name, msg, arg2, arg3, arg4):发送一条定向消息给 task_name;相当于在定向消息队列中的队尾位置增加一项{msg, arg2, arg3, arg4}

  3. 消息接收:有一个 api 可以接收定向消息

  4. sys.waitMsg(task_name, msg, timeout):读取 task_name 的一条定向消息,如果定向消息队列中有指定的 msg 消息,则读出处理;如果有其他非指定的消息,给非目标消息回调函数处理;如果没有消息,则阻塞等待

  5. 消息调度处理:有一个 api 对定向消息进行调度处理

  6. 如果发送定向消息后,接收者高级 task 处于阻塞状态,则 sys.run()调度器会控制处于阻塞状态的 task,退出阻塞状态,读取定向消息进行处理;

  7. 删除接收者:在消息调度处理过程中,sys.waitMsg(task_name, msg, timeout)读出指定的 msg 消息,处理完之后,会自动删除接收者,只有等到下次运行到 sys.waitMsg(task_name, msg, timeout)是才会创建新的接收者;
  8. 删除消息:sys.run()消息调度处理过程中,自动删除;

上一小节我们讨论了用户全局消息,还记得用户全局消息处理有两个注意事项:

  1. 在 sys.publish(msg, ...)之前,必须使用 sys.subscribe(msg, msg_cbfunc)注册消息回调函数;这样才能保证发布的 msg 消息可以被 msg_cbfunc 消息回调函数处理;
  2. 在 sys.publish(msg, ...)之前,必须保证 task 正在 sys.waitUntil(msg, timeout)代码处,处于阻塞等待状态;这样才能保证发布的 msg 消息可以被 task 处理;

现在我们讨论的用户定向消息已经不存在这个问题,消息发布和消息处理没有严格的时序要求,可以先发送定向消息,消息会存储到定向消息队列中,只有当调用 sys.waitMsg(task_name, msg, timeout)接口主动去读取定向消息时,才会从定向消息队列中读出消息进行处理,所以不存在消息丢失的问题;但是如果使用不当,可能会出现消息处理延迟的问题;

了解了用户全局消息的完整生命周期后,接下来我们:

1、先看下在整个生命周期中用到的几个 sys 核心库的 api

2、写一个完整的 demo 示例来实际运行演示一下如何使用;

sys.sendMsg(task_name, msg, arg2, arg3, arg4)

功能

向名称为 task_name 的 task 发布一个定向消息;

注意事项

定向消息的定义:参考本文第一章节的描述;

可以在能够执行到的任意代码位置使用此函数;

sys.sendMsg(task_name, msg, arg2, arg3, arg4)是定向消息的生产者,定向消息有生产就会有消费,不然消息就没有存在的意义了;

sys.waitMsg(task_name, msg, timeout)所在的 task 是定向消息的消费者;

sys.sendMsg(task_name, msg, arg2, arg3, arg4) 和 sys.waitMsg(task_name, msg, timeout)配合使用;

在 sys.sendMsg(task_name, msg, arg2, arg3, arg4)之前,需要保证名称为 task_name 的 task 已经被创建,否则定向消息也会丢失;

参数

task_name

参数含义:task的名称,和sys.taskInitEx(task_func, task_name, non_targeted_msg_cbfunc, ...)中的task_name保持一致
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"LED_TASK""GPIO_TASK"等任意自定义的字符串;

msg

参数含义:消息的名称;
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"SEND_DATA_REQ"等任意自定义的字符串;

arg2

参数含义:msg消息携带的第一个参数
数据类型:任意数据类型;
取值范围:无特别限制;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:
    不传入,或者boolean类型的true,或者number类型的2,或者string类型的"led",或者table类型的{name="LuatOS", password="123456"}等等等等;
    总之,任何数据类型的任何自定义内容都行;

arg3

参数含义:msg消息携带的第二个参数
数据类型:任意数据类型;
取值范围:无特别限制;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:
    不传入,或者boolean类型的true,或者number类型的2,或者string类型的"led",或者table类型的{name="LuatOS", password="123456"}等等等等;
    总之,任何数据类型的任何自定义内容都行;

arg4

参数含义:msg消息携带的第三个参数
数据类型:任意数据类型;
取值范围:无特别限制;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:
    不传入,或者boolean类型的true,或者number类型的2,或者string类型的"led",或者table类型的{name="LuatOS", password="123456"}等等等等;
    总之,任何数据类型的任何自定义内容都行;

返回值

local result = sys.sendMsg(task_name, msg, arg2, arg3, arg4)

result

含义说明:定向消息发布结果,成功返回true,失败返回false
数据类型:boolean
取值范围:true或者false
注意事项:暂无;

示例

-- 向名称为"TCP_CLIENT_TASK"的task发布一条定向消息"SEND_DATA_REQ",携带一个参数"123456"
sys.sendMsg("TCP_CLIENT_TASK", "SEND_DATA_REQ", "123456")

sys.waitMsg(task_name, msg, timeout)

功能

在 task 中阻塞等待名称为 task_name 的 task 的定向消息;

注意事项

只能在高级 task 处理函数的业务逻辑中使用此函数;

sys.sendMsg(task_name, msg, arg2, arg3, arg4)是定向消息的生产者,定向消息有生产就会有消费,不然消息就没有存在的意义了;

sys.waitMsg(task_name, msg, timeout)所在的 task 是定向消息的消费者;

sys.sendMsg(task_name, msg, arg2, arg3, arg4) 和 sys.waitMsg(task_name, msg, timeout)配合使用;

在 sys.sendMsg(task_name, msg, arg2, arg3, arg4)之前,需要保证名称为 task_name 的 task 已经被创建,否则定向消息也会丢失;

参数

task_name

参数含义:task的名称,和sys.taskInitEx(task_func, task_name, non_targeted_msg_cbfunc, ...)中的task_name保持一致
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"LED_TASK""GPIO_TASK"等任意自定义的字符串;

msg

参数含义:消息的名称,和sys.sendMsg(task_name, msg, arg2, arg3, arg4)中的msg保持一致
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"SEND_DATA_REQ"等任意自定义的字符串;

timeout

参数含义:阻塞等待定向消息msg的超时时长,单位毫秒,nil表示一直阻塞等待
数据类型:number或者nil
取值范围:
    大于等于1,小于等于0x7FFFFFFF,之间的所有正整数;最大时长0x7FFFFFFF毫秒  596小时  24.85天;
    如果为nil,表示一直阻塞等待全局消息,不会超时;
是否必选:可选传入此参数;
注意事项:
    此处的超时机制基于软件定时器实现,受系统负载、任务数量、消息堆积、网络中断优先级最高等多因素影响,无法实现高精度;
    尤其是几个毫秒级别,几十毫秒级别,几百毫秒级别的超时时长,误差都比较大,秒级别以上的超时时长误差较小;
    例如:设置的1毫秒,可能要等几十毫秒;设置的几十毫秒,可能要等上百毫秒;设置的几百毫秒,可能要等几百几十毫秒;
    可以简单的认为会延迟几十毫秒左右,以此来评估超时时长精度是否可以满足业务需求;


    调用本接口时,如果传入了timeout参数,则sys核心库内部会自动创建并且运行一个软件定时器,超时时长到达后,会自动删除这个定时器;
    LuatOS应用程序中可用的软件定时器总数量为64个    
    注意控制自已应用程序中的同时运行的软件定时器总数量不要超过64个,否则创建新的定时器会返回失败;
参数示例:50100060000等;

返回值

local message = sys.waitMsg(task_name, msg, timeout)

有一个返回值为 message

message

含义说明:
    阻塞等待的结果;table类型表示接收到msg消息false表示超时没有收到msg消息
    当接收到msg消息时message[1],message[2],message[3],message[4]sys.sendMsg(task_name, msg, arg2, arg3, arg4)中的 msg, arg2, arg3, arg4 一一对应;
    当超时没有收到msg消息时message为false
数据类型:table或者boolean
取值范围:无特别限制;
注意事项:暂无;

示例

local function send_data_extask1_func()
    -- 此处可以等到"SEND_DATA_REQ"消息,message为table类型
    -- message[1]为"SEND_DATA_REQ",message[2]为"123456",message[3]为"LuatOS"
    local message = sys.waitMsg("EXTASK1", "SEND_DATA_REQ", 5000)

    if message then
        log.info("send_data_extask1_func", message[1], message[2], message[3])
    end

    sys.cleanMsg("EXTASK1")
    sys.taskDel("EXTASK1")
end

local function send_data_extask2_func()
    -- 此处等不到"UNKNOWN_SEND_DATA_REQ"消息,超时5秒退出阻塞状态,message为false
    local message = sys.waitMsg("EXTASK2", "UNKNOWN_SEND_DATA_REQ", 5000)

    if not message then
        log.info("send_data_extask2_func", "wait timeout")
    end

    sys.cleanMsg("EXTASK2")
    sys.taskDel("EXTASK2")
end

sys.taskInitEx(send_data_extask1_func, "EXTASK1")
sys.taskInitEx(send_data_extask2_func, "EXTASK2")

-- 向名称为"EXTASK1"的task发布一条定向消息"SEND_DATA_REQ",携带的第一个参数为"123456",携带的第二个参数为"LuatOS"
sys.sendMsg("EXTASK1", "SEND_DATA_REQ" "123456", "LuatOS")

用户定向消息处理的代码示例

在了解了定向消息的两个 api 之后,我们再看下图回顾一下定向消息处理的完整周期

下面这个例子用来说明用户定向消息的完整处理过程;

这个例子的完整代码链接:tgted_msg_receiver.lua targeted_msg_sender.lua

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

--[[
@module  targeted_msg_sender
@summary “用户定向消息发送”演示功能模块
@version 1.0
@date    2025.08.12
@author  朱天华
@usage
本文件为targeted_msg_sender应用功能模块,用来演示“用户定向消息发送”功能,核心业务逻辑为:
1、创建并且启动一个基础task,每隔一秒钟向两个高级task发布各发送一条定向消息;

本文件没有对外接口,直接在main.lua中require "targeted_msg_sender"就可以加载运行;
]]

local function targeted_msg_sender_task_func()
    local count = 0

    while true do
        count = count+1

        -- 发布一条定向消息到名称为"nromal_wait_msg_task"的高级task
        -- 消息名称为"SEND_DATA_REQ"
        -- 消息携带两个参数:
        -- 第一个参数是"from task"
        -- 第二个参数是number类型的count
        sys.sendMsg("nromal_wait_msg_task", "SEND_DATA_REQ", "from task", count)

        -- 发布一条定向消息到名称为"delay_wait_msg_task"的高级task
        -- 消息名称为"SEND_DATA_REQ"
        -- 消息携带两个参数:
        -- 第一个参数是"from task"
        -- 第二个参数是number类型的count
        sys.sendMsg("delay_wait_msg_task", "SEND_DATA_REQ", "from task", count)

        -- 延时等待1秒
        sys.wait(1000)
    end
end

-- 创建并且启动一个基础task
-- 运行这个task的任务处理函数targeted_msg_sender_task_func
sys.taskInit(targeted_msg_sender_task_func)
--[[
@module  tgted_msg_receiver
@summary “使用sys.waitMsg接口实现task内用户定向消息接收”功能演示模块
@version 1.0
@date    2025.08.12
@author  朱天华
@usage
本文件为tgted_msg_receiver应用功能模块;
用来演示“使用sys.waitMsg接口实现task内用户定向消息接收”的功能,核心业务逻辑为:
1、创建并且启动一个高级task,task名称为"nromal_wait_msg_task",在task的任务处理函数内及时接收发送给自己的定向消息;
2、创建并且启动另一个高级task,task名称为"delay_wait_msg_task",在task的任务处理函数内延时接收发送给自己的定向消息;

本文件没有对外接口,直接在main.lua中require "tgted_msg_receiver"就可以加载运行;
]]

local function normal_wait_msg_task_func()
    local msg
    while true do
        msg = sys.waitMsg("nromal_wait_msg_task", "SEND_DATA_REQ")
        if msg then
            log.info("normal_wait_msg_task_func", msg[1], msg[2], msg[3], msg[4])
        end
    end
end

local function delay_wait_msg_task_func()
    local msg
    while true do
        -- 阻塞等待3秒钟
        -- 在这段时间内,本task无法及时处理定向消息发送模块发布的"SEND_DATA_REQ"消息
        -- 但是不会造成消息丢失,消息会存储到本task绑定的定向消息队列中
        -- 虽然不会造成消息丢失,但是业务逻辑中这样写明显也存在问题,因为消息处理的及时性很差
        sys.wait(3000)

        msg = sys.waitMsg("delay_wait_msg_task", "SEND_DATA_REQ")
        if msg then
            log.info("delay_wait_msg_task_func", msg[1], msg[2], msg[3], msg[4])
        end
    end
end

-- 创建并且启动一个高级task,task名称为"nromal_wait_msg_task"
-- 运行这个task的任务处理函数normal_wait_msg_task_func
sys.taskInitEx(normal_wait_msg_task_func, "nromal_wait_msg_task")

-- 创建并且启动一个高级task,task名称为"delay_wait_msg_task"
-- 运行这个task的任务处理函数delay_wait_msg_task_func
sys.taskInitEx(delay_wait_msg_task_func, "delay_wait_msg_task")

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

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

运行 15 秒钟的日志如下,为了方便分析,我把日志做了处理,每个消息接收者的日志汇总到了一起:

normal_wait_msg_task 接收者

nromal_wait_msg_task 的任务处理函数中,调用 sysplus.waitMsg("nromal_wait_msg_task", "SEND_DATA_REQ")接收消息,在 15 秒的时间内,一共收到 15 条消息,运行正常,符合预期,每秒接收到一次消息;

I/user.normal_wait_msg_task_func SEND_DATA_REQ from task 1 nil
I/user.normal_wait_msg_task_func SEND_DATA_REQ from task 2 nil
I/user.normal_wait_msg_task_func SEND_DATA_REQ from task 3 nil
I/user.normal_wait_msg_task_func SEND_DATA_REQ from task 4 nil
I/user.normal_wait_msg_task_func SEND_DATA_REQ from task 5 nil
I/user.normal_wait_msg_task_func SEND_DATA_REQ from task 6 nil
I/user.normal_wait_msg_task_func SEND_DATA_REQ from task 7 nil
I/user.normal_wait_msg_task_func SEND_DATA_REQ from task 8 nil
I/user.normal_wait_msg_task_func SEND_DATA_REQ from task 9 nil
I/user.normal_wait_msg_task_func SEND_DATA_REQ from task 10 nil
I/user.normal_wait_msg_task_func SEND_DATA_REQ from task 11 nil
I/user.normal_wait_msg_task_func SEND_DATA_REQ from task 12 nil
I/user.normal_wait_msg_task_func SEND_DATA_REQ from task 13 nil
I/user.normal_wait_msg_task_func SEND_DATA_REQ from task 14 nil
I/user.normal_wait_msg_task_func SEND_DATA_REQ from task 15 nil

delay_wait_msg_task 接收者

delay_wait_msg_task 的任务处理函数中,调用 sysplus.waitMsg("nromal_wait_msg_task", "SEND_DATA_REQ")接收消息,在 15 秒的时间内,一共收到 5 条消息,和代码设计相符,每延时 3 秒接收到一次消息;

但是我们再仔细观察下收到消息,消息的计数参数依次为 1,2,3,4,5,说明虽然延时收到消息,但是消息并没有出现丢失;

这一点儿和上一小节讲的全局消息的处理完全不同,全局消息在这种情况下会丢失消息;

I/user.delay_wait_msg_task_func SEND_DATA_REQ from task 1 nil
I/user.delay_wait_msg_task_func SEND_DATA_REQ from task 2 nil
I/user.delay_wait_msg_task_func SEND_DATA_REQ from task 3 nil
I/user.delay_wait_msg_task_func SEND_DATA_REQ from task 4 nil
I/user.delay_wait_msg_task_func SEND_DATA_REQ from task 5 nil

3.2.6 用户定向消息 vs 用户全局消息,如何选择?

在了解了用户定向消息和用户全局消息之后,大家可能会有一个疑问:我平时开发程序时,怎么判断应该使用定向消息还是全局消息呢?一般来说,建议大家按照以下几步来做选择:

  1. 首先确认你的程序中有没有使用高级 task,因为有些应用功能模块是必须要使用高级 task 的,例如创建一个 socket 时,要使用高级 task;如果使用了高级 task,那就只能使用用户定向消息了;如果没有使用高级 task,继续向下判断;
  2. 如果你对用户全局消息的处理过程以及其中容易出现的消息丢失问题,非常清楚,知道如何规避这种消息丢失问题,则可以使用全局消息,因为全局消息使用起来简单;
  3. 如果你对用户全局消息的处理过程以及其中容易出现的问题都不是很清楚,这种情况下,建议使用定向消息和高级 task,因为这种方式下,只要在发送定向消息前,高级 task 已经创建,则消息不会丢失;

3.3 LuatOS 的定时器(timer)

对于 LuatOS 应用程序来说,定时器本质上也算是一种特殊的消息,因为定时器太常用了,所以把他单独拎出来,单独的一个章节进行讲解;

3.3.1 基本概念

LuatOS 定时器的分类如下:

LuatOS 定时器管理的 API 列表如下:

(1) 单次定时器创建并且启动:sys.timerStart(cbfunc, timeout, ...)

(2) 循环定时器创建并且启动:sys.timerLoopStart(cbfunc, timeout, ...)

(3) 单个定时器停止并且删除:sys.timerStop(timer_id)

(4) 单个定时器停止并且删除:sys.timerStop(cbfunc, ...)

(5) 多个定时器停止并且删除:sys.timerStopAll(cbfunc)

(6) 阻塞等待一段时间(只能在 task 中使用):sys.wait(timeout)

(7) 阻塞等待全局消息或者阻塞等待一段时间(只能在 task 中使用):sys.waitUntil(msg, timeout)

(8) 阻塞等待定向消息或者阻塞等待一段时间(只能在 task 中使用):sys.waitMsg(task_name, msg, timeout)

3.3.2 定时器消息处理的完整周期

3.3.3 sys.timerStart(cbfunc, timeout, ...)

功能

创建并且运行一个单次定时器;

注意事项

可以在能够执行到的任意代码位置使用此函数;

有两种方式可以唯一标识一个定时器:

1、定时器 id;如果使用 sys.timerStart(cbfunc, timeout, ...)创建定时器成功,会返回定时器 id

2、定时器回调函数 cbfunc 和可变参数...,此种方式的说明如下:

如果 cbfunc 和...相同,重复调用 sys.timerStart(cbfunc, timeout, ...)接口创建并且运行定时器;

在 sys.timerStart 内部会自动停止并且删除已经存在的重复定时器;

例如执行如下三行代码后:

sys.timerStart(led_on_timer_cbfunc, 1000, "red")

sys.timerStart(led_on_timer_cbfunc, 2000, "red")

sys.timerStart(led_on_timer_cbfunc, 3000, "red")

最后只有 sys.timerStart(led_on_timer_cbfunc, 3000, "red") 这个定时器在运行,前面创建的两个定时器都被自动删除了,没有完整运行;

参数

cbfunc

参数含义:
    定时器的回调函数,超时时长之后,回自动执行此函数;回调函数的格式为:
    使用具体的形参名来接收sys.timerStart(cbfunc, timeout, ...)创建定时器时的...可变参数    
    形参的个数只要不少于sys.timerStart(cbfunc, timeout, ...)...的参数个数就行

    function led_on_timer_cbfunc(arg1, arg2, arg3)
        log.info("led_on_timer_cbfunc", arg1, arg2, arg3)
    end

    回调函数是在task之外的业务逻辑中被执行的        
    在回调函数内部无法使用sys.wait(timeout)sys.waitUntil(msg, timeout)sys.waitMsg(task_name, msg, timeout)等必须用在task中的函数
数据类型:function
取值范围:无特别限制;
是否必选:必须传入此参数;
注意事项:
    特别需要注意传入的函数名的作用域,如果使用不当,很可能会使用一个无效的函数名;
    如下代码就是一个典型的错误示例

    -- 此处sys.timerStart(led_on_timer_cbfunc, 1000, 12)中使用的led_on_timer_cbfunc并不是一个function类型,而是nil类型;    
    -- 因为下文定义的函数led_on_timer_cbfunc,没办法在其上部去使用;
    -- 任何函数都要先定义后使用,不允许先使用后定义

    sys.timerStart(led_on_timer_cbfunc, 1000, 12)

    local function led_on_timer_cbfunc(gpio_id)
        -- 此处省略了代码
    end
参数示例:
    如下所示,定义了一个函数led_on_timer_cbfuncled_on_timer_cbfunc就可以做为此参数传入

    local function led_on_timer_cbfunc(gpio_id)
        -- 此处的gpio_id的值为12
        log.info("led_on_timer_cbfunc", gpio_id)
    end

    sys.timerStart(led_on_timer_cbfunc, 1000, 12)

timeout

参数含义:定时器的超时时长,单位毫秒;
数据类型:number
取值范围:
    大于等于1,小于等于0x7FFFFFFF,之间的所有正整数;最大时长0x7FFFFFFF毫秒  596小时  24.85天;
是否必选:必须传入此参数;
注意事项:
    此处的超时机制基于软件定时器实现,受系统负载、任务数量、消息堆积、网络中断优先级最高等多因素影响,无法实现高精度;
    尤其是几个毫秒级别,几十毫秒级别,几百毫秒级别的超时时长,误差都比较大,秒级别以上的超时时长误差较小;
    例如:设置的1毫秒,可能要等几十毫秒;设置的几十毫秒,可能要等上百毫秒;设置的几百毫秒,可能要等几百几十毫秒;
    可以简单的认为会延迟几十毫秒左右,以此来评估超时时长精度是否可以满足业务需求;


    调用本接口时,sys核心库内部会自动创建并且运行一个软件定时器,超时时长到达后,会自动删除这个定时器;
    LuatOS应用程序中可用的软件定时器总数量为64个        
    注意控制自已应用程序中的同时运行的软件定时器总数量不要超过64个,否则创建新的定时器会返回失败;
参数示例:50100060000等;

...

参数含义:定时器携带的可变参数,当定时器超时时长到达后,自动执行回调函数时,这些可变参数按照顺序传递给回调函数使用;
数据类型:任意数据类型;
取值范围:无特别限制;
是否必选:可选传入此参数;
注意事项:...Lua语言的一种语法,表示可变参数,参数数量可以是0个,1个,2个,...,多个;
参数示例:
    不传入,或者boolean类型的true,或者number类型的2,或者string类型的"led",或者table类型的{name="LuatOS", password="123456"}等等等等;
    总之,任何数据类型的任何自定义内容都行;

返回值

local timer_id = sys.timerStart(cbfunc, timeout, ...)

有一个返回值为 timer_id

timer_id

含义说明:创建结果;如果是number类型,表示创建成功,timer_id就是定时器id;如果为nil,表示创建失败;
数据类型:number或者nil
取值范围:无特别限制;
注意事项:暂无;

示例

local function led_on_timer_cbfunc(color)
    log.info("led_on_timer_cbfunc", color)
end

-- 以下三行代码创建的是同一个定时器,当三行代码顺序执行结束后
-- 只有sys.timerStart(led_on_timer_cbfunc, 3000, "red")这一个定时器存在

-- 创建一个单次定时器,1秒后执行回调函数led_on_timer_cbfunc,"red"传递给回调函数
sys.timerStart(led_on_timer_cbfunc, 1000, "red")
-- 创建一个单次定时器,2秒后执行回调函数led_on_timer_cbfunc,"red"传递给回调函数
sys.timerStart(led_on_timer_cbfunc, 2000, "red")
-- 创建一个单次定时器,3秒后执行回调函数led_on_timer_cbfunc,"red"传递给回调函数
sys.timerStart(led_on_timer_cbfunc, 3000, "red")



-- 以下三行代码创建的是三个定时器,当三行代码顺序执行结束后
-- 三个定时器都在运行

-- 创建一个单次定时器,1秒后执行回调函数led_on_timer_cbfunc,"yellow"传递给回调函数
sys.timerStart(led_on_timer_cbfunc, 1000, "yellow")
-- 创建一个单次定时器,2秒后执行回调函数led_on_timer_cbfunc,"green"传递给回调函数
sys.timerStart(led_on_timer_cbfunc, 2000, "green")
-- 创建一个单次定时器,3秒后执行回调函数led_on_timer_cbfunc,"blue"传递给回调函数
sys.timerStart(led_on_timer_cbfunc, 3000, "blue")

3.3.4 sys.timerLoopStart(cbfunc, timeout, ...)

功能

创建并且运行一个循环定时器;

注意事项

可以在能够执行到的任意代码位置使用此函数;

有两种方式可以唯一标识一个定时器:

1、定时器 id;如果使用 sys.timerLoopStart(cbfunc, timeout, ...)创建定时器成功,会返回定时器 id

2、定时器回调函数 cbfunc 和可变参数...,此种方式的说明如下:

如果 cbfunc 和...相同,重复调用 sys.timerLoopStart(cbfunc, timeout, ...)接口创建并且运行定时器;

在 sys.timerLoopStart 内部会自动停止并且删除已经存在的重复定时器;

例如执行如下三行代码后:

sys.timerLoopStart(led_on_timer_cbfunc, 1000, "red")

sys.timerLoopStart(led_on_timer_cbfunc, 2000, "red")

sys.timerLoopStart(led_on_timer_cbfunc, 3000, "red")

最后只有 sys.timerLoopStart(led_on_timer_cbfunc, 3000, "red") 这个定时器在运行,前面创建的两个定时器都被自动删除了,没有完整运行;

参数

cbfunc

参数含义:
    定时器的回调函数,超时时长之后,回自动执行此函数;回调函数的格式为:
    使用具体的形参名来接收sys.timerLoopStart(cbfunc, timeout, ...)创建定时器时的...可变参数    
    形参的个数只要不少于sys.timerLoopStart(cbfunc, timeout, ...)...的参数个数就行

    function led_on_timer_cbfunc(arg1, arg2, arg3)
        log.info("led_on_timer_cbfunc", arg1, arg2, arg3)
    end

    回调函数是在task之外的业务逻辑中被执行的        
    在回调函数内部无法使用sys.wait(timeout)sys.waitUntil(msg, timeout)sys.waitMsg(task_name, msg, timeout)等必须用在task中的函数
数据类型:function
取值范围:无特别限制;
是否必选:必须传入此参数;
注意事项:
    特别需要注意传入的函数名的作用域,如果使用不当,很可能会使用一个无效的函数名;
    如下代码就是一个典型的错误示例

    -- 此处sys.timerLoopStart(led_on_timer_cbfunc, 1000, 12)中使用的led_on_timer_cbfunc并不是一个function类型,而是nil类型;    
    -- 因为下文定义的函数led_on_timer_cbfunc,没办法在其上部去使用;
    -- 任何函数都要先定义后使用,不允许先使用后定义

    sys.timerLoopStart(led_on_timer_cbfunc, 1000, 12)

    local function led_on_timer_cbfunc(gpio_id)
        -- 此处省略了代码
    end
参数示例:
    如下所示,定义了一个函数led_on_timer_cbfuncled_on_timer_cbfunc就可以做为此参数传入
    local function led_on_timer_cbfunc(gpio_id)
        -- 此处的gpio_id的值为12
        log.info("led_on_timer_cbfunc", gpio_id)
    end

    sys.timerLoopStart(led_on_timer_cbfunc, 1000, 12)

timeout

参数含义:定时器的超时时长,单位毫秒;
数据类型:number
取值范围:
    大于等于1,小于等于0x7FFFFFFF,之间的所有正整数;最大时长0x7FFFFFFF毫秒  596小时  24.85天;
是否必选:必须传入此参数;
注意事项:
    此处的超时机制基于软件定时器实现,受系统负载、任务数量、消息堆积、网络中断优先级最高等多因素影响,无法实现高精度;
    尤其是几个毫秒级别,几十毫秒级别,几百毫秒级别的超时时长,误差都比较大,秒级别以上的超时时长误差较小;
    例如:设置的1毫秒,可能要等几十毫秒;设置的几十毫秒,可能要等上百毫秒;设置的几百毫秒,可能要等几百几十毫秒;
    可以简单的认为会延迟几十毫秒左右,以此来评估超时时长精度是否可以满足业务需求;


    调用本接口时,sys核心库内部会自动创建并且运行一个软件定时器,循环运行;
    LuatOS应用程序中可用的软件定时器总数量为64个    
    注意控制自已应用程序中的同时运行的软件定时器总数量不要超过64个,否则创建新的定时器会返回失败;
参数示例:50100060000等;

...

参数含义:定时器携带的可变参数,当定时器超时时长到达后,自动执行回调函数时,这些可变参数按照顺序传递给回调函数使用;
数据类型:任意数据类型;
取值范围:无特别限制;
是否必选:可选传入此参数;
注意事项:...Lua语言的一种语法,表示可变参数,参数数量可以是0个,1个,2个,...,多个;
参数示例:
    不传入,或者boolean类型的true,或者number类型的2,或者string类型的"led",或者table类型的{name="LuatOS", password="123456"}等等等等;
    总之,任何数据类型的任何自定义内容都行;

返回值

local timer_id = sys.timerLoopStart(cbfunc, timeout, ...)

有一个返回值为 timer_id

timer_id

含义说明:创建结果;如果是number类型,表示创建成功,timer_id就是定时器id;如果为nil,表示创建失败;
数据类型:number或者nil
取值范围:无特别限制;
注意事项:暂无;

示例

local function led_on_timer_cbfunc(color)
    log.info("led_on_timer_cbfunc", color)
end

-- 以下三行代码创建的是同一个定时器,当三行代码顺序执行结束后
-- 只有sys.timerLoopStart(led_on_timer_cbfunc, 3000, "red")这一个循环定时器存在

-- 创建一个单次定时器,1秒后执行回调函数led_on_timer_cbfunc,"red"传递给回调函数
sys.timerStart(led_on_timer_cbfunc, 1000, "red")
-- 创建一个循环定时器,每隔2秒后执行回调函数led_on_timer_cbfunc,"red"传递给回调函数
sys.timerLoopStart(led_on_timer_cbfunc, 2000, "red")
-- 创建一个循环定时器,每隔3秒后执行回调函数led_on_timer_cbfunc,"red"传递给回调函数
sys.timerLoopStart(led_on_timer_cbfunc, 3000, "red")



-- 以下三行代码创建的是三个定时器,当三行代码顺序执行结束后
-- 三个定时器都在运行

-- 创建一个循环定时器,每隔1秒后执行回调函数led_on_timer_cbfunc,"yellow"传递给回调函数
sys.timerLoopStart(led_on_timer_cbfunc, 1000, "yellow")
-- 创建一个单次定时器,2秒后执行回调函数led_on_timer_cbfunc,"green"传递给回调函数
sys.timerStart(led_on_timer_cbfunc, 2000, "green")
-- 创建一个循环定时器,每隔3秒后执行回调函数led_on_timer_cbfunc,"blue"传递给回调函数
sys.timerLoopStart(led_on_timer_cbfunc, 3000, "blue")

3.3.5 sys.timerStop(timer_id)

功能

根据定时器 id 停止运行并且删除一个定时器;

注意事项

可以在能够执行到的任意代码位置使用此函数;

有两种方式可以唯一标识一个定时器:

1、定时器 id;如果使用 sys.timerStart(cbfunc, timeout, ...)或者 sys.timerLoopStart(cbfunc, timeout, ...)创建定时器成功,会返回定时器 id

2、定时器回调函数 cbfunc 和可变参数...;

参数

timer_id

参数含义:定时器id,和sys.timerStart(cbfunc, timeout, ...)或者sys.timerLoopStart(cbfunc, timeout, ...)创建定时器成功后的返回值保持一致
数据类型:number
取值范围:无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:
    如下所示,timer_id就可以做为此参数传入

    local function led_on_timer_cbfunc(gpio_id)
        -- 此处的gpio_id的值为12
        log.info("led_on_timer_cbfunc", gpio_id)
    end

    local timer_id = sys.timerLoopStart(led_on_timer_cbfunc, 1000, 12)

返回值

nil

示例

local function led_on_timer_cbfunc(color)
    log.info("led_on_timer_cbfunc", color)
end

-- 创建一个循环定时器,每隔1秒后执行回调函数led_on_timer_cbfunc,"yellow"传递给回调函数
local timer1_id = sys.timerLoopStart(led_on_timer_cbfunc, 1000, "yellow")
-- 创建一个单次定时器,2秒后执行回调函数led_on_timer_cbfunc,"green"传递给回调函数
local timer2_id = sys.timerStart(led_on_timer_cbfunc, 2000, "green")
-- 创建一个循环定时器,每隔3秒后执行回调函数led_on_timer_cbfunc,"blue"传递给回调函数
local timer3_id = sys.timerLoopStart(led_on_timer_cbfunc, 3000, "blue")

-- 停止并且删除timer1_id标识的第一个定时器
sys.timerStop(timer1_id)
-- 停止并且删除timer3_id标识的第三个定时器
sys.timerStop(timer3_id)

-- 至此,只有timer2_id所标识的定时器还在运行

3.3.6 sys.timerStop(cbfunc, ...)

功能

根据定时器的回调函数 cbfunc 和可变参数...停止运行并且删除一个定时器;

注意事项

可以在能够执行到的任意代码位置使用此函数;

有两种方式可以唯一标识一个定时器:

1、定时器 id;如果使用 sys.timerStart(cbfunc, timeout, ...)或者 sys.timerLoopStart(cbfunc, timeout, ...)创建定时器成功,会返回定时器 id;

2、定时器回调函数 cbfunc 和可变参数...;

参数

cbfunc

参数含义:定时器回调函数,和sys.timerStart(cbfunc, timeout, ...)或者sys.timerLoopStart(cbfunc, timeout, ...)创建定时器时的cbfunc保持一致
数据类型:function
取值范围:无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:
    如下所示,led_on_timer_cbfunc就可以做为此参数传入

    local function led_on_timer_cbfunc(gpio_id)
        -- 此处的gpio_id的值为12
        log.info("led_on_timer_cbfunc", gpio_id)
    end

    sys.timerStart(led_on_timer_cbfunc, 1000, 12)

    sys.timerStop(led_on_timer_cbfunc, 12)

返回值

nil

示例

local function led_on_timer_cbfunc(color)
    log.info("led_on_timer_cbfunc", color)
end

-- 创建一个循环定时器,每隔1秒后执行回调函数led_on_timer_cbfunc,"yellow"传递给回调函数
sys.timerLoopStart(led_on_timer_cbfunc, 1000, "yellow")
-- 创建一个单次定时器,2秒后执行回调函数led_on_timer_cbfunc,"green"传递给回调函数
sys.timerStart(led_on_timer_cbfunc, 2000, "green")
-- 创建一个循环定时器,每隔3秒后执行回调函数led_on_timer_cbfunc,"blue"传递给回调函数
sys.timerLoopStart(led_on_timer_cbfunc, 3000, "blue")

-- 停止并且删除led_on_timer_cbfunc和"yellow"标识的第一个定时器
sys.timerStop(led_on_timer_cbfunc, "yellow")
-- 停止并且删除led_on_timer_cbfunc和"blue"标识的第一个定时器
sys.timerStop(led_on_timer_cbfunc, "blue")

-- 至此,只有led_on_timer_cbfunc和"green"标识的定时器还在运行

3.3.7 sys.timerStopAll(cbfunc)

功能

停止运行并且删除回调函数为 cbfunc 的所有定时器;

注意事项

可以在能够执行到的任意代码位置使用此函数;

参数

cbfunc

参数含义:定时器回调函数,和sys.timerStart(cbfunc, timeout, ...)或者sys.timerLoopStart(cbfunc, timeout, ...)创建定时器时的cbfunc保持一致
数据类型:function
取值范围:无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:
    如下所示,led_on_timer_cbfunc就可以做为此参数传入

    local function led_on_timer_cbfunc(gpio_id)
        -- 此处的gpio_id的值为12
        log.info("led_on_timer_cbfunc", gpio_id)
    end

    sys.timerStart(led_on_timer_cbfunc, 1000, 12)

    sys.timerStopAll(led_on_timer_cbfunc)

返回值

nil

示例

local function led_on_timer_cbfunc(color)
    log.info("led_on_timer_cbfunc", color)
end

-- 创建一个循环定时器,每隔1秒后执行回调函数led_on_timer_cbfunc,"yellow"传递给回调函数
sys.timerLoopStart(led_on_timer_cbfunc, 1000, "yellow")
-- 创建一个单次定时器,2秒后执行回调函数led_on_timer_cbfunc,"green"传递给回调函数
sys.timerStart(led_on_timer_cbfunc, 2000, "green")
-- 创建一个循环定时器,每隔3秒后执行回调函数led_on_timer_cbfunc,"blue"传递给回调函数
sys.timerLoopStart(led_on_timer_cbfunc, 3000, "blue")

-- 停止并且删除回调函数为led_on_timer_cbfunc的所有定时器
-- 运行下面这样代码后,之前创建的三个定时器都被停止运行并且被删除
sys.timerStopAll(led_on_timer_cbfunc)

3.3.8 sys.wait(timeout)

功能

在 task 中阻塞等待一段时间;

注意事项

只能在基础 task 和高级 task 处理函数的业务逻辑中使用此函数;

参数

timeout

参数含义:阻塞等待的超时时长,单位毫秒;
数据类型:number
取值范围:大于等于1,小于等于0x7FFFFFFF,之间的所有正整数;最大时长0x7FFFFFFF毫秒  596小时  24.85天;
是否必选:必须传入此参数;
注意事项:
    此处的超时机制基于软件定时器实现,受系统负载、任务数量、消息堆积、网络中断优先级最高等多因素影响,无法实现高精度;
    尤其是几个毫秒级别,几十毫秒级别,几百毫秒级别的超时时长,误差都比较大,秒级别以上的超时时长误差较小;
    例如:设置的1毫秒,可能要等几十毫秒;设置的几十毫秒,可能要等上百毫秒;设置的几百毫秒,可能要等几百几十毫秒;
    可以简单的认为会延迟几十毫秒左右,以此来评估超时时长精度是否可以满足业务需求;


    调用本接口时,如果传入了timeout参数,则sys核心库内部会自动创建并且运行一个软件定时器,超时时长到达后,会自动删除这个定时器;
    LuatOS应用程序中可用的软件定时器总数量为64个    
    注意控制自已应用程序中的同时运行的软件定时器总数量不要超过64个,否则创建新的定时器会返回失败;
参数示例:50100060000等;

返回值

nil

示例

local function send_data_task_func()
    while true do
        -- 阻塞等待1秒钟
        sys.wait(1000)
        log.info("send_data_task_func")
    end
end

local function send_data_extask_func()
    while true do
        -- 阻塞等待1秒钟
        sys.wait(1000)
        log.info("send_data_extask_func")
    end

    sys.cleanMsg("EXTASK2")
    sys.taskDel("EXTASK2")
end

sys.taskInit(send_data_task_func)
sys.taskInitEx(send_data_extask_func, "EXTASK2")

sys.waitUntil(msg, timeout)

功能

在 task 中阻塞等待一个全局消息;

注意事项

基础 task 和高级 task 的定义:参考本文第一章节的描述;

只能在基础 task 和高级 task 处理函数的业务逻辑中使用此函数;

sys.publish(msg, ...) 和 sys.waitUntil(msg, timeout)配合使用时:

在 sys.publish(msg, ...)之前,必须保证 task 正在 sys.waitUntil(msg, timeout)代码处,处于阻塞等待状态;

这样才能保证发布的 msg 消息可以被 task 处理;

同一个全局消息 msg,可以被多个正在 sys.waitUntil(msg, timeout)代码处阻塞等待的 task 处理;

参数

msg

参数含义:全局消息的名称,和sys.publish(msg, ...)中的msg保持一致
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"SEND_DATA_REQ"等任意自定义的字符串;

timeout

参数含义:阻塞等待全局消息msg的超时时长,单位毫秒;
数据类型:number或者nil
取值范围:
    大于等于1,小于等于0x7FFFFFFF,之间的所有正整数;最大时长0x7FFFFFFF毫秒  596小时  24.85天;
    如果为nil,表示一直阻塞等待全局消息,不会超时;
是否必选:可选传入此参数;
注意事项:
    此处的超时机制基于软件定时器实现,受系统负载、任务数量、消息堆积、网络中断优先级最高等多因素影响,无法实现高精度;
    尤其是几个毫秒级别,几十毫秒级别,几百毫秒级别的超时时长,误差都比较大,秒级别以上的超时时长误差较小;
    例如:设置的1毫秒,可能要等几十毫秒;设置的几十毫秒,可能要等上百毫秒;设置的几百毫秒,可能要等几百几十毫秒;
    可以简单的认为会延迟几十毫秒左右,以此来评估超时时长精度是否可以满足业务需求;

    调用本接口时,如果传入了timeout参数,则sys核心库内部会自动创建并且运行一个软件定时器,超时时长到达后,会自动删除这个定时器;
    LuatOS应用程序中可用的软件定时器总数量为64个    
    注意控制自已应用程序中的同时运行的软件定时器总数量不要超过64个,否则创建新的定时器会返回失败;
参数示例:50100060000等;

返回值

local result, arg1, arg2, arg3, argN = sys.waitUntil(msg, timeout)

有数量不固定的返回值:

第一个返回值为 result

剩余的返回值 arg1, arg2, arg3, argN,表示可变数量的返回值,只有当第一个返回值 result 为 true 时,这些可变数量的返回值才有意义,和 sys.publish(msg, ...)中...表示的可变参数一一对应

result

含义说明:阻塞等待的结果;true表示收到了msg消息false表示超时没有收到msg消息
数据类型:boolean
取值范围:true或者false
注意事项:暂无;

arg1, arg2, arg3, argN

含义说明:
    result为true时arg1, arg2, arg3, argN表示sys.publish(msg, ...)...可变参数,从前到后一一对应;
    result为false时arg1, arg2, arg3, argN全部都为nil,没有任何意义;
数据类型:
    result为true时arg1, arg2, arg3, argN的数据类型和sys.publish(msg, ...)...可变参数的数据类型,从前到后一一对应;
    result为false时arg1, arg2, arg3, argN全部都为nil类型
取值范围:无特别限制;
注意事项:暂无;

正确示例

local function send_data_task_func()
    -- 此处可以等到"SEND_DATA_REQ"消息,result为true, data为"123456",tag为"LuatOS"
    local result, data, tag = sys.waitUntil("SEND_DATA_REQ", 5000)
    log.info("send_data_task_func", result, data, tag)
end

local function send_data_extask_func()
    -- 此处接收不到"UNKNOWN_SEND_DATA_REQ"消息,因为没有发布这个消息,所以超时5秒后退出阻塞等待状态
    -- result为false, data为nil,tag为nil
    local result, data, tag = sys.waitUntil("UNKNOWN_SEND_DATA_REQ", 5000)
    log.info("send_data_extask_func", result, data, tag)

    sys.cleanMsg("EXTASK2")
    sys.taskDel("EXTASK2")
end

sys.taskInit(send_data_task_func)
sys.taskInitEx(send_data_extask_func, "EXTASK2")

-- 发布一条全局消息"SEND_DATA_REQ",携带的第一个参数为"123456",携带的第二个参数为"LuatOS"
sys.publish("SEND_DATA_REQ", "123456", "LuatOS")

错误示例

local function send_data_task_func()
    -- 此处阻塞等待1秒钟是一种典型的错误写法;
    -- sys.publish("SEND_DATA_REQ", "123456", "LuatOS")时,task在sys.wait(1000)代码处阻塞等待
    -- 会导致后续的sys.waitUntil("SEND_DATA_REQ", 5000)接收不到"SEND_DATA_REQ"消息
    sys.wait(1000)

    -- 此处接收不到"SEND_DATA_REQ"消息
    -- 因为在sys.publish("SEND_DATA_REQ", "123456", "LuatOS")时,task没有在sys.waitUntil("SEND_DATA_REQ", 5000)代码处阻塞等待
    -- 所以超时5秒后退出阻塞等待状态
    -- result为false, data为nil,tag为nil
    local result, data, tag = sys.waitUntil("SEND_DATA_REQ", 5000)
    log.info("send_data_task_func", result, data, tag)
end

local function send_data_extask_func()
    -- 此处阻塞等待1秒钟是一种典型的错误写法;
    -- sys.publish("SEND_DATA_REQ", "123456", "LuatOS")时,task在sys.wait(1000)代码处阻塞等待
    -- 会导致后续的sys.waitUntil("SEND_DATA_REQ", 5000)接收不到"SEND_DATA_REQ"消息
    sys.wait(1000)

    -- 此处接收不到"SEND_DATA_REQ"消息
    -- 因为在sys.publish("SEND_DATA_REQ", "123456", "LuatOS")时,task没有在sys.waitUntil("SEND_DATA_REQ", 5000)代码处阻塞等待
    -- 所以超时5秒后退出阻塞等待状态
    -- result为false, data为nil,tag为nil
    local result, data, tag = sys.waitUntil("SEND_DATA_REQ", 5000)
    log.info("send_data_extask_func", result, data, tag)

    sys.cleanMsg("EXTASK2")
    sys.taskDel("EXTASK2")
end

sys.taskInit(send_data_task_func)
sys.taskInitEx(send_data_extask_func, "EXTASK2")

-- 发布一条全局消息"SEND_DATA_REQ",携带的第一个参数为"123456",携带的第二个参数为"LuatOS"
sys.publish("SEND_DATA_REQ", "123456", "LuatOS")

3.3.9 sys.waitUntil(msg, timeout)

功能

在 task 中阻塞等待一个全局消息;

注意事项

基础 task 和高级 task 的定义:参考本文第一章节的描述;

只能在基础 task 和高级 task 处理函数的业务逻辑中使用此函数;

sys.publish(msg, ...) 和 sys.waitUntil(msg, timeout)配合使用时:

在 sys.publish(msg, ...)之前,必须保证 task 正在 sys.waitUntil(msg, timeout)代码处,处于阻塞等待状态;

这样才能保证发布的 msg 消息可以被 task 处理;

同一个全局消息 msg,可以被多个正在 sys.waitUntil(msg, timeout)代码处阻塞等待的 task 处理;

参数

msg

参数含义:全局消息的名称,和sys.publish(msg, ...)中的msg保持一致
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"SEND_DATA_REQ"等任意自定义的字符串;

timeout

参数含义:阻塞等待全局消息msg的超时时长,单位毫秒;
数据类型:number或者nil
取值范围:
    大于等于1,小于等于0x7FFFFFFF,之间的所有正整数;最大时长0x7FFFFFFF毫秒  596小时  24.85天;
    如果为nil,表示一直阻塞等待全局消息,不会超时;
是否必选:可选传入此参数;
注意事项:
    此处的超时机制基于软件定时器实现,受系统负载、任务数量、消息堆积、网络中断优先级最高等多因素影响,无法实现高精度;
    尤其是几个毫秒级别,几十毫秒级别,几百毫秒级别的超时时长,误差都比较大,秒级别以上的超时时长误差较小;
    例如:设置的1毫秒,可能要等几十毫秒;设置的几十毫秒,可能要等上百毫秒;设置的几百毫秒,可能要等几百几十毫秒;
    可以简单的认为会延迟几十毫秒左右,以此来评估超时时长精度是否可以满足业务需求;

    调用本接口时,如果传入了timeout参数,则sys核心库内部会自动创建并且运行一个软件定时器,超时时长到达后,会自动删除这个定时器;
    LuatOS应用程序中可用的软件定时器总数量为64个    
    注意控制自已应用程序中的同时运行的软件定时器总数量不要超过64个,否则创建新的定时器会返回失败;
参数示例:50100060000等;

返回值

local result, arg1, arg2, arg3, argN = sys.waitUntil(msg, timeout)

有数量不固定的返回值:

第一个返回值为 result

剩余的返回值 arg1, arg2, arg3, argN,表示可变数量的返回值,只有当第一个返回值 result 为 true 时,这些可变数量的返回值才有意义,和 sys.publish(msg, ...)中...表示的可变参数一一对应

result

含义说明:阻塞等待的结果;true表示收到了msg消息false表示超时没有收到msg消息
数据类型:boolean
取值范围:true或者false
注意事项:暂无;

arg1, arg2, arg3, argN

含义说明:
    result为true时arg1, arg2, arg3, argN表示sys.publish(msg, ...)...可变参数,从前到后一一对应;
    result为false时arg1, arg2, arg3, argN全部都为nil,没有任何意义;
数据类型:
    result为true时arg1, arg2, arg3, argN的数据类型和sys.publish(msg, ...)...可变参数的数据类型,从前到后一一对应;
    result为false时arg1, arg2, arg3, argN全部都为nil类型
取值范围:无特别限制;
注意事项:暂无;

正确示例

local function send_data_task_func()
    -- 此处可以等到"SEND_DATA_REQ"消息,result为true, data为"123456",tag为"LuatOS"
    local result, data, tag = sys.waitUntil("SEND_DATA_REQ", 5000)
    log.info("send_data_task_func", result, data, tag)
end

local function send_data_extask_func()
    -- 此处接收不到"UNKNOWN_SEND_DATA_REQ"消息,因为没有发布这个消息,所以超时5秒后退出阻塞等待状态
    -- result为false, data为nil,tag为nil
    local result, data, tag = sys.waitUntil("UNKNOWN_SEND_DATA_REQ", 5000)
    log.info("send_data_extask_func", result, data, tag)

    sys.cleanMsg("EXTASK2")
    sys.taskDel("EXTASK2")
end

sys.taskInit(send_data_task_func)
sys.taskInitEx(send_data_extask_func, "EXTASK2")

-- 发布一条全局消息"SEND_DATA_REQ",携带的第一个参数为"123456",携带的第二个参数为"LuatOS"
sys.publish("SEND_DATA_REQ", "123456", "LuatOS")

错误示例

local function send_data_task_func()
    -- 此处阻塞等待1秒钟是一种典型的错误写法;
    -- sys.publish("SEND_DATA_REQ", "123456", "LuatOS")时,task在sys.wait(1000)代码处阻塞等待
    -- 会导致后续的sys.waitUntil("SEND_DATA_REQ", 5000)接收不到"SEND_DATA_REQ"消息
    sys.wait(1000)

    -- 此处接收不到"SEND_DATA_REQ"消息
    -- 因为在sys.publish("SEND_DATA_REQ", "123456", "LuatOS")时,task没有在sys.waitUntil("SEND_DATA_REQ", 5000)代码处阻塞等待
    -- 所以超时5秒后退出阻塞等待状态
    -- result为false, data为nil,tag为nil
    local result, data, tag = sys.waitUntil("SEND_DATA_REQ", 5000)
    log.info("send_data_task_func", result, data, tag)
end

local function send_data_extask_func()
    -- 此处阻塞等待1秒钟是一种典型的错误写法;
    -- sys.publish("SEND_DATA_REQ", "123456", "LuatOS")时,task在sys.wait(1000)代码处阻塞等待
    -- 会导致后续的sys.waitUntil("SEND_DATA_REQ", 5000)接收不到"SEND_DATA_REQ"消息
    sys.wait(1000)

    -- 此处接收不到"SEND_DATA_REQ"消息
    -- 因为在sys.publish("SEND_DATA_REQ", "123456", "LuatOS")时,task没有在sys.waitUntil("SEND_DATA_REQ", 5000)代码处阻塞等待
    -- 所以超时5秒后退出阻塞等待状态
    -- result为false, data为nil,tag为nil
    local result, data, tag = sys.waitUntil("SEND_DATA_REQ", 5000)
    log.info("send_data_extask_func", result, data, tag)

    sys.cleanMsg("EXTASK2")
    sys.taskDel("EXTASK2")
end

sys.taskInit(send_data_task_func)
sys.taskInitEx(send_data_extask_func, "EXTASK2")

-- 发布一条全局消息"SEND_DATA_REQ",携带的第一个参数为"123456",携带的第二个参数为"LuatOS"
sys.publish("SEND_DATA_REQ", "123456", "LuatOS")

3.3.10 sys.waitMsg(task_name, msg, timeout)

功能

在 task 中阻塞等待名称为 task_name 的 task 的定向消息;

注意事项

只能在高级 task 处理函数的业务逻辑中使用此函数;

sys.sendMsg(task_name, msg, arg2, arg3, arg4)是定向消息的生产者,定向消息有生产就会有消费,不然消息就没有存在的意义了;

sys.waitMsg(task_name, msg, timeout)所在的 task 是定向消息的消费者;

sys.sendMsg(task_name, msg, arg2, arg3, arg4) 和 sys.waitMsg(task_name, msg, timeout)配合使用;

在 sys.sendMsg(task_name, msg, arg2, arg3, arg4)之前,需要保证名称为 task_name 的 task 已经被创建,否则定向消息也会丢失;

参数

task_name

参数含义:task的名称,和sys.taskInitEx(task_func, task_name, non_targeted_msg_cbfunc, ...)中的task_name保持一致
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"LED_TASK""GPIO_TASK"等任意自定义的字符串;

msg

参数含义:消息的名称,和sys.sendMsg(task_name, msg, arg2, arg3, arg4)中的msg保持一致
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"SEND_DATA_REQ"等任意自定义的字符串;

timeout

参数含义:阻塞等待定向消息msg的超时时长,单位毫秒,nil表示一直阻塞等待
数据类型:number或者nil
取值范围:
    大于等于1,小于等于0x7FFFFFFF,之间的所有正整数;最大时长0x7FFFFFFF毫秒  596小时  24.85天;
    如果为nil,表示一直阻塞等待全局消息,不会超时;
是否必选:可选传入此参数;
注意事项:
    此处的超时机制基于软件定时器实现,受系统负载、任务数量、消息堆积、网络中断优先级最高等多因素影响,无法实现高精度;
    尤其是几个毫秒级别,几十毫秒级别,几百毫秒级别的超时时长,误差都比较大,秒级别以上的超时时长误差较小;
    例如:设置的1毫秒,可能要等几十毫秒;设置的几十毫秒,可能要等上百毫秒;设置的几百毫秒,可能要等几百几十毫秒;
    可以简单的认为会延迟几十毫秒左右,以此来评估超时时长精度是否可以满足业务需求;


    调用本接口时,如果传入了timeout参数,则sys核心库内部会自动创建并且运行一个软件定时器,超时时长到达后,会自动删除这个定时器;
    LuatOS应用程序中可用的软件定时器总数量为64个    
    注意控制自已应用程序中的同时运行的软件定时器总数量不要超过64个,否则创建新的定时器会返回失败;
参数示例:50100060000等;

返回值

local message = sys.waitMsg(task_name, msg, timeout)

有一个返回值为 message

message

含义说明:
    阻塞等待的结果;table类型表示接收到msg消息false表示超时没有收到msg消息
    当接收到msg消息时message[1],message[2],message[3],message[4]sys.sendMsg(task_name, msg, arg2, arg3, arg4)中的 msg, arg2, arg3, arg4 一一对应;
    当超时没有收到msg消息时message为false
数据类型:table或者boolean
取值范围:无特别限制;
注意事项:暂无;

示例

local function send_data_extask1_func()
    -- 此处可以等到"SEND_DATA_REQ"消息,message为table类型
    -- message[1]为"SEND_DATA_REQ",message[2]为"123456",message[3]为"LuatOS"
    local message = sys.waitMsg("EXTASK1", "SEND_DATA_REQ", 5000)

    if message then
        log.info("send_data_extask1_func", message[1], message[2], message[3])
    end

    sys.cleanMsg("EXTASK1")
    sys.taskDel("EXTASK1")
end

local function send_data_extask2_func()
    -- 此处等不到"UNKNOWN_SEND_DATA_REQ"消息,超时5秒退出阻塞状态,message为false
    local message = sys.waitMsg("EXTASK2", "UNKNOWN_SEND_DATA_REQ", 5000)

    if not message then
        log.info("send_data_extask2_func", "wait timeout")
    end

    sys.cleanMsg("EXTASK2")
    sys.taskDel("EXTASK2")
end

sys.taskInitEx(send_data_extask1_func, "EXTASK1")
sys.taskInitEx(send_data_extask2_func, "EXTASK2")

-- 向名称为"EXTASK1"的task发布一条定向消息"SEND_DATA_REQ",携带的第一个参数为"123456",携带的第二个参数为"LuatOS"
sys.sendMsg("EXTASK1", "SEND_DATA_REQ" "123456", "LuatOS")

3.3.11 定时器代码示例

在了解了定时器的 api 之后,我们再看下图回顾一下定时器消息处理的完整周期

下面这个例子用来说明定时器的使用方法;

这个例子的完整代码链接:timer.lua

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

--[[
@module  timer
@summary “定时器”演示功能模块
@version 1.0
@date    2025.08.12
@author  朱天华
@usage
本文件为timer应用功能模块,用来演示“定时器”如何使用,核心业务逻辑为:
1、演示单次定时器,循环定时器,task内的延时定时器的创建,启动,停止和删除功能;

本文件没有对外接口,直接在main.lua中require "timer"就可以加载运行;
]]

local function timer_test_task_func()
    -- 以下三行代码执行后,只有最后一个定时器存在
    sys.timerStart(log.info, 1000, "red")
    sys.timerStart(log.info, 2000, "red")
    sys.timerStart(log.info, 3000, "red")

    -- 阻塞等待3秒钟,实际上创建了一个3秒钟超时时长的单次定时器
    -- 超时时长到达后,会控制本task退出阻塞状态,继续运行
    sys.wait(3000)

    -- 创建并且启动一个循环定时器,每隔1秒钟执行一次sys.publish("loop_timer_cbfunc_msg")
    -- 相当于每隔1秒钟发布一条用户全局消息"loop_timer_cbfunc_msg"
    sys.timerLoopStart(sys.publish, 1000, "loop_timer_cbfunc_msg")

    -- 创建并且启动一个单次定时器,5.5秒后执行sys.timerStop(sys.publish, "loop_timer_cbfunc_msg")
    -- 相当于5.5秒后主动停止并且删除了上一行代码创建的循环定时器
    sys.timerStart(sys.timerStop, 5500, sys.publish, "loop_timer_cbfunc_msg")

    while true do
        -- 阻塞等待用户全局消息"loop_timer_cbfunc_msg",超时时长为2秒钟
        -- 5秒内,每秒都会收到一次消息;
        -- 5秒后,不再收到消息,超时2秒退出阻塞状态;
        local result = sys.waitUntil("loop_timer_cbfunc_msg", 2000)

        if result then
            log.info("receive loop_timer_cbfunc_msg")
        else
            log.info("no loop_timer_cbfunc_msg, 2000ms timeout")
            break
        end
    end

    -- 以下五行代码执行后,创建并且启动了5个不同的定时器
    local timer_id = sys.timerStart(log.info, 1000, "1")
    sys.timerStart(log.info, 2000, "2")
    sys.timerStart(log.info, 3000, "3")
    sys.timerStart(log.info, 4000, "4")
    sys.timerStart(log.info, 5000, "5")

    -- 根据定时器id停止并且删除刚才创建的5个定时器中的第一个定时器
    sys.timerStop(timer_id)

    -- 阻塞等待2秒钟
    sys.wait(2000)

    -- 运行到这里
    -- 刚才创建的5个定时器中的后4个定时器:
    -- sys.timerStart(log.info, 2000, "2"),这个定时器已经超时,并且自动停止和删除
    -- 还剩下另外3个定时器处于运行状态,超时时长未到达
    -- 执行下面这行代码后,可以将这3个定时器全部停止并且删除
    sys.timerStopAll(log.info)
end

-- 创建并且启动一个单次定时器,超时时长为3秒
-- 3秒后执行sys.taskInit(timer_test_task_func)
-- 相当于3秒后,创建并且启动一个基础task,然后执行这个task的任务处理函数timer_test_task_func
sys.timerStart(sys.taskInit, 3000, timer_test_task_func)

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

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

运行日志如下:

[2025-08-28 22:55:23.678][00000000.060] I/user.main luatos_framework_luatos_task 001.000.000
[2025-08-28 22:55:29.625][00000006.007] I/user.red
[2025-08-28 22:55:30.632][00000007.013] I/user.receive loop_timer_cbfunc_msg
[2025-08-28 22:55:31.641][00000008.023] I/user.receive loop_timer_cbfunc_msg
[2025-08-28 22:55:32.644][00000009.026] I/user.receive loop_timer_cbfunc_msg
[2025-08-28 22:55:33.653][00000010.035] I/user.receive loop_timer_cbfunc_msg
[2025-08-28 22:55:34.660][00000011.041] I/user.receive loop_timer_cbfunc_msg
[2025-08-28 22:55:36.661][00000013.042] I/user.no loop_timer_cbfunc_msg, 2000ms timeout
[2025-08-28 22:55:38.672][00000015.054] I/user.2

我们结合运行日志分析一下代码的业务逻辑是否执行正常;

3.4 task 内部运行环境 vs task 外部运行环境

在前文内容中,我们提到了应用脚本代码的两种运行环境;当时仅仅对这两种概念做了一个初步的介绍,并没有结合示例来讲解,现在我们已经学习了 task,msg,timer,可以结合 task,msg,timer 来举一些实际的例子,来进一步理解这两种运行环境;

3.4.1 基本概念

首先复现一下这两种运行环境的概念:

在 LuatOS 应用脚本开发过程中,我们所编写的应用脚本代码,存在两种业务逻辑的运行环境:

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

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

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

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

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

3.4.2 sys api 需要的运行环境

接下来对 task、msg、timer 的 api 需要的运行环境做一个说明

api
在task内部和task外部 都可以运行
只能在task内部运行
只能在task外部运行
sys.taskInit(task_func, ...)



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



sys.taskDel(task_name)



sys.publish(msg, ...)



sys.subscribe(msg, msg_cbfunc)



sys.unsubscribe(msg, msg_cbfunc)



sys.waitUntil(msg, timeout)



sys.sendMsg(task_name, msg, arg2, arg3, arg4)



sys.waitMsg(task_name, msg, timeout)



sys.cleanMsg(task_name)



sys.timerStart(cbfunc, timeout, ...)



sys.timerLoopStart(cbfunc, timeout, ...)



sys.timerStop(timer_id)



sys.timerStop(cbfunc, ...)



sys.timerStopAll(cbfunc)



sys.wait(timeout)



sys.run()



从以上表格可以看出,sys 核心库中的 api,从需要的运行环境来看,分为以下三类:

  1. 大部分的 api,既可以在 task 内部运行,也可以在 task 外部运行;
  2. sys.waitUntil,sys.waitMsg,sys.wait,这三个 spi,只能在 task 内部运行;
  3. sys.run,只能在 task 外部运行;

3.4.3 sys api 的回调函数提供的运行环境

api中的回调函数
回调函数是task外部运行环境
回调函数是task内部运行环境
sys.taskInitEx(task_func, task_name, **non_targeted_msg_cbfunc**, ...)


sys.subscribe(msg, **msg_cbfunc**)


sys.timerStart(**cbfunc**, timeout, ...)


sys.timerLoopStart(**cbfunc**, timeout, ...)


从以上表格可以看出,sys 核心库中的 api,如果支持回调函数,这些回调函数内部提供的运行环境,分为以下两类:

  1. sys.taskInitEx(task_func, task_name, non_targeted_msg_cbfunc, ...)中的回调函数 non_targeted_msg_cbfunc,提供的是 task 内部运行环境;
  2. sys.subscribe(msg, msg_cbfunc),sys.timerStart(cbfunc, timeout, ...),sys.timerLoopStart(cbfunc, timeout, ...)中的回调函数,提供的是 task 外部运行环境;所以这些回调函数内部不能调用“只能在 task 内部运行”的 api,例如在 sys.subscribe(msg, msg_cbfunc)的 msg_cbfunc 内部不能调用 sys.waitUntil,sys.waitMsg,sys.wait;

3.4.4 常犯的错误

新接触 LuatOS 开发的用户,经常会犯上面黄色背景标注的这个错误;

下面这个例子用来说明常犯的这种错误;

这个例子的完整代码链接:task_inout_env_err.lua

核心代码片段如下,我们首先分析下这段代码的业务逻辑(实际运行演示时,每次打开三段黄色背景代码中的其中一段)

--[[
@module  targeted_msg_sender
@summary “task内和task外运行环境典型错误”演示功能模块
@version 1.0
@date    2025.08.12
@author  朱天华
@usage
本文件为task_inout_env_err应用功能模块,用来演示“task内和task外运行环境典型错误”,核心业务逻辑为:
演示“task内外运行环境使用不当”而出现的典型错误
1、在用户全局消息订阅的回调函数中执行sys.wait接口;
2、在单次定时器的回调函数中执行sys.waitUntil接口;
3、在循环定时器的回调函数中执行sys.waitMsg接口;
4、以上三种都是在task外的运行环境中执行“必须在task内运行”的接口,还有其他类似的使用错误,不再一一列举;

本文件没有对外接口,直接在main.lua中require "task_inout_env_err"就可以加载运行;
]]

local function mqtt_event_cbfunc()
    log.info("mqtt_event_cbfunc")
    -- 在用户全局消息订阅的回调函数中执行sys.wait接口,会报错
    sys.wait(1000)
end

-- sys.subscribe("MQTT_EVENT", mqtt_event_cbfunc)
-- sys.timerStart(sys.publish, 1000, "MQTT_EVENT")

local function timer_cbfunc()
    log.info("timer_cbfunc")
    -- 在单次定时器的回调函数中执行sys.waitUntil接口,会报错
    sys.waitUntil("UNKNOWN_MSG", 1000)
end

-- sys.timerStart(timer_cbfunc, 1000)

local function loop_timer_cbfunc()
    log.info("loop_timer_cbfunc")
    -- 在循环定时器的回调函数中执行sys.waitMsg接口,会报错
    sys.waitMsg("SEND_MSG_TASK", "UNKNOWN_MSG", 1000)
end

local function send_targeted_msg_task_func()
    while true do
        -- 延时等待1秒
        sys.wait(1000)
    end
end

-- 创建并且启动一个高级task
-- task的任务处理函数为send_targeted_msg_task_func
-- task的名称为SEND_TASK_NAME
-- 运行这个task的任务处理函数send_targeted_msg_task_func
sys.taskInitEx(send_targeted_msg_task_func, "SEND_MSG_TASK")
sys.timerLoopStart(loop_timer_cbfunc, 1000)

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

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

运行日志如下:

I/user.mqtt_event_cbfunc
[00000001.011]E/main Luat:
[00000001.012]E/main /lua/sys.lua:81: attempt to yield from outside a coroutine
stack traceback:
        /lua/sys.lua:81: in function 'sys.check_task'
        /lua/sys.lua:94: in function 'sys.wait'
        [string "task_inout_env_err.lua"]:5: in local 'callback'
        /lua/sys.lua:365: in upvalue 'dispatch'
        /lua/sys.lua:390: in function 'sys.safeRun'
        /lua/sys.lua:435: in function 'sys.run'
        [string "main.lua"]:110: in main chunk
        [C]: in ?
[00000001.014]E/main Lua VM exit!! reboot in 15000ms
I/user.timer_cbfunc
[00000001.007]E/main Luat:
[00000001.028]E/main /lua/sys.lua:81: attempt to yield from outside a coroutine
stack traceback:
        /lua/sys.lua:81: in function 'sys.check_task'
        /lua/sys.lua:127: in function 'sys.waitUntil'
        [string "task_inout_env_err.lua"]:14: in local 'cb'
        /lua/sys.lua:414: in function 'sys.safeRun'
        /lua/sys.lua:435: in function 'sys.run'
        [string "main.lua"]:110: in main chunk
        [C]: in ?
[00000001.031]E/main Lua VM exit!! reboot in 15000ms
I/user.loop_timer_cbfunc
[00000001.013]E/main Luat:
[00000001.017]E/main attempt to yield from outside a coroutine
[00000001.025]E/main Lua VM exit!! reboot in 15000ms

3.5 sys 核心库 api 的组合使用关系

我们已经学习过了 sys 核心库中的 task,msg,timer 的 api,在这些 api 中:

  1. 有些 api 必须在一起组合使用,才能实现完整的业务流程;
  2. 有些 api 禁止在一起组合使用,否则会导致业务出错;

在这些 api 中,主要是消息的发送和接收 api 容易混用,组合使用关系参考下表(每一行的两个单元格所表示的 api 必须组合使用):

消息发送api
消息接收api
sys.publish(msg, ...)
sys.subscribe(msg, msg_cbfunc)
sys.publish(msg, ...)
sys.waitUntil(msg, timeout)
sys.sendMsg(task_name, msg, arg2, arg3, arg4)
sys.waitMsg(task_name, msg, timeout)

3.6 LuatOS 应用软件调度机制(sys.run()函数)

  1. sys 核心库是 LuatOS 运行框架库,是 LuatOS 应用程序运行的核心大脑,所有 LuatOS 应用项目都会使用到 sys 核心库;
  2. 截止到目前,我们已经学习了 sys 核心库提供的 task,msg,timer 功能;
  3. sys 核心库还剩最后一个功能 api,sys.run();
  4. sys 核心库是 LuatOS 应用程序运行的核心大脑,sys.run()是 sys 核心库的大脑,负责整个 LuatOS 应用脚本程序的调度和管理,是 LuatOS 应用程序的调度器;

sys.run()非常重要,但是 sys.run()使用起来非常简单,仅仅在 main.lua 的最后一行调用 sys.run()即可。

虽然 sys.run()使用起来非常简单,但是如果大家对 sys.run()的运行原理有一个总体性的理解和认识,对开发 LuatOS 应用项目来说,帮助很大。

所以在这里,我先对 sys.run()内部的工作原理做一个简化后的总体介绍,至于更详细的原理介绍,我们会在后续的 LuatOS 直播课程中讲解;

我们看上面这张图:

  1. LuatOS 内核固件中的 FreeRTOS 会创建一个 Lua 虚拟机任务;
  2. Lua 虚拟机任务的处理函数中,首先进行初始化:

  3. 在内核固件的 C 代码中,加载 Lua 标准库和 LuatOS 核心库;

  4. 从 LuatOS 的脚本分区找到 main.lua
  5. 开始逐行嵌套解析执行 main.lua 中的脚本代码(加载必要的扩展库脚本文件和自己开发的应用脚本文件,并且运行这些脚本文件的初始化代码)
  6. 运行 main.lua 的最后一行代码 sys.run()
  7. sys.run()中的实现是一个 while true 循环,在这个循环内,不断地从内核消息队列和用户全局消息队列中读取消息,并且分发消息给接收者进行处理。

3.7 分析 mqtt demo 中的 task,msg,timer,run 的使用案例

现在,LuatOS 框架的使用,基本上讲完了,接下来,我们来实际看一个完整 mqtt demo 项目代码,重点分析下这份 demo 项目代码中,使用到的本节课程讲解的知识点;

mqtt demo 代码路径:Air8000 mqtt demo

Mqtt demo 项目的总体设计框图如下:

这份 mqtt demo 中的 readme 文件,以及代码中的注释都比较详细,接下来我用 vscode 直接打开这份 demo 项目代码,从以下两方面讲解一下:

  1. mqtt demo 的 readme 解读;
  2. 分析 demo 项目中的每个 Lua 脚本文件,重点分析脚本文件中的 sys 和 sysplus 核心库使用的相关代码和业务逻辑,通过分析和本讲课程有关的代码,让大家对本节课程理解更加深刻;

现在我们开始进入 mqtt demo 项目中去分析;

第四部分:课后作业

至少二选一

4.1 开发代码,在 LuatOS 模拟器 上验证可以同时运行的定时器数量

作业提交内容:

  1. 6 个 Lua 文件

  2. main.lua:初始化,加载下面的 5 个 lua 文件功能模块(每次只打开其中的 1 个进行验证),执行 sys.run;(可以参考本讲课程中的 demo)

  3. timer_start.lua:使用 sys.timerStart 接口来验证可以同时运行的定时器数量;
  4. timer_loop_start.lua:使用 sys.timerLoopStart 接口来验证可以同时运行的定时器数量;
  5. wait.lua:使用 sys.wait 接口来验证可以同时运行的定时器数量;
  6. wait_until.lua:使用 sys.waitUntil 接口来验证可以同时运行的定时器数量;
  7. wait_msg.lua:使用 sys.waitMsg 接口来验证可以同时运行的定时器数量;
  8. 1 个运行日志文件
  9. 1 个分析文件,给出可以同时运行多少个定时器的结论,然后结合代码和日志分析出来为什么可以同时运行这么多的定时器;

4.1 开发代码,在 合宙 Air 系列模组的开发板或者核心板 上验证可以同时运行的定时器数量

作业提交内容:

  1. 6 个 Lua 文件

  2. main.lua:初始化,加载下面的 5 个 lua 文件功能模块(每次只打开其中的 1 个进行验证),执行 sys.run;(可以参考本讲课程中的 demo)

  3. timer_start.lua:使用 sys.timerStart 接口来验证可以同时运行的定时器数量;
  4. timer_loop_start.lua:使用 sys.timerLoopStart 接口来验证可以同时运行的定时器数量;
  5. wait.lua:使用 sys.wait 接口来验证可以同时运行的定时器数量;
  6. wait_until.lua:使用 sys.waitUntil 接口来验证可以同时运行的定时器数量;
  7. wait_msg.lua:使用 sys.waitMsg 接口来验证可以同时运行的定时器数量;
  8. 1 个运行日志文件
  9. 1 个分析文件,给出可以同时运行多少个定时器的结论,然后结合代码和日志分析出来为什么可以同时运行这么多的定时器;每个主题和章节都布置一项作业