接上一篇
对于 LuatOS 应用程序来说,定时器本质上也算是一种特殊的消息,因为定时器太常用了,所以把他单独拎出来,单独的一个章节进行讲解;
2.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)
2.3.2 定时器消息处理的完整周期

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

timeout

关于定时器精度的问题,我们再来看下面这张图来理解:
1、FreeRTOS中的一些任务优先级比Lua虚拟机任务优先级高,尤其是4G网络中断的任务优先级最高,这些高优先级的任务的抢占执行,会直接影响Lua虚拟机任务执行的实时性,进而导致sys.run()调度器的运行实时性也不会很高;
2、在Lua虚拟机任务内部的sys.run()调度器中,首先是遍历并且分发处理用户全局消息队列中的所有消息,这些消息全部处理完,才会去执行内核消息队列中的第一条消息,定时器事件到达的消息是存储在内核消息队列中的,如果用户全局消息队列中的消息处理耗时较长,或者内核消息队列中在定时器消息之前还有其他消息(例如串口消息,mqtt消息等),定时器消息都要排队才能执行,所以整个项目的业务越复杂,系统负载就越重,消息数量就越多,定时器消息处理的实时性就越低;


返回值
local timer_id = sys.timerStart(cbfunc, timeout, ...)
有一个返回值为 timer_id
timer_id

示例

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

timeout

...

返回值
local timer_id = sys.timerLoopStart(cbfunc, timeout, ...)
有一个返回值为 timer_id
timer_id

示例

2.3.5 sys.timerStop(timer_id)
功能
根据定时器 id 停止运行并且删除一个定时器;
注意事项
可以在能够执行到的任意代码位置使用此函数;
有两种方式可以唯一标识一个定时器:
1、定时器 id;如果使用 sys.timerStart(cbfunc, timeout, ...)或者 sys.timerLoopStart(cbfunc, timeout, ...)创建定时器成功,会返回定时器 id
2、定时器回调函数 cbfunc 和可变参数...;
参数
timer_id

返回值
nil
示例

2.3.6 sys.timerStop(cbfunc, ...)
功能
根据定时器的回调函数 cbfunc 和可变参数...停止运行并且删除一个定时器;
注意事项
可以在能够执行到的任意代码位置使用此函数;
有两种方式可以唯一标识一个定时器:
1、定时器 id;如果使用 sys.timerStart(cbfunc, timeout, ...)或者 sys.timerLoopStart(cbfunc, timeout, ...)创建定时器成功,会返回定时器 id;
2、定时器回调函数 cbfunc 和可变参数...;
参数
cbfunc

返回值
nil
示例

2.3.7 sys.timerStopAll(cbfunc)
功能
停止运行并且删除回调函数为 cbfunc 的所有定时器;
注意事项
可以在能够执行到的任意代码位置使用此函数;
参数
cbfunc

返回值
nil
示例

2.3.8 sys.wait(timeout)
功能
在 task 中阻塞等待一段时间;
注意事项
只能在基础 task 和高级 task 处理函数的业务逻辑中使用此函数;
参数
timeout

返回值
nil
示例

sys.waitUntil(msg, timeout)
功能
在 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

timeout

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

arg1, arg2, arg3, argN

正确示例

错误示例

2.3.9 sys.waitUntil(msg, timeout)
功能
在 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

timeout

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

arg1, arg2, arg3, argN

正确示例

错误示例

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

msg

timeout

返回值
local message = sys.waitMsg(task_name, msg, timeout)
有一个返回值为 message
message

示例

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

下面这个例子用来说明定时器的使用方法;
这个例子的完整代码链接:timer.lua
核心代码片段如下,我们首先分析下这段代码的业务逻辑

我们在模拟器上实际运行一下看看,输入命令
luatos --llt=H:Luatoolsprojectluatos_framework_luatos_task_Air8000.ini
运行日志如下:

我们结合运行日志分析一下代码的业务逻辑是否执行正常;
2.4 task 内部运行环境 vs task 外部运行环境
在前文内容中,我们提到了应用脚本代码的两种运行环境;当时仅仅对这两种概念做了一个初步的介绍,并没有结合示例来讲解,现在我们已经学习了 task,msg,timer,可以结合 task,msg,timer 来举一些实际的例子,来进一步理解这两种运行环境;
2.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 内部运行环境;


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

从以上表格可以看出,sys 核心库中的 api,从需要的运行环境来看,分为以下三类:
1、大部分的 api,既可以在 task 内部运行,也可以在 task 外部运行;
2、sys.waitUntil,sys.waitMsg,sys.wait,这三个 spi,只能在 task 内部运行;
3、sys.run,只能在 task 外部运行;
2.4.3 sys api 的回调函数提供的运行环境

从以上表格可以看出,sys 核心库中的 api,如果支持回调函数,这些回调函数内部提供的运行环境,分为以下两类:
sys.taskInitEx(task_func, task_name,non_targeted_msg_cbfunc, ...)中的回调函数non_targeted_msg_cbfunc,提供的是 task 内部运行环境;
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;
2.4.4 常犯的错误
新接触 LuatOS 开发的用户,经常会犯上面黄色背景标注的这个错误;
下面这个例子用来说明常犯的这种错误;
这个例子的完整代码链接:task_inout_env_err.lua
核心代码片段如下,我们首先分析下这段代码的业务逻辑(实际运行演示时,每次打开三段黄色背景代码中的其中一段)

