對于無 os 的系統(tǒng),流行的設(shè)計(jì)是主程序(主循環(huán) ) + (定時(shí))中斷,這種結(jié)構(gòu)雖然符合自然想法,不過卻有很多不利之處,首先是中斷可以在主程序的任何地方發(fā)生,隨意打斷主程序。其次主程序與中斷之間的耦合性(關(guān)聯(lián)度)較大,這種做法 使得主程序與中斷纏繞在一起,必須仔細(xì)處理以防不測。
那么換一種思路,如果把主程序全部放入(定時(shí))中斷中會(huì)怎么樣?這么做至少可以立即看到幾個(gè)好處: 系統(tǒng)可以處于低功耗的休眠狀態(tài),將由中斷喚醒進(jìn)入主程序; 如果程序跑飛,則中斷可以拉回;沒有了主從之分(其他中斷另計(jì)),程序易于模塊化。
(題外話:這種方法就不會(huì)有何處喂狗的說法,也沒有中斷是否應(yīng)該盡可能的簡短的爭論了)
為了把主程序全部放入(定時(shí))中斷中,必須把程序化分成一個(gè)個(gè)的模塊,即任務(wù),每個(gè)任務(wù)完成一個(gè)特定的功能,例如掃描鍵盤并檢測按鍵。
設(shè)定一個(gè)合理的時(shí)基
(tick), 例如
這里的 IDLE 是一條sleep 指令,讓 mcu 進(jìn)入低功耗模式。中斷程序的構(gòu)成
進(jìn)入中斷后,首先重置Timer, 這主要針對8051, 8051 自動(dòng)重裝分頻器只有 8-bit, 難以做到長時(shí)間定時(shí);復(fù)位 stack ,即把stack 指針賦值為棧頂或棧底(對于 pic, TI DSP 等使用循環(huán)棧的 mcu 來說,則無此必要),用以表示與過去決裂,而且不準(zhǔn)備返回到中斷點(diǎn),保證不會(huì)保留程序在跑飛時(shí)stack 中的遺體。Enable_Timer_Interrupt 也主要是針對8051。8051 由于中斷控制較弱,只有兩級中斷優(yōu)先級,而且使用了如果中斷程序不用 reti 返回,則不能響應(yīng)同級中斷這種偷懶方法,所以對于 8051, 必須調(diào)用一次 reti 來開放中斷:
下面就是任務(wù)的執(zhí)行了,這里有幾種方法。第一種是采用固定順序,由于mcu 程序復(fù)雜度不高,多數(shù)情況下可以采用這種方法:
…
可以看到中斷把所有任務(wù)調(diào)用一遍,至于任務(wù)是否需要運(yùn)行,由程序員自己控制。另一種做法是通過函數(shù)指針數(shù)組:
typedef void (*FUNCTIONPTR)();
const FUNCTIONPTR[] tasks = {
ProcessKey,
RunTask2,
…
RunTaskN
};
}
使用const 是讓數(shù)組內(nèi)容位于 code segment (ROM) 而非 data segment (RAM) 中,8051 中使用 code 作為 const 的替代品。
(題外話:關(guān)于函數(shù)指針賦值時(shí)是否需要取地址操作符 & 的問題,與數(shù)組名一樣,取決于 compiler. 對于熟悉匯編的人來說,函數(shù)名和數(shù)組名都是常數(shù)地址,無需也不能取地址。對于不熟悉匯編的人來說,用 & 取地址是理所當(dāng)然的事情。Visual C++ 2005對此兩者都支持)
這種方法在匯編下表現(xiàn)為散轉(zhuǎn), 一個(gè)小技巧是利用 stack 獲取跳轉(zhuǎn)表入口:
MultiJump:
還有一種方法是把函數(shù)指針數(shù)組(動(dòng)態(tài)數(shù)組,鏈表更好,不過在 mcu 中不適用)放在 data segment 中,便于修改函數(shù)指針以運(yùn)行不同的任務(wù),這已經(jīng)接近于動(dòng)態(tài)調(diào)度了:
FUNCTIONPTR[COUNTOFTASKS] tasks;
通過上面的手段,一個(gè)中斷驅(qū)動(dòng)的框架形成了,下面的事情就是保證每個(gè)
tick 內(nèi)所有任務(wù)的運(yùn)行時(shí)間總和不能超過一個(gè)
tick 的時(shí)間。為了做到這一點(diǎn),必須把每個(gè)任務(wù)切分成一個(gè)個(gè)的時(shí)間片,每個(gè)
tick 內(nèi)運(yùn)行一片。這里引入了狀態(tài)機(jī)
(state machine) 來實(shí)現(xiàn)切分。關(guān)于
state machine,
(題外話:實(shí)踐升華出理論,理論再作用于實(shí)踐。我很長時(shí)間不知道我一直沿用的方法就是state machine,直到學(xué)習(xí)UML/C++,書中介紹 tachniques for identifying dynamic behvior,方才豁然開朗。功夫在詩外,掌握 C++, 甚至C# JAVA, 對理解嵌入式程序設(shè)計(jì),會(huì)有莫大的幫助)
狀態(tài)機(jī)的程序?qū)崿F(xiàn)相當(dāng)簡單,第一種方法是用 swich-case 實(shí)現(xiàn):
}
另一種方法還是用更通用簡潔的函數(shù)指針數(shù)組:
const FUNCTIONPTR[] states = { state0, state1, …, stateM };
void RunTaskN()
{
(*states[state])();
}
下面是 state machine 控制的例子:
void state0() { }
void state1() { state++; }
void state2() { state+=2;
}
void state3() { state--; }
void state4() { delay = 100; state++; }
void state5() { delay--; if (delay
<= 0) state++; }
void state6() { state=0; }
一個(gè)小技巧是把第一個(gè)狀態(tài) state0 設(shè)置為空狀態(tài),即:
這樣,state =0可以讓整個(gè)task 停止運(yùn)行,如果需要投入運(yùn)行,簡單的讓 state = 1 即可。
以下是一個(gè)鍵盤掃描的例子,這里假設(shè) tick = 20 ms, ScanKeyboard() 函數(shù)控制口線的輸出掃描,并檢測輸入轉(zhuǎn)換為鍵碼,利用每個(gè)state 之間 20 ms 的間隔去抖動(dòng)。
EnumKey_NoKey =
…
struct StructKeyProcess key;
void ProcessKey() {
(*states[state])(); }
上面的鍵盤處理過程顯然比通常使用標(biāo)志去抖的程序簡潔清晰,而且沒有軟件延時(shí)去抖的困擾。以此類推,各個(gè)任務(wù)都可以劃分成一個(gè)個(gè)的state, 每個(gè)state 實(shí)際上占用不多的處理時(shí)間。某些任務(wù)可以劃分成若干個(gè)子任務(wù),每個(gè)子任務(wù)再劃分成若干個(gè)狀態(tài)。
(題外話:對于常數(shù)類型,建議使用 enum 分類組織,避免使用大量 #define 定義常數(shù))
對于一些完全不能分割,必須獨(dú)占的任務(wù)來說,比如我以前一個(gè)低成本應(yīng)用中紅外遙控器的軟件解碼任務(wù),這時(shí)只能犧牲其他的任務(wù)了。兩種做法:一種是關(guān)閉中斷,完全的獨(dú)占;
第二種,允許定時(shí)中斷發(fā)生,保證某些時(shí)基 register 得以更新;
只要watchDogCounter 不為 0,那么中斷正常返回到中斷點(diǎn),繼續(xù)執(zhí)行先前被中斷的任務(wù),否則,復(fù)位 stack, 重新進(jìn)行任務(wù)循環(huán)。這種狀況下,中斷處理過程極短,對獨(dú)占任務(wù)的影響也有限。
中斷驅(qū)動(dòng)多任務(wù)配合狀態(tài)機(jī)的使用,我相信這是mcu 下無os 系統(tǒng)較好的設(shè)計(jì)結(jié)構(gòu)。對于絕大多數(shù) mcu 程序設(shè)計(jì)來說,可以極大的減輕程序結(jié)構(gòu)的安排,無需過多的考慮各個(gè)任務(wù)之間的時(shí)間安排,而且可以讓程序簡潔易懂。缺點(diǎn)是,程序員必須花費(fèi)一定的時(shí)間考慮如何切分任務(wù)。
下面是一段用 C 改寫的CD Player 中檢測 disc 是否存在的偽代碼,用以展示這種結(jié)構(gòu)的設(shè)計(jì)技巧,原源代碼為Z8 mcu 匯編, 基于 Sony 的 DSP, Servo and RF 處理芯片, 通過送出命令字來控制主軸/滑板/聚焦/尋跡電機(jī),并讀取狀態(tài)以及 CD 的sub Q 碼。這個(gè)處理任務(wù)只是一個(gè)大任務(wù)下用state machine切開的一個(gè)二級子任務(wù),tick = 20 ms。
if (innerSwitch != ON) {
SendCommand(EnumCommand_SlidingMotorBackward);
timeout = MILLISECOND(10000);
state++;
}
else
}
SendCommand(EnumCommand_SlidingMotorForward);
timeout = MILLISECOND(2000);
state++;
}
}
else {
}
SendCommand(FocusUp);
state++;
timeout = MILLISECOND(2000);
歡迎光臨 (http://www.raoushi.com/bbs/)
Powered by Discuz! X3.1