想搞懂LuatOS如何運行Lua腳本?本文深入剖析其嵌入式運行框架,涵蓋虛擬機加載、任務(wù)協(xié)程、系統(tǒng)初始化等關(guān)鍵環(huán)節(jié),適合初學者。
一、LuatOS 編程起步
1.1 底層固件怎么啟動 LuatOS 腳本
1.1.1 腳本入口執(zhí)行文件
簡單來說,底層固件首先就是要找到 main.lua 這個文件,然后啟動它。
所有的其他功能,都需要在 main.lua 發(fā)起。
1.1.2 LuatOS 啟動腳本的詳細流程
進一步詳細的說,LuatOS 的底層固件啟動腳本的流程如下:
1,系統(tǒng)上電或者復位后,底層固件(core)首先啟動,進行硬件初始化、內(nèi)存分配、文件系統(tǒng)掛載等系統(tǒng)底層的基礎(chǔ)操作。
2,加載 Lua 虛擬機:底層固件加載 Lua 虛擬機,為執(zhí)行 Lua 腳本提供運行環(huán)境;
3,自動查找并加載存儲在設(shè)備上的主腳本 main.lua;
4,按順序執(zhí)行 main.lua 腳本中的代碼,通常包括任務(wù)創(chuàng)建(如sys.taskInit)、功能初始化等,從這一步,已經(jīng)正式開始運行用戶邏輯。
5,進入任務(wù)調(diào)度:腳本最后通常調(diào)用sys.run(),進入事件循環(huán)和多任務(wù)調(diào)度。
1.1.3 怎么把固件和腳本燒錄到硬件:
1,使用LuatTools ,將底層固件和用戶 Lua 腳本燒錄到模組或者引擎硬件;
2,上電后,底層固件自動完成上述啟動和腳本加載流程,無需手動干預。
1.2 main.lua 需要包含哪些部分?
1.2.1 項目信息聲明
在 main.lua 的文件開頭,需要聲明項目名和版本號,便于管理和調(diào)試。后續(xù)的遠程升級,也需要用到項目名和版本號。
例如:

1.2.2 核心庫,擴展庫以及如何加載
在 main.lua 需要加載 LuatOS 的基礎(chǔ)庫和擴展庫(如 zbuff,onewire,gnss 等)用來實現(xiàn)具體的業(yè)務(wù)邏輯。
核心庫和擴展庫的內(nèi)容,在后續(xù)的章節(jié)里面介紹。
核心庫在底層固件加載Lua虛擬機的時候,在底層固件已經(jīng)自動加載,不需要在用戶腳本中再去加載,例如sys,rtos等;
擴展庫是Lua腳本文件寫的庫,需要在用戶腳本中,使用require語句加載,例如libnet,httpplus等,加載方式如下:

1.2.3 至少啟動一個任務(wù)
在 main.lua 里面,至少需要啟動一個任務(wù),否則這個 main 就無所事事,是一個沒什么實際用處的主腳本了。
啟動一個任務(wù)的方法,分為 2 個步驟:
1,創(chuàng)建一個函數(shù),把要做的事情,放在這個函數(shù)里面使用。這個函數(shù)必須是無限循環(huán)的,防止很快結(jié)束生命,不妨把這個函數(shù)命名為 task1(),
2,調(diào)用 sys.taskInit(task1),啟動這個函數(shù),于是這個任務(wù),就放在待運行的任務(wù)列表里面了。
1.2.4 初步理解 sys.run()
sys.run() 是一個無限循環(huán)的函數(shù)。
main.lua 的最后一行, 只能是 sys.run(),代表 sys.run() 接管了 LuatOS 的所有的執(zhí)行調(diào)度工作。
sys.run() 是 LuatOS 的運行中樞。
在本文的 3.3 節(jié)和 7.3 節(jié),還會繼續(xù)介紹 sys.run()這個函數(shù)。
1.3 LuatOS 腳本編程的核心要點
1.3.1 LuatOS 實現(xiàn)的典型功能
LuatOS 腳本是利用了 Lua 的語法,以及基于 LuatOS 的核心庫和擴展庫提供的 API,進行簡便的編程,實現(xiàn)如下功能:
1,實現(xiàn)和云端服務(wù)器通信;
2,采集外設(shè)的數(shù)據(jù),控制外設(shè)設(shè)備;
3,實現(xiàn)人機交互,包括圖形交互和語音交互;
1.3.2 LuatOS 的學習要點
要想寫好 LuatOS 的軟件,實現(xiàn)上述三個功能,除了逐漸掌握 Lua 的基本語法之外,還需要熟悉 LuatOS 的核心庫和擴展庫,這樣才能開發(fā)出優(yōu)質(zhì)的基于 LuatOS 的物聯(lián)網(wǎng)設(shè)備軟件。
學習的方法有如下幾個:
1, 運行各個功能模塊的 demo 代碼;
2, 閱讀 docs.openluat.com 的教程文檔;
3, 遇到不懂問 AI;
1.3.3 一個典型的 LuatOS 實現(xiàn)
一個典型的 LuatOS 實現(xiàn),包含 main.lua 入口文件和若干個功能模塊文件。
這里用 Air780EPM 模組的蜂鳴器的代碼為例, 有兩個腳本文件以及一個管腳描述 json 文件:
1, main.lua 文件, 作用是啟動一個任務(wù),讓蜂鳴器響一秒鐘,再停頓一秒鐘,如此往復;
2, airbuzzer.lua 封裝了驅(qū)動蜂鳴器的功能實現(xiàn);
3, pins_Air780EPM.json 描述了本例使用到的管腳的功能,780EPM 的 26 管腳,用作 PWM4。
main.lua 內(nèi)容如下:

airbuzzer.lua 內(nèi)容如下:

pins_air780EPM.json 內(nèi)容如下:

