文章目录

  • 简介
  • QUESTION
  • START
    • sys.init(0,0)
    • sys.run
    • lua内部消息机制
      • publish
      • subscribe
      • waitUntil
      • dispatch
    • 定时器的实现
      • **timerStart**
      • wait
      • timerStop
      • 定时器的处理
    • 注册底层消息

简介

LuatOS现阶段变得越来越热门,主要由上海合宙通信科技有限公司推出的嵌入式脚本系统。该系统具有短小精悍的特点。对于LuatOS开发(下面简称Lua开发)的人都知道,开发合宙的产品需要具有下面几个部分:

  1. 底层lua固件
  2. 上层应用脚本
  3. 脚本库
    底层lua固件,这点我们大概不需要怎么关心,主要是在原有的平台上合成了lua虚拟机,并添加了许多底层接口。
    上层应用脚本,也就是客户的脚本,主要根据脚本库提供的接口,以及官方提供的许多demo,进行自己的应用开发。
    脚本库,为了提供上网即一些方便快捷的开发,官方使用lua封装了许多lua的库供脚本层调用。我们平时开发过程中,一般只需要调用其接口就可以了。但是我觉得想先用什么,必须先弄懂该实现的原理。弄懂了再写不更爽嘛。

QUESTION

开始之前我们先贴一段ADC demo的main.lua里面的代码。main.lua可以看成lua脚本执行的入口函数。

注意到没,在sys.init(0,0)之前,有加载许多的模块。那就有点疑问了,为什么这些模块的加载需要放到sys.init之前尼,不应该是sys.init之后再执行应用程序吗?
带着这个问题,我们来研究一下,lua开发重中之重的模块sys模块吧。

START

sys.init(0,0)

--- Luat平台初始化
-- @param mode 充电开机是否启动GSM协议栈,1不启动,否则启动
-- @param lprfnc 用户应用脚本中定义的“低电关机处理函数”,如果有函数名,则低电时,本文件中的run接口不会执行任何动作,否则,会延时1分钟自动关机
-- @return 无
-- @usage sys.init(1,0)
function init(mode, lprfnc)--[[ 用户应用脚本中必须定义PROJECT和VERSION两个全局变量,否则会死机重启,如何定义请参考各个demo中的main.lua ]]assert(PROJECT and PROJECT ~= "" and VERSION and VERSION ~= "", "Undefine PROJECT or VERSION")--[[lua的垃圾回收机制:collectgarbage("setpause", 80),setpause第二个参数80代表在开始一个新的收集周期之前要等待多久。当这个值小于等于100的时候,就代表执行完一个周期之后不会等待,直接进入下一个周期。当这个值为200的时候,就代表当内存达到上一个周期结束时的两倍的时候,再进入下一个周期--]]collectgarbage("setpause", 80)--[[ 设置AT命令的虚拟串口,这个作用就大了,因为有些功能不易开放接口也是为了兼容之前的平台,所以lua脚本会与底层之间进行AT交互,AT交互又不能占用实际的串口,所以只能用虚拟串口(实际上可以理解为底层提供了一个pipe,供上层往里面塞数据,pipe有数据就会进入AT引擎处理)]]uart.setup(uart.ATC, 0, 0, uart.PAR_NONE, uart.STOP_1)log.info("poweron reason:", rtos.poweron_reason(), PROJECT, VERSION, SCRIPT_LIB_VER, rtos.get_version())pcall(rtos.set_lua_info,"\r\n"..rtos.get_version().."\r\n"..(_G.PROJECT or "NO PROJECT").."\r\n"..(_G.VERSION or "NO VERSION"))--获取编译时间,获取之前还判断下是不是funciton的类型,主要为了防止底层没有开放这个接口吧if type(rtos.get_build_time)=="function" then log.info("core build time", rtos.get_build_time()) end-- 第一个参数mode,如果为1,当开机方式为充电开机的时候就关闭GSM协议栈if mode == 1 then-- 充电开机if rtos.poweron_reason() == rtos.POWERON_CHARGER then-- 关闭GSM协议栈rtos.poweron(0)endend
end

从上面的注释可以看到,实际上sys.init(0,0)主要就是初始化了一个虚拟通道,这个后面介绍上网和收发短信,打电话会用到。还有就是垃圾回收的设置。

