mcu由于內(nèi)部資源的限制,軟件設(shè)計有其特殊性,程序一般沒有復(fù)雜的算法以及數(shù)據(jù)結(jié)構(gòu),代碼量也不大,通常不會使用OS (Operating System),因為對于一個只有若干K ROM,一百多byte RAM的mcu來說,一個簡單OS也會吃掉大部分的資源。
對于無os的系統(tǒng),流行的設(shè)計是主程序(主循環(huán)) +(定時)中斷,這種結(jié)構(gòu)雖然符合自然想法,不過卻有很多不利之處,首先是中斷可以在主程序的任何地方發(fā)生,隨意打斷主程序。其次主程序與中斷之間的耦合性(關(guān)聯(lián)度)較大,這種做法使得主程序與中斷纏繞在一起,必須仔細(xì)處理以防不測。
那么換一種思路,如果把主程序全部放入(定時)中斷中會怎么樣?這么做至少可以立即看到幾個好處:系統(tǒng)可以處于低功耗的休眠狀態(tài),將由中斷喚醒進(jìn)入主程序;如果程序跑飛,則中斷可以拉回;沒有了主從之分(其他中斷另計),程序易于模塊化。
(題外話:這種方法就不會有何處喂狗的說法,也沒有中斷是否應(yīng)該盡可能的簡短的爭論了)
為了把主程序全部放入(定時)中斷中,必須把程序化分成一個個的模塊,即任務(wù),每個任務(wù)完成一個特定的功能,例如掃描鍵盤并檢測按鍵。設(shè)定一個合理的時基(tick),例如5, 10或20 ms,每次定時中斷,把所有任務(wù)執(zhí)行一遍,為減少復(fù)雜性,一般不做動態(tài)調(diào)度(最多使用固定數(shù)組以簡化設(shè)計,做動態(tài)調(diào)度就接近os了),這實際上是一種無優(yōu)先級時間片輪循的變種。來看看主程序的構(gòu)成:
void main()
{
….// Initialize
while (true) {
IDLE;//sleep
}
}
這里的IDLE是一條sleep指令,讓mcu進(jìn)入低功耗模式。中斷程序的構(gòu)成
void Timer_Interrupt()
{
SetTimer();
ResetStack();
Enable_Timer_Interrupt;
….
進(jìn)入中斷后,首先重置Timer,這主要針對8051, 8051自動重裝分頻器只有8-bit,難以做到長時間定時;復(fù)位stack,即把stack指針賦值為棧頂或棧底(對于pic,TI DSP等使用循環(huán)棧的mcu來說,則無此必要),用以表示與過去決裂,而且不準(zhǔn)備返回到中斷點,保證不會保留程序在跑飛時stack中的遺體。Enable_Timer_Interrupt也主要是針對8051。8051由于中斷控制較弱,只有兩級中斷優(yōu)先級,而且使用了如果中斷程序不用reti返回,則不能響應(yīng)同級中斷這種偷懶方法,所以對于8051,必須調(diào)用一次reti來開放中斷:
_Enable_Timer_Interrupt:
acall_reti
_reti:reti
下面就是任務(wù)的執(zhí)行了,這里有幾種方法。第一種是采用固定順序,由于mcu程序復(fù)雜度不高,多數(shù)情況下可以采用這種方法:
…
Enable_Timer_Interrupt;
ProcessKey();
RunTask2();
…
RunTaskN();
while (1) IDLE;
可以看到中斷把所有任務(wù)調(diào)用一遍,至于任務(wù)是否需要運(yùn)行,由程序員自己控制。另一種做法是通過函數(shù)指針數(shù)組:
#define CountOfArray(x) (sizeof(x)/sizeof(x[0]))
typedef void (*FUNCTIONPTR)();
const FUNCTIONPTR[] tasks = {
ProcessKey,
RunTask2,
…
RunTaskN
};
void Timer_Interrupt()
{
SetTimer();
ResetStack();
Enable_Timer_Interrupt;
for (i=0; i
(*tasks[i])();
while (1) IDLE;
}
使用const是讓數(shù)組內(nèi)容位于code segment(ROM)而非data segment (RAM)中,8051中使用code作為const的替代品。
(題外話:關(guān)于函數(shù)指針賦值時是否需要取地址操作符&的問題,與數(shù)組名一樣,取決于compiler.對于熟悉匯編的人來說,函數(shù)名和數(shù)組名都是常數(shù)地址,無需也不能取地址。對于不熟悉匯編的人來說,用&取地址是理所當(dāng)然的事情。Visual C++ 2005對此兩者都支持)
這種方法在匯編下表現(xiàn)為散轉(zhuǎn),一個小技巧是利用stack獲取跳轉(zhuǎn)表入口:
movA, state
acallMultiJump
ajmpstate0
ajmpstate1
...
MultiJump:
popDPH
popDPL
rlA
jmp@A+DPTR
還有一種方法是把函數(shù)指針數(shù)組(動態(tài)數(shù)組,鏈表更好,不過在mcu中不適用)放在data segment中,便于修改函數(shù)指針以運(yùn)行不同的任務(wù),這已經(jīng)接近于動態(tài)調(diào)度了:
FUNCTIONPTR[COUNTOFTASKS] tasks;
tasks[0] = ProcessKey;
tasks[0] = RunTaskM;
tasks[0] = NULL;
...
FUNCTIONPTR pFunc;
for (i=0; i< COUNTOFTASKS; i++)??{
pFunc = tasks[i]);
if (pFunc != NULL)
(*pFunc)();
}
通過上面的手段,一個中斷驅(qū)動的框架形成了,下面的事情就是保證每個tick內(nèi)所有任務(wù)的運(yùn)行時間總和不能超過一個tick的時間。為了做到這一點,必須把每個任務(wù)切分成一個個的時間片,每個tick內(nèi)運(yùn)行一片。這里引入了狀態(tài)機(jī)(state machine)來實現(xiàn)切分。關(guān)于state machine,很多書中都有介紹,這里就不多說了。
(題外話:實踐升華出理論,理論再作用于實踐。我很長時間不知道我一直沿用的方法就是state machine,直到學(xué)習(xí)UML/C++,書中介紹tachniques for identifying dynamic behvior,方才豁然開朗。功夫在詩外,掌握C++,甚至C# JAVA,對理解嵌入式程序設(shè)計,會有莫大的幫助)
狀態(tài)機(jī)的程序?qū)崿F(xiàn)相當(dāng)簡單,第一種方法是用swich-case實現(xiàn):
void RunTaskN()
{
switch (state) {
case 0: state0(); break; case 1: state1(); break;
…
case M: stateM(); break;
default:
state = 0;
}
}
另一種方法還是用更通用簡潔的函數(shù)指針數(shù)組:
const FUNCTIONPTR[] states = { state0, state1, …, stateM };
void RunTaskN()
{
(*states[state])();
}
下面是state machine控制的例子:
void state0() { }
void state1() { state++; }//next state;
void state2() { state+=2; }//go to state 4;
void state3() { state--; }//go to previous state;
void state4() { delay = 100; state++; }
void state5() { delay--; if (delay <= 0) state++; }???//delay 100*tick
void state6() { state=0; }//go to the first state
一個小技巧是把第一個狀態(tài)state0設(shè)置為空狀態(tài),即:
void state0() { }
這樣,state =0可以讓整個task停止運(yùn)行,如果需要投入運(yùn)行,簡單的讓state = 1即可。
以下是一個鍵盤掃描的例子,這里假設(shè)tick = 20 ms, ScanKeyboard()函數(shù)控制口線的輸出掃描,并檢測輸入轉(zhuǎn)換為鍵碼,利用每個state之間20 ms的間隔去抖動。
enum EnumKey {
EnumKey_NoKey =0,
…
};
struct StructKey {
intkeyValue;
boolkeyPressed;
} ;
struct StructKeyProcess key;
void ProcessKey() { (*states[state])(); }
void state0() { }
void state1() { key.keyPressed = false; state++; }
void state2() { if (ScanKey() != EnumKey_NoKey) state++; }//next state if a key pressed
void state3()
{//debouncing state
key.keyValue = ScanKey();
if (key.keyValue == EnumKey_NoKey)
state--;
else {
key.keyPressed = true;
state++;
}
}
void state4() {if (ScanKey() == EnumKey_NoKey) state++; }//next state if the key released
void state5() {ScanKey() == EnumKey_NoKey? state = 1 : state--; }
上面的鍵盤處理過程顯然比通常使用標(biāo)志去抖的程序簡潔清晰,而且沒有軟件延時去抖的困擾。以此類推,各個任務(wù)都可以劃分成一個個的state,每個state實際上占用不多的處理時間。某些任務(wù)可以劃分成若干個子任務(wù),每個子任務(wù)再劃分成若干個狀態(tài)。
(題外話:對于常數(shù)類型,建議使用enum分類組織,避免使用大量#define定義常數(shù))
對于一些完全不能分割,必須獨占的任務(wù)來說,比如我以前一個低成本應(yīng)用中紅外遙控器的軟件解碼任務(wù),這時只能犧牲其他的任務(wù)了。兩種做法:一種是關(guān)閉中斷,完全的獨占;
void RunTaskN()
{
Disable_Interrupt;
…
Enable_Interrupt;
}
第二種,允許定時中斷發(fā)生,保證某些時基register得以更新;
void Timer_Interrupt()
{
SetTimer();
Enable_Timer_Interrupt;
UpdateTimingRegisters();
if (watchDogCounter = 0) {
ResetStack();
for (i=0; i
(*tasks[i])();
while (1) IDLE;
}
else
watchDogCounter--;
}
只要watchDogCounter不為0,那么中斷正常返回到中斷點,繼續(xù)執(zhí)行先前被中斷的任務(wù),否則,復(fù)位stack,重新進(jìn)行任務(wù)循環(huán)。這種狀況下,中斷處理過程極短,對獨占任務(wù)的影響也有限。
中斷驅(qū)動多任務(wù)配合狀態(tài)機(jī)的使用,我相信這是mcu下無os系統(tǒng)較好的設(shè)計結(jié)構(gòu)。對于絕大多數(shù)mcu程序設(shè)計來說,可以極大的減輕程序結(jié)構(gòu)的安排,無需過多的考慮各個任務(wù)之間的時間安排,而且可以讓程序簡潔易懂。缺點是,程序員必須花費一定的時間考慮如何切分任務(wù)。
下面是一段用C改寫的CD Player中檢測disc是否存在的偽代碼,用以展示這種結(jié)構(gòu)的設(shè)計技巧,原源代碼為Z8 mcu匯編,基于Sony的DSP, Servo and RF處理芯片,通過送出命令字來控制主軸/滑板/聚焦/尋跡電機(jī),并讀取狀態(tài)以及CD的sub Q碼。這個處理任務(wù)只是一個大任務(wù)下用state machine切開的一個二級子任務(wù),tick = 20 ms。
state1() { InitializeMotor(); state++; }
state2() {
if (innerSwitch != ON) {
SendCommand(EnumCommand_SlidingMotorBackward);
timeout = MILLISECOND(10000);
state++;//滑板電機(jī)向內(nèi)運(yùn)動,直至觸及最內(nèi)開關(guān)。
}
else
state +=2;
}
state3() {
if ((--timeout) == 0) {//note: some C compliers do not support (--timeout) ==
SendCommand(EnumCommand_SlidingMotorStop)
systemErrorCode = EnumErrorCode_InnerSwitch;
state = 0;// 10 s超時錯誤,
}
else {
if (innerSwitch == ON) {
SendCommand(EnumCommand _SlidingMotorStop)
timeout = MILLISECOND(200);// 200ms電機(jī)停止時間
state++;
}
}
}
state4() { if ((--timeout) == 0) state++; }//等待電機(jī)完全停止
state5() {
SendCommand(EnumCommand_SlidingMotorForward);
timeout = MILLISECOND(2000);
state++;
}//滑板電機(jī)向外運(yùn)動,脫離inner switch
state6() {
if ((--timeout) == 0) {
SendCommand(EnumCommand_SlidingMotorStop)
systemErrorCode = EnumErrorCode_InnerSwitch;
state = 0;// 2 s超時錯誤,
}
else {
if (innerSwitch == OFF) {
SendCommand(EnumCommand_SlidingMotorStop)
timeout = MILLISECOND(200);// 200ms電機(jī)停止時間
state++;
}
}
}
state7() { state4(); }
state8() { LaserOn(); state++; retryCounter = 3;}//打開激光器
state9() {
SendCommand(FocusUp);
state++;
timeout = MILLISECOND(2000);
}//光頭上舉,檢測聚焦過零3次,判斷cd是否存在
state10() {
if (FocusCrossZero){
systemStatus.Disc = EnumStatus_DiscExist;
SendCommand(EnumCommand_AutoFocusOn);//有cd,打開自動聚焦。
state = 0;//本任務(wù)結(jié)束。
playProcess.state = 1;//啟動play任務(wù)
}
else if ((--timeout) == 0) {
SendCommand(EnumCommand_ FocusClose);//光頭聚焦復(fù)位
if ((--retryCounter) == 0) {
systemStatus.Disc = EnumStatus_Nodisc;//無盤
displayProcess.state = EnumDisplayState_NoDisc;//顯示閃爍的無盤
LaserOff();
state = 0;//任務(wù)停止
}
else
state--;//再試
}
}
stateStop() {
SendCommand(EnumCommand_SlidingMotorStop);
SendCommand(EnumCommand_FocusClose);
state = 0;
}
-
中斷系統(tǒng)
+關(guān)注
關(guān)注
1文章
96瀏覽量
61008 -
單片機(jī)系統(tǒng)
+關(guān)注
關(guān)注
1文章
73瀏覽量
103827
原文標(biāo)題:把主程序放入中斷如何?單片機(jī)的一種軟件設(shè)計新思路
文章出處:【微信號:mcu168,微信公眾號:硬件攻城獅】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論