IFFIOT资料大全
ML307开发板
开发板硬件PCB图
307N DC和DL区别
307N的Lua开发资料
ML307N的Lua开发资料和烧录
ML307N运行Lua功耗
NT26-4G模组
DTU固件(iRTU固件)
DTU固件开源代码及说明
DTU固件云配置
Lua基础知识
LuatOS运行框架讲解
LuatOS 的消息机制
LuatOS系统消息
LuatOS库函数开发手册
-
+
首页
LuatOS运行框架讲解
>i **信息提示** > **资料来自合宙官网** [https://docs.openluat.com/osapi/luatos_framework/](https://docs.openluat.com/osapi/luatos_framework/) ## 一、LuatOS 编程起步 本文档的读者,默认都已经初步了解了 Lua 语法。 如果不懂 Lua 语法的话,可以参考如下链接,或者直接问 Ai 也可以。 <http://docs.openluat.com/air780epm/common/lua_lesson/> 本文是对 LuatOS 编程框架的概述,有些抽象,如果想更完善的了解 LuatOS 编程,可以点击如下链接: [LuatOS编程框架培训稿](https://docs.openluat.com/luatos_lesson/001_luatos_framework/) 在 B站也有6个多小时的培训视频跟培训文档配套: [LuatOS培训视频](https://www.bilibili.com/video/BV1FLabzmEk2/?spm_id_from=333.337.search-card.all.click) ### 1.1 底层固件怎么启动 LuatOS 脚本 #### 1.1.1 脚本入口执行文件 简单来说,底层固件首先就是要找到 main.lua 这个文件,然后启动它。 所有的其他功能,都需要在 main.lua 发起。 #### 1.1.2 LuatOS 启动脚本的详细流程 进一步详细的说,LuatOS 的底层固件启动脚本的流程如下: 1,系统上电或者复位后,底层固件(core)首先启动,进行硬件初始化、内存分配、文件系统挂载等系统底层的基础操作。 2,加载 Lua 虚拟机:底层固件加载 Lua 虚拟机,为执行 Lua 脚本提供运行环境; 3,自动查找并加载存储在设备上的主脚本 main.lua; 4,按顺序执行 main.lua 脚本中的代码,通常包括任务创建(如 `sys.taskInit`)、功能初始化等,从这一步,已经正式开始运行用户逻辑。 5,进入任务调度:脚本最后通常调用 `sys.run()`,进入事件循环和多任务调度。 #### 1.1.3 怎么把固件和脚本烧录到硬件: 1,使用官方 LuatTools ,将底层固件和用户 Lua 脚本烧录到合宙模组或者引擎硬件; 2,上电后,底层固件自动完成上述启动和脚本加载流程,无需手动干预。 ### 1.2 main.lua 需要包含哪些部分? #### 1.2.1 项目信息声明 在 main.lua 的文件开头,需要声明项目名和版本号,便于管理和调试。后续的远程升级,也需要用到项目名和版本号。 例如: ``` PROJECT = "Air780EPM_GPIO_test" VERSION = "1.0.0" ``` #### 1.2.2 核心库,扩展库以及如何加载 在 main.lua 需要加载 LuatOS 的基础库和扩展库(如 zbuff,onewire,gnss 等)用来实现具体的业务逻辑。 核心库和扩展库的内容,在后续的章节里面介绍。 核心库在底层固件加载Lua虚拟机的时候,在底层固件已经自动加载,不需要在用户脚本中再去加载,例如sys,rtos等; 扩展库是Lua脚本文件写的库,需要在用户脚本中,使用require语句加载,例如libnet,httpplus等,加载方式如下 ``` require "libnet" require "httpplus" ``` #### 1.2.3 至少启动一个任务 在 main.lua 里面,至少需要启动一个任务,否则这个 main 就无所事事,是一个没什么实际用处的主脚本了。 启动一个任务的方法,分为 2 个步骤: 1,创建一个函数,把要做的事情,放在这个函数里面使用。这个函数必须是无限循环的,防止很快结束生命,不妨把这个函数命名为 task1(), 2,调用 sys.taskInit(task1),启动这个函数,于是这个任务,就放在待运行的任务列表里面了。 #### 1.2.4 初步理解 sys.run() sys.run() 是一个无限循环的函数。 main.lua 的最后一行, 只能是 sys.run(),代表 sys.run() 接管了 LuatOS 的所有的执行调度工作。 sys.run() 是 LuatOS 的运行中枢。 在本文的 3.3 节和 7.3 节,还会继续介绍 sys.run()这个函数。 ### 1.3 LuatOS 脚本编程的核心要点 #### 1.3.1 LuatOS 实现的典型功能 LuatOS 脚本是利用了 Lua 的语法,以及基于 LuatOS 的核心库和扩展库提供的 API,进行简便的编程,实现如下功能: 1,实现和云端服务器通信; 2,采集外设的数据,控制外设设备; 3,实现人机交互,包括图形交互和语音交互; #### 1.3.2 LuatOS 的学习要点 要想写好 LuatOS 的软件,实现上述三个功能,除了逐渐掌握 Lua 的基本语法之外,还需要熟悉 LuatOS 的核心库和扩展库,这样才能开发出优质的基于 LuatOS 的物联网设备软件。 学习的方法有如下几个: 1, 运行各个功能模块的 demo 代码; 2, 阅读 docs.openluat.com 的教程文档; 3, 遇到不懂问 AI; 4, 在合宙 QQ 大群和微信大群里面做技术交流。 > 进群二维码请翻阅至本文末尾。 #### 1.3.3 一个典型的 LuatOS 实现 一个典型的 LuatOS 实现,包含 main.lua 入口文件和若干个功能模块文件。 这里用 Air780EPM 模组的蜂鸣器的代码为例, 有两个脚本文件以及一个管脚描述 json 文件: 1, main.lua 文件, 作用是启动一个任务,让蜂鸣器响一秒钟,再停顿一秒钟,如此往复; 2, airbuzzer.lua 封装了驱动蜂鸣器的功能实现; 3, pins_Air780epmjson 描述了本例使用到的管脚的功能,780EPM 的 26 管脚,用作 PWM4。 main.lua 内容如下: ``` PROJECT = "pwm_buzzer" VERSION = "1.0.0" airbuzzer = require "airbuzzer" -- sys = require("sys") local function buzzer_work() log.info("pwm", "ch", PWM_ID) while true do sys.wait(1000) airbuzzer.start_buzzer() sys.wait(1000) airbuzzer.stop_buzzer() end end sys.taskInit(buzzer_work) sys.run() ``` airbuzzer.lua 内容如下: ``` local airbuzzer = {} local pwid = 4 function airbuzzer.start_buzzer() pwm.setup(pwid, 1000, 50) pwm.start(pwid) --pwm.open(pwid,1000,50) end function airbuzzer.stop_buzzer() pwm.stop(pwid) --pwm.close(pwid) end return airbuzzer ``` pins_Air780epmjson 内容如下: ``` { "model": "Air780EPM", "pins": [ [7, "PWR_KEY", "开机键"], [26, "PWM4", "蜂鸣器控制"] ] } ``` 把上述几个文件,连同 airr780EPM 最新的固件版本,用 Luatools 建立一个工程,烧录到 780EPM 开发板,就可以听到蜂鸣器的播放声音了。 ## 二、几个要熟悉的常识 ### 2.1 匿名函数 在 Lua 代码里面,经常看到没有名字的函数。 这种函数定义之后, 要么马上运行,要么作为另一个函数的返回值赋给其他变量,所以并不需要一个函数名字。 这种函数,称为匿名函数。 匿名函数可以某些时候简化代码,初学者写代码可以先不考虑匿名函数。 但是由于匿名函数在你能阅读到的 Lua 代码里面出现的频次实在是太高了,所以你也不得不重视和习惯匿名函数。 ### 2.2 闭包 ``` 闭包的实现通常是通过在外部函数内部定义一个函数,并将这个内部函数作为外部函数的返回值。 这样一来,内部函数就可以访问外部函数作用域中的变量,即使外部函数已经执行完毕,这些变量依然可以被内部函数访问,从而形成闭包。 常见的闭包实现模式如下: ``` ``` function outer(x) local y = x return function(z) return y + z end end local f = outer(10) print(f(5)) -- 输出 15 ``` 这样的好处是,可以定义一个函数,能够在一定范围内,访问外部的变量,实现可控的持续行为。 很多初学者会被这段代码迷惑,会被绕晕。 这里做一下解释: (1)z 不是函数里面声明的变量,z 是函数的参数; 所以 在代码里面, 因为 f=outer(10), 所以, f(5)就意味着是调用了 两次函数,传入了两个函数的参数: outer(10)(5)。 第一次调用,out(10) ,意味着 在 outer 函数里面, y = x 这句, x 换成 10, 就是 y = 10; outer(10)(5)意味着 5 是内部匿名函数的参数,就是替代 z 的; 匿名函数返回 y+z, 这里 y 是 10,z 是 5, 返回的就是 10+5=15. 这里比较绕的,就是给了两次参数,一个是 10 对应 x, 一个是 5 对应 z。 匿名参数和闭包,对初学者有点绕,很多读者不明白为什么 z 为什么是 outer 的第二个参数, 这里需要特别搞清楚的是, outer 这个函数的返回值是个函数, 而且这个函数是有参数的。 那么,这个带参数的函数赋值给 f 之后, f 就是个函数了, 于是给 f 一个参数 5, 这个 5 自然就是返回的函数的参数了,也就是 z 了。 虽然并不是所有的闭包都是上面这种代码的实现形式,但是初学者可以先记住这样的闭包形式。 如果不习惯闭包,初学者可以先避免在代码里面体现闭包的代码形式。 ### 2.3 回调函数 #### 2.3.1 回调函数是什么 回调函数是在 LuatOS 编程过程中经常用到的一个技术。 理解 LuatOS 的回调函数,可以从“事件驱动”和“函数作为参数”两个角度来把握: 回调函数(Callback)是在特定事件发生时,由系统或框架自动调用你事先定义好的函数。你只需要把自己的函数注册给系统,等事件触发时,系统就会帮你调用它。 本质上,回调函数就是一个普通函数,但它被作为参数传递或注册到其他地方,由系统或其他代码在合适的时机自动执行。 ``` 回调函数的作用是实现事件响应,异步处理。 消息到来,定时器到点,网络收发等功能都经常会用到回调函数的处理。 总之,LuatOS 的回调函数,就是你注册给系统的,在特定事件发生时自动被调用的函数。 回调函数让事件响应、异步处理、任务解耦变得简单灵活,是 LuatOS 事件驱动编程的核心机制之一。 ``` #### 2.3.2 回调函数做消息订阅与发布 LuatOS 支持通过 `sys.subscribe` 订阅消息并注册回调函数,消息发布时自动调用回调: ``` -- 注册回调函数 sys.subscribe("TEST", function(a) log.info("收到TEST消息,参数为", a) end) -- 发布消息 sys.publish("TEST", 123) ``` 当 `sys.publish("TEST", 123)` 被调用时,"TEST"消息以及携带的参数123会被插入到用户消息列表中,LuatOS 内部的sys.run()调度中枢会遍历订阅者列表,找到所有订阅了 "TEST" 的回调函数,并自动把参数 123 传给这些回调函数。 通过这样的处理,事件触发和处理逻辑就被解耦,方便扩展和维护。 #### 2.3.3 回调函数做定时器和异步操作 定时器到点后自动调用注册的回调函数: ``` sys.timerStart(function() log.info("定时器到点") end, 1000) ``` #### 2.3.4 任务和协程场景的回调函数使用 在多任务,也就是 LuatOS 的协程场景下,回调函数也常用于任务唤醒、事件响应等。 解耦调用者与被调用者:调用者只需知道“有回调”,不用关心回调具体做什么,提升灵活性。 你只需更换回调函数,就能实现不同的处理逻辑,无需修改底层框架代码。 任务和协程的详细信息,在下一章讲解。 ## 三、LuatOS 的多任务并行实现详解 ### 3.1 LuatOS 的多任务是怎么实现的 #### 3.1.1 通过协程实现多任务的效果 LuatOS 使用一种协程(coroutine)的机制,实现多任务。 协程并不是真的多任务,也不是多线程,而是通过同一时间只可能有一个协程执行,来等价实现多任务的效果。 和 RTOS 的抢占式多任务方式不同,协程不能抢占其他任务的时间片,只能由一个独立的调度器来判断是哪个协程占用 CPU 时间来运行。 一个 LuatOS 可以创建多个任务,每一个任务都是协程,为了简化描述,后续我们经常会用”任务“这个词来指代协程。 LuatOS 创建的任务无法设定优先级, 所以 LuatOS 的每个任务的优先级都是相同的。 每一个 LuatOS 的任务在做运算的时候,是 100% 占用了 CPU 时间片的。 执行完运算之后,要主动调用 yield() 函数,让自己挂起,其他任务才能获得时间片运行。 如果某个任务, 持续进行运算,不做 yield() 调用,其他任务是无法获取 CPU 时间片的。 协程挂起后,自己是无法恢复的,只能其他的任务调用 resume 系统函数来恢复。 我们在写代码的时候,不需要调用 yield() 把自己挂起,只需要调用 sys.wait() 做时延,由调度器统一在 sys.wait()里面把任务挂起。 在 LuatOS 里面,所有挂起的协程,都由一个独立的调度器通过调用 resume 来恢复。 这个独立的调度器, 在 LuatOS 里面是 sys.run() 函数。 #### 3.1.2 LuatOS 的任务函数怎么挂起和恢复 LuatOS 的每一个通过 sys.taskInit() 发起的任务函数,都不会直接调用 yield 把自己挂起,因为直接调用 yield 挂起的话,并不知道什么时候恢复这个任务。 LuatOS 的做法是,每个任务在执行完自己的事情之后,都必须是调用一个等待函数, 这样的等待函数有如下几个: 1,sys.wait(timeout) 这个函数,会在挂起任务的同时,启动一个定时器,定时器的触发时间就是 timeout,并且把任务 id 跟这个定时器绑定。 到定时器触发之后,sys.run 会根据该定时器绑定的任务 id,重新恢复该任务的运行。 2,sys.waitUntil(topic, timeout) ``` 在挂起任务的同时,订阅一个名为 topic 的消息。待到有其他的任务发布这个消息后,sys.run 恢复这个任务。 如果没有等到其他任务发布这个topic 消息,超时timeout 了,sys.run()也会恢复任务的运行。 ``` 总结来说,LuatOS 的任务在挂起自己之前,会在系统的表里面,放一个让自己恢复运行的条件,这个条件或者是一个超时时间,或者是其他任务发布一个消息。sys.run() 函数会去判断这些恢复运行的条件是否满足,一旦满足条件,就会恢复对应的任务。 ### 3.2 怎么实现单个任务 在 LuatOS 里面,一个任务,可以理解为一个无限循环的函数,启动一个任务,有如下步骤: 1,定义这个无限循环的函数 task1; 2,调用 sys.taskInit(task1), 在 taskInit 函数里面,先为 task1 函数创建一个协程,同时把这个协程注册到系统的协程列表,这样 sys.run() 就会去运行这个协程。 这样就新增了一个持续运行,永不退出的协程了。 一个在 LuatOS 系统里面合法的任务, 必须运行很少量的时间,执行完自己的操作之后,马上就把自己挂起。 挂起的方式就是 调用 sys.wait 或者 sys.waitUtil 函数。 一个正常的 LuatOS 任务,执行计算的时间是很短暂的,绝大部分的时间,都是在挂起状态。 在挂起状态, 是不消耗 CPU 资源的。 所以, LuatOS 的协程机制,具备了实现低功耗系统的前提。 ### 3.3 进一步理解 sys.run() LuatOS 的 `sys.run()` 函数是系统任务调度器的启动入口,其主要工作流程如下: #### 3.3.1 进入任务调度主循环 当执行到 `sys.run()` 时,LuatOS 会启动任务调度器,正式进入事件驱动和多任务调度阶段。 此后,所有通过 `sys.taskInit` 注册的任务都会被纳入系统统一调度。 #### 3.3.2 循环处理底层消息与事件 `sys.run()` 会不断从底层(如硬件中断、驱动、系统内核,定时器等)获取消息或事件,并将这些消息分发到相应的任务或回调函数进行处理。 这包括定时器到期、外设事件、网络数据到达、用户自定义消息等。 #### 3.3.3 定时器与任务切换 sys.run 会周期性检查所有注册的定时器,并在定时器到期时唤醒相应的任务协程。 同时,系统会根据任务的挂起或唤醒状态,合理切换协程,实现多任务并发。 #### 3.3.4 任务间消息通信与同步 `sys.run()` 支持任务间通过消息发布/订阅、等待/唤醒等机制进行通信与同步。 例如,任务可以通过 `sys.publish` 发布消息,其他任务通过 `sys.waitUntil` 或 `sys.subscribe` 等方式等待或响应这些消息。 #### 3.3.5 持续运行,直至系统重启或退出 `sys.run()` 会持续运行,不会主动退出。 sys.run() 系统的主循环,确保所有任务和事件都能被及时处理。 只有在系统重启、脚本异常终止或手动退出时,sys.run() 这个调度循环才会结束。 #### 3.3.6 简要流程图 (1)启动任务调度器; (2)进入主循环 (3)轮询底层消息、定时器 (4)唤醒/调度任务协程 (5)分发和处理事件、消息 (6)返回主循环,直到系统重启或退出 ### 3.4 怎么实现多个任务 #### 3.4.1 协程大多数时间应该是挂起状态 ``` 由于协程的运行原理是,同一时间只有一个协程在运行,其他协程在挂起状态。 所以如果有多个协程存在的话,多个协程的运行,只可能有两种情况: 第一种情况, 所有的协程都在挂起状态,这时候系统有可能进入低功耗; 第二种情况, 有一个协程在运行,其他协程在挂起。这时候系统是唤醒状态,不可能是低功耗状态。 ``` #### 3.4.2 LuatOS 多任务的核心是挂起和恢复的调度 ``` 一个协程运行的时间越长,挂起的就越慢,其他的协程就无法得到时间片运行。 只有所有的协程都尽量减少时间占用, 都尽快挂起自己,这样的多任务的调度的效率才能更高。 因此, LuatOS 多任务的编程核心,是使得每个任务函数的执行时间尽可能的短,尽可能快速的挂起自己,整个系统的多任务并发处理的效率才会更高。 如果某个协程的运算时间很长,导致自己无法很快挂起,就会拖累整个系统,使得整个系统的实时响应的性能降低。 ``` #### 3.4.3 怎么防止某个协程长时间不挂起 ``` 为了防止某个协程长时间做运算,不把自己挂起,LuatOS 设计了 watchdog 机制,起一个定时器,几秒钟喂狗一次。 如果超时没有喂狗,系统就会被重启。 把下面这段代码放到 main.lua,即可实现喂狗的功能: ``` ``` --添加硬狗防止程序卡死 if wdt then wdt.init(9000)--初始化watchdog设置为9s sys.timerLoopStart(wdt.feed, 3000)--3s喂一次狗 end ``` ### 3.5 多个任务之间怎么分配时间片 LuatOS 系统里面,是没有给某个任务分配时间片这样的动作的。 LuatOS 的任务,必须尽快把自己挂起,释放出 CPU,才能够让整个系统实时运行。 当所有任务都把自己挂起后,系统就就可能会低功耗休眠状态。 只要有任何一个任务没有挂起,系统都不可能进入低功耗休眠状态。 通过 sys.run()函数, 对多个任务按照业务需要进行恢复运行的调度,保证整个系统的顺畅运行。 sys.run()调度的依据,一个是定时器机制,一个是消息机制。 ## 四、LuatOS 的定时器机制 LuatOS 的定时器机制是实现多任务系统的核心组件之一。 支持单次触发和周期循环,适用于物联网设备中的定时任务、数据采集、状态监测等场景。 ### 4.1 定时器类型与适用场景 | 类型 | 特点 | 适用场景 | | --- | --- | --- | | 单次定时器 | 延迟指定时间后触发一次 | 初始化延时、事件超时处理 | | 循环定时器 | 周期性触发,可指定次数 | 心跳包发送、传感器轮询 | ### 4.2 核心 API 与用法 #### 4.2.1 单次定时器 _功能: 延迟 timeout 毫秒后执行函数, 可传多个参数_ local timerId = sys.timerStart(callback, timeout, arg1, arg2, ...) 参数说明: callback: 定时器触发时执行的函数 timeout: 延迟时间(毫秒) argN: 传递给回调函数的参数 代码示例: ``` local function timerFun(msg) log.info("单次定时器:", msg) end sys.timerStart(timerFunc, 3000, "Hello") ``` ``` 单次定时器:hello ``` #### 4.2.2 循环定时器 _功能: 每隔 timeout 毫秒重复执行函数_ `` local `timerId = sys.timerLoopStart(callback, timeout, arg1, arg2, ...)` 代码示例: ``` local count = 0 local function timerFun() count = count + 1 log.info("循环定时器", "触发次数:", count) end sys.timerLoopStart(timerFun,2000) -- 每2秒触发_ ``` 运行结果为: ``` 循环定时器 触发次数:1 循环定时器 触发次数:2 循环定时器 触发次数:3 循环定时器 触发次数:4 ``` #### 4.2.3 定时器停止 LuatOS 有两个 API 用于停止正在生效的定时器: **1, 停止制定 timerid 的单个定时器** `sys.timerStop(timerId)` **2,停止制定回调函数的所有定时器。** `sys.timerStopAll(callback)` ### 4.3 典型代码示例 #### 4.3.1 组合使用单次与循环定时器 ``` local function timerFun() local count = 0 sys.timerLoopStart(function() count = count + 1 log.info("循环任务", "计数:", count) end, 1000) end -- 启动一个单次定时器,5秒后定时器时间到达,执行回调函数timerFun -- 在timerFun中启动一个循环定时器 sys.timerStart(timerFun, 5000) ``` #### 4.3.2 动态管理定时器 ``` local tid function start() tid = sys.timerLoopStart(function() log.info("动态任务", "运行中...") end, 2000) end function stop() sys.timerStop(tid) end ``` #### 4.3.3 5 秒后重连网络 ``` function connect() -- 尝试连接网络_ if fail then sys.timerStart(connect, 5000) -- 5秒后重试_ end end ``` ### 4.4 定时器的数量限制 LuatOS 最多支持 64 个定时器。 由于任务里面的 sys.wait()、带timeout参数的sys.waitUntil()、带timeout参数的sys.waitMsg()调用也会引发调度器启动一个定时器管理该任务的运行恢复,所以用户实际能够启用的定时器,会比 64 个更少。 所以,在开发过程中, 需要注意这一点,不要无节制的使用定时器。 ### 4.5 为什么 LuatOS 的定时器不太准 LuatOS 的定时器往往“不太准”,主要原因在于其定时器机制依赖于消息总线(Message Bus)和系统调度,而不是直接精准地控制硬件定时。具体来说有如下几点原因: #### 4.5.1 定时器基于消息机制 LuatOS 的定时器设计是基于 RTOS 的 timer API。 当定时器超时时,系统只是在消息总线中插入一条定时器消息,由主循环 sys.run()消费和处理,这会带来两种可能的时延: 1,当调度器在处理消息时,可能会因为其他任务、消息队列长度、系统负载等原因出现延迟。 2,定时器回调的实际执行时机,取决于消息被调度和消费的时刻,而不是定时器超时的精确时刻。 #### 4.5.2 系统调度与任务竞争 LuatOS 采用事件驱动和多任务协作,主循环需要处理各种消息(包括定时器、外设、网络等); 如果系统中有大量任务或消息,定时器消息可能会被延后处理,导致定时精度下降。 #### 4.5.3 软件定时器的局限 (1)软件定时器本质上依赖于系统 tick(通常为 1ms),但 tick 的处理、消息入队、Lua 虚拟机调度等环节都会引入微小延迟。 (2)在高负载或消息堆积时,这种延迟会被放大,表现为“定时器不准”。 #### 4.5.4 Lua 脚本无法实现高精度定时器 ``` LuatOS 定时器不太准的根本原因是:定时器只是触发消息,实际执行依赖消息总线和主循环调度,受系统负载、任务数量、消息堆积、网络中断优先级最高等多因素影响。 LuatOS 定时器不能实现高精度定时器(例如微秒级别,几十毫秒级别,甚至几百毫秒级别也会有误差)。 如果要实现高精度定时器,只能外挂单片机实现,或者后续合宙有可能推出集成一款单片机的产品,专门用来实现高精度定时器和其他对实时性和精度要求比较高的需求。 ``` ### 4.6 sys.lua 里面的 timerPool 变量 如果你有兴趣查看 sys.lua 的话,会发现 timerPool 这个 table 类型的变量,在 0-0x1FFFFF 范围内存储 恢复运行协程的定时器消息 ID, 在 0x200000-0x7FFFF 范围内存储有回调函数的定时器消息 ID。 所以,凡是某个协程调用 sys.wait()延时函数,都会在注册一个定时器,定时器超时后,就会由调度器重新恢复这个协程的运行; 当使用 timerStart 函数注册的定时器超时后, 调度器会调用定时器回调函数。 这两种情况的超时处理,都是在 timerPool 这个变量实现的。 ### 4.7 LuatOS 定时器总结 LuatOS 的定时器机制通过 `sys` 库提供了消息驱动架构,合理运用定时器可显著提升物联网设备的自动化程度和能效比。 在使用定时器机制的时候,需要注意如下几点: #### 4.7.1 避免阻塞回调 **1, 定时器回调函数中禁止使用****sys.wait****操作** 因为定时器回调函数是由调度器直接调用的,如果在定时器回调函数里面使用 sys.wait 操作,会使得调度器阻塞,从而使得整个系统停止运行。 **2, 定时器回调函数禁止进行长时间阻塞操作** ``` 这样会极大的降低系统效率,使得系统的反应变慢。 ``` #### 4.7.2 注意资源释放 任务退出时,如果在任务运行过程中创建的定时器不再需要,需调用 `sys.timerStop()` 或者 `sys.timerStopAll()` 清理关联定时器,防止内存泄漏,或者引起定时器资源耗尽。 如果不想主动写代码清理关联定时器,只能等待定时器时间到了之后,自动清除,这时就会多占用了一个没有任何实际功能的定时器,如果定时器资源非常紧张的情况下,创建新的定时器有可能会失败。 #### 4.7.3 不要期待有高精确度的延时和定时 ``` 由于消息机制和虚拟机的运行限制,导致延时函数和定时器的精度都不会很高,在实现业务逻辑的时候,一定要注意这一点。 ``` ## 五、 LuatOS 的消息机制 LuatOS 的消息机制是其多任务协作的核心,通过 `sys` 库实现事件驱动编程。以下从消息发送、消息接收、消息订阅三个维度详细解析: ### 5.1 发送消息 #### 5.1.1 广播式消息(一对多) API:`sys.publish(topic, arg1, arg2, ...)` 功能:向所有订阅者广播消息,无目标标识。 代码示例: ``` sys.publish("NET_READY", true) -- 发布网络就绪事件_ sys.publish("SENSOR_DATA", 25.5, "℃") -- 携带多个参数_ ``` #### 5.1.2 定向消息(点对点) API:`sys.sendMsg(taskName, target, arg2, arg3, arg4)` 功能:向指定任务发送消息,支持目标标识和参数。 代码示例: ``` -- 向名称为 "NET_TASK"的任务,发送名称为 "HTTP_RESP" 的消息,携带多个参数_ sys.sendMsg("NET_TASK", "HTTP_RESP", 200, "{data:123}") ``` ### 5.2 消息接收 #### 5.2.1 等待消息 在协程内部等待:`sys.waitUntil(topic, timeout)` 特别提醒: 该 API 只能在协程内执行 代码示例: ``` -- 在任务中等待消息,超时30秒,注意这段代码必须是在协程执行_ local ok, data = sys.waitUntil("NET_READY", 30000) if ok then log.info("网络就绪:", data) end ``` #### 5.2.2 定向接收 API:`sys.waitMsg(taskName, target, timeout)` 特点:按任务名和目标标识精准接收,支持超时,该代码只能在协程内执行。 注意,该 API 的第一个参数 taskName, 是指等待消息的任务名称,也就是自己的任务名称,不是发送消息的任务名称。 调用该 API 的任务,和接收任务,不一定是同一个任务。 当接收消息的任务在挂起的时候,可以由其他任务或者调度器通过 WaitMsg API 唤醒挂起的任务。 代码示例: ``` -- 等待来自其他任务的 "CONFIG" 消息,如果等不到的话,5秒钟后超时返回_ local msg = sys.waitMsg("NET_TASK", "CONFIG", 5000) if msg then log.info("收到配置:", msg.arg2, msg.arg3) end ``` ### 5.3 消息订阅 #### 5.3.1 全局订阅 API:`sys.subscribe(topic, func)` 特点:如果订阅了同一主题有多个回调函数,这些回调函数都会被触发。 代码示例: ``` sys.subscribe("ALARM", function(level, msg) log.warn("警报", level, msg) end) ``` #### 5.3.2 任务私有订阅 实现方式:通过 `sys.taskInitEx` 创建任务时注册回调。 当有其他的任务发送消息给目标任务的时候, 但是目标任务并没有通过 WaitMsg 函数设定消息处理,这时候该消息的处理就交给回调函数处理。 代码示例: ``` -- 创建名为“NET_TASK”的任务,并且定义非目标消息的回调函数_ local function net_cb(msg) if msg.target == "ERROR" then log.error("网络异常:", msg.arg2) end end sys.taskInitEx(function() while true do local msg = sys.waitMsg("NET_TASK", "DATA", -1) -- 处理数据..._ end end, "NET_TASK", net_cb) -- 第三个参数为回调函数_ ``` ### 5.4 LuatOS 消息机制的典型应用场景 #### 5.4.1 网络模块与主任务通信 ``` -- 当前的任务名称为 “MAIN_TASK”,创建了 NET_TASK 任务。_ sys.taskInitEx(function() while true do local msg = sys.waitMsg("NET_TASK", "REQ", -1) local resp = http.get(msg.arg2) sys.sendMsg(msg.src_task, "RESP", resp) end end, "NET_TASK") -- 主任务请求数据_ sys.sendMsg("NET_TASK", "REQ", "https://api.example.com") local resp = sys.waitMsg("MAIN_TASK", "RESP", 5000) ``` #### 5.4.2 全局事件通知(sys) ``` -- 传感器任务广播数据_ sys.taskInit(function() while true do local temp = read_sensor() sys.publish("TEMP_UPDATE", temp) sys.wait(1000) end end) -- 多个订阅者处理数据_ sys.subscribe("TEMP_UPDATE", function(t) if t > 30 then sys.publish("FAN_ON") end end) ``` ### 5.5 消息机制设计要点 #### 5.5.1 消息机制的不同设计 (1)处理全局事件,sys.publish发布的消息,已经订阅这个消息的所有sys.subscribe对应的处理函数都能收到。 (2)处理模块间通信(如网络请求-响应),sys.sendMsg发布的消息,会携带一个task name参数,只有sys.waitMsg时也携带同样的task name参数,才能收到消息。 #### 5.5.2 避免消息风暴: 高频消息(如传感器数据)建议合并发送或降低频率。 #### 5.5.3 消息机制的核心目的之一是软件解耦 通过合理运用 `sys` 库的消息机制,可构建高效、解耦的物联网应用架构。 ## 六、多任务之间的信息交换 ### 6.1 用全局变量做信息交换 如果信息量很小,比如就一个字符串或者标志位,任务之间可以通过共享全局变量来通信,一个任务去对这个全局变量赋值,其他任务读取这个全局变量,任务之间就达到了通信的目的了; ### 6.2 用消息做信息交换 但是如果想要交换多个数据,每个数据都用全局变量的话,就有点过于累赘了。 这时候,可以通过发送消息来通信。 任务之间怎么发送消息,接收消息,参考第五章的内容。 ## 七、再次理解调度器 sys 库 LuatOS 的 sys 库是系统调度和多任务管理的核心库,提供了丰富的 API 用于任务创建、延时、消息通信、定时器管理等。 #### 7.1.1 任务与协程管理 `API: sys.taskInit(func, arg1, arg2, ...)` 功能: 创建一个新的任务(协程),并传递参数给任务函数。 #### 7.1.2 延时与等待 `(1) sys.wait(timeout)` 功能: 任务延时挂起指定毫秒数,只能在任务函数中调用。 `(2)sys.waitUntil(topic, timeout)` 功能: 任务挂起,直到收到指定 topic 的消息或超时,只能在任务函数中调用。 #### 7.1.3 定时器相关 `(1) sys.timerStart(func, timeout, arg1, ...)` 创建单次定时器,到时后执行回调函数。 `(2)sys.timerLoopStart(func, timeout, arg1, ...)` 创建循环定时器,周期性执行回调函数。 `(3)sys.timerStop(timerId)` 停止指定 ID 的定时器。 `(4)sys.timerStopAll(func)` 停止所有与指定回调函数相关的定时器。 `(5)sys.timerIsActive(timerId)` 判断定时器是否处于激活状态。 #### 7.1.4 消息通信 `(1)sys.publish(topic, arg1, ...)` 发布(广播)一个消息,唤醒等待该 topic 的任务或触发订阅回调。 `(2)sys.subscribe(topic, callback)` 订阅指定 topic 的消息,消息到来时自动执行回调。 `(3)sys.unsubscribe(topic, callback)` 取消订阅。 #### 7.1.5 主循环控制 `sys.run()` 功能: 是 LuatOS 的调度器,是系统主循环,调度所有注册的任务和定时器。 #### 7.1.6 典型用法示例 ``` sys.taskInit(function() while true do log.info("task1", "tick") sys.wait(1000) sys.publish("TICK") end end) sys.taskInit(function() sys.waitUntil("TICK") log.info("task2", "收到TICK消息") end) sys.subscribe("TICK", function() log.info("订阅者", "收到TICK") end) sys.timerStart(function() log.info("定时器触发") end, 2000) sys.run() ``` #### 7.1.7 任务与协程管理 (1)sys.taskInitEx(func, taskName, cbFun, ...) 功能:创建一个具名任务线程,并注册任务函数和非目标消息回调。 (2)sys.taskDel(taskName) 功能:删除由 `taskInitEx` 创建的任务线程,释放资源。 #### 7.1.8 消息通信机制 (1)sys.waitMsg(taskName, target, timeout) 功能:等待接收一个目标消息(可指定超时),任务会挂起直到收到目标消息或超时。 (2)sys.sendMsg(taskName, target, arg2, arg3, arg4) * 功能:向目标任务发送一个消息,可携带最多 4 个参数。 (3)sys.cleanMsg(taskName) 功能:清除指定任务的消息队列,防止消息堆积。 ### 7.3 sys.run() 怎么实现多个任务的协同工作 sys.run()函数的实现过程是这样的: 1, 查看消息队列里面是否有未处理的消息, 如果有,就根据消息的处理类型,调用回调函数或者是唤醒对应的任务进行消息处理; 2, 等待底层 RTOS 操作系统的定时器消息;等待的过程,就是低功耗的过程; 3, 定时器消息等到之后, 调用定时器回调函数或者唤醒对应的任务。 4, 循环 1-3 步。 通过以上过程,我们可以看到,这个 LuatOS 系统, 大多数时间都是在等待底层 RTOS 操作系统的定时器消息,在等待期间,系统是可以处于低功耗休眠状态的。 当任务的时延很短, 或者定时器非常频繁,或者是消息太多,是会影响到系统的低功耗性能的。 ## 八、怎么封装一个 LuatOS 的软件功能模块 在 LuatOS 中封装功能模块为单独 Lua 文件的标准做法: 1. 新建一个 Lua 文件,定义一个 table,比如名字为 myflib,所有对外接口作为其字段。 2. 用 local 修饰内部变量和函数,实现信息隐藏。 3. 定义 myflib 的成员变量,成员函数,用作对外的接口。 4. 文件末尾用 `return` myflib ,导出模块 table。 5. 外部的文件,用 `require("模块名")` 加载和复用模块。 这样可以让你的功能模块独立、可维护、易扩展,是 Lua 及 LuatOS 推荐的开发范式。 代码示例: ``` myflib.lua 的内容: -- [必须] 定义一个 table 作为模块_ local myflib = {} -- [可选] 局部变量,仅模块内部可见_ local myid = "23456" -- [可选] 模块变量,可被外部访问和修改_ myflib.mykey = "abcdefg" -- [可选] 局部函数,仅模块内部可用_ local function myabc() -- 实现细节_ end -- [可选] 导出函数,供外部调用_ function myflib.myfunc(key, value) log.info("key: ",key, value) if myflib.mykey == key then myflib.mykey = value end return myflib.mykey end -- [必须] 返回模块 table_ return myflib 调用 myflib 的main.lua的内容: PROJECT = "test_demo" VERSION = "1.0.1" local sys = require("sys") local mylib = require("myflib") local function task1() local sid = 0 local nkey while(1) do nkey = "test" .. tostring(sid) mylib.myfunc(mylib.mykey, nkey) log.info("new key:", mylib.mykey) sid = sid + 1 sys.wait(3000) end end sys.taskInit(task1) sys.run() ``` ## 九、LuatOS 的核心库和扩展库 LuatOS 在 Lua 5.3 版本的基础上, 封装了 74 个核心库,17 个扩展库,提供了极其强大的通信和硬件的开发功能。 ### 9.1 LuatOS 核心库 LuatOS 核心库,提供了 LuatOS 系统的核心功能。 不同的硬件型号,支持不同的核心库的子集。 LuatOS 的核心库, 是不需要用户 require,可以直接调用的。 780EPM 对这些核心库的支持情况参见如下链接: <http://docs.openluat.com/osapi/core/> ### 9.2 LuatOS 扩展库 除了用户可以直接使用的核心库之外, LuatOS 还提供了丰富的扩展库。 使用扩展库,需要用户在代码里面做 require 动作,Luatools 看到 require 关键字后,会把用到的扩展库合并入烧录包,一起烧录到硬件里面。 如果用户不做 require 的动作, luatools 就不会合并这个扩展库的代码。 所有的扩展库,都是用 Lua 代码实现的。 当前 LuatOS 已经支持的扩展库参见如下链接: <http://docs.openluat.com/osapi/ext/> ## 十、LuatOS 实际工程代码解读 780EPM 1.3 开发板的出厂固件代码, 是一个实际的 LuatOS 开发的简单案例。 代码的位置在: [780EPM 开发板 V1.3 出厂固件源码](https://gitee.com/openLuat/LuatOS/tree/master/olddemo/project/Air780EPM/780EPM_1.3%E5%BC%80%E5%8F%91%E6%9D%BF%E5%87%BA%E5%8E%82%E5%9B%BA%E4%BB%B6) 这个案例代码已经比较老了,仅供参考阅读使用,如果要开发项目,参考 [LUATOS代码仓库](https://gitee.com/openLuat/LuatOS/tree/master/module)下各产品的demo和project代码 这个固件分为几个部分: 1, 780EPM core 固件: 目前最新的固件是 2005 版本,后续更新的固件版本也可以继续使用; 最新的 780EPM 固件在这里下载: <http://docs.openluat.com/air780epm/luatos/firmware/780epm_version/> 2,管脚复用描述文件 ``` pins_Air780epmjson ``` 3, 资源图片 ``` 实现开机固件所需的图片。(详见780EPM 开发板 V1.3 出厂固件源码的pic目录下的图片 ) ``` 4, 实现脚本。 下面重点讲解一下脚本实现的逻辑。 ### 10.1 编码要求 为了降低用户理解成本,这份开机固件的代码有如下要求: (1)不允许用云编译扩大脚本区,不允许用云编译扩大文件系统,保持脚本 + 资源总体尺寸不能大于 256K 字节; (2)main.lua 作为逻辑主线,其他的功能代码封装成子模块,提供成员函数,也可以提供成员变量, 被 main.lua 调用; (3)不允许使用匿名函数; ### 10.2 已实现功能 如下代码,已经实现了如下功能: (1)主界面九宫格的按键切换, (2)长按进入具体功能界面;再长按回到主界面; (3)图片显示功能, (4)摄像头预览, (5)俄罗斯方块, (6)天气数据获取,并显示不同的天气图标; 使用的是 780EPM 默认 2005 固件,不需要扩大文件系统和代码区。 其中,airlcd.lua, camera780epm_simple.lua, russia.lua,statusbar.lua, 分别用 table 的方式,封装了 LCD 的参数初始化,camera 的初始化,预览,退出,俄罗斯方块的初始化,更新数据,响应按键等事件。 在 main.lua 调用这些封装好的 table 的函数即可,不需要过度关心子模块的实现细节。 ### 10.3 待实现功能 (1)以太网 LAN (2)以太网 WAN (3)硬件自检 (4)modbus TCP (5)modbus RTU (6)CAN 总线 ### 10.4 main.lua 解读 #### 10.4.1 系统初始化 整个系统,做了两个全局初始化: 1, 看门狗的初始化,wdt.Init()防止系统被某个任务异常占用 CPU 让系统锁死; 2, LCD 的初始化: airlcd.lcd_init("AirLCD_0001"),其中 AirLCD_0001 是合宙 LCD 配件的型号。 #### 10.4.2 业务主循环 UITask() 函数, 是 main.lua 启动之后的主循环。 在 UITask 函数里面,先做按键的初始化之后,就无限循环的不断调用三个函数:keypressed, update, draw。 其中,keypressed 是查看按键是否有待处理的事件; update 是更新业务数据; draw 是更新 UI 画面。 #### 10.4.3 按键事件的处理 由于 780EPM 开发板只有三个按键: 开机键,boot 键,reset 键。 reset 键无法被捕获事件,只能复位硬件,所以固件只能处理 开机键和 boot 键的事件。 boot 按键是一个特殊的 GPIO, 编号为 GPIO0。 开机键也是个特殊的 GPIO, 编号为 GPIO.PWR_KEY。 在 KeyInit()这个函数, 分别配置了 gpio.PWR_KEY 和 GPIO0 为双边沿中断,中断处理函数分别为 PowerInterrupt 和 BootInterrupt。 根据开发板的原理图,开机键初始电平是上拉为高电平,boot 键初始电平为下拉低电平。 在 PowerInterrupt() 和 BootInterrupt()这两个函数的处理逻辑是类似的,都是计算按下和抬起的时间间隔,从而判断是短按还是长按,然后给 key 这个全局变量赋值。 key 是字符串类型,是一个比较关键的变量,根据 key 的值不同, main.lua 进入不同的功能。 这个逻辑是在 keypressed 来实现。 在 keypressed() 函数里面,检查 key 变量的值,然后做不同的处理。 在主界面, 处理 "main" 和 "enter"这两个值,分别是切换按钮加亮显示,以及进入具体功能按钮; 在具体功能界面, enter 按键时间会返回主界面; 在俄罗斯方块界面, 有 5 种按键: (1)right: 短按 boot, 往右移动方块; (2) left:短按开机,往左移动方块; (3)up:长按 boot,旋转方块; (4)fast:长按开机,快速下落; (5)quit:超长按开机,退出游戏回到主界面。 #### 10.4.4 UI 界面的循环刷新 在 draw 函数里面刷新界面。 当需要把绘图权限交给其他的功能模块的时候, 根据情况做不同的处理: (1)俄罗斯方块的刷新函数就是自己实现 drawrus 函数, draw 函数调用 drawrus 函数刷新屏幕; (2)摄像头预览功能接管了屏幕后,draw 函数判断当前是摄像头预览功能,就直接退出,如果判断不是摄像头,再继续处理刷新任务; (3)在刷新之前,调用 update 函数更新用于刷新的关键数据。 #### 10.4.5 管理当前功能状态机 有两个关键变量: cur_sel: 整数,范围是 1-9, 当前选择的九宫格是哪个; cur_fun: 字符串,10 种值: 记录当前已经进入的界面是主界面,还是 9 个之一。 “main”: 主界面; 另外 9 个界面用一个数组记录,并根据 cur_sel 赋值给到 cur_fun。 local funlist = { "picshow", "camshow","russia", "LAN", "WAN","selftest", "modbusTCP","modbusRTU","CAN" } cur_sel 和 cur_fun,结合 key 的值,组成了整个的逻辑切换,可以决定该进入什么软件功能,该显示什么界面。 理解了 cur_sel, cur_fun, key 这三个变量的运用,就可以看明白整个软件的逻辑。 ### 10.5 总结 这份 780EPM 的开发板的出厂固件的代码,展示了一个完整的 LuatOS 工程的基本实现的方法。 脚本文件一共只有 10 个,全部加一起只有 30k 字节,1000 行代码,实现了 9 宫格界面,电量,信号强度,天气的状态栏显示,包括俄罗斯方块在内的多种功能的演示。 这份代码后续还会继续更新,并且都不会采用非常高难度的编码技巧,只需要用最简单的编程逻辑就可以实现相对复杂的业务逻辑。 更详细的 LuatOS 编程框架, 请参见如下链接: [LuatOS编程框架培训稿](https://docs.openluat.com/luatos_lesson/001_luatos_framework/)
handrap
2026年11月4日 10:08
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码
AI