?
正文
作者?|?Alicedodo
狀態(tài)機(jī)是一種思想,事件驅(qū)動(dòng)也是一種思想。
事件驅(qū)動(dòng)的概念
生活中有很多事件驅(qū)動(dòng)的例子,上自習(xí)瞞著老師偷睡覺(jué)就是很生動(dòng)的一個(gè)。
我們都是從高中時(shí)代走過(guò)來(lái)的,高中的學(xué)生苦啊,覺(jué)得睡覺(jué)是世界上最奢侈的東西, 有時(shí)候站著都能睡著??!老師看的嚴(yán),上課睡覺(jué)不允許啊,要挨批啊!有木有!相比而言,晚自習(xí)是比較寬松的,老師只是不定時(shí)來(lái)巡視,還是有機(jī)會(huì)偷偷睡一會(huì)兒的。
現(xiàn)在的問(wèn)題是,怎么睡才能既睡得好又不會(huì)讓老師發(fā)現(xiàn)呢??晚自習(xí)是比較寬松的,老師只是不定時(shí)來(lái)巡視,還是有機(jī)會(huì)偷偷睡一會(huì)兒的?,F(xiàn)在的問(wèn)題是,怎么睡才能既睡得好又不會(huì)讓老師發(fā)現(xiàn)呢?
我們現(xiàn)在有三種睡覺(jué)方案:
方案 A:倒頭就睡,管你三七二十一,睡夠了再說(shuō),要知道有時(shí)候老師可能一整晚上都不來(lái)的。
方案 B:間歇著睡,先定上鬧鐘, 5 分鐘響一次,響了就醒,看看老師來(lái)沒(méi)來(lái),沒(méi)來(lái)的話定上鬧鐘再睡,如此往復(fù)。
方案 C:睡之前讓同桌給放哨,然后自己睡覺(jué),什么也不用管,什么時(shí)候老師來(lái)了,就讓同桌戳醒你。
不管你們選擇的是哪種方案,我高中那會(huì)兒用的可是方案 C,安全又舒服。
方案 C 是很有特點(diǎn)的:本來(lái)自習(xí)課偷睡覺(jué)是你自己的事兒, 有沒(méi)有被老師抓著也是你自己的事兒,這些和同桌是毫無(wú)利害關(guān)系的,但是同桌這個(gè)環(huán)節(jié)對(duì)方案 C 的重要性是不言而喻的,他肩負(fù)著監(jiān)控老師巡視和叫醒睡覺(jué)者兩項(xiàng)重要任務(wù),是事件驅(qū)動(dòng)機(jī)制實(shí)現(xiàn)的重要組成部分 。
在事件驅(qū)動(dòng)機(jī)制中,對(duì)象對(duì)于外部事件總是處于“休眠” 狀態(tài)的,而把對(duì)外部事件的檢測(cè)和監(jiān)控交給了第三方組件。
一旦第三方檢測(cè)到外部事件發(fā)生, 它就會(huì)啟動(dòng)某種機(jī)制, 將對(duì)象從“休眠” 狀態(tài)中喚醒, 并將事件告知對(duì)象。對(duì)象接到通知后, 做出一系列動(dòng)作, 完成對(duì)本次事件響應(yīng),然后再次進(jìn)入“休眠” 狀態(tài),如此周而復(fù)始。
有沒(méi)有發(fā)現(xiàn),事件驅(qū)動(dòng)機(jī)制和單片機(jī)的中斷原理上很相似 。
事件驅(qū)動(dòng)與單片機(jī)編程
在我們?cè)倩氐絾纹瑱C(jī)系統(tǒng)中來(lái),看看事件驅(qū)動(dòng)思想在單片機(jī)程序設(shè)計(jì)中的應(yīng)用。當(dāng)我還是一個(gè)單片機(jī)菜鳥(niǎo)的時(shí)候(當(dāng)然,我至今也沒(méi)有成為單片機(jī)高手),網(wǎng)絡(luò)上的大蝦們就諄諄教導(dǎo):一個(gè)好的單片機(jī)程序是要分層的。曾經(jīng)很長(zhǎng)一段時(shí)間, 我對(duì)分層這個(gè)概念完全沒(méi)有感覺(jué)。
什么是程序分層?
程序?yàn)槭裁匆謱樱?/p>
應(yīng)該怎么給程序分層?
隨著手里的代碼量越來(lái)越多,實(shí)現(xiàn)的功能也越來(lái)越多,軟件分層這個(gè)概念在我腦子里逐漸地清晰起來(lái),我越來(lái)越佩服大蝦們的高瞻遠(yuǎn)矚。
單片機(jī)的軟件確實(shí)要分層的,最起碼要分兩層:驅(qū)動(dòng)層和應(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ì)、繁雜。
與硬件打交道的代碼只鐘情于時(shí)序和寄存器,我們可以稱之為驅(qū)動(dòng)相關(guān)代碼;與應(yīng)用相關(guān)的代碼卻只專注于邏輯和運(yùn)算, 我們可稱之為應(yīng)用相關(guān)代碼。
這種客觀存在的情況是單片機(jī)軟件分層最直接的依據(jù),所以說(shuō),將軟件劃分為驅(qū)動(dòng)層和應(yīng)用層是程序功能分工的結(jié)果。那么驅(qū)動(dòng)層和應(yīng)用層之間是如何銜接的呢?
在單片機(jī)系統(tǒng)中,信息的流動(dòng)是雙向的,由內(nèi)向外是應(yīng)用層代碼主動(dòng)發(fā)起的,實(shí)現(xiàn)信息向外流動(dòng)很簡(jiǎn)單, 應(yīng)用層代碼只需要調(diào)用驅(qū)動(dòng)層代碼提供的 API 接口函數(shù)即可, 而由外向內(nèi)則是外界主動(dòng)發(fā)起的, 這時(shí)候應(yīng)用層代碼對(duì)于外界輸入需要被動(dòng)的接收, 這里就涉及到一個(gè)接收機(jī)制的問(wèn)題,事件驅(qū)動(dòng)機(jī)制足可勝任這個(gè)接收機(jī)制。
外界輸入可以理解為發(fā)生了事件,在單片機(jī)內(nèi)部直接的表現(xiàn)就是硬件生成了新的數(shù)據(jù),這些數(shù)據(jù)包含了事件的全部信息, 事件驅(qū)動(dòng)機(jī)制的任務(wù)就是將這些數(shù)據(jù)初步處理(也可能不處理),然后告知應(yīng)用層代碼, 應(yīng)用代碼接到通知后把這些數(shù)據(jù)取走, 做最終的處理, 這樣一次事件的響應(yīng)就完成了。
說(shuō)到這里,可能很多人突然會(huì)發(fā)現(xiàn),這種處理方法自己編程的時(shí)候早就用過(guò)了,只不過(guò)沒(méi)有使用“事件驅(qū)動(dòng)” 這個(gè)文縐縐的名詞罷了。其實(shí)事件驅(qū)動(dòng)機(jī)制本來(lái)就不神秘, 生活中數(shù)不勝數(shù)的例子足以說(shuō)明它應(yīng)用的普遍性。下面的這個(gè)小例子是事件驅(qū)動(dòng)機(jī)制在單片機(jī)程序中最常見(jiàn)的實(shí)現(xiàn)方法,假設(shè)某單片機(jī)系統(tǒng)用到了以下資源:
一個(gè)串口外設(shè) Uart0,用來(lái)接收串口數(shù)據(jù);
一個(gè)定時(shí)器外設(shè) Tmr0,用來(lái)提供周期性定時(shí)中斷;
一個(gè)外部中斷管腳 Exi0,用來(lái)檢測(cè)某種外部突發(fā)事件;
一個(gè) I/O 端口 Port0,連接獨(dú)立式鍵盤(pán),管理方式為定時(shí)掃描法,掛載到 Tmr0 的 ISR;
這樣,系統(tǒng)中可以提取出 4 類事件,分別是 UART、 TMR、 EXI、 KEY ,其中 UART 和KEY 事件發(fā)生后必須開(kāi)辟緩存存儲(chǔ)事件相關(guān)的數(shù)據(jù)。所有事件的檢測(cè)都在各自的 ISR 中完成,然后 ISR 再通過(guò)事件驅(qū)動(dòng)機(jī)制通知主函數(shù)處理。
為了實(shí)現(xiàn) ISR 和主函數(shù)通信, 我們定義一個(gè)數(shù)據(jù)類型為INT8U的全局變量 g_u8EvntFlgGrp,稱為事件標(biāo)志組,里面的每一個(gè) bit 位代表一類事件,如果該 bit 值為 0,表示此類事件沒(méi)有發(fā)生,如果該 bit 值為 1,則表示發(fā)生了此類事件,主函數(shù)必須及時(shí)地處理該事件。圖 5 所示為g_u8EvntFlgGrp 各個(gè) bit 位的作用 。
程序清單 List9 所示就是按上面的規(guī)劃寫(xiě)成的示例性代碼 。
程序清單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?&?FLG_UART) ???{ ????action_uart();?/*處理串口事件*/ ???} ???if(u8FlgTmp?&?FLG_TMR) ???{ ????action_tmr();?/*處理定時(shí)中斷事件*/ ???} ???if(u8FlgTmp?&?FLG_EXI) ???{ ????action_exi();?/*處理外部中斷事件*/ ???} ???if(u8FlgTmp?&?FLG_KEY) ???{ ????action_key();?/*處理?yè)翩I事件*/ ???} ??} ??else ??{ ???;/*idle?code*/ ??} ?} } /********************************************* *FuncName?:?read_envt_flg_grp *Description?:?讀取事件標(biāo)志組?g_u8EvntFlgGrp?, *?讀取完畢后將其清零。 *Arguments?:?void *Return?:?void *********************************************/ INT8U?read_envt_flg_grp(void) { ?INT8U?u8FlgTmp?=?0; ?gbl_int_disable(); ?u8FlgTmp?=?g_u8EvntFlgGrp;?/*讀取標(biāo)志組*/ ?g_u8EvntFlgGrp?=?0;?/*清零標(biāo)志組*/ ?gbl_int_enable(); ?return?u8FlgTmp; } /********************************************* *FuncName?:?uart0_isr *Description?:?uart0?中斷服務(wù)函數(shù) *Arguments?:?void *Return?:?void *********************************************/ void?uart0_isr(void) { ?...... ?push_uart_rcv_buf(new_rcvd_byte);?/*新接收的字節(jié)存入緩沖區(qū)*/ ?gbl_int_disable(); ?g_u8EvntFlgGrp?|=?FLG_UART;?/*設(shè)置?UART?事件標(biāo)志*/ ?gbl_int_enable(); ?...... } /********************************************* *FuncName?:?tmr0_isr *Description?:?timer0?中斷服務(wù)函數(shù) *Arguments?:?void *Return?:?void *********************************************/ void?tmr0_isr(void) { ?INT8U?u8KeyCode?=?0; ?...... ?gbl_int_disable(); ?g_u8EvntFlgGrp?|=?FLG_TMR;?/*設(shè)置?TMR?事件標(biāo)志*/ ?gbl_int_enable(); ?...... ?u8KeyCode?=?read_key();?/*讀鍵盤(pán)*/ ?if(u8KeyCode)?/*有擊鍵操作??*/ ?{ ??push_key_buf(u8KeyCode);?/*新鍵值存入緩沖區(qū)*/ ??gbl_int_disable(); ??g_u8EvntFlgGrp?|=?FLG_KEY;?/*設(shè)置?TMR?事件標(biāo)志*/ ??gbl_int_enable(); ?} ?...... } /********************************************* *FuncName?:?exit0_isr *Description?:?exit0?中斷服務(wù)函數(shù) *Arguments?:?void *Return?:?void *********************************************/ void?exit0_isr(void) { ?...... ?gbl_int_disable(); ?g_u8EvntFlgGrp?|=?FLG_EXI;?/*設(shè)置?EXI?事件標(biāo)志*/ ?gbl_int_enable(); ?...... }
?
看一下程序清單 List9 這樣的程序結(jié)構(gòu),是不是和自己寫(xiě)過(guò)的某些程序相似?對(duì)于事件驅(qū)動(dòng)機(jī)制的這種實(shí)現(xiàn)方式, 我們還可以做得更絕一些, 形成一個(gè)標(biāo)準(zhǔn)的代碼模板,做一個(gè)包含位段和函數(shù)指針數(shù)組的結(jié)構(gòu)體,位段里的每一個(gè)元素作為圖 5 那樣的事件標(biāo)志位,然后在函數(shù)指針數(shù)組中放置各個(gè)事件處理函數(shù)的函數(shù)地址, 每個(gè)處理函數(shù)對(duì)應(yīng)位段里的每個(gè)標(biāo)志位。
這樣, main()函數(shù)中的事件處理代碼就可以做成標(biāo)準(zhǔn)的框架代碼。應(yīng)該說(shuō),這樣的實(shí)現(xiàn)方式是很好的,足以輕松地應(yīng)對(duì)實(shí)際應(yīng)用中絕大多數(shù)的情況。但是,事件驅(qū)動(dòng)機(jī)制用這樣的方式實(shí)現(xiàn)真的是完美的么?在我看來(lái),這種實(shí)現(xiàn)方式至少存在兩個(gè)問(wèn)題:
不同事件集中爆發(fā)時(shí),無(wú)法記錄事件發(fā)生的前后順序。
同一事件集中爆發(fā)時(shí),容易遺漏后面發(fā)生的那次事件。
圖 6 所示為某一時(shí)段單片機(jī)程序的執(zhí)行情況,某些特殊情況下,會(huì)出現(xiàn)上面提到的兩個(gè)問(wèn)題。
圖中, f1 為某事件的處理函數(shù), f2 為另一事件的處理函數(shù), I1、 I2、 I3 為 3 個(gè)不同事件觸發(fā)的 ISR,假定 I1、 I2、 I3 分別對(duì)應(yīng)事件 E1、 E2、 E3。從圖中可以看出,主函數(shù)在調(diào)用事件處理函數(shù) f1 的時(shí)候,發(fā)生了 2 次事件,主函數(shù)被 I1和 I2 中斷了 2 次, I1 和 I2 執(zhí)行的時(shí)候各自置位了相應(yīng)的事件標(biāo)志位。
函數(shù) f1 返回后, 主函數(shù)又調(diào)用了另一個(gè)事件處理函數(shù) f2, f2 執(zhí)行期間先后發(fā)生了 2 次同樣的事件, f2 被 I3 中斷了 2次,對(duì)應(yīng)的事件標(biāo)志位被連續(xù)置位了 2 次。
在圖 6 中我們當(dāng)然可以看出 I1 先于 I2 執(zhí)行,即事件 E1 發(fā)生在事件 E2 之前,但是主函數(shù)再次讀取事件標(biāo)志組 g_u8EvntFlgGrp 的時(shí)候, 看到的是兩個(gè)“同時(shí)” 被置位的標(biāo)志位, 無(wú)法判斷出事件 E1 和 E2 發(fā)生的先后順序, 也就是說(shuō)有關(guān)事件發(fā)生先后順序的信息丟失了, 這就是前面說(shuō)的第 1 個(gè)問(wèn)題:不同事件集中爆發(fā)時(shí),無(wú)法記錄事件發(fā)生的前后順序。
在程序清單 List9 中, 主函數(shù)在處理事件時(shí), 按照程序預(yù)先設(shè)定好的順序, 一個(gè)一個(gè)地處理發(fā)生的事件, 如果不同事件某時(shí)段集中爆發(fā), 可能會(huì)出現(xiàn)事件的發(fā)生順序和事件的處理順序不一致的情況。倘若系統(tǒng)功能對(duì)事件的發(fā)生順序敏感,那么程序清單 List9 中的程序就不能滿足要求了。
同樣的道理,如果 I3 對(duì)應(yīng)的事件 E3 是程序清單 List9 中 EXI 那樣的事件(這種事件沒(méi)有緩沖機(jī)制), 事件 E3 第 2 次的發(fā)生就被遺漏了, 這就是前面所說(shuō)的第 2 個(gè)問(wèn)題:同一事件集中爆發(fā)時(shí),容易遺漏后后面發(fā)生的事件。
如果系統(tǒng)功能對(duì)事件 E3 的發(fā)生次數(shù)敏感,程序清單 List9 中的程序也是不能滿足要求的。既然事件驅(qū)動(dòng)機(jī)制這樣的實(shí)現(xiàn)方式存在缺陷, 那么有沒(méi)有一種更好的實(shí)現(xiàn)方式呢?當(dāng)然有!把事件轉(zhuǎn)換成消息存入消息隊(duì)列就能完美解決這個(gè)問(wèn)題, 只不過(guò)大家不要對(duì)我這種自導(dǎo)自演的行文方式產(chǎn)生反感就好 ?。
事件驅(qū)動(dòng)與消息
什么是消息?消息是數(shù)據(jù)信息的一種存儲(chǔ)形式。從程序的角度看,消息就是一段存儲(chǔ)著特定數(shù)據(jù)的內(nèi)存塊, 數(shù)據(jù)的存儲(chǔ)格式是設(shè)計(jì)者預(yù)先約定好的, 只要按照約定的格式讀取這段內(nèi)存, 就能獲得消息所承載的有用信息。
消息是有時(shí)效性的。任何一個(gè)消息實(shí)體都是有生命周期的,它從誕生到消亡先后經(jīng)歷了生成、 存儲(chǔ)、 派發(fā)、 消費(fèi)共 4 個(gè)階段:消息實(shí)體由生產(chǎn)者生成, 由管理者負(fù)責(zé)存儲(chǔ)和派發(fā), 最后由消費(fèi)者消費(fèi)。
被消費(fèi)者消費(fèi)之后, 這個(gè)消息就算消亡了, 雖然存儲(chǔ)消息實(shí)體的內(nèi)存中可能還殘留著原來(lái)的數(shù)據(jù), 但是這些數(shù)據(jù)對(duì)于系統(tǒng)來(lái)講已經(jīng)沒(méi)有任何意義了, 這也就是消息的時(shí)效性。說(shuō)到這里,大家有沒(méi)有發(fā)現(xiàn),這里的“消息” 和前面一直在說(shuō)的“事件” 是不是很相似?把“消息” 的這些特點(diǎn)套用在“事件” 身上是非常合適的, 在我看來(lái), 消息無(wú)非是事件的一個(gè)馬甲而已。
我們?cè)谠O(shè)計(jì)單片機(jī)程序的時(shí)候,都懷著一個(gè)夢(mèng)想,即讓程序?qū)κ录捻憫?yīng)盡可能的快,理想的情況下,程序?qū)κ录⒓错憫?yīng),不能有任何延遲。這當(dāng)然是不可能的,當(dāng)事件發(fā)生時(shí),程序總會(huì)因?yàn)檫@樣那樣的原因不能立即響應(yīng)事件。
為了不至于丟失事件,我們可以先在事件相關(guān)的 ISR 中把事件加工成消息并把它存儲(chǔ)在消息緩沖區(qū)里, ISR 做完這些后立即退出。主程序忙完了別的事情之后,去查看消息緩沖區(qū),把剛才 ISR 存儲(chǔ)的消息讀出來(lái), 分析出事件的有關(guān)信息, 再轉(zhuǎn)去執(zhí)行相應(yīng)的響應(yīng)代碼, 最終完成對(duì)本次事件的響應(yīng)。
只要整個(gè)過(guò)程的時(shí)間延遲在系統(tǒng)功能容許的范圍之內(nèi), 這樣處理就沒(méi)有問(wèn)題。將事件轉(zhuǎn)化為消息,體現(xiàn)了以空間換時(shí)間的思想。再插一句,雖然事件發(fā)生后對(duì)應(yīng)的 ISR 立即被觸發(fā),但是這不是嚴(yán)格意義上的“響應(yīng)”, 頂多算是對(duì)事件的“記錄”, “記錄” 和“響應(yīng)” 是不一樣的。事件是一種客觀的存在,而消息則是對(duì)這種客觀存在的記錄。
對(duì)于系統(tǒng)輸入而言,事件是其在時(shí)間維度上的描述;消息是其在空間維度上的描述,所以,在描述系統(tǒng)輸入這一功能上,事件和消息是等價(jià)的。對(duì)比一下程序清單 List9 中的實(shí)現(xiàn)方式, 那里是用全局標(biāo)志位的方式記錄事件, 對(duì)于某些特殊的事件還配備了專門(mén)的緩沖區(qū), 用來(lái)存儲(chǔ)事件的額外信息, 而這些額外信息單靠全局標(biāo)志位是無(wú)法記錄的。
現(xiàn)在我們用消息+消息緩沖區(qū)的方式來(lái)記錄事件,消息緩沖區(qū)就成了所有事件共用的緩沖區(qū),無(wú)論發(fā)生的事件有沒(méi)有額外的信息,一律以消息的形式存入緩沖區(qū) ?。
為了記錄事件發(fā)生的先后順序,消息緩沖區(qū)應(yīng)該做成以“先入先出” 的方式管理的環(huán)形緩沖隊(duì)列。事件生成的消息總是從隊(duì)尾入隊(duì),管理程序讀取消息的時(shí)候總是從隊(duì)頭讀取,這樣,消息在緩沖區(qū)中存儲(chǔ)的順序就是事件在時(shí)間上發(fā)生的順序,先發(fā)生的事件總是能先得到響應(yīng)。
一條消息被讀取之后, 管理程序回收存儲(chǔ)這個(gè)消息的內(nèi)存, 將其作為空閑節(jié)點(diǎn)再插入緩沖隊(duì)列的隊(duì)尾,用以存儲(chǔ)將來(lái)新生成的消息。圖 7 所示為使用了消息功能的事件驅(qū)動(dòng)機(jī)制示意圖。不知道有沒(méi)有人對(duì)圖中的“消費(fèi)者”有疑問(wèn), 這個(gè)“消費(fèi)者” 在程序中指的是什么呢?
既然這個(gè)事件/消息驅(qū)動(dòng)機(jī)制是為系統(tǒng)應(yīng)用服務(wù)的, 消費(fèi)者當(dāng)然就是指應(yīng)用層的代碼了, 更明確一點(diǎn)兒的話, 消費(fèi)者就是應(yīng)用代碼中的狀態(tài)機(jī) ?。
用消息的方法來(lái)實(shí)現(xiàn)事件驅(qū)動(dòng)機(jī)制完全解決了前面提到的那兩個(gè)問(wèn)題,即不同事件集中爆發(fā)時(shí),無(wú)法記錄事件發(fā)生的前后順序。同一事件集中爆發(fā)時(shí),容易遺漏后面發(fā)生的那次事件。對(duì)于第一種情況,消息(事件)在緩沖隊(duì)列中是以“先入先出” 的方式存儲(chǔ)的,存儲(chǔ)順序就代表了事件發(fā)生的先后順序。
對(duì)于第二種情況, 任何被 ISR 捕捉到的事件都會(huì)以一個(gè)獨(dú)立的消息實(shí)體存入緩沖隊(duì)列, 即使前后兩個(gè)是同一個(gè)事件, 只要 ISR 反應(yīng)夠快就不會(huì)遺漏事件。實(shí)際上, ISR 的主要工作就是填寫(xiě)消息實(shí)體, 然后將其存入緩沖隊(duì)列, 做這些工作只占用 CPU 很短的時(shí)間。
接下來(lái)再說(shuō)一說(shuō)這個(gè)消息機(jī)制在程序中如何實(shí)現(xiàn)。在程序中,消息機(jī)制可以看做是一個(gè)獨(dú)立的功能模塊,一個(gè)功能模塊的實(shí)現(xiàn)無(wú)非就是數(shù)據(jù)結(jié)構(gòu)+算法。先來(lái)看消息機(jī)制的數(shù)據(jù)結(jié)構(gòu)。這里的數(shù)據(jù)結(jié)構(gòu)是指和消息機(jī)制有關(guān)的數(shù)據(jù)組織形式,包含 2 個(gè)部分:
消息節(jié)點(diǎn)自身的數(shù)據(jù)組織形式
消息緩沖區(qū)的數(shù)據(jù)組織形式
程序清單 List10 所示就是消息機(jī)制的數(shù)據(jù)結(jié)構(gòu) 。
「程序清單List10:」
?
typedef?union?msg_arg?/*消息參數(shù)共用體*/ { ?INT8U?u8Arg;?/*成員:8 位無(wú)符號(hào)*/ ?INT8U?s8Arg;?/*成員:8 位有符號(hào)*/ ?#if?CFG_MSG_ARG_INT16_EN>0 ?INT16U?u16Arg;?/*可選成員:16 位無(wú)符號(hào)*/ ?INT16S?s16Arg;?/*可選成員:16 位有符號(hào)*/ ?#endif ?#if?CFG_MSG_ARG_INT32_EN>0 ?INT32U?u32Arg;?/*可選成員:32 位無(wú)符號(hào)*/ ?INT32S?s32Arg;?/*可選成員:32 位有符號(hào)*/ ?#endif ?#if?CFG_MSG_ARG_FP32_EN>0 ?FP32?f32Arg;?/*可選成員:32 位單精度浮點(diǎn)*/ ?#endif ?#if?CFG_MSG_ARG_PTR_EN>0 ?void*?pArg;?/*可選成員:void 指針*/ ?#endif }MSG_ARG; typedef?struct?_msg?/*消息結(jié)構(gòu)體*/ { ?INT8U?u8MsgID;?/*消息?ID*/ ?#if?CFG_MSG_USR_SUM?>?1 ?INT8U?u8UsrID;?/*消費(fèi)者?ID*/ ?#endif ?MSG_ARG?uMsgArg;?/*應(yīng)用消息參數(shù)*/ }?MSG; typedef?struct?msg_box?/*消息緩沖區(qū)結(jié)構(gòu)體*/ { ?INT8U?u8MBLock;?/*隊(duì)列上鎖標(biāo)識(shí)*/ ?INT8U?u8MsgSum;?/*隊(duì)列長(zhǎng)度*/ ?INT8U?u8MQHead;?/*隊(duì)列頭結(jié)點(diǎn)位置*/ ?INT8U?u8MQTail;?/*隊(duì)列尾節(jié)點(diǎn)位置*/ ?MSG?arMsgBox[CFG_MSG_SUM_MAX];?/*存放隊(duì)列的數(shù)組*/ }?MB; static?MB?g_stMsgUnit;?/*消息管理單元全局變量*/
?
消息的數(shù)據(jù)結(jié)構(gòu)包含 2 部分:消息頭和消息參數(shù),在消息結(jié)構(gòu)體 MSG 中, u8MsgID 和u8UsrID 就是消息頭,共用體 MSG_ARG 就是消息參數(shù)。
u8MsgID 是消息的類型標(biāo)志,也就是生成此消息的事件的事件類型標(biāo)志,程序根據(jù)這個(gè)成員選擇對(duì)應(yīng)的事件處理函數(shù);u8UsrID 是消息的消費(fèi)者代號(hào), 如果應(yīng)用代碼中只有一個(gè)消費(fèi)者,則成員 u8UsrID 可以忽略。MSG_ARG 就是消息附帶的參數(shù),也就是事件的內(nèi)容信息。
系統(tǒng)中的事件是多種多樣的,有的事件只需要類型標(biāo)志即可, 有的事件可能還需要整型變量存儲(chǔ)事件內(nèi)容, 還有的事件可能需要大塊的內(nèi)存來(lái)存儲(chǔ)一些附帶的數(shù)據(jù)。為了將各種類型的事件生成的消息納入統(tǒng)一管理, 要求 MSG_ARG 必須能存儲(chǔ)各種類型的數(shù)據(jù),因此 MSG_ARG 被定義成了共用體。
從程序清單 List10 中可以看出, MSG_ARG 既可以存儲(chǔ) 8 位~32 位有符號(hào)無(wú)符號(hào)整型數(shù)據(jù),又可以存儲(chǔ)單精度浮點(diǎn), 還可以存儲(chǔ) void* 型的指針變量, 而 void*的指針又可以強(qiáng)制轉(zhuǎn)換成任意類型的指針,所以 MSG_ARG 可以存儲(chǔ)指向任意類型的指針。
對(duì)于MSG_ARG中的某些成員, 還配備了預(yù)編譯常量 CFG_MSG_ARG_XXX_EN加以控制,如果實(shí)際應(yīng)用中不需要這些耗費(fèi)內(nèi)存較大的數(shù)據(jù)類型, 可以設(shè)置CFG_MSG_ARG_XXX_EN 去掉它們。全開(kāi)的情況下, 每個(gè)消息節(jié)點(diǎn)占用 6 個(gè)字節(jié)的內(nèi)存, 最精簡(jiǎn)的情況下, 每個(gè)消息節(jié)點(diǎn)只占用 2 個(gè)字節(jié)。
全局結(jié)構(gòu)體變量 g_stMsgUnit 是消息緩沖區(qū)的數(shù)據(jù)結(jié)構(gòu)。消息緩沖區(qū)是一個(gè)環(huán)形緩沖隊(duì)列,這里將環(huán)形隊(duì)列放在了一個(gè)一維數(shù)組中,也就是g_stMsgUnit 的成員 arMsgBox[],數(shù)組元素的數(shù)據(jù)類型就是消息結(jié)構(gòu)體 MSG ,數(shù)組的大小由預(yù)編譯常量 CFG_MSG_SUM_MAX 控制,該常量是環(huán)形緩沖隊(duì)列的最大容量。
理論上, CFG_MSG_SUM_MAX 值的選取越大越好,但考慮到單片機(jī)的 RAM 資源有CFG_MSG_SUM_MAX 值的選取要在資源消耗和實(shí)際最大需求之間折中, 只要能保證在最壞情況下環(huán)形緩沖隊(duì)列仍有裕量即可。用數(shù)組實(shí)現(xiàn)環(huán)形隊(duì)列還需要一些輔助變量,也就是 g_stMsgUnit 剩余的成員。
u8MBLock 是隊(duì)列的控制變量, u8MBLock>0 表示隊(duì)列處于鎖定/保護(hù)狀態(tài),不能寫(xiě)也不能讀, u8MBLock=0 表示隊(duì)列處于正常狀態(tài),可寫(xiě)可讀;u8MsgSum 是隊(duì)列長(zhǎng)度計(jì)數(shù)器,記錄著當(dāng)前隊(duì)列中存有多少條消息,存入一條消息u8MsgSum++,讀出一條消息 u8MsgSum--;
u8MQHead 記錄著當(dāng)前隊(duì)頭消息節(jié)點(diǎn)在數(shù)組 arMsgBox[]中的位置,其值就是數(shù)組元素的下標(biāo),消息讀取的時(shí)候所讀出的就是 u8MQHead 所指向的節(jié)點(diǎn),讀完之后 u8MQHead 向隊(duì)尾方向移動(dòng)一個(gè)位置,指向新的隊(duì)頭節(jié)點(diǎn);u8MQTail 記錄著當(dāng)前隊(duì)尾消息節(jié)點(diǎn)在數(shù)組 arMsgBox[]中的位置,其值是數(shù)組元素的下標(biāo),新消息寫(xiě)入之前, u8MQTail 向隊(duì)尾方向后移一個(gè)位置, 然后新寫(xiě)入的消息存入 u8MQTail 所指向的空閑節(jié)點(diǎn);
圖 8 所示為消息緩沖區(qū)結(jié)構(gòu)體變量 g_stMsgUnit 的示意圖 。
有了數(shù)據(jù)結(jié)構(gòu),還要有對(duì)應(yīng)的算法實(shí)現(xiàn),消息機(jī)制的數(shù)據(jù)主體就是一個(gè)數(shù)組化了的環(huán)形隊(duì)列,環(huán)形隊(duì)列的算法就是我們所要的算法。消息機(jī)制是一個(gè)獨(dú)立的功能模塊,應(yīng)該對(duì)外屏蔽其內(nèi)部實(shí)現(xiàn)細(xì)節(jié),而僅對(duì)外界開(kāi)放一定數(shù)量的接口函數(shù),外界通過(guò)調(diào)用這些接口來(lái)使用消息功能,這也就是我在聲明 g_stMsgUnit 變量的時(shí)候使用了 static 關(guān)鍵詞的原因。
消息模塊的接口函數(shù)一共有 9 個(gè):
void mq_init(void)?消息隊(duì)列初始化,負(fù)責(zé)初始化 g_stMsgUnit 。
void mq_clear(void)清空消息隊(duì)列,效果同 mq_init(),可有可無(wú)。
void mq_lock(void)消息隊(duì)列鎖定,鎖定的消息隊(duì)列不可讀不可寫(xiě)。
void mq_unlock(void)消息隊(duì)列解鎖,解鎖后消息隊(duì)列恢復(fù)正常功能。
BOOL mq_is_empty(void)消息隊(duì)列判空,返回 TRUE 表示消息隊(duì)列當(dāng)前為空,返回 FALSE 表示有消息存儲(chǔ)。
INT8U mq_get_msg_cur_sum(void)查詢消息隊(duì)列中當(dāng)前存儲(chǔ)的消息總數(shù),函數(shù)返回值為查詢結(jié)果
INT8U mq_get_msg_sum_max(void)查詢消息隊(duì)列的最大容量,函數(shù)返回值為查詢結(jié)果。
INT8U mq_msg_post_fifo(MSG* pMsg)向消息隊(duì)列中寄送消息,方式為先入先出,形參 pMsg 指向消息的備份內(nèi)存,函數(shù)返回操作結(jié)果。該函數(shù)多被 ISR 調(diào)用,所以必須為可重入函數(shù)。
INT8U mq_msg_req_fifo(MSG* pMsg)從消息隊(duì)列中讀取消息, 方式為先入先出, 函數(shù)將讀出的消息存入形參 pMsg 指向的內(nèi)存,函數(shù)返回操作結(jié)果。該函數(shù)被主程序調(diào)用, 可以不是可重入函數(shù), 但要對(duì)共享數(shù)據(jù)進(jìn)行臨界保護(hù) ?。
事件/消息驅(qū)動(dòng)機(jī)制是一個(gè)標(biāo)準(zhǔn)的通用的框架,配合 ISR,對(duì)任何系統(tǒng)輸入都能應(yīng)對(duì)自如。事件/消息驅(qū)動(dòng)機(jī)制屏蔽了應(yīng)用層程序獲取各種系統(tǒng)輸入的工作細(xì)節(jié),將系統(tǒng)輸入抽象整合, 以一種標(biāo)準(zhǔn)統(tǒng)一的格式提交應(yīng)用代碼處理, 極大地減輕了應(yīng)用層代碼獲取系統(tǒng)輸入的負(fù)擔(dān), 應(yīng)用層只需要專注于高級(jí)功能的實(shí)現(xiàn)就可以了。
從軟件分層的角度來(lái)看, 事件/消息驅(qū)動(dòng)機(jī)制相當(dāng)于驅(qū)動(dòng)層和應(yīng)用層之間的中間層, 這樣的層次結(jié)構(gòu)如圖 9 。
圖9 中之所以驅(qū)動(dòng)層和應(yīng)用層之間還有接觸,是因?yàn)橄到y(tǒng)輸出響應(yīng)的時(shí)候,應(yīng)用層可能還需要直接調(diào)用驅(qū)動(dòng)層提供的函數(shù)接口。如果一個(gè)單片機(jī)的軟件是圖 9 這樣的結(jié)構(gòu),并且應(yīng)用層的程序使用狀態(tài)機(jī)來(lái)實(shí)現(xiàn),在消息的驅(qū)動(dòng)下使應(yīng)用層的狀態(tài)機(jī)運(yùn)轉(zhuǎn)起來(lái), 那么這個(gè)軟件的設(shè)計(jì)思想就是整篇文章的主題:基于事件/消息驅(qū)動(dòng)+狀態(tài)機(jī)結(jié)構(gòu)的裸奔通用框架 。
程序框架:狀態(tài)機(jī)+事件/消息驅(qū)動(dòng)
事件/消息驅(qū)動(dòng)和狀態(tài)機(jī)是天生的搭檔,這對(duì)黃金組合是分析問(wèn)題解決問(wèn)題的利器 ?。
1、牛刀小試
規(guī)則描述:
L1L2 狀態(tài)轉(zhuǎn)換順序 OFF/OFF--->ON/OFF--->ON/ON--->OFF/ON--->OFF/OFF
通過(guò)按鍵控制 L1L2 的狀態(tài),每次狀態(tài)轉(zhuǎn)換只需按鍵 1 次
從上一次按鍵的時(shí)刻開(kāi)始計(jì)時(shí),如果 10 秒鐘之內(nèi)沒(méi)有按鍵事件,則不管當(dāng)前 L1L2 狀態(tài)如何,一律恢復(fù)至初始狀態(tài)。
L1L2 的初始狀態(tài) OFF/OFF
現(xiàn)在我們用狀態(tài)機(jī)+事件/消息驅(qū)動(dòng)的思想來(lái)分析問(wèn)題。系統(tǒng)中可提取出兩個(gè)事件:按鍵事件和超時(shí)事件,分別用事件標(biāo)志 KEY 和 TOUT 代替。L1L2 的狀態(tài)及轉(zhuǎn)換關(guān)系可做成一個(gè)狀態(tài)機(jī),稱為主狀態(tài)機(jī),共 4 個(gè)狀態(tài):LS_OFFOFF、LS_ONOFF、 LS_ONON、 LS_OFFON 。主狀態(tài)機(jī)會(huì)在事件 KEY 或 TOUT 的驅(qū)動(dòng)下發(fā)生狀態(tài)遷移,各個(gè)狀態(tài)之間的轉(zhuǎn)換關(guān)系比較簡(jiǎn)單,在此略過(guò)。
事件/消息驅(qū)動(dòng)機(jī)制的任務(wù)就是檢測(cè)監(jiān)控事件 KEY 和 TOUT,并提交給主狀態(tài)機(jī)處理。檢測(cè)按鍵需要加入消抖處理,消抖時(shí)間定為 20ms, 10S 超時(shí)檢測(cè)需要一個(gè)定時(shí)器進(jìn)行計(jì)時(shí)。
這里將按鍵檢測(cè)程序部分也做成一個(gè)狀態(tài)機(jī),共有 3 個(gè)狀態(tài):
WAIT_DOWN :空閑狀態(tài),等待按鍵按下
SHAKE :初次檢測(cè)到按鍵按下,延時(shí)消抖
WAIT_UP :消抖結(jié)束,確認(rèn)按鍵已按下,等待按鍵彈起
按鍵狀態(tài)機(jī)的轉(zhuǎn)換關(guān)系可在圖 10 中找到。按鍵檢測(cè)和超時(shí)檢測(cè)共用一個(gè)定時(shí)周期為 20ms 的定時(shí)中斷,這樣就可以把按鍵檢測(cè)和超時(shí)檢測(cè)的代碼全部放在這個(gè)定時(shí)中斷的 ISR 中。我把這個(gè)中斷事件用 TICK 標(biāo)記, 按鍵狀態(tài)機(jī)在 TICK 的驅(qū)動(dòng)下運(yùn)行, 按鍵按下且消抖完畢后觸發(fā) KEY 事件, 而超時(shí)檢測(cè)則對(duì) TICK 進(jìn)行軟時(shí)鐘計(jì)數(shù),記滿 500 個(gè) TICK 則超時(shí) 10S,觸發(fā) TOUT 事件。
有了上面的分析,實(shí)現(xiàn)這個(gè)功能的程序的結(jié)構(gòu)就十分清晰了, 圖 10 是這個(gè)程序的結(jié)構(gòu)示意圖,這張圖表述問(wèn)題足夠清晰了,具體的代碼就不寫(xiě)了。仔細(xì)瞅瞅,是不是有點(diǎn)兒那個(gè)意思了?
如果忽略定時(shí)中斷 ISR 中的細(xì)節(jié),圖 10 中的整個(gè)程序結(jié)構(gòu)就是事件/消息驅(qū)動(dòng)+主狀態(tài)機(jī)的結(jié)構(gòu), ISR 是消息的生產(chǎn)者,與消息緩沖、派發(fā)相關(guān)的程序部分是管理者,而主狀態(tài)機(jī)則是消息的消費(fèi)者,應(yīng)用層代碼中只有這一個(gè)狀態(tài)機(jī),是消息的唯一消費(fèi)者。
這個(gè)結(jié)構(gòu)就是通用框架 GF1.0 的標(biāo)準(zhǔn)結(jié)構(gòu):多個(gè) ISR + 一個(gè)消息緩沖區(qū) + 一個(gè)應(yīng)用層主狀態(tài)機(jī)。ISR 生成的消息(事件)全部提交主狀態(tài)機(jī)處理, 在消息的驅(qū)動(dòng)下主狀態(tài)機(jī)不斷地遷移。
如果把應(yīng)用層主狀態(tài)機(jī)看做是一臺(tái)發(fā)動(dòng)機(jī), 那么 ISR 生成的消息則是燃料, 事件不斷的發(fā)生, 消息不斷的生成,有了燃料(消息)的供給,發(fā)動(dòng)機(jī)(主狀態(tài)機(jī))就能永不停息地運(yùn)轉(zhuǎn)。
接下來(lái)關(guān)注一下圖 10 中的 ISR, 這個(gè) ISR 里面的內(nèi)容是很豐富的, 里面還套著 2 個(gè)小狀態(tài)機(jī):按鍵狀態(tài)機(jī)和計(jì)時(shí)狀態(tài)機(jī)。按鍵狀態(tài)機(jī)自不必說(shuō), 這個(gè)計(jì)時(shí)部分也可以看做是一個(gè)狀態(tài)機(jī),不過(guò)這個(gè)狀態(tài)機(jī)比較特殊,只有一個(gè)狀態(tài) DELAY。
既然是狀態(tài)機(jī), 想要跑起來(lái)就需要有事件來(lái)驅(qū)動(dòng), 在這個(gè) ISR 里, 定時(shí)器的中斷事件 TICK就是按鍵狀態(tài)機(jī)和計(jì)時(shí)狀態(tài)機(jī)的驅(qū)動(dòng),只不過(guò)這兩個(gè)事件驅(qū)動(dòng)+狀態(tài)機(jī)結(jié)構(gòu)沒(méi)有消息緩沖,當(dāng)然也不需要消息緩沖,因?yàn)闋顟B(tài)機(jī)在 ISR 中,對(duì)事件是立即響應(yīng)的。
從宏觀上看,圖 10 中是事件/消息驅(qū)動(dòng)+狀態(tài)機(jī),從微觀上看,圖 10 中的 ISR 也是事件驅(qū)動(dòng)+狀態(tài)機(jī)。ISR 中的狀態(tài)機(jī)在遷移過(guò)程中生成消息(事件),而這些消息(事件)對(duì)于主狀態(tài)機(jī)來(lái)講又是它自己的驅(qū)動(dòng)事件。事件的級(jí)別越高, 事件自身也就越抽象, 描述的內(nèi)容也就越接近人的思維方式。我覺(jué)得這種你中有我我中有你的特點(diǎn)正是事件驅(qū)動(dòng)+狀態(tài)機(jī)的精髓所在 ?。
2、通用框架GF1.0
前面說(shuō)過(guò), 狀態(tài)機(jī)總是被動(dòng)地接受事件, 而 ISR 也只是負(fù)責(zé)將消息(事件)送入消息緩沖區(qū),這些消息僅僅是數(shù)據(jù),自己肯定不會(huì)主動(dòng)地跑去找狀態(tài)機(jī)。那么存儲(chǔ)在緩沖區(qū)中的消息(事件)是怎么被發(fā)送到目標(biāo)狀態(tài)機(jī)呢?
把消息從緩沖區(qū)中取出并送到對(duì)應(yīng)的狀態(tài)機(jī)處理,這是狀態(tài)機(jī)調(diào)度程序的任務(wù),我把這部分程序稱作狀態(tài)機(jī)引擎(State Machine Engine , 簡(jiǎn)寫(xiě)作 SME)。圖 11 是 SME 的大致流程圖。
從圖 11 可以看出, SME 的主要工作就是不斷地查詢消息緩沖隊(duì)列,如果隊(duì)列中有消息,則將消息按先入先出的方式取出, 然后送入狀態(tài)機(jī)處理。SME 每次只處理一條消息, 反復(fù)循環(huán),直到消息隊(duì)列中的消息全部處理完畢。
當(dāng)消息隊(duì)列中沒(méi)有消息時(shí), CPU 處于空閑狀態(tài), SME 轉(zhuǎn)去執(zhí)行“空閑任務(wù)”。空閑任務(wù)指的是一些對(duì)單片機(jī)系統(tǒng)關(guān)鍵功能的實(shí)現(xiàn)無(wú)關(guān)緊要的工作,比如喂看門(mén)狗、算一算 CPU 使用率之類的工作,如果想降低功耗,甚至可以讓 CPU 在空閑任務(wù)中進(jìn)入休眠狀態(tài),只要事件一發(fā)生, CPU 就會(huì)被 ISR 喚醒,轉(zhuǎn)去執(zhí)行消息處理代碼。
實(shí)際上, 程序運(yùn)行的時(shí)候 CPU 大部分時(shí)間是很“閑” 的, 所以消息隊(duì)列查詢和空閑任務(wù)這兩部分代碼是程序中執(zhí)行得最頻繁的部分,也就是圖 11 的流程圖中用粗體框和粗體線標(biāo)出的部分。
如果應(yīng)用層的主狀態(tài)機(jī)用壓縮表格驅(qū)動(dòng)法實(shí)現(xiàn),結(jié)合上面給出的消息模塊, 則GF1.0 的狀態(tài)機(jī)引擎代碼如程序清單 List11 所示。
「程序清單List11:」
?
void?sme_kernel(void); /*************************************** *FuncName?:?main *Description?:?主函數(shù) *Arguments?:?void *Return?:?void *****************************************/ void?main(void) { ?sys_init(); ?sme_kernel();?/*GF1.0?狀態(tài)機(jī)引擎*/ } /*************************************** *FuncName?:?sme_kernel *Description?:?裸奔框架?GF1.0?的狀態(tài)機(jī)引擎函數(shù) *Arguments?:?void *Return?:?void *****************************************/ void?sme_kernel(void) { ?extern?struct?fsm_node?g_arFsmDrvTbl[];?/*狀態(tài)機(jī)壓縮驅(qū)動(dòng)表格*/ ?INT8U?u8Err?=?0;?/**/ ?INT8U?u8CurStat?=?0;?/*狀態(tài)暫存*/ ?MSG?stMsgTmp;?/*消息暫存*/ ?struct?fsm_node?stNodeTmp?=?{NULL,?0};?/*狀態(tài)機(jī)節(jié)點(diǎn)暫存*/ ?memset((void*)(&stMsgTmp),?0,?sizeof(MSG));?/*變量初始化*/ ?gbl_int_disable();?/*關(guān)全局中斷*/ ?mq_lock();?/*消息隊(duì)列鎖定*/ ?mq_init();?/*消息隊(duì)列初始化*/ ?mq_unlock();?/*消息隊(duì)列解鎖*/ ?fsm_init();?/*狀態(tài)機(jī)初始化*/ ?gbl_int_enable();?/*開(kāi)全局中斷*/ ? ?while(1) ?{ ??if(mq_is_empty()?==?FALSE) ??{ ???u8Err?=?mq_msg_req_fifo(&stMsgTmp);?/*讀取消息*/ ???if(u8Err?==?MREQ_NOERR) ???{ ????u8CurStat?=?get_cur_state();?/*讀取當(dāng)前狀態(tài)*/ ????stNodeTmp?=?g_arFsmDrvTbl[u8CurStat];?/*定位狀態(tài)機(jī)節(jié)點(diǎn)*/ ????if(stNodeTmp.u8StatChk?==?u8CurStat) ????{ ?????u8CurStat?=?stNodeTmp.fpAction(&stMsgTmp);?/*消息處理*/ ?????set_cur_state(u8CurStat?);?/*狀態(tài)遷移*/ ????} ????else ????{ ?????state_crash(u8CurStat?);?/*非法狀態(tài)處理*/ ????} ???} ??} ??else ??{ ???idle_task();?/*空閑任務(wù)*/ ??} ?} }
?
3、狀態(tài)機(jī)與ISR在驅(qū)動(dòng)程序中的應(yīng)用
在驅(qū)動(dòng)層的程序中使用狀態(tài)機(jī)和 ISR 能使程序的效率大幅提升。這種優(yōu)勢(shì)在通信接口中最為明顯,以串口程序?yàn)槔?/p>
單片機(jī)和外界使用串口通信時(shí)大多以數(shù)據(jù)幀的形式進(jìn)行數(shù)據(jù)交換,一幀完整的數(shù)據(jù)往往包含幀頭、接收節(jié)點(diǎn)地址、幀長(zhǎng)、數(shù)據(jù)正文、校驗(yàn)和幀尾等內(nèi)容,圖 12 所示為這種數(shù)據(jù)幀的常見(jiàn)結(jié)構(gòu)。
圖12 表明的結(jié)構(gòu)只是數(shù)據(jù)幀的一般通用結(jié)構(gòu), 使用時(shí)可根據(jù)實(shí)際情況適當(dāng)簡(jiǎn)化, 例如如果是點(diǎn)對(duì)點(diǎn)通信, 那么接收節(jié)點(diǎn)地址 FRM_USR 可省略;如果通信線路沒(méi)有干擾, 可確保數(shù)據(jù)正確傳輸,那么校驗(yàn)和 FRM_CHKSUM 也可省略。
假定一幀數(shù)據(jù)最長(zhǎng)不超過(guò) 256 個(gè)字節(jié)且串口網(wǎng)絡(luò)中通信節(jié)點(diǎn)數(shù)量少于 256 個(gè),那么幀頭、接收節(jié)點(diǎn)地址、幀長(zhǎng)、幀尾都可以用 1 個(gè)字節(jié)的長(zhǎng)度來(lái)表示。雖然數(shù)據(jù)的校驗(yàn)方式可能不同,但校驗(yàn)和使用 1~4 個(gè)字節(jié)的長(zhǎng)度來(lái)表示足以滿足要求。
先說(shuō)串口接收, 在裸奔框架 GF1.0 的結(jié)構(gòu)里, 串口接收可以有 2 種實(shí)現(xiàn)方式:ISR+消息 orISR+緩沖區(qū)+消息。ISR+消息比較簡(jiǎn)單, ISR 收到一個(gè)字節(jié)數(shù)據(jù),就把該字節(jié)以消息的形式發(fā)給應(yīng)用層程序,由應(yīng)用層的代碼進(jìn)行后續(xù)處理。這種處理方式使得串口接收 ISR 結(jié)構(gòu)很簡(jiǎn)單,負(fù)擔(dān)也很輕, 但是存在 2 個(gè)問(wèn)題。
數(shù)據(jù)的接收控制是一個(gè)很底層的功能, 按照軟件分層結(jié)構(gòu), 應(yīng)用代碼不應(yīng)該負(fù)責(zé)這些工作,混淆職責(zé)會(huì)使得軟件的結(jié)構(gòu)變差;用消息方式傳遞單個(gè)的字節(jié)效率太低, 占用了太多的消息緩沖資源,如果串口波特率很高并且消息緩沖區(qū)開(kāi)的不夠大,會(huì)直接導(dǎo)致消息緩沖區(qū)溢出。
相比之下, ISR+緩沖區(qū)+消息的處理方式就好多了, ISR 收到一個(gè)字節(jié)數(shù)據(jù)之后, 將數(shù)據(jù)先放入接收緩沖區(qū),等一幀數(shù)據(jù)全部接收完畢后(假設(shè)緩沖區(qū)足夠大),再以消息的形式發(fā)給應(yīng)用層,應(yīng)用層就可以去緩沖區(qū)讀取數(shù)據(jù)。
對(duì)于應(yīng)用層來(lái)講,整幀數(shù)據(jù)只有數(shù)據(jù)正文才是它想要的內(nèi)容,數(shù)據(jù)幀的其余部分僅僅是數(shù)據(jù)正文的封皮, 沒(méi)有意義。從功能劃分的角度來(lái)看, 確保數(shù)據(jù)正確接收是 ISR 的職責(zé), 所以這部分事情應(yīng)該放在 ISR 中做,給串口接收 ISR 配一個(gè)狀態(tài)機(jī),就能很容易的解決問(wèn)題。圖 13為串口接收 ISR 狀態(tài)轉(zhuǎn)換圖。
圖13 中的數(shù)據(jù)幀使用 16 位校驗(yàn)和,發(fā)送順序高字節(jié)在前,低字節(jié)在后。接收緩沖區(qū)屬于 ISR 和主程序的共享資源,必須實(shí)現(xiàn)互斥訪問(wèn),所以 ISR 收完一幀數(shù)據(jù)之后對(duì)緩沖區(qū)上鎖, 后面再發(fā)生的 ISR 發(fā)現(xiàn)緩沖區(qū)上鎖之后, 不接收新的數(shù)據(jù), 也不修改緩沖區(qū)中的數(shù)據(jù)。
應(yīng)用層程序收到消息, 讀取緩沖區(qū)中的數(shù)據(jù)之后再對(duì)緩沖區(qū)解鎖, 使能 ISR 接收串口數(shù)據(jù)和對(duì)緩沖區(qū)的寫(xiě)入。數(shù)據(jù)接收完畢后,應(yīng)該校驗(yàn)數(shù)據(jù),只有校驗(yàn)結(jié)果和收到的校驗(yàn)和相符,才能確信數(shù)據(jù)正確接收。
數(shù)據(jù)校驗(yàn)比較耗時(shí),不適合在 ISR 中進(jìn)行,所以應(yīng)該放在應(yīng)用代碼中處理。這樣實(shí)現(xiàn)的串口接收 ISR 比較復(fù)雜,代碼規(guī)模比較大,看似和 ISR 代碼盡量簡(jiǎn)短,執(zhí)行盡量迅速的原則相悖, 但是由于 ISR 里面是一個(gè)狀態(tài)機(jī), 每次中斷的時(shí)候 ISR 僅執(zhí)行全部代碼的一小部分,之后立刻退出,所以執(zhí)行時(shí)間是很短的,不會(huì)比“ISR+消息” 的方式慢多少。
串口發(fā)送比串口接收要簡(jiǎn)單的多,為提高效率也是用 ISR+緩沖區(qū)+消息的方式來(lái)實(shí)現(xiàn)。程序發(fā)送數(shù)據(jù)時(shí)調(diào)用串口模塊提供的接口函數(shù), 接口函數(shù)通過(guò)形參獲取要發(fā)送的數(shù)據(jù), 將數(shù)據(jù)打包后送入發(fā)送緩沖區(qū), 然后啟動(dòng)發(fā)送過(guò)程, 剩下的工作就在硬件和串口發(fā)送 ISR 的配合下自動(dòng)完成,數(shù)據(jù)全部發(fā)送完畢后, ISR 向應(yīng)用層發(fā)送消息,如果有需要,應(yīng)用層可以由此獲知數(shù)據(jù)發(fā)送完畢的時(shí)刻。圖 14 為串口發(fā)送 ISR 的狀態(tài)轉(zhuǎn)換圖。
上面只是討論了串口設(shè)備的管理方法, 其實(shí)這種狀態(tài)機(jī)+ISR 的處理方式可以應(yīng)用到很多的硬件設(shè)備中,一些適用的場(chǎng)合:
標(biāo)準(zhǔn)的或自制的單總線協(xié)議 (狀態(tài)機(jī)+定時(shí)中斷+消息)
用 I/O 模擬 I2C 時(shí)序并且通信速率要求不高 (狀態(tài)機(jī)+定時(shí)中斷+消息)
數(shù)碼管動(dòng)態(tài)掃描 (狀態(tài)機(jī)+定時(shí)中斷)
鍵盤(pán)動(dòng)態(tài)掃描 (狀態(tài)機(jī)+定時(shí)中斷 ?)
小結(jié)
裸奔框架 GF1.0 處處體現(xiàn)著事件驅(qū)動(dòng)+狀態(tài)機(jī)的思想, 大到程序整體的組織結(jié)構(gòu), 小到某個(gè)ISR 的具體實(shí)現(xiàn),都有這對(duì)黃金組合的身影。從宏觀上看, 裸奔框架 GF1.0 是一個(gè) ISR+消息管理+主狀態(tài)機(jī)的結(jié)構(gòu), 如圖 15 所示。
不管主狀態(tài)機(jī)使用的是 FSM(有限狀態(tài)機(jī))還是 HSM(層次狀態(tài)機(jī)), GF1.0 中有且只有 1 個(gè)主狀態(tài)機(jī)。主狀態(tài)機(jī)位于軟件的應(yīng)用層, 是整個(gè)系統(tǒng)絕對(duì)的核心, 承擔(dān)著邏輯和運(yùn)算功能, 外界和單片機(jī)系統(tǒng)的交互其實(shí)就是外界和主狀態(tài)機(jī)之間的交互, 單片機(jī)程序的其他部分都是給主狀態(tài)機(jī)打雜的。
從微觀上看, 裸奔框架 GF1.0 中的每一個(gè) ISR 也是事件驅(qū)動(dòng)+狀態(tài)機(jī)的結(jié)構(gòu)。ISR 的主要任務(wù)是減輕主狀態(tài)機(jī)獲取外界輸入的負(fù)擔(dān), ISR 負(fù)責(zé)處理獲取輸入時(shí)硬件上繁雜瑣細(xì)的操作,將各種輸入抽象化,以一種標(biāo)準(zhǔn)統(tǒng)一的數(shù)據(jù)格式(消息)提交給主狀態(tài)機(jī),好讓主狀態(tài)機(jī)能專注于高級(jí)功能的實(shí)現(xiàn)而不必關(guān)注具體的細(xì)節(jié)。
裸奔框架 GF1.0 應(yīng)用的難點(diǎn)在于主狀態(tài)機(jī)的具體實(shí)現(xiàn),對(duì)于一個(gè)實(shí)際的應(yīng)用,不管功能多復(fù)雜, 都必須將這些功能整合到一個(gè)主狀態(tài)機(jī)中來(lái)實(shí)現(xiàn)。這既要求設(shè)計(jì)者對(duì)系統(tǒng)的目標(biāo)功能有足夠詳細(xì)的了解, 還要求設(shè)計(jì)者對(duì)狀態(tài)機(jī)理論有足夠深的掌握程度, 如果設(shè)計(jì)出的狀態(tài)機(jī)不合理,程序的其他部分設(shè)計(jì)得再好,也不能很好的實(shí)現(xiàn)系統(tǒng)的要求。
將實(shí)際問(wèn)題狀態(tài)機(jī)化,最重要的是要合理地劃分狀態(tài),其次是要正確地提取系統(tǒng)事件,既不能遺漏, 也不能重復(fù)。有了狀態(tài)和事件, 狀態(tài)轉(zhuǎn)換圖的骨架就形成了, 接下來(lái)就是根據(jù)事件確定狀態(tài)之間的轉(zhuǎn)換關(guān)系,自頂向下,逐步細(xì)化,最終實(shí)現(xiàn)整個(gè)功能。
審核編輯:湯梓紅
評(píng)論