把上述幾個文件,連同 airr780EPM 最新的固件版本,用 Luatools 建立一個工程,燒錄到 780EPM 開發(fā)板,就可以聽到蜂鳴器的播放聲音了。
二、幾個要熟悉的常識
2.1 匿名函數(shù)
在 Lua 代碼里面,經(jīng)??吹?jīng)]有名字的函數(shù)。
這種函數(shù)定義之后, 要么馬上運行,要么作為另一個函數(shù)的返回值賦給其他變量,所以并不需要一個函數(shù)名字。
這種函數(shù),稱為匿名函數(shù)。
匿名函數(shù)可以某些時候簡化代碼,初學者寫代碼可以先不考慮匿名函數(shù)。
但是由于匿名函數(shù)在你能閱讀到的 Lua 代碼里面出現(xiàn)的頻次實在是太高了,所以你也不得不重視和習慣匿名函數(shù)。
2.2 閉包
閉包的實現(xiàn)通常是通過在外部函數(shù)內(nèi)部定義一個函數(shù),并將這個內(nèi)部函數(shù)作為外部函數(shù)的返回值。
這樣一來,內(nèi)部函數(shù)就可以訪問外部函數(shù)作用域中的變量,即使外部函數(shù)已經(jīng)執(zhí)行完畢,這些變量依然可以被內(nèi)部函數(shù)訪問,從而形成閉包。
常見的閉包實現(xiàn)模式如下:

這樣的好處是,可以定義一個函數(shù),能夠在一定范圍內(nèi),訪問外部的變量,實現(xiàn)可控的持續(xù)行為。
很多初學者會被這段代碼迷惑,會被繞暈。
這里做一下解釋:
(1)z 不是函數(shù)里面聲明的變量,z 是函數(shù)的參數(shù);
所以 在代碼里面, 因為 f=outer(10), 所以, f(5)就意味著是調(diào)用了 兩次函數(shù),傳入了兩個函數(shù)的參數(shù): outer(10)(5)。
第一次調(diào)用,out(10) ,意味著 在 outer 函數(shù)里面, y = x 這句, x 換成 10, 就是 y = 10;
outer(10)(5)意味著 5 是內(nèi)部匿名函數(shù)的參數(shù),就是替代 z 的;
匿名函數(shù)返回 y+z, 這里 y 是 10,z 是 5, 返回的就是 10+5=15.
這里比較繞的,就是給了兩次參數(shù),一個是 10 對應(yīng) x, 一個是 5 對應(yīng) z。
匿名參數(shù)和閉包,對初學者有點繞,很多讀者不明白為什么 z 為什么是 outer 的第二個參數(shù),
這里需要特別搞清楚的是, outer 這個函數(shù)的返回值是個函數(shù), 而且這個函數(shù)是有參數(shù)的。
那么,這個帶參數(shù)的函數(shù)賦值給 f 之后, f 就是個函數(shù)了, 于是給 f 一個參數(shù) 5, 這個 5 自然就是返回的函數(shù)的參數(shù)了,也就是 z 了。
雖然并不是所有的閉包都是上面這種代碼的實現(xiàn)形式,但是初學者可以先記住這樣的閉包形式。
如果不習慣閉包,初學者可以先避免在代碼里面體現(xiàn)閉包的代碼形式。
2.3 回調(diào)函數(shù)
2.3.1 回調(diào)函數(shù)是什么
回調(diào)函數(shù)是在 LuatOS 編程過程中經(jīng)常用到的一個技術(shù)。
理解 LuatOS 的回調(diào)函數(shù),可以從“事件驅(qū)動”和“函數(shù)作為參數(shù)”兩個角度來把握:
回調(diào)函數(shù)(Callback)是在特定事件發(fā)生時,由系統(tǒng)或框架自動調(diào)用你事先定義好的函數(shù)。你只需要把自己的函數(shù)注冊給系統(tǒng),等事件觸發(fā)時,系統(tǒng)就會幫你調(diào)用它。
本質(zhì)上,回調(diào)函數(shù)就是一個普通函數(shù),但它被作為參數(shù)傳遞或注冊到其他地方,由系統(tǒng)或其他代碼在合適的時機自動執(zhí)行。
回調(diào)函數(shù)的作用是實現(xiàn)事件響應(yīng),異步處理。
消息到來,定時器到點,網(wǎng)絡(luò)收發(fā)等功能都經(jīng)常會用到回調(diào)函數(shù)的處理。
總之,LuatOS 的回調(diào)函數(shù),就是你注冊給系統(tǒng)的,在特定事件發(fā)生時自動被調(diào)用的函數(shù)。
回調(diào)函數(shù)讓事件響應(yīng)、異步處理、任務(wù)解耦變得簡單靈活,是 LuatOS 事件驅(qū)動編程的核心機制之一。
2.3.2 回調(diào)函數(shù)做消息訂閱與發(fā)布
LuatOS 支持通過sys.subscribe訂閱消息并注冊回調(diào)函數(shù),消息發(fā)布時自動調(diào)用回調(diào):

當sys.publish("TEST", 123)被調(diào)用時,"TEST"消息以及攜帶的參數(shù)123會被插入到用戶消息列表中,LuatOS 內(nèi)部的sys.run()調(diào)度中樞會遍歷訂閱者列表,找到所有訂閱了 "TEST" 的回調(diào)函數(shù),并自動把參數(shù) 123 傳給這些回調(diào)函數(shù)。
通過這樣的處理,事件觸發(fā)和處理邏輯就被解耦,方便擴展和維護。
2.3.3 回調(diào)函數(shù)做定時器和異步操作
定時器到點后自動調(diào)用注冊的回調(diào)函數(shù):

