跳转至

LuatOS运行框架讲解

一、LuatOS 编程起步

本文档的读者,默认都已经了初步的 Lua 语法。

如果不懂 Lua 语法的话,可以参考如下链接,或者直接问 Ai 也可以。

http://docs.openluat.com/air780epm/common/lua_lesson/

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 等)用来实现具体的业务逻辑。

LuatOS 提供了 87 个核心库,以及 59 个扩展库(截止到 2025 年 4 月的数据)。

核心库和扩展库的内容,在后续的章节里面介绍。

例如:

require "zbuff"             -- 如需网络功能
require "onewire"          -- 如需传感器功能

1.2.3 至少启动一个任务

在 main.lua 里面,至少需要启动一个任务,否则这个 main 就无所事事,是一个没什么实际用处的主脚本了。

启动一个任务的方法,分为 2 个步骤:

1,创建一个函数,把要做的事情,放在这个函数里面使用。这个函数必须是无限循环的,防止很快结束生命,不妨把这个函数命名为 task1(),

2,调用 sys.taskInit(task2),启动这个函数,于是这个任务,就放在待运行的任务列表里面了。

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 的 87 个核心库和 59 个扩展库提供的 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_Air780EPM.json, 描述了本例使用到的管脚的功能,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 1 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_air780EPM.json 内容如下:

{
  "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) 被调用时,LuatOS 内部会遍历订阅者列表,找到所有订阅了 "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 的任务在做运算的时候,是 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() 函数是系统任务调度器的启动入口,其主要工作流程如下:

1. 进入任务调度主循环

当执行到 sys.run() 时,LuatOS 会启动任务调度器,正式进入事件驱动和多任务调度阶段。

此后,所有通过 sys.taskInit 注册的任务都会被纳入系统统一调度。

2. 循环处理底层消息与事件

sys.run() 会不断从底层(如硬件中断、驱动、系统内核,定时器等)获取消息或事件,并将这些消息分发到相应的任务或回调函数进行处理。

这包括定时器到期、外设事件、网络数据到达、用户自定义消息等。

3. 定时器与任务切换

sys.run 会周期性检查所有注册的定时器,并在定时器到期时唤醒相应的任务协程。

同时,系统会根据任务的挂起或唤醒状态,合理切换协程,实现多任务并发。

4. 任务间消息通信与同步

sys.run() 支持任务间通过消息发布/订阅、等待/唤醒等机制进行通信与同步。

例如,任务可以通过 sys.publish 发布消息,其他任务通过 sys.waitUntilsys.subscribe 等方式等待或响应这些消息。

5. 持续运行,直至系统重启或退出

sys.run() 会持续运行,不会主动退出。

sys.run() 系统的主循环,确保所有任务和事件都能被及时处理。

只有在系统重启、脚本异常终止或手动退出时,sys.run() 这个调度循环才会结束。

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秒触发_

运行结果为:

循环定时器   触发次数:0
循环定时器   触发次数:1
循环定时器   触发次数:2
循环定时器   触发次数:3

4.2.3 定时器停止

LuatOS 有两个 API 用于停止正在生效的定时器:

1, 停止制定 timerid 的单个定时器

sys.timerStop(timerId)

2,停止制定回调函数的所有定时器。

sys.timerStopAll(callback)

4.3 典型代码示例

4.3.1 组合使用单次与循环定时器

local timerFun()
    local count = 0
    sys.timerLoopStart(function()
        count = count + 1
        log.info("循环任务", "计数:", count)
    end, 1000)
end

-- 5秒后启动循环定时器_
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()调用也会引发调度器启动一个定时器管理该任务的运行恢复,所以用户实际能够启用的定时器,会比 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 定时器不太准的根本原因是:定时器只是触发消息,实际执行依赖消息总线和主循环调度,受系统负载、任务数量、消息堆积等多因素影响。

如果需要高精度的定时器的话,需要在底层实现,而不应该使用Lua脚本实现。

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.timerStopAll() 清理关联定时器,防止内存泄漏,或者引起定时器资源耗尽。

如果不想用代码清理关联定时器,也可以等待系统垃圾回收自动清理,这时候就需要在创建新定时器之前加一个 sys.wait()时延,给系统留出来垃圾回收的时间,以免创建新定时器失败。

4.7.3 不要期待有高精确度的延时和定时

由于消息机制和虚拟机的运行限制,导致延时函数和定时器的精度都不会很高,在实现业务逻辑的时候,一定要注意这一点。

五、 LuatOS 的消息机制

LuatOS 的消息机制是其多任务协作的核心,通过 syssysplus 库实现事件驱动编程。以下从消息发送、消息接收、消息订阅三个维度详细解析:

5.1 发送消息

5.1.1 sys 库:广播式消息(一对多)

API:sys.publish(topic, arg1, arg2, ...)

功能:向所有订阅者广播消息,无目标标识。

代码示例:

sys.publish("NET_READY", true)  -- 发布网络就绪事件_
sys.publish("SENSOR_DATA", 25.5, "℃")  -- 携带多个参数_

5.1.2 sysplus 库:定向消息(点对点)

API:sysplus.sendMsg(taskName, target, arg2, arg3, arg4)

功能:向指定任务发送消息,支持目标标识和参数。

代码示例:

-- 向名称为 "NET_TASK"的任务,发送名称为 "HTTP_RESP" 的消息,携带多个参数_
sysplus.sendMsg("NET_TASK", "HTTP_RESP", 200, "{data:123}")

5.2 消息接收

5.2.1 sys 库:等待消息

在协程内部等待:sys.waitUntil(topic, timeout)

特别提醒: 该 API 只能在协程内执行

代码示例:

-- 在任务中等待消息,超时30秒,注意这段代码必须是在协程执行_
local ok, data = sys.waitUntil("NET_READY", 30000)
if ok then
  log.info("网络就绪:", data)
end

5.2.2 sysplus 库:定向接收

API:sysplus.waitMsg(taskName, target, timeout)

特点:按任务名和目标标识精准接收,支持超时,该代码只能在协程内执行。

注意,该 API 的第一个参数 taskName, 是指等待消息的任务名称,也就是自己的任务名称,不是发送消息的任务名称。

调用该 API 的任务,和接收任务,不一定是同一个任务。

当接收消息的任务在挂起的时候,可以由其他任务或者调度器通过 WaitMsg API 唤醒挂起的任务。

代码示例:

-- 等待来自其他任务的 "CONFIG" 消息,如果等不到的话,5秒钟后超时返回_
local msg = sysplus.waitMsg("NET_TASK", "CONFIG", 5000)
if msg then
  log.info("收到配置:", msg.arg2, msg.arg3)
end

5.3 消息订阅

5.3.1 sys 库:全局订阅

API:sys.subscribe(topic, func)

特点:如果订阅了同一主题有多个回调函数,这些回调函数都会被触发。

代码示例:

sys.subscribe("ALARM", function(level, msg)
  log.warn("警报", level, msg)
end)

5.3.2 sysplus 库:任务私有订阅

实现方式:通过 sysplus.taskInitEx 创建任务时注册回调。

当有其他的任务发送消息给目标任务的时候, 但是目标任务并没有通过 WaitMsg 函数设定消息处理,这时候该消息的处理就交给回调函数处理。

代码示例:

-- 创建名为“NET_TASK”的任务,并且定义非目标消息的回调函数_
local function net_cb(msg)
  if msg.target == "ERROR" then
    log.error("网络异常:", msg.arg2)
  end
end

sysplus.taskInitEx(function()
  while true do
    local msg = sysplus.waitMsg("NET_TASK", "DATA", -1)
    -- 处理数据..._
  end
end, "NET_TASK", net_cb)  -- 第三个参数为回调函数_

5.4 LuatOS 消息机制的典型应用场景

5.4.1 网络模块与主任务通信(sysplus)

-- 当前的任务名称为 “MAIN_TASK”,创建了 NET_TASK 任务。_
sysplus.taskInitEx(function()
    while true do
        local msg = sysplus.waitMsg("NET_TASK", "REQ", -1)
        local resp = http.get(msg.arg2)
        sysplus.sendMsg(msg.src_task, "RESP", resp)
    end
end, "NET_TASK")

-- 主任务请求数据_
sysplus.sendMsg("NET_TASK", "REQ", "https://api.example.com")
local resp = sysplus.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 sys 和 sysplus 由不同的设计

(1)使用 sys 处理全局事件(如硬件状态变化)。 (2)使用 sysplus 处理模块间通信(如网络请求-响应)。

5.5.2 避免消息风暴:

高频消息(如传感器数据)建议合并发送或降低频率。

5.5.3 消息机制的核心目的之一是软件解耦

通过合理运用 syssysplus 的消息机制,可构建高效、解耦的物联网应用架构。

六、多任务之间的信息交换

6.1 用全局变量做信息交换

如果信息量很小,比如就一个字符串或者标志位,任务之间可以通过共享全局变量来通信,一个任务去对这个全局变量赋值,其他任务读取这个全局变量,任务之间就达到了通信的目的了;

6.2 用消息做信息交换

但是如果想要交换多个数据,每个数据都用全局变量的话,就有点过于累赘了。

这时候,可以通过发送消息来通信。

任务之间怎么发送消息,接收消息,参考第五章的内容。

七、再次理解调度器 sys 库和 sysplus 库

7.1 sys 库的 API

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 = require("sys")

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.2 sysplus 库的 API

LuatOS 的 sysplus 库 是对 sys 库的增强补充,主要提供更强大的任务消息机制和协程管理能力,适合复杂的多任务、异步通信等场景。

sysplus 库的 API 主要包括以下几个部分:

7.2.1 任务与协程管理

(1)sysplus.taskInitEx(func, taskName, cbFun, ...)

功能:创建一个具名任务线程,并注册任务函数和非目标消息回调。

(2)sysplus.taskDel(taskName)

功能:删除由 taskInitEx 创建的任务线程,释放资源。

7.2.2 消息通信机制

(1)sysplus.waitMsg(taskName, target, timeout)

功能:等待接收一个目标消息(可指定超时),任务会挂起直到收到目标消息或超时。

(2)sysplus.sendMsg(taskName, target, arg2, arg3, arg4)

  • 功能:向目标任务发送一个消息,可携带最多 4 个参数。

(3)sysplus.cleanMsg(taskName)

功能:清除指定任务的消息队列,防止消息堆积。

7.3 sys.run() 怎么实现多个任务的协同工作

sys.run()函数的实现过程是这样的:

1, 查看消息队列里面是否有未处理的消息, 如果有,就根据消息的处理类型,调用回调函数或者是唤醒对应的任务进行消息处理;

2, 等待底层 RTOS 操作系统的定时器消息;等待的过程,就是低功耗的过程;

3, 定时器消息等到之后, 调用定时器回调函数或者唤醒对应的任务。

4, 循环 1-3 步。

通过以上过程,我们可以看到,这个 LuatOS 系统, 大多数时间都是在等待底层 RTOS 操作系统的定时器消息,在等待期间,系统是可以处于低功耗休眠状态的。

当任务的时延很短, 或者定时器非常频繁,或者是消息太多,是会影响到系统的低功耗性能的。

7.4 sysplus 库对 sys 库做了什么改进?

sysplus 最大的改进, 是提供了 任务注册的新 API, taskIniEx, 该 API 为创建的任务分配了全局唯一的名称。

有了任务名称,LuatOS 代码就可以对任务定向发送消息,从而实现更精准的任务间通信。

八、怎么封装一个 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 版本的基础上, 封装了 87 个核心库,59 个扩展库,提供了极其强大的通信和硬件的开发功能。

9.1 LuatOS 核心库

LuatOS 核心库,提供了 LuatOS 系统的核心功能,针对不同的硬件型号,适配了这 87 个核心库的部分功能。

LuatOS 的核心库, 是不需要用户 require,可以直接调用的。

780EPM 对这 87 个核心库的支持情况参见下表:

序号
API库
简介
类别
780EM支持否
1
adc
模数转换
外设驱动

2
audio
多媒体-音频
外设驱动

3
bit64

32位系统上对64位数据的基本算术运算和逻辑运算
基础软件

4
camera
摄像头
外设驱动

5
can
can操作库
外设驱动

6
cc
VoLTE通话功能
通信组件

7
codec
多媒体-编解码
基础软件

8
crypto
加解密和hash函数
加密解密

9
dac
数模转换
外设驱动

10
eink
墨水屏操作库
外设驱动

11
ercoap
新的Coap协议解析库
协议组件

12
errDump
错误上报
基础软件

13
fastlz
FastLZ压缩
基础软件

14
fatfs
读写fatfs格式
基础软件

15
fonts
字体库
基础软件

16
fota
底层固件升级
基础软件

17
fs
文件系统额外操作
基础软件

18
fskv
kv数据库,掉电不丢数据
基础软件

19
ftp
ftp 客户端
协议组件

20
gmssl
国密算法(SM2/SM3/SM4)
加密解密

21
gpio
GPIO操作
外设驱动

22
gtfont
高通字库芯片
外设驱动

23
hmeta
硬件元数据
通信组件

24
ht1621
液晶屏驱动(HT1621/HT1621B)
外设驱动

25
http
http 客户端
协议组件

26
httpsrv
http服务端
协议组件

27
i2c
I2C操作
外设驱动

28
i2s
数字音频
外设驱动

29
iconv
iconv操作
基础软件

30
io
io操作(扩展)
基础软件

31
ioqueue
io序列操作
基础软件

32
iotauth
IoT鉴权库, 用于生成各种云平台的参数
协议组件

33
iperf
吞吐量测试
通信组件

34
ir
红外遥控
外设驱动

35
json
json生成和解析库
基础软件

36
keyboard
键盘矩阵
外设驱动

37
lcd
lcd驱动模块
外设驱动

38
lcdseg
段式lcd
外设驱动

39
libcoap
coap数据处理
协议组件

40
libgnss
NMEA数据处理
协议组件

41
little_flash
LITTLE FLASH 软件包
外设驱动

42
log
日志库
基础软件

43
lora
lora驱动模块
外设驱动

44
lora2
lora2驱动模块(支持多挂)
外设驱动

45
lvgl
LVGL图像库
基础软件

46
max30102
心率模块(MAX30102)
外设驱动

47
mcu
封装mcu一些特殊操作
基础软件

48
miniz
简易zlib压缩
基础软件

49
mlx90640
红外测温(MLX90640)
外设驱动

50
mobile
蜂窝网络
通信组件

51
mqtt
mqtt客户端
协议组件

52
nes
nes模拟器
基础软件

53
netdrv
网络设备管理
外设驱动

54
onewire
单总线协议驱动
外设驱动

55
os
os操作
基础软件

56
otp
OTP操作库
基础软件

57
pack
打包和解包格式串
基础软件

58
pm
电源管理
基础软件

59
protobuf
ProtoBuffs编解码
基础软件

60
pwm
PWM模块
外设驱动

61
repl
"读取-求值-输出" 循环
基础软件

62
rsa
RSA加密解密
加密解密

63
rtc
实时时钟
基础软件

64
rtos
RTOS底层操作库
基础软件

65
sdio
sdio
外设驱动

66
sfd
SPI FLASH操作库
外设驱动

67
sfud
SPI FLASH sfud软件包
外设驱动

68
sms
短信
通信组件

69
socket
网络接口
协议组件

70
softkb
软件键盘矩阵
外设驱动

71
spi
spi操作库
外设驱动

72
statem
SM状态机
基础软件

73
string
字符串操作函数
基础软件

74
sys
sys库
基础软件

75
sysplus
sys库的强力补充
基础软件

76
timer
操作底层定时器
基础软件

77
tp
触摸库
外设驱动

78
u8g2
u8g2图形处理库
外设驱动

79
uart
串口操作库
外设驱动

80
w5500
w5500以太网驱动
外设驱动

81
wdt
watchdog操作库
基础软件

82
websocket
websocket客户端
协议组件

83
wlan
wifi操作
通信组件

84
xxtea
xxtea加密解密
加密解密

85
yhm27xx
yhm27xx充电芯片
外设驱动

86
ymodem
ymodem协议
基础软件

87
zbuff
c内存数据操作库
基础软件


9.2 LuatOS 扩展库

除了用户可以直接使用的核心库之外, LuatOS 还提供了 59 个扩展库。

使用扩展库,需要用户在代码里面做 require 动作,Luatools 看到 require 关键字后,会把用到的扩展库合并入烧录包,一起烧录到硬件里面。

如果用户不做 require 的动作, luatools 就不会合并这个扩展库的代码。

所有的扩展库,都是用 Lua 代码实现的。

当前 LuatOS 已经支持的 59 个扩展库如下表:

序号
名称
简介
接口
类别
1
ads1115
模数转换器
I2C
外设驱动
2
ads1115plus
模数转换器
I2C
外设驱动
3
adxl34x
3轴加速度计 目前支持 adxl345 adxl346
I2C
外设驱动
4
aht10
- aht10 温湿度传感器
I2C
外设驱动
5
air153C_wtd
看门狗

外设驱动
6
airlbs
收费服务

通信组件
7
ak8963
地磁传感器
I2C
外设驱动
8
aliyun
阿里云物联网平台

协议组件
9
am2320
温湿度传感器
I2C
外设驱动
10
ap3216c
光照传感器
I2C
外设驱动
11
bh1750
数字型光强度传感器
I2C
外设驱动
12
bmx
气压传感器 目前支持bmp180 bmp280 bme280 bme680 会自动判断器件
I2C
外设驱动
13
cht8305c
温湿度传感器
I2C
外设驱动
14
dhcpsrv
DHCP服务器

协议组件
15
dnsproxy
DNS代理转发

协议组件
16
ds3231
实时时钟传感器
I2C
外设驱动
17
ec11
旋转编码器
GPIO
外设驱动
18
gt911
gt911驱动,汇顶的电容触摸芯片
I2C
外设驱动
19
gy53l1
激光测距传感器
UART
外设驱动
20
httpdns
使用Http进行域名解析

协议组件
21
httpplus
http库的补充

协议组件
22
ina226
TI的高精度电流/电压/功率监测芯片
I2C
外设驱动
23
iotcloud
云平台库,已支持: 腾讯云 阿里云 onenet 华为云 涂鸦云 百度云 Tlink云

协议组件
24
l3g4200d
三轴数字陀螺仪传感器
I2C
外设驱动
25
lbsLoc
基站定位

通信组件
26
lbsLoc2
基站定位

通信组件
27
libfota
远程升级

基础软件
28
libfota2
远程升级

基础软件
29
libnet
在socket库基础上的同步阻塞api,socket库本身是异步非阻塞api

协议组件
30
lis2dh12
三轴传感器
I2C
外设驱动
31
lm75
温度传感器
I2C
外设驱动
32
max31856
热电偶温度检测
SPI
外设驱动
33
mcp2515
CAN协议控制器驱动,SPI转CAN
SPI
外设驱动
34
mlx90614
红外温度
I2C
外设驱动
35
modbus_rtu


协议组件
36
mpu6xxx
六轴/九轴传感器 支持 mpu6500,mpu6050,mpu9250,icm2068g
I2C
外设驱动
37
necir
NEC协议红外接收
SPI
外设驱动
38
netLed
网络状态指示灯

基础软件
39
openai
对接OpenAI兼容的平台,例如deepseek

协议组件
40
pca9685
16路PWM驱动舵机
I2C
外设驱动
41
pcf8563t
时钟模块
I2C
外设驱动
42
pcf8574
IO扩展
I2C
外设驱动
43
qmc5883l
地磁传感器
I2C
外设驱动
44
rc522
非接触式读写卡驱动
SPI
外设驱动
45
rtkv
远程KV数据库

协议组件
46
sc7a20
士兰微三轴加速度传感器
I2C
外设驱动
47
shift595
8位串行转并行移位寄存器,用于LED/数码管/扩展IO
GPIO
外设驱动
48
si24r1
2.4GHz 无线收发器
SPI
外设驱动
49
spl06
气压传感器
I2C
外设驱动
50
tcs3472
颜色传感器
I2C
外设驱动
51
tm1637
数码管
GPIO
外设驱动
52
tm1640
数码管和LED驱动芯片
GPIO
外设驱动
53
tm1650
数码管和按键扫描芯片
GPIO
外设驱动
54
tsl2561
光强传感器
I2C
外设驱动
55
udpsrv
UDP 服务器

协议组件
56
vl6180
ST 的激光测距传感器
I2C
外设驱动
57
xmodem
xmodem驱动
UART
外设驱动
58
ze08g_ch2o
电化学甲醛模组
UART
外设驱动
59
zh07
激光粉尘传感器
UART
外设驱动

十、LuatOS 实际工程代码解读

780EPM 1.3 开发板的出厂固件代码, 是一个实际的 LuatOS 开发的简单案例。

代码的位置在:

780EPM 开发板 V1.3 出厂固件源码

这个固件分为几个部分:

1, 780EPM core 固件: 目前最新的固件是 2005 版本,后续更新的固件版本也可以继续使用;

最新的 780EPM 固件在这里下载:

http://docs.openluat.com/air780epm/luatos/firmware/version/

2,管脚复用描述文件

 pins_Air780EPM.json

3, 资源图片

 实现开机固件所需的图片。

4, 实现脚本。

下面重点讲解一下脚本实现的逻辑。

10.1 编码要求

为了降低用户理解成本,这份开机固件的代码有如下要求:

(1)不允许用云编译扩大脚本区,不允许用云编译扩大文件系统,保持脚本 + 资源总体尺寸不能大于 256K 字节;

(2)main.lua 作为逻辑主线,其他的功能代码封装成子模块,提供成员函数,也可以提供成员变量, 被 main.lua 调用;

(3)不允许使用匿名函数;

10.2 已实现功能

如下代码,已经实现了如下功能:

(1)主界面九宫格的按键切换,

(2)长按进入具体功能界面;再长按回到主界面;

(3)图片显示功能,

(5)摄像头预览,

(6)俄罗斯方块,

(7)天气数据获取,并显示不同的天气图标;

使用的是 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, 看门狗的初始化,wdtInit(),放置系统被某个任务异常占用 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 总结

这份 780PEM 的开发板的出厂固件的代码,展示了一个完整的 LuatOS 工程的基本实现的方法。

脚本文件一共只有 10 个,全部加一起只有 30k 字节,1000 行代码,实现了 9 宫格界面,电量,信号强度,天气的状态栏显示,包括俄罗斯方块在内的多种功能的演示。

这份代码后续还会继续更新,并且都不会采用非常高难度的编码技巧,只需要用最简单的编程逻辑就可以实现相对复杂的业务逻辑。