我们在模拟器上实际运行一下看看,输入命令
luatos --llt=H:Luatoolsprojectluatos_framework_luatos_task_Air8000.ini
运行日志如下:



2.5 sys 核心库 api 的组合使用关系
我们已经学习过了 sys 核心库中的 task,msg,timer 的 api,在这些 api 中:
1、有些 api 必须在一起组合使用,才能实现完整的业务流程;
2、有些 api 禁止在一起组合使用,否则会导致业务出错;
在这些 api 中,主要是消息的发送和接收 api 容易混用,组合使用关系参考下表(每一行的两个单元格所表示的 api 必须组合使用):

2.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 虚拟机任务的处理函数中,首先进行初始化:
(1) 在内核固件的 C 代码中,加载 Lua 标准库和 LuatOS 核心库;
(2) 从 LuatOS 的脚本分区找到 main.lua
(3) 开始逐行嵌套解析执行 main.lua 中的脚本代码(加载必要的扩展库脚本文件和自己开发的应用脚本文件,并且运行这些脚本文件的初始化代码)
3、运行 main.lua 的最后一行代码 sys.run()
4、sys.run()中的实现是一个 while true 循环,在这个循环内,不断地从内核消息队列和用户全局消息队列中读取消息,并且分发消息给接收者进行处理。

2.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文件,让大家对这个demo项目的业务逻辑有一个总体的认识;
2、从以下几方面来详细分析mqtt demo项目代码:
mqtt demo项目脚本的整体运行逻辑;
mqtt demo项目脚本中使用到的LuatOS task,message,timer,调度器代码解读;
通过分析和本篇文章有关的代码,让大家对本节理解更加深刻;
现在我们开始进入mqtt demo项目中去分析;
三、课后作业
至少二选一
3.1 开发代码,在 LuatOS 模拟器 上验证可以同时运行的定时器数量
作业提交内容:
1、 6 个 Lua 文件
(1) main.lua:初始化,加载下面的 5 个 lua 文件功能模块(每次只打开其中的 1 个进行验证),执行 sys.run;(可以参考本讲课程中的 demo)
(2) timer_start.lua:使用 sys.timerStart 接口来验证可以同时运行的定时器数量;
(3) timer_loop_start.lua:使用 sys.timerLoopStart 接口来验证可以同时运行的定时器数量;
(4) wait.lua:使用 sys.wait 接口来验证可以同时运行的定时器数量;
(5) wait_until.lua:使用 sys.waitUntil 接口来验证可以同时运行的定时器数量;
(6) wait_msg.lua:使用 sys.waitMsg 接口来验证可以同时运行的定时器数量;
2、1 个运行日志文件
3、1 个分析文件,给出可以同时运行多少个定时器的结论,然后结合代码和日志分析出来为什么可以同时运行这么多的定时器;
3.2 开发代码,在 Air 系列模组的开发板或者核心板 上验证可以同时运行的定时器数量
作业提交内容:
1、 6 个 Lua 文件
(1) main.lua:初始化,加载下面的 5 个 lua 文件功能模块(每次只打开其中的 1 个进行验证),执行 sys.run;(可以参考本讲课程中的 demo)
(2) timer_start.lua:使用 sys.timerStart 接口来验证可以同时运行的定时器数量;
(3) timer_loop_start.lua:使用 sys.timerLoopStart 接口来验证可以同时运行的定时器数量;
(4) wait.lua:使用 sys.wait 接口来验证可以同时运行的定时器数量;
(5) wait_until.lua:使用 sys.waitUntil 接口来验证可以同时运行的定时器数量;
(6) wait_msg.lua:使用 sys.waitMsg 接口来验证可以同时运行的定时器数量;
2、 1 个运行日志文件
3、 1 个分析文件,给出可以同时运行多少个定时器的结论,然后结合代码和日志分析出来为什么可以同时运行这么多的定时器;
今天的内容就分享到这里了~
审核编辑 黄宇
-
定时器
+关注
关注
23文章
3372浏览量
124446 -
LuatOS
+关注
关注
0文章
169浏览量
2745
发布评论请先 登录
LuatOS 框架的嵌入式系统架构设计原理
LuatOS 系统框架的模块化设计原理
基于LuatOS的MQTT物联网通信全解
轻松掌握——LuatOS socket基础知识和应用开发
LuatOS框架的使用(上)
LuatOS-Air转LuatOS常见故障排查手册
警惕兼容性陷阱:LuatOS-Air脚本在LuatOS中的运行异常分析
掌握LuatOS系统消息:新手也能看懂的列表详解
LuatOS脚本开发入门:嵌入式运行框架全解析!
嵌入式开发新选择:LuatOS脚本框架入门教程
Task任务:LuatOS实现“任务级并发”的核心引擎
揭秘LuatOS Task:多任务管理的“智能中枢”
解码LuatOS:短信功能的底层运作机制
解锁LuatOS新世界:二次开发必备的3个核心常识
解锁LuatOS-log库:全栈工程师的日志管理实战课!
解锁:LuatOS框架的使用(下篇)
评论