2.3.4 任務(wù)和協(xié)程場景的回調(diào)函數(shù)使用
在多任務(wù),也就是 LuatOS 的協(xié)程場景下,回調(diào)函數(shù)也常用于任務(wù)喚醒、事件響應(yīng)等。
解耦調(diào)用者與被調(diào)用者:調(diào)用者只需知道“有回調(diào)”,不用關(guān)心回調(diào)具體做什么,提升靈活性。
你只需更換回調(diào)函數(shù),就能實現(xiàn)不同的處理邏輯,無需修改底層框架代碼。
任務(wù)和協(xié)程的詳細信息,在下一章講解。
三、LuatOS 的多任務(wù)并行實現(xiàn)詳解
3.1 LuatOS 的多任務(wù)是怎么實現(xiàn)的
3.1.1 通過協(xié)程實現(xiàn)多任務(wù)的效果
LuatOS 使用一種協(xié)程(coroutine)的機制,實現(xiàn)多任務(wù)。
協(xié)程并不是真的多任務(wù),也不是多線程,而是通過同一時間只可能有一個協(xié)程執(zhí)行,來等價實現(xiàn)多任務(wù)的效果。
和 RTOS 的搶占式多任務(wù)方式不同,協(xié)程不能搶占其他任務(wù)的時間片,只能由一個獨立的調(diào)度器來判斷是哪個協(xié)程占用 CPU 時間來運行。
一個 LuatOS 可以創(chuàng)建多個任務(wù),每一個任務(wù)都是協(xié)程,為了簡化描述,后續(xù)我們經(jīng)常會用”任務(wù)“這個詞來指代協(xié)程。
LuatOS 創(chuàng)建的任務(wù)無法設(shè)定優(yōu)先級, 所以 LuatOS 的每個任務(wù)的優(yōu)先級都是相同的。
每一個 LuatOS 的任務(wù)在做運算的時候,是 100% 占用了 CPU 時間片的。
執(zhí)行完運算之后,要主動調(diào)用 yield() 函數(shù),讓自己掛起,其他任務(wù)才能獲得時間片運行。
如果某個任務(wù), 持續(xù)進行運算,不做 yield() 調(diào)用,其他任務(wù)是無法獲取 CPU 時間片的。
協(xié)程掛起后,自己是無法恢復的,只能其他的任務(wù)調(diào)用 resume 系統(tǒng)函數(shù)來恢復。
我們在寫代碼的時候,不需要調(diào)用 yield() 把自己掛起,只需要調(diào)用 sys.wait() 做時延,由調(diào)度器統(tǒng)一在 sys.wait()里面把任務(wù)掛起。
在 LuatOS 里面,所有掛起的協(xié)程,都由一個獨立的調(diào)度器通過調(diào)用 resume 來恢復。
這個獨立的調(diào)度器, 在 LuatOS 里面是 sys.run() 函數(shù)。
3.1.2 LuatOS 的任務(wù)函數(shù)怎么掛起和恢復
LuatOS 的每一個通過 sys.taskInit() 發(fā)起的任務(wù)函數(shù),都不會直接調(diào)用 yield 把自己掛起,因為直接調(diào)用 yield 掛起的話,并不知道什么時候恢復這個任務(wù)。
LuatOS 的做法是,每個任務(wù)在執(zhí)行完自己的事情之后,都必須是調(diào)用一個等待函數(shù), 這樣的等待函數(shù)有如下幾個:
1,sys.wait(timeout)
這個函數(shù),會在掛起任務(wù)的同時,啟動一個定時器,定時器的觸發(fā)時間就是 timeout,并且把任務(wù) id 跟這個定時器綁定。
到定時器觸發(fā)之后,sys.run 會根據(jù)該定時器綁定的任務(wù) id,重新恢復該任務(wù)的運行。
2,sys.waitUntil(topic, timeout)
在掛起任務(wù)的同時,訂閱一個名為 topic 的消息。待到有其他的任務(wù)發(fā)布這個消息后,sys.run 恢復這個任務(wù)。
如果沒有等到其他任務(wù)發(fā)布這個topic 消息,超時timeout 了,sys.run()也會恢復任務(wù)的運行。
總結(jié)來說,LuatOS 的任務(wù)在掛起自己之前,會在系統(tǒng)的表里面,放一個讓自己恢復運行的條件,這個條件或者是一個超時時間,或者是其他任務(wù)發(fā)布一個消息。sys.run() 函數(shù)會去判斷這些恢復運行的條件是否滿足,一旦滿足條件,就會恢復對應(yīng)的任務(wù)。
3.2 怎么實現(xiàn)單個任務(wù)
在 LuatOS 里面,一個任務(wù),可以理解為一個無限循環(huán)的函數(shù),啟動一個任務(wù),有如下步驟:
1,定義這個無限循環(huán)的函數(shù) task1;
2,調(diào)用 sys.taskInit(task1), 在 taskInit 函數(shù)里面,先為 task1 函數(shù)創(chuàng)建一個協(xié)程,同時把這個協(xié)程注冊到系統(tǒng)的協(xié)程列表,這樣 sys.run() 就會去運行這個協(xié)程。
這樣就新增了一個持續(xù)運行,永不退出的協(xié)程了。
一個在 LuatOS 系統(tǒng)里面合法的任務(wù), 必須運行很少量的時間,執(zhí)行完自己的操作之后,馬上就把自己掛起。 掛起的方式就是 調(diào)用 sys.wait 或者 sys.waitUtil 函數(shù)。
一個正常的 LuatOS 任務(wù),執(zhí)行計算的時間是很短暫的,絕大部分的時間,都是在掛起狀態(tài)。
在掛起狀態(tài), 是不消耗 CPU 資源的。
所以, LuatOS 的協(xié)程機制,具備了實現(xiàn)低功耗系統(tǒng)的前提。
3.3 進一步理解 sys.run()
LuatOS 的sys.run()函數(shù)是系統(tǒng)任務(wù)調(diào)度器的啟動入口,其主要工作流程如下:
3.3.1 進入任務(wù)調(diào)度主循環(huán)
當執(zhí)行到sys.run()時,LuatOS 會啟動任務(wù)調(diào)度器,正式進入事件驅(qū)動和多任務(wù)調(diào)度階段。
此后,所有通過sys.taskInit注冊的任務(wù)都會被納入系統(tǒng)統(tǒng)一調(diào)度。
3.3.2 循環(huán)處理底層消息與事件
sys.run()會不斷從底層(如硬件中斷、驅(qū)動、系統(tǒng)內(nèi)核,定時器等)獲取消息或事件,并將這些消息分發(fā)到相應(yīng)的任務(wù)或回調(diào)函數(shù)進行處理。
這包括定時器到期、外設(shè)事件、網(wǎng)絡(luò)數(shù)據(jù)到達、用戶自定義消息等。
3.3.3 定時器與任務(wù)切換
sys.run 會周期性檢查所有注冊的定時器,并在定時器到期時喚醒相應(yīng)的任務(wù)協(xié)程。
同時,系統(tǒng)會根據(jù)任務(wù)的掛起或喚醒狀態(tài),合理切換協(xié)程,實現(xiàn)多任務(wù)并發(fā)。
3.3.4 任務(wù)間消息通信與同步
sys.run()支持任務(wù)間通過消息發(fā)布/訂閱、等待/喚醒等機制進行通信與同步。
例如,任務(wù)可以通過sys.publish發(fā)布消息,其他任務(wù)通過sys.waitUntil或sys.subscribe等方式等待或響應(yīng)這些消息。
3.3.5 持續(xù)運行,直至系統(tǒng)重啟或退出
sys.run()會持續(xù)運行,不會主動退出。
sys.run() 系統(tǒng)的主循環(huán),確保所有任務(wù)和事件都能被及時處理。
只有在系統(tǒng)重啟、腳本異常終止或手動退出時,sys.run() 這個調(diào)度循環(huán)才會結(jié)束。
3.3.6 簡要流程圖
(1)啟動任務(wù)調(diào)度器;
(2)進入主循環(huán)
(3)輪詢底層消息、定時器
(4)喚醒/調(diào)度任務(wù)協(xié)程
(5)分發(fā)和處理事件、消息
(6)返回主循環(huán),直到系統(tǒng)重啟或退出
3.4 怎么實現(xiàn)多個任務(wù)
3.4.1 協(xié)程大多數(shù)時間應(yīng)該是掛起狀態(tài)
由于協(xié)程的運行原理是,同一時間只有一個協(xié)程在運行,其他協(xié)程在掛起狀態(tài)。
所以如果有多個協(xié)程存在的話,多個協(xié)程的運行,只可能有兩種情況:
第一種情況, 所有的協(xié)程都在掛起狀態(tài),這時候系統(tǒng)有可能進入低功耗;
第二種情況, 有一個協(xié)程在運行,其他協(xié)程在掛起。這時候系統(tǒng)是喚醒狀態(tài),不可能是低功耗狀態(tài)。
3.4.2 LuatOS 多任務(wù)的核心是掛起和恢復的調(diào)度
一個協(xié)程運行的時間越長,掛起的就越慢,其他的協(xié)程就無法得到時間片運行。
只有所有的協(xié)程都盡量減少時間占用, 都盡快掛起自己,這樣的多任務(wù)的調(diào)度的效率才能更高。
因此, LuatOS 多任務(wù)的編程核心,是使得每個任務(wù)函數(shù)的執(zhí)行時間盡可能的短,盡可能快速的掛起自己,整個系統(tǒng)的多任務(wù)并發(fā)處理的效率才會更高。
如果某個協(xié)程的運算時間很長,導致自己無法很快掛起,就會拖累整個系統(tǒng),使得整個系統(tǒng)的實時響應(yīng)的性能降低。
3.4.3 怎么防止某個協(xié)程長時間不掛起
為了防止某個協(xié)程長時間做運算,不把自己掛起,LuatOS 設(shè)計了 watchdog 機制,起一個定時器,幾秒鐘喂狗一次。
如果超時沒有喂狗,系統(tǒng)就會被重啟。
把下面這段代碼放到 main.lua,即可實現(xiàn)喂狗的功能:

3.5 多個任務(wù)之間怎么分配時間片
LuatOS 系統(tǒng)里面,是沒有給某個任務(wù)分配時間片這樣的動作的。
LuatOS 的任務(wù),必須盡快把自己掛起,釋放出 CPU,才能夠讓整個系統(tǒng)實時運行。
當所有任務(wù)都把自己掛起后,系統(tǒng)就就可能會低功耗休眠狀態(tài)。
只要有任何一個任務(wù)沒有掛起,系統(tǒng)都不可能進入低功耗休眠狀態(tài)。
通過 sys.run()函數(shù), 對多個任務(wù)按照業(yè)務(wù)需要進行恢復運行的調(diào)度,保證整個系統(tǒng)的順暢運行。
sys.run()調(diào)度的依據(jù),一個是定時器機制,一個是消息機制。
四、LuatOS 的定時器機制
LuatOS 的定時器機制是實現(xiàn)多任務(wù)系統(tǒng)的核心組件之一。
支持單次觸發(fā)和周期循環(huán),適用于物聯(lián)網(wǎng)設(shè)備中的定時任務(wù)、數(shù)據(jù)采集、狀態(tài)監(jiān)測等場景。
4.1 定時器類型與適用場景
|
類型 |
特點 |
適用場景 |
|
單次定時器 |
延遲指定時間后觸發(fā)一次 |
初始化延時、事件超時處理 |
|
循環(huán)定時器 |
周期性觸發(fā),可指定次數(shù) |
心跳包發(fā)送、傳感器輪詢 |
4.2 核心 API 與用法
4.2.1 單次定時器
功能: 延遲 timeout 毫秒后執(zhí)行函數(shù), 可傳多個參數(shù)local timerId = sys.timerStart(callback, timeout, arg1, arg2, ...) 參數(shù)說明: callback: 定時器觸發(fā)時執(zhí)行的函數(shù) timeout: 延遲時間(毫秒) argN: 傳遞給回調(diào)函數(shù)的參數(shù) 代碼示例:


4.2.2 循環(huán)定時器
功能: 每隔 timeout 毫秒重復執(zhí)行函數(shù)localtimerId = sys.timerLoopStart(callback, timeout, arg1, arg2, ...)
代碼示例:

運行結(jié)果為:

4.2.3 定時器停止
LuatOS 有兩個 API 用于停止正在生效的定時器:
1, 停止制定 timerid 的單個定時器
sys.timerStop(timerId)
2,停止制定回調(diào)函數(shù)的所有定時器。
sys.timerStopAll(callback)
4.3 典型代碼示例
4.3.1 組合使用單次與循環(huán)定時器

4.3.2 動態(tài)管理定時器

4.3.3 5 秒后重連網(wǎng)絡(luò)