sys.run

------------------------------------------ Luat 主调度框架  ------------------------------------------
--- run()从底层获取core消息并及时处理相关消息,查询定时器并调度各注册成功的任务线程运行和挂起
-- @return 无
-- @usage sys.run()
function run()while true do-- 分发内部消息,lua脚本自己维护的消息机制,下面会介绍dispatch()--[[ 阻塞读取外部消息,理解为线程的wait_message,这里其实就回答了上面我们的问题(为什么sys.run放在应用模块的调用之后),因为lua脚本是逐行执行的,如果把这个接口放在前面那么就会一直等待底层的消息,所有应用永远也不可能执行,就好似于,现在许多的python框架后面都有个loop_forver一样。]]local msg, param = rtos.receive(rtos.INF_TIMEOUT)-- 判断是否为定时器消息,并且消息是否注册if msg == rtos.MSG_TIMER and timerPool[param] thenif param <= TASK_TIMER_ID_MAX then--[[定时器池中查找taskID(协程ID),这里主要是lua脚本定时器的实现下面会介绍]]local taskId = timerPool[param]--释放timerPool[param] = nilif taskTimerPool[taskId] == param thentaskTimerPool[taskId] = nil--恢复协程 coroutine.resume(taskId)endelselocal cb = timerPool[param]--如果不是循环定时器,从定时器id表中删除此定时器if not loop[param] then timerPool[param] = nil endif para[param] ~= nil thencb(unpack(para[param]))if not loop[param] then para[param] = nil endelsecb()end--如果是循环定时器,继续启动此定时器if loop[param] then rtos.timer_start(param, loop[param]) endend--其他消息(音频消息、充电管理消息、按键消息等)elseif type(msg) == "number" thenhandlers[msg](param)elsehandlers[msg.id](msg)endend
end

这里其实可以看成lua与底层的媒介。底层与lua之间通过消息进行通信。
lua实际上就是就是单线程,不过它拥有高效的协程,所以看起来也有线程的效果。定时器的操作就是协程的挂起和恢复。
还有其它底层消息的回调处理。
弄懂sys模块还需要知道

  1. lua内部消息是如何实现的
  2. lua的定时器是怎么实现的

lua内部消息机制

lua内部消息机制主要有三个接口一个引擎

  1. publish(发布消息)
  2. subscribe(订阅消息)
  3. waitUntil(线程等待消息)
  4. dispatch(消息分发系统)

publish

--- 发布内部消息,存储在内部消息队列中
-- @param ... 可变参数,用户自定义
-- @return 无
-- @usage publish("NET_STATUS_IND")
function publish(...)local arg = { ... }table.insert(messageQueue, arg)
end

publish函数主要就是发布一个消息,将消息所有参数都赋值给一个表并插入messageQueue这个表中。messageQueue这个表可以看成消息队列。
有意思的是,可变参数直接还可以这样赋值(学到了)

subscribe

--- 订阅消息
-- @param id 消息id
-- @param callback 消息回调处理
-- @usage subscribe("NET_STATUS_IND", callback)
function subscribe(id, callback)if type(id) ~= "string" or (type(callback) ~= "function" and type(callback) ~= "thread") thenlog.warn("warning: sys.subscribe invalid parameter", id, callback)returnendif not subscribers[id] then subscribers[id] = {} endsubscribers[id][callback] = true
end

订阅消息有两个参数一个是ID,一个是回调函数。主要实现就是下面的表示。

subscribers = {id = {.callback = true }
}

waitUntil

