合宙lua库详解-sys
文章目录
- 简介
- QUESTION
- START
- sys.init(0,0)
- sys.run
- lua内部消息机制
- publish
- subscribe
- waitUntil
- dispatch
- 定时器的实现
- **timerStart**
- wait
- timerStop
- 定时器的处理
- 注册底层消息
简介
LuatOS现阶段变得越来越热门,主要由上海合宙通信科技有限公司推出的嵌入式脚本系统。该系统具有短小精悍的特点。对于LuatOS开发(下面简称Lua开发)的人都知道,开发合宙的产品需要具有下面几个部分:
- 底层lua固件
- 上层应用脚本
- 脚本库
底层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模块还需要知道
- lua内部消息是如何实现的
- lua的定时器是怎么实现的
lua内部消息机制
lua内部消息机制主要有三个接口一个引擎
- publish(发布消息)
- subscribe(订阅消息)
- waitUntil(线程等待消息)
- 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
表进行消息的匹配如果是回调就调用回调函数,如果是协程就恢复协程。
注意:
从上面的逻辑可以看出,我们可以发布多个消息,但是订阅消息只能有一个地方,如果有多个地方订阅的话,实际上会覆盖上一个地方的订阅。
定时器的实现
定时器的介绍主要涉及到下面几个函数:
- timerStart
- wait
- stop
- 主要处理部分
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
上面的代码主要是:
- 累加
taskTimerId
,并获取当前协程号作为taskTimerPool
的KEY,将taskTimerId
作为值。 timerPool
中taskTimerId
作为key,协程号作为value。
这里就奇怪了,上面的timerStart
用了msgId
作为KEY,这里又用taskTimerId
作为KEY,这样两个相当的时候value不就被覆盖了吗?请看下面的代码
- 挂起线程,并将resume的时候传入的参数全部作为表的形式传给
message
- 如果表中参数不为,就关闭定时器,并释放定时器表中资源。
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
- 如果传入的参数是数字就认为是
timerID
,直接停止定时器 - 如果是其它,遍历
timerPool
,取了v
值。 - 进行回调函数的匹配,如果是多参数的,还进行参数的匹配,这里我感觉
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相关推荐
- 合宙lua库详解-socket
文章目录 简介 START 建立连接 发送数据 接收数据 关闭连接 主动关闭 被动关闭 简介 作为通讯的基础,只要你上网就需要用到lua的socket模块,即使使用mqtt,http等其它模块间接也调 ...
- 合宙lua库详解-ril
简介 肝了这么久的讲解,一个评论和点赞都没有,没有一丝丝反馈,没有一丝丝赞扬.难受哦!!! 但是我还得肝,必须肝出一个赞. 如果大家能给点反馈,给点一键三连,我肝的更起劲.大家的评价是我前进最大的鼓舞 ...
- python re库 详解(正则表达式)
python re库 详解(正则表达式) 说明 则表达式(英文名称:regular expression,regex,RE)是用来简洁表达一组字符串特征的表达式.最主要应用在字符串匹配中. 1).re ...
- (转)树莓派wiringPi库详解
https://www.cnblogs.com/lulipro/p/5992172.html <div id="post_detail"> 树莓派wiringPi库详解 ...
- matlab中sinks,MATLAB Simulink模块库详解(二)Sinks篇
MATLAB Simulink模块库详解(二)Sinks篇 Simulink模块库概述 1.Sources模块库,为仿真提供各种信号源 2.Sinks模块库,为仿真提供输出设备元件 3.Continu ...
- STM32 HAL库详解 及 手动移植
源: STM32 HAL库详解 及 手动移植
- stm32 IOT_基于STM32平台的cubeMX和HAL库详解
课程简介: <朱有鹏老师单片机完全学习系列课程>总共5季,其中第1.2季是51单片机学习,第3.4.5季是STM32单片机与RTOS学习.整个课程时长约250小时,是一套零基础.全面系统. ...
- 爬虫笔记:Requests库详解
什么是Requests 之前讲解了爬虫笔记:Urllib库详解发现确实有不方便的地方,比如加一个代理,cookie,发送post请求比较繁琐. Request库能用几句话实现这些. Requests ...
- python爬虫之urllib库详解
python爬虫之urllib库详解 前言 一.urllib库是什么? 二.urllib库的使用 urllib.request模块 urllib.parse模块 利用try-except,进行超时处理 ...
最新文章
- mysql平台workb_MySQL 总结
- html select选择事件_用 Java 拿下 HTML,分分钟写个小爬虫
- Microsoft Edge 浏览器开始支持webkit私有样式
- ios公司开发者账号申请分享攻略
- 【AI视野·今日CV 计算机视觉论文速览 第183期】28 Apr 2020
- Form表单的五个属性
- NumPy库—random模块
- 一文了解什么是DoIP协议(超详细)
- 非合作关系设定下的多智能体强化学习
- 2011最新笔记本、一体机显卡性能排行
- Vue Elements 可用的省市县数据
- vscode调试用的launch.json
- 如何合并多个PDF文件并自动生成目录
- 网络安全——sql注入漏洞拓扑图
- KANBAN专题一:KANBAN管理的基本系统操作
- 弱网优化,GCC 动态带宽评估算法(内附详细公式)
- 如何能做好软件项目(迭代开发)
- 宫廷计获取服务器配置信息出错,宫廷计手游宫廷会试全题目答案汇总 宫廷会试题及答案...
- 教程:用强化学习玩转恐龙跳跳
- 《一只狗的使命2》影评
热门文章
- python educoder 第3关:列表基本操作
- juc笔记之callable详解
- 阿里云个人账号实名认证和企业账号实名认证的区别在哪里
- 23级应届硕士招聘-给北京户口
- 开学季 | 用十本书打破固有思维,“文理兼修”
- 中图法检索计算机科学方面,千兆位以太网中可以使用超5类UTP双绞线。【
- SAP ABAP 四舍五入函数
- python requests模拟登录淘宝购物车下单_Python使用requests库模拟登录淘宝账号(下)...
- workflow是什么?
- Hyperledger Fabric/Fabric-samples 安装及使用(Mac)