简介

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

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

开始之前我们先贴一段ADC demo的main.lua里面的代码。main.lua可以看成lua脚本执行的入口函数。
合宙lua库详解-sys_Air720
注意到没,在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模块还需要知道

  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") 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表进行消息的匹配如果是回调就调用回调函数,如果是协程就恢复协程。
注意:
从上面的逻辑可以看出,我们可以发布多个消息,但是订阅消息只能有一个地方,如果有多个地方订阅的话,实际上会覆盖上一个地方的订阅。


定时器的实现

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

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

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

上面的代码主要是:

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

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
  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] = 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,一起沟通交流。