4.4 定時器的數(shù)量限制
LuatOS 最多支持 64 個定時器。
由于任務(wù)里面的 sys.wait()、帶timeout參數(shù)的sys.waitUntil()、帶timeout參數(shù)的sys.waitMsg()調(diào)用也會引發(fā)調(diào)度器啟動一個定時器管理該任務(wù)的運行恢復,所以用戶實際能夠啟用的定時器,會比 64 個更少。
所以,在開發(fā)過程中, 需要注意這一點,不要無節(jié)制的使用定時器。
4.5 為什么 LuatOS 的定時器不太準
LuatOS 的定時器往往“不太準”,主要原因在于其定時器機制依賴于消息總線(Message Bus)和系統(tǒng)調(diào)度,而不是直接精準地控制硬件定時。具體來說有如下幾點原因:
4.5.1 定時器基于消息機制
LuatOS 的定時器設(shè)計是基于 RTOS 的 timer API。
當定時器超時時,系統(tǒng)只是在消息總線中插入一條定時器消息,由主循環(huán) sys.run()消費和處理,這會帶來兩種可能的時延:
1,當調(diào)度器在處理消息時,可能會因為其他任務(wù)、消息隊列長度、系統(tǒng)負載等原因出現(xiàn)延遲。
2,定時器回調(diào)的實際執(zhí)行時機,取決于消息被調(diào)度和消費的時刻,而不是定時器超時的精確時刻。
4.5.2 系統(tǒng)調(diào)度與任務(wù)競爭
LuatOS 采用事件驅(qū)動和多任務(wù)協(xié)作,主循環(huán)需要處理各種消息(包括定時器、外設(shè)、網(wǎng)絡(luò)等);
如果系統(tǒng)中有大量任務(wù)或消息,定時器消息可能會被延后處理,導致定時精度下降。
4.5.3 軟件定時器的局限
(1)軟件定時器本質(zhì)上依賴于系統(tǒng) tick(通常為 1ms),但 tick 的處理、消息入隊、Lua 虛擬機調(diào)度等環(huán)節(jié)都會引入微小延遲。
(2)在高負載或消息堆積時,這種延遲會被放大,表現(xiàn)為“定時器不準”。
4.5.4 Lua 腳本無法實現(xiàn)高精度定時器
LuatOS 定時器不太準的根本原因是:定時器只是觸發(fā)消息,實際執(zhí)行依賴消息總線和主循環(huán)調(diào)度,受系統(tǒng)負載、任務(wù)數(shù)量、消息堆積、網(wǎng)絡(luò)中斷優(yōu)先級最高等多因素影響。
LuatOS 定時器不能實現(xiàn)高精度定時器(例如微秒級別,幾十毫秒級別,甚至幾百毫秒級別也會有誤差)。
如果要實現(xiàn)高精度定時器,只能外掛單片機實現(xiàn),或者后續(xù)有可能推出集成單片機,專門用來實現(xiàn)高精度定時器和其他對實時性和精度要求比較高的需求。
4.6 sys.lua 里面的 timerPool 變量
如果你有興趣查看 sys.lua 的話,會發(fā)現(xiàn) timerPool 這個 table 類型的變量,在 0-0x1FFFFF 范圍內(nèi)存儲 恢復運行協(xié)程的定時器消息 ID, 在 0x200000-0x7FFFF 范圍內(nèi)存儲有回調(diào)函數(shù)的定時器消息 ID。
所以,凡是某個協(xié)程調(diào)用 sys.wait()延時函數(shù),都會在注冊一個定時器,定時器超時后,就會由調(diào)度器重新恢復這個協(xié)程的運行;
當使用 timerStart 函數(shù)注冊的定時器超時后, 調(diào)度器會調(diào)用定時器回調(diào)函數(shù)。
這兩種情況的超時處理,都是在 timerPool 這個變量實現(xiàn)的。
4.7 LuatOS 定時器總結(jié)
LuatOS 的定時器機制通過sys庫提供了消息驅(qū)動架構(gòu),合理運用定時器可顯著提升物聯(lián)網(wǎng)設(shè)備的自動化程度和能效比。
在使用定時器機制的時候,需要注意如下幾點:
4.7.1 避免阻塞回調(diào)
1, 定時器回調(diào)函數(shù)中禁止使用sys.wait操作
因為定時器回調(diào)函數(shù)是由調(diào)度器直接調(diào)用的,如果在定時器回調(diào)函數(shù)里面使用 sys.wait 操作,會使得調(diào)度器阻塞,從而使得整個系統(tǒng)停止運行。
2, 定時器回調(diào)函數(shù)禁止進行長時間阻塞操作
這樣會極大的降低系統(tǒng)效率,使得系統(tǒng)的反應(yīng)變慢。
4.7.2 注意資源釋放
任務(wù)退出時,如果在任務(wù)運行過程中創(chuàng)建的定時器不再需要,需調(diào)用sys.timerStop()或者sys.timerStopAll()清理關(guān)聯(lián)定時器,防止內(nèi)存泄漏,或者引起定時器資源耗盡。
如果不想主動寫代碼清理關(guān)聯(lián)定時器,只能等待定時器時間到了之后,自動清除,這時就會多占用了一個沒有任何實際功能的定時器,如果定時器資源非常緊張的情況下,創(chuàng)建新的定時器有可能會失敗。
4.7.3 不要期待有高精確度的延時和定時
由于消息機制和虛擬機的運行限制,導致延時函數(shù)和定時器的精度都不會很高,在實現(xiàn)業(yè)務(wù)邏輯的時候,一定要注意這一點。
五、 LuatOS 的消息機制
LuatOS 的消息機制是其多任務(wù)協(xié)作的核心,通過sys庫實現(xiàn)事件驅(qū)動編程。以下從消息發(fā)送、消息接收、消息訂閱三個維度詳細解析:
5.1 發(fā)送消息
5.1.1 廣播式消息(一對多)
API:sys.publish(topic, arg1, arg2, ...)
功能:向所有訂閱者廣播消息,無目標標識。
代碼示例:

5.1.2 定向消息(點對點)
API:sys.sendMsg(taskName, target, arg2, arg3, arg4)
功能:向指定任務(wù)發(fā)送消息,支持目標標識和參數(shù)。
代碼示例:

5.2 消息接收
5.2.1 等待消息
在協(xié)程內(nèi)部等待:sys.waitUntil(topic, timeout)
特別提醒: 該 API 只能在協(xié)程內(nèi)執(zhí)行
代碼示例:

