文章目录
简介
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)
end
end
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] then
if param <= TASK_TIMER_ID_MAX then
--[[定时器池中查找taskID(协程ID),这里主要是lua脚本定时器的实现
下面会介绍]]
local taskId = timerPool[param]
--释放
timerPool[param] = nil
if taskTimerPool[taskId] == param then
taskTimerPool[taskId] = nil
--恢复协程
coroutine.resume(taskId)
end
else
local cb = timerPool[param]
--如果不是循环定时器,从定时器id表中删除此定时器
if not loop[param] then timerPool[param] = nil end
if para[param] ~= nil then
cb(unpack(para[param]))
if not loop[param] then para[param] = nil end
else
cb()
end
--如果是循环定时器,继续启动此定时器
if loop[param] then rtos.timer_start(param, loop[param]) end
end
--其他消息(音频消息、充电管理消息、按键消息等)
elseif type(msg) == "number" then
handlers[msg](param)
else
handlers[msg.id](msg)
end
end
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") then
log.warn("warning: sys.subscribe invalid parameter", id, callback)
return
end
if not subscribers[id] then subscribers[id] = {} end
subscribers[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 do
if #messageQueue == 0 then
break
end
--移出表中第一个消息
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" then
callback(unpack(message, 2, #message))
elseif type(callback) == "thread" then
coroutine.resume(callback, unpack(message))
end
end
end
--没明白这段,感觉多余了
if subscribers[message[1]] then
for callback, flag in pairs(subscribers[message[1]]) do
if not flag then
subscribers[message[1]][callback] = nil
end
end
end
end
end
end
dispatch
的作用主要就是移出publish表messageQueue
中的消息,再遍历subscribers
表进行消息的匹配如果是回调就调用回调函数,如果是协程就恢复协程。
注意:
从上面的逻辑可以看出,我们可以发布多个消息,但是订阅消息只能有一个地方,如果有多个地方订阅的话,实际上会覆盖上一个地方的订阅。
定时器的实现
定时器的介绍主要涉及到下面几个函数:
- timerStart
- wait
- stop
- 主要处理部分
timerStart
function timerStart(fnc, ms, ...)
--回调函数和时长检测
local arg={ ... }
local argcnt=0
for i, v in pairs(arg) do
argcnt = argcnt+1
end
--上面这部分主要是分解参数,统计参数有多少个
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 then
timerStop(fnc)
else
timerStop(fnc, ...)
end
-- 为定时器申请ID,ID值 1-0X1FFFFFFF 留给任务,0X1FFFFFFF-0x7FFFFFFF留给消息专用定时器
-- 这里我们注意了,我们申请了一个定时器ID > msgId ,这个值被当做timerPool的索引
-- 后面又被当做参数传入底层的rtos.timer_start,这个我猜测,是在定时器回调里面作为参数
-- 抛给lua的。
while true do
if msgId >= MSG_TIMER_ID_MAX then msgId = TASK_TIMER_ID_MAX end
msgId = msgId + 1
if timerPool[msgId] == nil then
timerPool[msgId] = fnc
break
end
end
--调用底层接口启动定时器
if rtos.timer_start(msgId, ms) ~= 1 then log.debug("rtos.timer_start error") return end
--如果存在可变参数,在定时器参数表中保存参数
if argcnt ~= 0 then
para[msgId] = arg
end
--返回定时器id
return 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 end
taskTimerId = taskTimerId + 1
local timerid = taskTimerId
taskTimerPool[coroutine.running()] = timerid
timerPool[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 then
rtos.timer_stop(timerid)
taskTimerPool[coroutine.running()] = nil
timerPool[timerid] = nil
return 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 为定时器ID
local arg={ ... }
if type(val) == 'number' then
timerPool[val], para[val], loop[val] = nil
rtos.timer_stop(val)
else
for k, v in pairs(timerPool) do
-- 回调函数相同
if type(v) == 'table' and v.cb == val or v == val then
-- 可变参数相同
if cmpTable(arg, para[k]) then
rtos.timer_stop(k)
timerPool[k], para[k], loop[val] = nil
break
end
end
end
end
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] = nil
if taskTimerPool[taskId] == param then
taskTimerPool[taskId] = nil
coroutine.resume(taskId)
end
else
local cb = timerPool[param]
--如果不是循环定时器,从定时器id表中删除此定时器
if not loop[param] then timerPool[param] = nil end
--调用回调,并将para储存的参数值传给回调
if para[param] ~= nil then
cb(unpack(para[param]))
if not loop[param] then para[param] = nil end
else
cb()
end
--如果是循环定时器,继续启动此定时器
if loop[param] then rtos.timer_start(param, loop[param]) end
end
--其他消息(音频消息、充电管理消息、按键消息等)
这里逻辑已经说通,但是想到了一个问题,就是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" then
handlers[msg](param)
else
handlers[msg.id](msg)
end
至此,sys模块已经分析结束,感兴趣的可以加群912452346
,一起沟通交流。