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

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

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

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

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

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

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

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

3.5 多個(gè)任務(wù)之間怎么分配時(shí)間片
LuatOS 系統(tǒng)里面,是沒有給某個(gè)任務(wù)分配時(shí)間片這樣的動(dòng)作的。
LuatOS 的任務(wù),必須盡快把自己掛起,釋放出 CPU,才能夠讓整個(gè)系統(tǒng)實(shí)時(shí)運(yùn)行。
當(dāng)所有任務(wù)都把自己掛起后,系統(tǒng)就就可能會低功耗休眠狀態(tài)。
只要有任何一個(gè)任務(wù)沒有掛起,系統(tǒng)都不可能進(jìn)入低功耗休眠狀態(tài)。
通過 sys.run()函數(shù), 對多個(gè)任務(wù)按照業(yè)務(wù)需要進(jìn)行恢復(fù)運(yùn)行的調(diào)度,保證整個(gè)系統(tǒng)的順暢運(yùn)行。
sys.run()調(diào)度的依據(jù),一個(gè)是定時(shí)器機(jī)制,一個(gè)是消息機(jī)制。
四、LuatOS 的定時(shí)器機(jī)制
LuatOS 的定時(shí)器機(jī)制是實(shí)現(xiàn)多任務(wù)系統(tǒng)的核心組件之一。
支持單次觸發(fā)和周期循環(huán),適用于物聯(lián)網(wǎng)設(shè)備中的定時(shí)任務(wù)、數(shù)據(jù)采集、狀態(tài)監(jiān)測等場景。
4.1 定時(shí)器類型與適用場景

4.2 核心 API 與用法
4.2.1 單次定時(shí)器
功能: 延遲 timeout 毫秒后執(zhí)行函數(shù), 可傳多個(gè)參數(shù)local timerId = sys.timerStart(callback, timeout, arg1, arg2, ...) 參數(shù)說明: callback: 定時(shí)器觸發(fā)時(shí)執(zhí)行的函數(shù) timeout: 延遲時(shí)間(毫秒) argN: 傳遞給回調(diào)函數(shù)的參數(shù) 代碼示例:


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

運(yùn)行結(jié)果為:

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

4.3.2 動(dòng)態(tài)管理定時(shí)器

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

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

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

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

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

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

5.4.2 全局事件通知(sys)

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

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

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

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