function waitUntil(id, ms)subscribe(id, coroutine.running())local message = ms and {wait(ms)} or {coroutine.yield()}unsubscribe(id, coroutine.running())return message[1] ~= nil, unpack(message, 2, #message)
end

waitUntil就涉及到了协程, coroutine.running()返回当前运行的协程号。subscribe来注册的时候传入的第二个参数是协程号。然后开始挂起当前线程,这里有个wait(ms)涉及到定时器,后面再说。如果超时或者被恢复,就解注册消息。

dispatch

-- 分发消息
local function dispatch()while true doif #messageQueue == 0 thenbreakend--移出表中第一个消息local message = table.remove(messageQueue, 1)--如果订阅表中存在发布的消息if subscribers[message[1]] then--[[遍历发送消息的参数表,前面已经介绍,subscribers实际上是双层表,外层是消息id,里面是消息id对应得参数表 ]]for callback, flag in pairs(subscribers[message[1]]) do--消息已经打开if flag then--判断callback的类型if type(callback) == "function" thencallback(unpack(message, 2, #message))elseif type(callback) == "thread" thencoroutine.resume(callback, unpack(message))endendend--没明白这段,感觉多余了if subscribers[message[1]] thenfor callback, flag in pairs(subscribers[message[1]]) doif not flag thensubscribers[message[1]][callback] = nilendendendendend
end

dispatch的作用主要就是移出publish表messageQueue中的消息,再遍历subscribers表进行消息的匹配如果是回调就调用回调函数,如果是协程就恢复协程。
注意:
从上面的逻辑可以看出,我们可以发布多个消息,但是订阅消息只能有一个地方,如果有多个地方订阅的话,实际上会覆盖上一个地方的订阅。


定时器的实现

定时器的介绍主要涉及到下面几个函数:

  1. timerStart
  2. wait
  3. stop
  4. 主要处理部分

timerStart

function timerStart(fnc, ms, ...)--回调函数和时长检测local arg={ ... }local argcnt=0for i, v in pairs(arg) doargcnt = argcnt+1end--上面这部分主要是分解参数,统计参数有多少个assert(fnc ~= nil, "sys.timerStart(first param) is nil !")assert(ms > 0, "sys.timerStart(Second parameter) is <= zero !")--4G底层不支持小于5ms的定时器if ms < 5 then ms = 5 end-- 关闭完全相同的定时器--这个地方就有意思了,相当于你如果起了多次定时器,时间会被覆盖,也就是定时器被关闭重新打开if argcnt == 0 thentimerStop(fnc)elsetimerStop(fnc, ...)end-- 为定时器申请ID,ID值 1-0X1FFFFFFF 留给任务,0X1FFFFFFF-0x7FFFFFFF留给消息专用定时器-- 这里我们注意了,我们申请了一个定时器ID > msgId ,这个值被当做timerPool的索引-- 后面又被当做参数传入底层的rtos.timer_start,这个我猜测,是在定时器回调里面作为参数-- 抛给lua的。while true doif msgId >= MSG_TIMER_ID_MAX then msgId = TASK_TIMER_ID_MAX endmsgId = msgId + 1if timerPool[msgId] == nil thentimerPool[msgId] = fncbreakendend--调用底层接口启动定时器if rtos.timer_start(msgId, ms) ~= 1 then log.debug("rtos.timer_start error") return end--如果存在可变参数,在定时器参数表中保存参数if argcnt ~= 0 thenpara[msgId] = argend--返回定时器idreturn msgId
end

从上面的代码可以看出,timerStart主要是timerPool中保存timer信息,以及在para中保存了回调参数信息。两者的索引都是累加的msgId,并且这个msgId作为参数传给了底层定时器。

wait

该函数主要是在协程中被调用,调用的时候可以挂起相应时间的协程,到时间后自动恢复协程。

--- task任务延时函数
-- 只能直接或者间接的被task任务主函数调用,如果定时器创建成功,则本task会挂起
function wait(ms)-- 参数检测,参数不能为负值assert(ms > 0, "The wait time cannot be negative!")--4G底层不支持小于5ms的定时器if ms < 5 then ms = 5 end-- 选一个未使用的定时器ID给该任务线程if taskTimerId >= TASK_TIMER_ID_MAX then taskTimerId = 0 endtaskTimerId = taskTimerId + 1local timerid = taskTimerIdtaskTimerPool[coroutine.running()] = timeridtimerPool[timerid] = coroutine.running()-- 调用core的rtos定时器if 1 ~= rtos.timer_start(timerid, ms) then log.debug("rtos.timer_start error") return end-- 挂起调用的任务线程local message = {coroutine.yield()}if #message ~= 0 thenrtos.timer_stop(timerid)taskTimerPool[coroutine.running()] = niltimerPool[timerid] = nilreturn unpack(message)end
end

上面的代码主要是:

  1. 累加taskTimerId,并获取当前协程号作为taskTimerPool的KEY,将taskTimerId作为值。
  2. timerPooltaskTimerId作为key,协程号作为value。
    这里就奇怪了,上面的timerStart用了msgId作为KEY,这里又用taskTimerId作为KEY,这样两个相当的时候value不就被覆盖了吗?请看下面的代码
  3. 挂起线程,并将resume的时候传入的参数全部作为表的形式传给message
  4. 如果表中参数不为,就关闭定时器,并释放定时器表中资源。

timerStop

该函数主要是停止定时器,所停止的定时依据传入的参数。一般是回调函数。

function timerStop(val, ...)-- val 为定时器IDlocal arg={ ... }if type(val) == 'number' thentimerPool[val], para[val], loop[val] = nilrtos.timer_stop(val)elsefor k, v in pairs(timerPool) do-- 回调函数相同if type(v) == 'table' and v.cb == val or v == val then-- 可变参数相同if cmpTable(arg, para[k]) thenrtos.timer_stop(k)timerPool[k], para[k], loop[val] = nilbreakendendendend
end
  1. 如果传入的参数是数字就认为是timerID,直接停止定时器
  2. 如果是其它,遍历timerPool,取了v值。
  3. 进行回调函数的匹配,如果是多参数的,还进行参数的匹配,这里我感觉v不是设置进去的func吗,为什么这里的判断是table尼?奇怪!!!

定时器的处理

在说sys.run的时候我们就说过,定时器消息,这里再贴下

--[[ 还记得我们开启定时器和`wait`的时候都调用 `rtos.timer_start`的时候都传入了 `timerid `,
这个`timerid`作为`timerPool`的`key`。所以底层定时器回调会将这个`key`带给我们。]]
if msg == rtos.MSG_TIMER and timerPool[param] then--[[
`wait`和t`imerStart`我们说过`timerID`可能有覆盖的风险,所以`taskTimerId`和`msgId`的初始化值
不一样。相当于各有一片数据区间,这里param <= TASK_TIMER_ID_MAX 就是这个区间的边界判断。]]if param <= TASK_TIMER_ID_MAX then--[[如果是线程就恢复线程,并将taskId作为参数给挂起端 ]]local taskId = timerPool[param]timerPool[param] = nilif taskTimerPool[taskId] == param thentaskTimerPool[taskId] = nilcoroutine.resume(taskId)endelselocal cb = timerPool[param]--如果不是循环定时器,从定时器id表中删除此定时器if not loop[param] then timerPool[param] = nil end--调用回调,并将para储存的参数值传给回调if para[param] ~= nil thencb(unpack(para[param]))if not loop[param] then para[param] = nil endelsecb()end--如果是循环定时器,继续启动此定时器if loop[param] then rtos.timer_start(param, loop[param]) endend--其他消息(音频消息、充电管理消息、按键消息等)

这里逻辑已经说通,但是想到了一个问题,就是para中存储的是局部变量,栈结束就会被释放,这个参数还生效吗?官方这样写肯定生效,但是机制还没有弄清楚,这里埋个坑,后面分析lua虚拟机源码。
是不是被引用就不会被释放。

注册底层消息

lua如何注册底层消息尼?也就是底层消息来了,我该用什么函数去处理尼?
如下所示:

-- rtos消息回调
local handlers = {}
setmetatable(handlers, {__index = function() return function() end end, })--- 注册rtos消息回调处理函数
-- @number id 消息类型id
-- @param handler 消息处理函数
-- @return 无
-- @usage rtos.on(rtos.MSG_KEYPAD, function(param) handle keypad message end)
rtos.on = function(id, handler)handlers[id] = handler
end

创建了一个rtos.on函数,这个函数等价于function(id, handler),主要将消息ID和handler的对应关系存储在
handlers元表中。
sys.run中,底层来消息了,就会在handlers中找到相应的处理函数并处理。

--其他消息(音频消息、充电管理消息、按键消息等)elseif type(msg) == "number" thenhandlers[msg](param)elsehandlers[msg.id](msg)end

至此,sys模块已经分析结束,感兴趣的可以加群912452346,一起沟通交流。

合宙lua库详解-sys相关推荐

  1. 合宙lua库详解-socket

    文章目录 简介 START 建立连接 发送数据 接收数据 关闭连接 主动关闭 被动关闭 简介 作为通讯的基础,只要你上网就需要用到lua的socket模块,即使使用mqtt,http等其它模块间接也调 ...

  2. 合宙lua库详解-ril

    简介 肝了这么久的讲解,一个评论和点赞都没有,没有一丝丝反馈,没有一丝丝赞扬.难受哦!!! 但是我还得肝,必须肝出一个赞. 如果大家能给点反馈,给点一键三连,我肝的更起劲.大家的评价是我前进最大的鼓舞 ...

  3. python re库 详解(正则表达式)

    python re库 详解(正则表达式) 说明 则表达式(英文名称:regular expression,regex,RE)是用来简洁表达一组字符串特征的表达式.最主要应用在字符串匹配中. 1).re ...

  4. (转)树莓派wiringPi库详解

    https://www.cnblogs.com/lulipro/p/5992172.html <div id="post_detail"> 树莓派wiringPi库详解 ...

  5. matlab中sinks,MATLAB Simulink模块库详解(二)Sinks篇

    MATLAB Simulink模块库详解(二)Sinks篇 Simulink模块库概述 1.Sources模块库,为仿真提供各种信号源 2.Sinks模块库,为仿真提供输出设备元件 3.Continu ...

  6. STM32 HAL库详解 及 手动移植

    源: STM32 HAL库详解 及 手动移植

  7. stm32 IOT_基于STM32平台的cubeMX和HAL库详解

    课程简介: <朱有鹏老师单片机完全学习系列课程>总共5季,其中第1.2季是51单片机学习,第3.4.5季是STM32单片机与RTOS学习.整个课程时长约250小时,是一套零基础.全面系统. ...

  8. 爬虫笔记:Requests库详解

    什么是Requests 之前讲解了爬虫笔记:Urllib库详解发现确实有不方便的地方,比如加一个代理,cookie,发送post请求比较繁琐. Request库能用几句话实现这些. Requests ...

  9. python爬虫之urllib库详解

    python爬虫之urllib库详解 前言 一.urllib库是什么? 二.urllib库的使用 urllib.request模块 urllib.parse模块 利用try-except,进行超时处理 ...