5.2.2 定向接收
API:sys.waitMsg(taskName, target, timeout)
特點:按任務(wù)名和目標標識精準接收,支持超時,該代碼只能在協(xié)程內(nèi)執(zhí)行。
注意,該 API 的第一個參數(shù) taskName, 是指等待消息的任務(wù)名稱,也就是自己的任務(wù)名稱,不是發(fā)送消息的任務(wù)名稱。
調(diào)用該 API 的任務(wù),和接收任務(wù),不一定是同一個任務(wù)。
當接收消息的任務(wù)在掛起的時候,可以由其他任務(wù)或者調(diào)度器通過 WaitMsg API 喚醒掛起的任務(wù)。
代碼示例:

5.3 消息訂閱
5.3.1 全局訂閱
API:sys.subscribe(topic, func)
特點:如果訂閱了同一主題有多個回調(diào)函數(shù),這些回調(diào)函數(shù)都會被觸發(fā)。
代碼示例:

5.3.2 任務(wù)私有訂閱
實現(xiàn)方式:通過sys.taskInitEx創(chuàng)建任務(wù)時注冊回調(diào)。
當有其他的任務(wù)發(fā)送消息給目標任務(wù)的時候, 但是目標任務(wù)并沒有通過 WaitMsg 函數(shù)設(shè)定消息處理,這時候該消息的處理就交給回調(diào)函數(shù)處理。
代碼示例:

5.4 LuatOS 消息機制的典型應(yīng)用場景
5.4.1 網(wǎng)絡(luò)模塊與主任務(wù)通信

5.4.2 全局事件通知(sys)

5.5 消息機制設(shè)計要點
5.5.1 消息機制的不同設(shè)計
(1)處理全局事件,sys.publish發(fā)布的消息,已經(jīng)訂閱這個消息的所有sys.subscribe對應(yīng)的處理函數(shù)都能收到。 (2)處理模塊間通信(如網(wǎng)絡(luò)請求-響應(yīng)),sys.sendMsg發(fā)布的消息,會攜帶一個task name參數(shù),只有sys.waitMsg時也攜帶同樣的task name參數(shù),才能收到消息。
5.5.2 避免消息風暴:
高頻消息(如傳感器數(shù)據(jù))建議合并發(fā)送或降低頻率。
5.5.3 消息機制的核心目的之一是軟件解耦
通過合理運用sys庫的消息機制,可構(gòu)建高效、解耦的物聯(lián)網(wǎng)應(yīng)用架構(gòu)。
六、多任務(wù)之間的信息交換
6.1 用全局變量做信息交換
如果信息量很小,比如就一個字符串或者標志位,任務(wù)之間可以通過共享全局變量來通信,一個任務(wù)去對這個全局變量賦值,其他任務(wù)讀取這個全局變量,任務(wù)之間就達到了通信的目的了;
6.2 用消息做信息交換
但是如果想要交換多個數(shù)據(jù),每個數(shù)據(jù)都用全局變量的話,就有點過于累贅了。
這時候,可以通過發(fā)送消息來通信。
任務(wù)之間怎么發(fā)送消息,接收消息,參考第五章的內(nèi)容。
七、再次理解調(diào)度器 sys 庫
LuatOS 的 sys 庫是系統(tǒng)調(diào)度和多任務(wù)管理的核心庫,提供了豐富的 API 用于任務(wù)創(chuàng)建、延時、消息通信、定時器管理等。
7.1.1 任務(wù)與協(xié)程管理
API: sys.taskInit(func, arg1, arg2, ...)功能: 創(chuàng)建一個新的任務(wù)(協(xié)程),并傳遞參數(shù)給任務(wù)函數(shù)。
7.1.2 延時與等待
(1) sys.wait(timeout)功能: 任務(wù)延時掛起指定毫秒數(shù),只能在任務(wù)函數(shù)中調(diào)用。
(2)sys.waitUntil(topic, timeout)功能: 任務(wù)掛起,直到收到指定 topic 的消息或超時,只能在任務(wù)函數(shù)中調(diào)用。
7.1.3 定時器相關(guān)
(1) sys.timerStart(func, timeout, arg1, ...)創(chuàng)建單次定時器,到時后執(zhí)行回調(diào)函數(shù)。
(2)sys.timerLoopStart(func, timeout, arg1, ...)創(chuàng)建循環(huán)定時器,周期性執(zhí)行回調(diào)函數(shù)。
(3)sys.timerStop(timerId)停止指定 ID 的定時器。
(4)sys.timerStopAll(func)停止所有與指定回調(diào)函數(shù)相關(guān)的定時器。
(5)sys.timerIsActive(timerId)判斷定時器是否處于激活狀態(tài)。
7.1.4 消息通信
(1)sys.publish(topic, arg1, ...)發(fā)布(廣播)一個消息,喚醒等待該 topic 的任務(wù)或觸發(fā)訂閱回調(diào)。
(2)sys.subscribe(topic, callback)訂閱指定 topic 的消息,消息到來時自動執(zhí)行回調(diào)。
(3)sys.unsubscribe(topic, callback)取消訂閱。
7.1.5 主循環(huán)控制
sys.run()功能: 是 LuatOS 的調(diào)度器,是系統(tǒng)主循環(huán),調(diào)度所有注冊的任務(wù)和定時器。
7.1.6 典型用法示例

