解锁:LuatOS框架的使用(下篇)

电子说

1.4w人已加入

描述

接上一篇

2.3 LuatOS 的定时器(timer)

对于 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 个分析文件,给出可以同时运行多少个定时器的结论,然后结合代码和日志分析出来为什么可以同时运行这么多的定时器;

今天的内容就分享到这里了~


审核编辑 黄宇

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分