今天來講一講我開發SmartTimer的思路。在上一篇介紹SmartTimer的文章 "《SmartTImer——一個基於STM32的時鐘管理器》" 中,我提到了要實現延遲XX毫秒執行XX函數的功能,比較好的方式是在定時器中斷中設置溢出標誌,而在程式主迴圈中檢測這個標誌,如果標誌置位則運行回調函數。 ...
今天來講一講我開發SmartTimer的思路。在上一篇介紹SmartTimer的文章《SmartTImer——一個基於STM32的時鐘管理器》中,我提到了要實現延遲XX毫秒執行XX函數的功能,比較好的方式是在定時器中斷中設置溢出標誌,而在程式主迴圈中檢測這個標誌,如果標誌置位則運行回調函數。這樣不僅可以實現非同步的操作,最大化利用MCU的計算資源,而且可以最小化定時器的中斷時間。只不過每設置一個定時器事件,就需要為這個事件設定全局標誌位,並且為其單獨計數、檢測標誌位。如果處理不好,將會使全局標誌散落在程式的各個角落,不僅使程式結構臃腫,而且會留下bug隱患。
基於以上原因,我明確了SmartTimer的開發目的,就是統一管理各個定時器事件,將定時計數、判斷定時溢出,執行回調函數都封裝起來。使用者在SmartTimer外部不必關心具體的內部實現,只需要用一行語句即可完成非同步延遲的操作,不必進行額外的操作。除了簡便外另一個好處是,即便定時器部分出現了程式上的bug,也使得問題更加集中,便於定位bug而進行修改。
在對外功能上,提供runlater,runloop,delay這三個功能。
SmartTimer的初步構想
明確了開發目的,就開始構建SmartTImer了。我考慮的SmartTimer架構其實很簡單,總體上分為三大塊,如下圖所示:
簡要說明一下這個系統框圖:
- 用戶調用runlater、runloop、delay後,會從記憶體池中拿出一個event對象並初始化,push進event鏈表中;
- 定時器中斷函數每隔Xms執行一次,遍歷整個event鏈表,給每個event事件定時+1,並判斷定時時間是否到達。如果到達則將該事件的溢出計數+1;如果定時迴圈次數不為0,則重新計數,否則將該事件對象從event鏈表中轉移到recycle數組中。
- 在主while迴圈中,查詢mark table,如某事件的溢出計數大於0則執行對應的回調函數,並將溢出計數-1;查詢recycle數組,如果有需要回收的event事件,則釋放資源到記憶體池中。
架構出來後,本著先完成再優化的原則,稍微考慮下優化方針,就可以動手實現了。由於SmartTimer沒有搶占機制,所以屬於非實時性工具。但理論上來說,如果以上描述的3大塊程式都執行的非常快,那麼就可以理解為接近“實時”了。所以為了提高實時性,需要提高程式的執行速度。在代碼優化上,我主要採用了空間換時間的方法,犧牲了一部分記憶體來換取執行速度。主要用在event pool上,以及用數組映射來設置溢出標誌和執行回調函數等地方。
SmartTimer的初步實現
首先抽象出定時器事件的定義:
struct stim_event{
uint16_t interval; //定時事件的定時間隔
uint16_t now; //定時事件的當前計數
uint16_t looptimes; //loop次數
uint8_t addIndex; //事件對象在記憶體池中的序號
uint8_t stat; //定時事件的當前狀態
struct stim_event *next;
struct stim_event *prev;
};
從結構體定義上可以看出,我準備用雙向鏈表來組織這些定時器事件。用鏈表而不是數組來構建事件list,是因為定時器事件的加入和移除是動態的,且有時會從list中間增減事件,所以用鏈表比數組更加合理。
在SmartTimer初始化時,會預先創建一個stim_event的數組作為event pool來使用。另外還會創建兩個元素數量與event pool相同的數組,一個是MarkArray,用來存儲event的溢出計數;一個是CallbackArray,用來存儲回調函數的函數指針。event、溢出計數、回調函數用stim_event中的addIndex聯繫起來。
event->addIndex為該event在event pool數組中的序號(數組下標),相應的MarkArray和CallbackArray數組中,序號為event->addIndex的元素,存儲著該event的溢出計數和回調函數指針。當某個event計時時間到,那麼MarkList中第event->addIndex個元素溢出計數+1,將CallbackArray數組中第event->addIndex個元素存儲的函數指針調用一次。
定時器中斷函數,主要是遍歷event鏈表,進行定時計數並判斷是否定時溢出。
void stim_tick (void)
{
struct stim_event *tmp;
if (event_list.count == 0)
return;
tmp = event_list.head;
while(tmp != NULL){ //遍歷event鏈表
if(tmp->stat != STIM_EVENT_ACTIVE){
tmp = tmp->next;
continue;
}
if(tmp->now == tmp->interval){
mark_list[tmp->addIndex] += 1; //時間到,溢出計數+1
if((tmp->looptimes != STIM_LOOP_FOREVER) &&
(--tmp->looptimes == 0)){ // 迴圈次數為0,移除事件鏈表
tmp->stat = STIM_EVENT_RECYCLE;
recycle_list[recycle_count] = tmp;
recycle_count++;
}
tmp->now = 0;
}
tmp->now++;
tmp = tmp->next;
}
}
在主while迴圈中,主要是執行callback函數,以及回收event事件對象
void stim_mainloop ( void )
{
uint8_t i;
struct stim_event *tmp;
for(i = 0; i < STIM_EVENT_MAX_SIZE; i++){ //檢查溢出標誌,並執行回調函數
if((mark_list[i] != STIM_INVALID) && (mark_list[i] > 0)){
if(callback_list[i] != NULL){
callback_list[i]();
}
mark_list[i] -= 1;
}
}
if(recycle_count > 0){
for(i = 0; i < STIM_EVENT_MAX_SIZE; i++){ //如果有需要回收的event,則回收對象
tmp = recycle_list[i];
if(tmp != NULL && mark_list[tmp->addIndex] == 0){
pop_event(tmp);
recycle_list[i] = NULL;
recycle_count--;
break;
}
}
}
}
以上是SmartTimer 1.0版本的核心代碼。至此,SmartTimer已經可以運行起來了,它大致上完成了我的設計目的,但是並沒有完成我的設計目標——它運行的效率並不高,這樣以來,實時性就會大打折扣。但是SmartTimer的設計架構我認為還是合理的,只需在這個框架內對調度演算法進一步優化,就可以更完善了。下一篇文章,我會介紹我的優化思路和實現方法,敬請期待!
SmartTimer的應用技巧
在《SmartTImer——一個基於STM32的時鐘管理器》中,我已經詳細介紹過SmartTimer的一般使用方法,不再贅述,下麵來簡單談談高級用法。
我們先來看看傳統的單片機程式架構:
void main (void)
{
system_init(); //系統初始化
peripheral_init();//外設初始化
while(1){
switch(step){ //主程式狀態機
case A: // do something
break;
case B: //do something
break;
……
default: //do something
break;
}
usart_driver();//串口驅動
timer_event_handler();//定時器事件處理
……
}
}
一般來說都是先進行系統各項初始化,然後就進入了主while迴圈中,在while迴圈中,主要執行系統狀態機,用來完成當發生xx事後,執行xx函數的功能。在狀態機之外,有一些輔助性的功能,例如系統Led呼吸燈驅動、串口驅動、定時事件回調函數等等。
我們單看主while迴圈內的部分,做一個最簡模型。如果假定switch狀態機部分的執行需要100ms,串口驅動執行需要10ms,定時器事件處理執行需要20ms,那麼while內的程式每迴圈一次使用130ms。我們其實可以換個角度來看這段程式,即switch狀態機每隔30ms執行一次,串口驅動每隔120ms執行一次,而定時事件處理函數每隔110ms執行一次。
是不是很神奇?如果基於時間的角度來考慮問題,那麼本來交織在一起的程式被輕易的模塊化了,模塊化的好處顯而易見,不僅程式架構簡單清晰,而且便於維護。利用SmartTimer的runloop功能可以輕鬆實現基於時間的模塊化程式架構。我們來看看將上面的例子進行基於時間的模塊化修改:
static void state_machine(void)
{
switch(step){ //主程式狀態機
case A: // do something
break;
case B: //do something
break;
……
default: //do something
break;
}
}
void main (void)
{
system_init(); //系統初始化
peripheral_init();//外設初始化
stim_loop(30,state_machine,STIM_LOOP_FOREVER);
stim_loop(120,usart_driver,STIM_LOOP_FOREVER);
stim_loop(110,timer_event_handler,STIM_LOOP_FOREVER);
……
while(1){
stim_mainloop();
}
}
架構是不是瞬間變簡潔了?另外,照這個思路考慮問題,狀態機部分的程式也可以分解成若幹個模塊,使程式更加簡潔、優雅。