7.1.7 任務(wù)與協(xié)程管理
(1)sys.taskInitEx(func, taskName, cbFun, ...)
功能:創(chuàng)建一個具名任務(wù)線程,并注冊任務(wù)函數(shù)和非目標消息回調(diào)。
(2)sys.taskDel(taskName)
功能:刪除由taskInitEx創(chuàng)建的任務(wù)線程,釋放資源。
7.1.8 消息通信機制
(1)sys.waitMsg(taskName, target, timeout)
功能:等待接收一個目標消息(可指定超時),任務(wù)會掛起直到收到目標消息或超時。
(2)sys.sendMsg(taskName, target, arg2, arg3, arg4)
功能:向目標任務(wù)發(fā)送一個消息,可攜帶最多 4 個參數(shù)。
(3)sys.cleanMsg(taskName)
功能:清除指定任務(wù)的消息隊列,防止消息堆積。
7.1.9 sys.run() 怎么實現(xiàn)多個任務(wù)的協(xié)同工作
sys.run()函數(shù)的實現(xiàn)過程是這樣的:
1, 查看消息隊列里面是否有未處理的消息, 如果有,就根據(jù)消息的處理類型,調(diào)用回調(diào)函數(shù)或者是喚醒對應(yīng)的任務(wù)進行消息處理;
2, 等待底層 RTOS 操作系統(tǒng)的定時器消息;等待的過程,就是低功耗的過程;
3, 定時器消息等到之后, 調(diào)用定時器回調(diào)函數(shù)或者喚醒對應(yīng)的任務(wù)。
4, 循環(huán) 1-3 步。
通過以上過程,我們可以看到,這個 LuatOS 系統(tǒng), 大多數(shù)時間都是在等待底層 RTOS 操作系統(tǒng)的定時器消息,在等待期間,系統(tǒng)是可以處于低功耗休眠狀態(tài)的。
當任務(wù)的時延很短, 或者定時器非常頻繁,或者是消息太多,是會影響到系統(tǒng)的低功耗性能的。
八、怎么封裝一個 LuatOS 的軟件功能模塊
在 LuatOS 中封裝功能模塊為單獨 Lua 文件的標準做法:
1、新建一個 Lua 文件,定義一個 table,比如名字為 myflib,所有對外接口作為其字段。
2、用 local 修飾內(nèi)部變量和函數(shù),實現(xiàn)信息隱藏。
3、定義 myflib 的成員變量,成員函數(shù),用作對外的接口。
4、文件末尾用returnmyflib ,導出模塊 table。
5、外部的文件,用require("模塊名")加載和復用模塊。
這樣可以讓你的功能模塊獨立、可維護、易擴展,是 Lua 及 LuatOS 推薦的開發(fā)范式。
代碼示例:

九、LuatOS 的核心庫和擴展庫
LuatOS 在 Lua 5.3 版本的基礎(chǔ)上, 封裝了 74 個核心庫,17 個擴展庫,提供了極其強大的通信和硬件的開發(fā)功能。
9.1 LuatOS 核心庫
LuatOS 核心庫,提供了 LuatOS 系統(tǒng)的核心功能。 不同的硬件型號,支持不同的核心庫的子集。
LuatOS 的核心庫, 是不需要用戶 require,可以直接調(diào)用的。
780EPM 對這些核心庫的支持情況參見如下鏈接:
http://docs.openluat.com/air780epm/common/lutos_coreapilist/
9.2 LuatOS 擴展庫
除了用戶可以直接使用的核心庫之外, LuatOS 還提供了豐富的擴展庫。
使用擴展庫,需要用戶在代碼里面做 require 動作,Luatools 看到 require 關(guān)鍵字后,會把用到的擴展庫合并入燒錄包,一起燒錄到硬件里面。
如果用戶不做 require 的動作, luatools 就不會合并這個擴展庫的代碼。
所有的擴展庫,都是用 Lua 代碼實現(xiàn)的。
當前 LuatOS 已經(jīng)支持的擴展庫參見如下鏈接:
http://docs.openluat.com/air780epm/common/lutos_coreapilist/
十、LuatOS 實際工程代碼解讀
780EPM 1.3 開發(fā)板的出廠固件代碼, 是一個實際的 LuatOS 開發(fā)的簡單案例。
代碼的位置在:
780EPM 開發(fā)板 V1.3 出廠固件源碼
這個固件分為幾個部分:
1, 780EPM core 固件: 目前最新的固件是 2005 版本,后續(xù)更新的固件版本也可以繼續(xù)使用;
最新的 780EPM 固件在這里下載:
http://docs.openluat.com/air780epm/luatos/firmware/version/
2,管腳復用描述文件

