單片機的非OS的事件驅動
單片機的非OS的事件驅動
Part 1 前言
很多單片機項目恐怕都是沒有操作系統的前後臺結構,就是main函數裏用while無限循環各種任務,中斷處理緊急任務。這種結構最簡單,上手很容易,可是當項目比較大時,這種結構就不那麽適合了,編寫代碼前你必須非常小心的設計各個模塊和全局變量,否則最終會使整個代碼結構雜亂無序,不利於維護,而且往往會因為修改了某部分代碼而莫名其妙的影響到其他功能,而使調試陷入困境。
改變其中局面的最有效措施當然是引入嵌入式操作系統,但是大多數的操作系統都是付費的(特別是商業項目)。我們熟悉的uc-os/II如果你應用於非商業項目它是免費的,而應用於商業項目的話則要付費,而且價格不菲。
我們也可以自己編寫一套嵌入式OS,這當然最好了。可要編寫一套完整的OS並非易事,而且當項目並不是非常復雜的話也不需要一個完整的os支持。我們只要用到OS最基本的任務調度和上下文切換就夠了。正是基於這樣的想法,最近的一個項目中我就嘗試采用事件驅動的思想重新構建了代碼架構,實際使用的效果還不錯,在這裏做個總結。
本質上新架構仍然是前後臺結構,只不過原來的函數直接調用改成通過指向函數的指針來調用。實際上這也是嵌入式OS任務調度的一個核心。C語言中可以定義指向函數的指針:
void (*handle)(void);
這裏的handle就是一個指向函數的指針,我們只要將某函數的函數名賦給該指針,就能通過實現函數的調用了:
1 void func1(void) 2 { 3 // Code 4 } 5 6 handle = func1; 7 (*handle)(); // 實現func1的調用
有了這個函數調用新方法,我們就可以想辦法將某個事件與某個函數關聯,實現所謂的事件驅動。例如,按鍵1按下就是一個事件,func1響應按鍵1按下事件。但是,如果是單純的調用方法替代又有什麽意義呢?這又怎麽會是事件驅動呢?關鍵就在於使用函數指針調用方法可以使模塊和模塊之間的耦合度將到最低。一個例子來說明這個問題,一個按鍵檢測模塊用於檢測按鍵,一個電源模塊處理按鍵1動作。
傳統的前後臺處理方法:
main.c
1 voidmain() 2 { 3 ... 4 while(1) 5 { 6 ... 7 keyScan(); 8 if(flagKeyPress) 9 { 10 keyHandle(); // 檢測到按鍵就設置flagKeyPress標誌,進入處理函數 11 } 12 } 13 }
key.c
1 void keyHandle(void) 2 { 3 switch (_keyName) // 存放按鍵值的全局變量 4 { 5 ...6 case KEY1: pwrOpen(); break; 7 case KEY2: pwrClose(); break; 8 } 9 }
power.c
1 void pwrOpen(void) 2 { 3 ... 4 } 5 6 void pwrClose(void) 7 { 8 ... 9 }
這樣的結構的缺點在哪裏呢?
1. key代碼中直接涉及到power代碼的函數,如果power代碼裏的函數變更,將引起key代碼的變更
2. 一個按鍵值對應一個處理函數,如果要增加響應處理函數就要再次修改key代碼
3. 當項目越來越大時,引入的全局變量會越來越多,占用過多的內存
很顯然key模塊與其他模塊的耦合程度太高了,修改其他模塊的代碼都勢必去修改key代碼。理想的狀態是key模塊只負責檢測按鍵,並觸發一個按鍵事件,至於這個按鍵被哪個模塊處理,它壓根不需要知道,大大減少模塊之間的耦合度,也減少出錯的幾率。這不正好是事件驅動的思想嗎?
接下來,該如何實現呢?
Part 2 事件驅動的實現
需要一個事件隊列:
u16 _event[MAX_EVENT_QUEUE];
它是一個循環隊列,保存事件編號,我們可以用一個16位數為各種事件編號,可以定義65535個事件足夠使用了。
一個處理函數隊列:
1 typedef struct 2 { 3 u16 event; // 事件編號 4 void (*handle)(void); // 處理函數 5 }handleType; 6 7 handleType _handle[MAX_HANDLE_QUEUE];
它實際是一個數組,每個元素保存事件編號和對應的處理函數指針。
一個驅動函數:
1 void eventProc(void) 2 { 3 u16 event; 4 u8 i; 5 6 if((_eventHead!=_eventTail) || _eventFull) // 事件隊列裏有事件時才處理 7 { 8 event = _eq[_eventHead]; 9 _event [_eventHead++] = 0; // 清除事件 10 11 if(_eventHead>= MAX_EVENT_QUEUE) 12 { 13 _eventHead= 0; // 循環隊列 14 } 15 16 // 依次比較,執行與事件編號相對應的函數 17 for(i=0; i<_handleTail; i++) 18 { 19 if(_handle[i].event == event) 20 { 21 (*_handle[i].handle)(); 22 } 23 } 24 } 25 }
main函數可以精簡成這樣:
1 void main(void) 2 { 3 ... 4 while(1) 5 { 6 eventProc(); 7 } 8 }
這樣代碼的缺陷是需要為處理函數隊列分配一個非常大的內存空間,項目越復雜分配的空間越大,顯然不可取。需要做個變通,減少內存空間的使用量。
Part3 改進與變通
這樣代碼的缺陷是需要為處理函數隊列分配一個非常大的內存空間,項目越復雜分配的空間越大,顯然不可取。需要做個變通,減少內存空間的使用量。
1 typedef struct 2 { 3 void (*handle)(u16 event); // 僅保存模塊總的散轉函數 4 }handleType; 5 6 handleType _handle[MAX_HANDLE_QUEUE];
修改驅動函數:
1 void eventProc(void) 2 { 3 u16 event; 4 u8 i; 5 6 if((_eventHead!=_eventTail) || _eventFull) // 事件隊列裏有事件時才處理 7 { 8 ... 9 10 for(i=0; i<_handleTail; i++) 11 { 12 (*_handle[i].handle)(event); // 將事件編號傳遞給模塊散轉函數 13 } 14 } 15 }
把散轉處理交回給各模塊,例如power模塊的散轉函數:
1 void pwrEventHandle(u16 event) 2 { 3 switch (event) 4 { 5 ... 6 case EVENT_KEY1_PRESS: pwrOpen(); break; 7 ... 8 } 9 }
在power模塊的初始化函數中,將該散轉函數加入到處理函數隊列中:
1 // 該函數在系統初始化時調用 2 void pwrInit(void) 3 { 4 ... 5 addEventListener(pwrEventHandle); 6 ... 7 }
addEventListener定義如下:
1 void addEventListener(void (*pFunc)(u16 event)) 2 { 3 if(!_handleFull) 4 { 5 _handle[_handleTail].handle = pFunc; 6 _handleTail++; 7 8 if(_handleTail>= MAX_HANDLE_QUEUE) 9 { 10 _handleFull= TRUE; 11 } 12 } 13 }
每個模塊都定義各自的散轉處理,然後在初始化的時候將該函數存入處理事件隊列中,即能實現事件處理又不會占用很多的內存空間。
加入到事件隊列需要封裝成統一的函數dispatchEven,由各模塊直接調用。例如,key模塊就可以dispatchEvent(EVENT_KEY1_PRESS)來觸發一個事件
1 void dispatchEvent(u16 event) 2 { 3 u8 i; 4 bool canDispatch; 5 6 canDispatch = TRUE; 7 8 if(!_eventFull) 9 { 10 // 為了避免同一事件被多次加入到事件隊列中 11 for(i=_eventHead; i!=_eventTail;) 12 { 13 if(_event[i] == event) 14 { 15 canDispatch = FALSE; 16 break; 17 } 18 19 i++; 20 if(i >= MAX_EVENT_QUEUE) 21 { 22 i = 0; 23 } 24 } 25 26 if(canDispatch) 27 { 28 _event[_eventTail++] = event; 29 30 if(_eventTail>= MAX_EVENT_QUEUE) 31 { 32 _eventTail= 0; 33 } 34 if(_eventTail== _eventHead) 35 { 36 _eventFull = TRUE; 37 } 38 } 39 } 40 }
part 4 深一步:針對與時間相關的事件
對於與時間相關的事件(循環事件和延時處理事件)需要做進一步處理。
首先要設定系統Tick,可以用一個定時器來生成,例如配置一個定時器,每10ms中斷一次。
註:tick一般指os的kernel計時單位,用於處理定時、延時事件之類。一般使用硬件定時器中斷處理tick事件
定義一個時間事件隊列:
1 typedef struct 2 { 3 u8 type; // 事件類別,循環事件還是延時事件 4 u16 event; // 觸發的事件編號 5 u16 timer; // 延時或周期時間計數器 6 u16 timerBackup; // 用於周期事件的時間計數備份 7 }timerEventType; 8 9 timerEventType _timerEvent[MAX_TIMER_EVENT_Q];
在定時器Tick中斷中將時間事件轉換成系統事件:
1 void SysTickHandler(void) 2 { 3 ... 4 for(i=0; i<_timerEventTail; i++) 5 { 6 _timerEvent[i].timer--; 7 if(_timerEvent[i].timer == 0) 8 { 9 dispatchEvent(_timerEvent[i].event);// 事件觸發器 10 11 if(_timerEvent[i].type == CYCLE_EVENT) 12 { 13 // 循環事件,重新計數 14 _timerEvent[i].timer = _timerEvent[i].timerBackup; 15 } 16 else 17 { 18 // 延時事件,觸發後刪除 19 delTimerEvent(_timerEvent[i].event); 20 } 21 } 22 } 23 }
將增加和刪除時間事件封裝成函數,便以調用:
1 void addTimerEvent(u8 type, u16 event, u16 timer) 2 { 3 _timerEvent[_timerEventTail].type = type; 4 _timerEvent[_timerEventTail].event = event; 5 _timerEvent[_timerEventTail].timer = timer; // 時間單位是系統Tick間隔時間 6 _timerEvent[_timerEventTail].timerBackup = timer; // 延時事件並不使用 7 _timerEventTail++; 8 } 9 10 void delTimerEvent(u16 event) 11 { 12 ... 13 for(i=0; i<_timerEventTail; i++) 14 { 15 if(_timerEvent[i].event == event) 16 { 17 for(j=i; j<_timerEventTail; j++) 18 { 19 _timerEvent[j] = _timerEvent[j+1]; 20 } 21 22 _timerEventFull= FALSE; 23 _timerEventTail--; 24 } 25 } 26 }
對於延時處理,用事件驅動的方法並不理想,因為這可能需要將一段完整的代碼拆成兩個函數,破壞了代碼的完整性。解決的方法需要采用OS的上下文切換,這就涉及到程序堆棧問題,用純C代碼不容易實現。
——【感謝】資料來源於https://wenku.baidu.com/view/5465391d10a6f524ccbf8591.html
單片機的非OS的事件驅動