關(guān)注「嵌入式大雜燴」,選擇「星標(biāo)公眾號」一起進(jìn)步!
作者 | Alicedodo
狀態(tài)機(jī)是一種思想,
事件驅(qū)動也是一種思想。
狀態(tài)機(jī)推文:
干貨 | 嵌入式之狀態(tài)機(jī)編程
改變嵌軟開發(fā)思維方式之:狀態(tài)機(jī)的三種實(shí)現(xiàn)方法
本篇來一起學(xué)習(xí)事件驅(qū)動。
事件驅(qū)動的概念
生活中有很多事件驅(qū)動的例子,上自習(xí)瞞著老師偷睡覺就是很生動的一個。
我們都是從高中時代走過來的,高中的學(xué)生苦啊,覺得睡覺是世界上最奢侈的東西, 有時候站著都能睡著??!老師看的嚴(yán),上課睡覺不允許 啊,要挨批?。∮心居?!相比而言,晚自習(xí)是比較寬松的,老師只是不定時來巡視,還是有機(jī)會偷偷睡一會兒的。
現(xiàn)在的問題是,怎么睡才能既睡得好又不會讓老師發(fā)現(xiàn)呢? 晚自習(xí)是比較寬松的,老師只是不定時來巡視,還是有機(jī)會偷偷睡一會兒的?,F(xiàn)在的問題是,怎么睡才能既睡得好又不會讓老師發(fā)現(xiàn)呢?
我們現(xiàn)在有三種睡覺方案:
-
方案 A:倒頭就睡,管你三七二十一,睡夠了再說,要知道有時候老師可能一整晚上都不來的。
-
方案 B:間歇著睡,先定上鬧鐘, 5 分鐘響一次,響了就醒,看看老師來沒來,沒來的話定上鬧鐘再睡,如此往復(fù)。
-
方案 C:睡之前讓同桌給放哨,然后自己睡覺,什么也不用管,什么時候老師來了,就讓同桌戳醒你。
不管你們選擇的是哪種方案,我高中那會兒用的可是方案 C,安全又舒服。
方案 C 是很有特點(diǎn)的:本來自習(xí)課偷睡覺是你自己的事兒, 有沒有被老師抓著也是你自己的事兒,這些和同桌是毫無利害關(guān)系的,但是同桌這個環(huán)節(jié)對方案 C 的重要性是不言而喻的,他肩負(fù)著監(jiān)控老師巡視和叫醒睡覺者兩項重要任務(wù),是事件驅(qū)動機(jī)制實(shí)現(xiàn)的重要組成部分 。
在事件驅(qū)動機(jī)制中,對象對于外部事件總是處于“休眠” 狀態(tài)的,而把對外部事件的檢測和監(jiān)控交給了第三方組件。
一旦第三方檢測到外部事件發(fā)生, 它就會啟動某種機(jī)制, 將對象從“休眠” 狀態(tài)中喚醒, 并將事件告知對象。對象接到通知后, 做出一系列動作, 完成對本次事件響應(yīng),然后再次進(jìn)入“休眠” 狀態(tài),如此周而復(fù)始。
有沒有發(fā)現(xiàn),
事件驅(qū)動機(jī)制和單片機(jī)的中斷原理上很相似 。
事件驅(qū)動與單片機(jī)編程
在我們再回到單片機(jī)系統(tǒng)中來,看看事件驅(qū)動思想在單片機(jī)程序設(shè)計中的應(yīng)用。當(dāng)我還是一個單片機(jī)菜鳥的時候(當(dāng)然,我至今也沒有成為單片機(jī)高手),網(wǎng)絡(luò)上的大蝦們就諄諄教導(dǎo):一個好的單片機(jī)程序是要分層的。曾經(jīng)很長一段時間, 我對分層這個概念完全沒有感覺。
-
什么是程序分層?
-
程序?yàn)槭裁匆謱樱?
-
應(yīng)該怎么給程序分層?
隨著手里的代碼量越來越多,實(shí)現(xiàn)的功能也越來越多,軟件分層這個概念在我腦子里逐漸地清晰起來,我越來越佩服大蝦們的高瞻遠(yuǎn)矚。
單片機(jī)的軟件確實(shí)要分層的,最起碼要分兩層:驅(qū)動層和應(yīng)用層。應(yīng)用是單片機(jī)系統(tǒng)的核心,與應(yīng)用相關(guān)的代碼擔(dān)負(fù)著系統(tǒng)關(guān)鍵的邏輯和運(yùn)算功能,是單片機(jī)程序的靈魂。
硬件是程序感知外界并與外界打交道的物質(zhì)基礎(chǔ),硬件的種類是多種多樣的,各類硬件的操作方式也各不相同,這些操作要求嚴(yán)格、精確、瑣細(xì)、繁雜。
與硬件打交道的代碼只鐘情于時序和寄存器,我們可以稱之為驅(qū)動相關(guān)代碼;與應(yīng)用相關(guān)的代碼卻只專注于邏輯和運(yùn)算, 我們可稱之為應(yīng)用相關(guān)代碼。
這種客觀存在的情況是單片機(jī)軟件分層最直接的依據(jù),所以說,將軟件劃分為驅(qū)動層和應(yīng)用層是程序功能分工的結(jié)果。那么驅(qū)動層和應(yīng)用層之間是如何銜接的呢?
在單片機(jī)系統(tǒng)中,信息的流動是雙向的,由內(nèi)向外是應(yīng)用層代碼主動發(fā)起的,實(shí)現(xiàn)信息向外流動很簡單, 應(yīng)用層代碼只需要調(diào)用驅(qū)動層代碼提供的 API 接口函數(shù)即可, 而
由外向內(nèi)則是外界主動發(fā)起的, 這時候應(yīng)用層代碼對于外界輸入需要被動的接收, 這里就涉及到一個
接收機(jī)制的問題,事件驅(qū)動機(jī)制足可勝任這個接收機(jī)制。
外界輸入可以理解為發(fā)生了事件,在單片機(jī)內(nèi)部直接的表現(xiàn)就是硬件生成了新的數(shù)據(jù),這些數(shù)據(jù)包含了事件的全部信息, 事件驅(qū)動機(jī)制的任務(wù)就是將這些數(shù)據(jù)初步處理(也可能不處理),然后告知應(yīng)用層代碼, 應(yīng)用代碼接到通知后把這些數(shù)據(jù)取走, 做最終的處理, 這樣一次事件的響應(yīng)就完成了。
說到這里,可能很多人突然會發(fā)現(xiàn),這種處理方法自己編程的時候早就用過了,只不過沒有使用“事件驅(qū)動” 這個文縐縐的名詞罷了。其實(shí)事件驅(qū)動機(jī)制本來就不神秘, 生活中數(shù)不勝數(shù)的例子足以說明它應(yīng)用的普遍性。下面的這個小例子是事件驅(qū)動機(jī)制在單片機(jī)程序中最常見的實(shí)現(xiàn)方法,假設(shè)某單片機(jī)系統(tǒng)用到了以下資源:
-
一個串口外設(shè) Uart0,用來接收串口數(shù)據(jù);
-
一個定時器外設(shè) Tmr0,用來提供周期性定時中斷;
-
一個外部中斷管腳 Exi0,用來檢測某種外部突發(fā)事件;
-
一個 I/O 端口 Port0,連接獨(dú)立式鍵盤,管理方式為定時掃描法,掛載到 Tmr0 的 ISR;
這樣,系統(tǒng)中可以提取出 4 類事件,分別是 UART、 TMR、 EXI、 KEY ,其中 UART 和KEY 事件發(fā)生后必須開辟緩存存儲事件相關(guān)的數(shù)據(jù)。所有事件的檢測都在各自的 ISR 中完成,然后 ISR 再通過
事件驅(qū)動機(jī)制通知主函數(shù)處理。
為了實(shí)現(xiàn) ISR 和主函數(shù)通信, 我們定義一個數(shù)據(jù)類型為INT8U的全局變量 g_u8EvntFlgGrp,稱為事件標(biāo)志組,里面的每一個 bit 位代表一類事件,如果該 bit 值為 0,表示此類事件沒有發(fā)生,如果該 bit 值為 1,則表示發(fā)生了此類事件,主函數(shù)必須及時地處理該事件。圖 5 所示為g_u8EvntFlgGrp 各個 bit 位的作用 。
程序清單 List9 所示就是按上面的規(guī)劃寫成的示例性代碼 。
程序清單List9:
#define FLG_UART 0x01
#define FLG_TMR 0x02
#define FLG_EXI 0x04
#define FLG_KEY 0x08
volatile INT8U g_u8EvntFlgGrp = 0; /*事件標(biāo)志組*/
INT8U read_envt_flg_grp(void);
/***************************************
*FuncName : main
*Description : 主函數(shù)
*Arguments : void
*Return : void
*****************************************/
void main(void)
{
INT8U u8FlgTmp = 0;
sys_init();
while(1)
{
u8FlgTmp = read_envt_flg_grp(); /*讀取事件標(biāo)志組*/
if(u8FlgTmp ) /*是否有事件發(fā)生? */
{
if(u8FlgTmp