3, 資源圖片
實現(xiàn)開機固件所需的圖片。(詳見780EPM 開發(fā)板 V1.3 出廠固件源碼的pic目錄下的圖片 )
4, 實現(xiàn)腳本。
下面重點講解一下腳本實現(xiàn)的邏輯。
10.1 編碼要求
為了降低用戶理解成本,這份開機固件的代碼有如下要求:
(1)不允許用云編譯擴大腳本區(qū),不允許用云編譯擴大文件系統(tǒng),保持腳本 + 資源總體尺寸不能大于 256K 字節(jié);
(2)main.lua 作為邏輯主線,其他的功能代碼封裝成子模塊,提供成員函數(shù),也可以提供成員變量, 被 main.lua 調(diào)用;
(3)不允許使用匿名函數(shù);
10.2 已實現(xiàn)功能
如下代碼,已經(jīng)實現(xiàn)了如下功能:
(1)主界面九宮格的按鍵切換,
(2)長按進入具體功能界面;再長按回到主界面;
(3)圖片顯示功能,
(4)攝像頭預覽,
(5)俄羅斯方塊,
(6)天氣數(shù)據(jù)獲取,并顯示不同的天氣圖標;
使用的是 780EPM 默認 2005 固件,不需要擴大文件系統(tǒng)和代碼區(qū)。
其中,airlcd.lua, camera780epm_simple.lua, russia.lua,statusbar.lua, 分別用 table 的方式,封裝了 LCD 的參數(shù)初始化,camera 的初始化,預覽,退出,俄羅斯方塊的初始化,更新數(shù)據(jù),響應(yīng)按鍵等事件。
在 main.lua 調(diào)用這些封裝好的 table 的函數(shù)即可,不需要過度關(guān)心子模塊的實現(xiàn)細節(jié)。
10.3 待實現(xiàn)功能
(1)以太網(wǎng) LAN
(2)以太網(wǎng) WAN
(3)硬件自檢
(4)modbus TCP
(5)modbus RTU
(6)CAN 總線
10.4 main.lua 解讀
10.4.1 系統(tǒng)初始化
整個系統(tǒng),做了兩個全局初始化:
1, 看門狗的初始化,wdt.Init()防止系統(tǒng)被某個任務(wù)異常占用 CPU 讓系統(tǒng)鎖死;
2, LCD 的初始化: airlcd.lcd_init("AirLCD_0001"),其中 AirLCD_0001 是合宙 LCD 配件的型號。
10.4.2 業(yè)務(wù)主循環(huán)
UITask() 函數(shù), 是 main.lua 啟動之后的主循環(huán)。
在 UITask 函數(shù)里面,先做按鍵的初始化之后,就無限循環(huán)的不斷調(diào)用三個函數(shù):keypressed, update, draw。
其中,keypressed 是查看按鍵是否有待處理的事件;
update 是更新業(yè)務(wù)數(shù)據(jù);
draw 是更新 UI 畫面。
10.4.3 按鍵事件的處理
由于 780EPM 開發(fā)板只有三個按鍵: 開機鍵,boot 鍵,reset 鍵。
reset 鍵無法被捕獲事件,只能復位硬件,所以固件只能處理 開機鍵和 boot 鍵的事件。
boot 按鍵是一個特殊的 GPIO, 編號為 GPIO0。
開機鍵也是個特殊的 GPIO, 編號為 GPIO.PWR_KEY。
在 KeyInit()這個函數(shù), 分別配置了 gpio.PWR_KEY 和 GPIO0 為雙邊沿中斷,中斷處理函數(shù)分別為 PowerInterrupt 和 BootInterrupt。
根據(jù)開發(fā)板的原理圖,開機鍵初始電平是上拉為高電平,boot 鍵初始電平為下拉低電平。
在 PowerInterrupt() 和 BootInterrupt()這兩個函數(shù)的處理邏輯是類似的,都是計算按下和抬起的時間間隔,從而判斷是短按還是長按,然后給 key 這個全局變量賦值。
key 是字符串類型,是一個比較關(guān)鍵的變量,根據(jù) key 的值不同, main.lua 進入不同的功能。
這個邏輯是在 keypressed 來實現(xiàn)。
在 keypressed() 函數(shù)里面,檢查 key 變量的值,然后做不同的處理。
在主界面, 處理 "main" 和 "enter"這兩個值,分別是切換按鈕加亮顯示,以及進入具體功能按鈕;
在具體功能界面, enter 按鍵時間會返回主界面;
在俄羅斯方塊界面, 有 5 種按鍵:
(1)right: 短按 boot, 往右移動方塊;
(2) left:短按開機,往左移動方塊;
(3)up:長按 boot,旋轉(zhuǎn)方塊;
(4)fast:長按開機,快速下落;
(5)quit:超長按開機,退出游戲回到主界面。
10.4.4 UI 界面的循環(huán)刷新
在 draw 函數(shù)里面刷新界面。
當需要把繪圖權(quán)限交給其他的功能模塊的時候, 根據(jù)情況做不同的處理:
(1)俄羅斯方塊的刷新函數(shù)就是自己實現(xiàn) drawrus 函數(shù), draw 函數(shù)調(diào)用 drawrus 函數(shù)刷新屏幕;
(2)攝像頭預覽功能接管了屏幕后,draw 函數(shù)判斷當前是攝像頭預覽功能,就直接退出,如果判斷不是攝像頭,再繼續(xù)處理刷新任務(wù);
(3)在刷新之前,調(diào)用 update 函數(shù)更新用于刷新的關(guān)鍵數(shù)據(jù)。
10.4.5 管理當前功能狀態(tài)機
有兩個關(guān)鍵變量:
cur_sel: 整數(shù),范圍是 1-9, 當前選擇的九宮格是哪個;
cur_fun: 字符串,10 種值:
記錄當前已經(jīng)進入的界面是主界面,還是 9 個之一。
“main”: 主界面;
另外 9 個界面用一個數(shù)組記錄,并根據(jù) cur_sel 賦值給到 cur_fun。
local funlist = {
"picshow", "camshow","russia",
"LAN", "WAN","selftest",
"modbusTCP","modbusRTU","CAN"
}
cur_sel 和 cur_fun,結(jié)合 key 的值,組成了整個的邏輯切換,可以決定該進入什么軟件功能,該顯示什么界面。
理解了 cur_sel, cur_fun, key 這三個變量的運用,就可以看明白整個軟件的邏輯。
10.5 總結(jié)
這份 780EPM 的開發(fā)板的出廠固件的代碼,展示了一個完整的 LuatOS 工程的基本實現(xiàn)的方法。
腳本文件一共只有 10 個,全部加一起只有 30k 字節(jié),1000 行代碼,實現(xiàn)了 9 宮格界面,電量,信號強度,天氣的狀態(tài)欄顯示,包括俄羅斯方塊在內(nèi)的多種功能的演示。
這份代碼后續(xù)還會繼續(xù)更新,并且都不會采用非常高難度的編碼技巧,只需要用最簡單的編程邏輯就可以實現(xiàn)相對復雜的業(yè)務(wù)邏輯。
今天的內(nèi)容就分享到這里了~
審核編輯 黃宇
-
嵌入式
+關(guān)注
關(guān)注
5198文章
20436瀏覽量
333946 -
腳本
+關(guān)注
關(guān)注
1文章
409瀏覽量
29187 -
LuatOS
+關(guān)注
關(guān)注
0文章
156瀏覽量
2691
發(fā)布評論請先 登錄
LuatOS 框架的嵌入式系統(tǒng)架構(gòu)設(shè)計原理
LuatOS 系統(tǒng)框架的模塊化設(shè)計原理
LuatOS框架的使用(上)
警惕兼容性陷阱:LuatOS-Air腳本在LuatOS中的運行異常分析
C語言單元測試在嵌入式軟件開發(fā)中的作用及專業(yè)工具的應(yīng)用
從小白到大牛:Linux嵌入式系統(tǒng)開發(fā)的完整指南
Air780EPM開發(fā)板NTP對時教程:LuatOS腳本開發(fā)入門指南
Air780EPM開發(fā)板FTP功能實戰(zhàn):LuatOS嵌入式開發(fā)全解析
嵌入式達到什么水平才能就業(yè)?
嵌入式從入門到進階,怎么學?
BitsButton嵌入式按鍵處理框架
Linux嵌入式和單片機嵌入式的區(qū)別?
嵌入式開發(fā)入門指南:從零開始學習嵌入式
嵌入式教育科普|GPIO接口全面解析
LuatOS腳本開發(fā)入門:嵌入式運行框架全解析!
評論