最新文章

  1. mysql平台workb_MySQL 总结
  2. html select选择事件_用 Java 拿下 HTML,分分钟写个小爬虫
  3. Microsoft Edge 浏览器开始支持webkit私有样式
  4. ios公司开发者账号申请分享攻略
  5. 【AI视野·今日CV 计算机视觉论文速览 第183期】28 Apr 2020
  6. Form表单的五个属性
  7. NumPy库—random模块
  8. 一文了解什么是DoIP协议(超详细)
  9. 非合作关系设定下的多智能体强化学习
  10. 2011最新笔记本、一体机显卡性能排行
  11. Vue Elements 可用的省市县数据
  12. vscode调试用的launch.json
  13. 如何合并多个PDF文件并自动生成目录
  14. 网络安全——sql注入漏洞拓扑图
  15. KANBAN专题一:KANBAN管理的基本系统操作
  16. 弱网优化,GCC 动态带宽评估算法(内附详细公式)
  17. 如何能做好软件项目(迭代开发)
  18. 宫廷计获取服务器配置信息出错,宫廷计手游宫廷会试全题目答案汇总 宫廷会试题及答案...
  19. 教程:用强化学习玩转恐龙跳跳
  20. 《一只狗的使命2》影评

热门文章

  1. python educoder 第3关:列表基本操作
  2. juc笔记之callable详解
  3. 阿里云个人账号实名认证和企业账号实名认证的区别在哪里
  4. 23级应届硕士招聘-给北京户口
  5. 开学季 | 用十本书打破固有思维,“文理兼修”
  6. 中图法检索计算机科学方面,千兆位以太网中可以使用超5类UTP双绞线。【
  7. SAP ABAP 四舍五入函数
  8. python requests模拟登录淘宝购物车下单_Python使用requests库模拟登录淘宝账号(下)...
  9. workflow是什么?
  10. Hyperledger Fabric/Fabric-samples 安装及使用(Mac)