mcu由于內部資源的限制,軟件設計有其特殊性,程序一般沒有復雜的算法以及數據結構,代碼量也不大,通常不會使用OS(OperatingSystem),因為對于一個只有若干KROM,一百多byteRAM的mcu來說,一個簡單OS也會吃掉大部分的資源。
對于無os的系統,流行的設計是主程序(主循環)+(定時)中斷,這種結構雖然符合自然想法,不過卻有很多不利之處,首先是中斷可以在主程序的任何地方發生,隨意打斷主程序。其次主程序與中斷之間的耦合性(關聯度)較大,這種做法使得主程序與中斷纏繞在一起,必須仔細處理以防不測。
那么換一種思路,如果把主程序全部放入(定時)中斷中會怎么樣?這么做至少可以立即看到幾個好處:系統可以處于低功耗的休眠狀態,將由中斷喚醒進入主程序;如果程序跑飛,則中斷可以拉回;沒有了主從之分(其他中斷另計),程序易于模塊化。
(題外話:這種方法就不會有何處喂狗的說法,也沒有中斷是否應該盡可能的簡短的爭論了)
為了把主程序全部放入(定時)中斷中,必須把程序化分成一個個的模塊,即任務,每個任務完成一個特定的功能,例如掃描鍵盤并檢測按鍵。設定一個合理的時基(tick),例如5,10或20ms,每次定時中斷,把所有任務執行一遍,為減少復雜性,一般不做動態調度(最多使用固定數組以簡化設計,做動態調度就接近os了),這實際上是一種無優先級時間片輪循的變種。來看看主程序的構成:
voidmain()
{
….//Initialize
while(true){
IDLE;//sleep
}
}
這里的IDLE是一條sleep指令,讓mcu進入低功耗模式。中斷程序的構成
voidTimer_Interrupt()
{
SetTimer();
ResetStack();
Enable_Timer_Interrupt;
….
進入中斷后,首先重置Timer,這主要針對8051,8051自動重裝分頻器只有8-bit,難以做到長時間定時;復位stack,即把stack指針賦值為棧頂或棧底(對于pic,TIDSP等使用循環棧的mcu來說,則無此必要),用以表示與過去決裂,而且不準備返回到中斷點,保證不會保留程序在跑飛時stack中的遺體。Enable_Timer_Interrupt也主要是針對8051。8051由于中斷控制較弱,只有兩級中斷優先級,而且使用了如果中斷程序不用reti返回,則不能響應同級中斷這種偷懶方法,所以對于8051,必須調用一次reti來開放中斷:
_Enable_Timer_Interrupt:
acall_reti
_reti:reti
下面就是任務的執行了,這里有幾種方法。第一種是采用固定順序,由于mcu程序復雜度不高,多數情況下可以采用這種方法:
…
Enable_Timer_Interrupt;
ProcessKey();
RunTask2();
…
RunTaskN();
while(1)IDLE;
可以看到中斷把所有任務調用一遍,至于任務是否需要運行,由程序員自己控制。另一種做法是通過函數指針數組:
#defineCountOfArray(x)(sizeof(x)/sizeof(x[0]))
typedefvoid(*FUNCTIONPTR)();
constFUNCTIONPTR[]tasks={
ProcessKey,
RunTask2,
…
RunTaskN
};
voidTimer_Interrupt()
{
SetTimer();
ResetStack();
Enable_Timer_Interrupt;
for(i=0;i<CountOfArray(tasks),i++)
(*tasks
)();
while(1)IDLE;
}
使用const是讓數組內容位于codesegment(ROM)而非datasegment(RAM)中,8051中使用code作為const的替代品。
(題外話:關于函數指針賦值時是否需要取地址操作符&的問題,與數組名一樣,取決于compiler.對于熟悉匯編的人來說,函數名和數組名都是常數地址,無需也不能取地址。對于不熟悉匯編的人來說,用&取地址是理所當然的事情。VisualC++2005對此兩者都支持)
這種方法在匯編下表現為散轉,一個小技巧是利用stack獲取跳轉表入口:
movA,state
acallMultiJump
ajmpstate0
ajmpstate1
...
MultiJump:popDPH
popDPL
rlA
jmp@A+DPTR
還有一種方法是把函數指針數組(動態數組,鏈表更好,不過在mcu中不適用)放在datasegment中,便于修改函數指針以運行不同的任務,這已經接近于動態調度了:
FUNCTIONPTR[COUNTOFTASKS]tasks;
tasks[0]=ProcessKey;
tasks[0]=RunTaskM;
tasks[0]=NULL;
...
FUNCTIONPTRpFunc;
for(i=0;i<COUNTOFTASKS;i++){
pFunc=tasks);
if(pFunc!=NULL)
(*pFunc)();
}
通過上面的手段,一個中斷驅動的框架形成了,下面的事情就是保證每個tick內所有任務的運行時間總和不能超過一個tick的時間。為了做到這一點,必須把每個任務切分成一個個的時間片,每個tick內運行一片。這里引入了狀態機(statemachine)來實現切分。關于statemachine,很多書中都有介紹,這里就不多說了。
(題外話:實踐升華出理論,理論再作用于實踐。我很長時間不知道我一直沿用的方法就是statemachine,直到學習UML/C++,書中介紹tachniquesforidentifyingdynamicbehvior,方才豁然開朗。功夫在詩外,掌握C++,甚至C#JAVA,對理解嵌入式程序設計,會有莫大的幫助)
狀態機的程序實現相當簡單,第一種方法是用swich-case實現:
voidRunTaskN()
{
switch(state){
case0:state0();break;
case1:state1();break;
…
caseM:stateM();break;
default:
state=0;
}
}
另一種方法還是用更通用簡潔的函數指針數組:
constFUNCTIONPTR[]states={state0,state1,…,stateM};
voidRunTaskN()
{
(*states[state])();
}
下面是statemachine控制的例子:
voidstate0(){}
voidstate1(){state++;}//nextstate;
voidstate2(){state+=2;}//gotostate4;
voidstate3(){state--;}//gotopreviousstate;
voidstate4(){delay=100;state++;}
voidstate5(){delay--;if(delay<=0)state++;}//delay100*tick
voidstate6(){state=0;}//gotothefirststate
一個小技巧是把第一個狀態state0設置為空狀態,即:
voidstate0(){}
這樣,state=0可以讓整個task停止運行,如果需要投入運行,簡單的讓state=1即可。
以下是一個鍵盤掃描的例子,這里假設tick=20ms,ScanKeyboard()函數控制口線的輸出掃描,并檢測輸入轉換為鍵碼,利用每個state之間20ms的間隔去抖動。
enumEnumKey{
EnumKey_NoKey=0,
…
};
structStructKey{
intkeyValue;
boolkeyPressed;
};
structStructKeyProcesskey;
voidProcessKey(){(*states[state])();}
voidstate0(){}
voidstate1(){key.keyPressed=false;state++;}
voidstate2(){if(ScanKey()!=EnumKey_NoKey)state++;}//nextstateifakeypressed
voidstate3()
{//debouncingstate
key.keyValue=ScanKey();
if(key.keyValue==EnumKey_NoKey)
state--;
else{
key.keyPressed=true;
state++;
}
}
voidstate4(){if(ScanKey()==EnumKey_NoKey)state++;}//nextstateifthekeyreleased
voidstate5(){ScanKey()==EnumKey_NoKey?state=1:state--;}
上面的鍵盤處理過程顯然比通常使用標志去抖的程序簡潔清晰,而且沒有軟件延時去抖的困擾。以此類推,各個任務都可以劃分成一個個的state,每個state實際上占用不多的處理時間。某些任務可以劃分成若干個子任務,每個子任務再劃分成若干個狀態。
(題外話:對于常數類型,建議使用enum分類組織,避免使用大量#define定義常數)
對于一些完全不能分割,必須獨占的任務來說,比如我以前一個低成本應用中紅外遙控器的軟件解碼任務,這時只能犧牲其他的任務了。兩種做法:一種是關閉中斷,完全的獨占;
voidRunTaskN()
{
Disable_Interrupt;
…
Enable_Interrupt;
}
第二種,允許定時中斷發生,保證某些時基register得以更新;
voidTimer_Interrupt()
{
SetTimer();
Enable_Timer_Interrupt;
UpdateTimingRegisters();
if(watchDogCounter=0){
ResetStack();
for(i=0;i<CountOfArray(tasks),i++)
(*tasks)();
while(1)IDLE;
}
else
watchDogCounter--;
}
只要watchDogCounter不為0,那么中斷正常返回到中斷點,繼續執行先前被中斷的任務,否則,復位stack,重新進行任務循環。這種狀況下,中斷處理過程極短,對獨占任務的影響也有限。
中斷驅動多任務配合狀態機的使用,我相信這是mcu下無os系統較好的設計結構。對于絕大多數mcu程序設計來說,可以極大的減輕程序結構的安排,無需過多的考慮各個任務之間的時間安排,而且可以讓程序簡潔易懂。缺點是,程序員必須花費一定的時間考慮如何切分任務。
下面是一段用C改寫的CDPlayer中檢測disc是否存在的偽代碼,用以展示這種結構的設計技巧,原源代碼為Z8mcu匯編,基于Sony的DSP,ServoandRF處理芯片,通過送出命令字來控制主軸/滑板/聚焦/尋跡電機,并讀取狀態以及CD的subQ碼。這個處理任務只是一個大任務下用statemachine切開的一個二級子任務,tick=20ms。
state1(){InitializeMotor();state++;}
state2(){
if(innerSwitch!=ON){
SendCommand(EnumCommand_SlidingMotorBackward);
timeout=MILLISECOND(10000);
state++;//滑板電機向內運動,直至觸及最內開關。
}
else
state+=2;
}
state3(){
if((--timeout)==0){//note:someCcompliersdonotsupport(--timeout)==
SendCommand(EnumCommand_SlidingMotorStop)
systemErrorCode=EnumErrorCode_InnerSwitch;
state=0;//10s超時錯誤,
}
else{
if(innerSwitch==ON){
SendCommand(EnumCommand_SlidingMotorStop)
timeout=MILLISECOND(200);//200ms電機停止時間
state++;
}
}
}
state4(){if((--timeout)==0)state++;}//等待電機完全停止
state5(){
SendCommand(EnumCommand_SlidingMotorForward);
timeout=MILLISECOND(2000);
state++;
}//滑板電機向外運動,脫離innerswitch
state6(){
if((--timeout)==0){
SendCommand(EnumCommand_SlidingMotorStop)
systemErrorCode=EnumErrorCode_InnerSwitch;
state=0;//2s超時錯誤,
}
else{
if(innerSwitch==OFF){
SendCommand(EnumCommand_SlidingMotorStop)
timeout=MILLISECOND(200);//200ms電機停止時間
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;//本任務結束。
playProcess.state=1;//啟動play任務
}
elseif((--timeout)==0){
SendCommand(EnumCommand_FocusClose);//光頭聚焦復位
if((--retryCounter)==0){
systemStatus.Disc=EnumStatus_Nodisc;//無盤
displayProcess.state=EnumDisplayState_NoDisc;//顯示閃爍的無盤
LaserOff();
state=0;//任務停止
}
else
state--;//再試
}
}
stateStop(){
SendCommand(EnumCommand_SlidingMotorStop);
SendCommand(EnumCommand_FocusClose);
state=0;
}
網友評論:^_^
網友評論:如果象LZ這樣的高手被你一句話氣跑了,豈不是讓很多新手失去請教的機會?。!
網友評論:為什么害死的是一只貓而不是一只老鼠或者別的???
網友評論:感謝樓主分享
網友評論:雖然很欣賞這種模式,可以降低功耗,但還是不愿意嘗試
貌似中斷服務程序還是簡單點好
網友評論:不看好!
網友評論:處理一個跑馬燈程序可能這樣做可以。但是真的需要處理一些實時度要求高一點的程序就不可行了。
網友評論:這是典型的協作式,但是這樣做不安全,容易丟失時間片。建議中斷里面設置標志,把那一大套移到外面,這樣更安全。
網友評論:不錯!不錯。。。。。。!
網友評論:1.不是什么單片機都支持中斷嵌套,咱的意思,能放到外面的就不要放到中斷中,盡可能快的釋放中斷是中斷機制的本意
2.低功耗啥的和處理時間有關,在終端中處理同樣不低功耗
網友評論:學習學習。。。。。。。。。。。!
網友評論:非常歡迎來自五湖四海的朋友,電子技術交流群:29303696
網友評論:還沒有認真地看,不過,粗略地看一下有點像RTX51的辦法。
RTX51就是用T0作時間中斷計時器,主程序就可以是一個maintask,然后給每個任務分配一定的運行時間片,到時就中斷執行別的任務,
不知道我說的對不對,我現在就新接手一個這樣的項目,遇到的問題是每個任務的運行時間有比較嚴格的要求,不可以太長,想請問一下樓主你怎么預算一下每個task的執行時間?
網友評論:收藏
網友評論:中斷復雜化與簡約化并不矛盾,應用不同,靈活選擇了
網友評論:沒法細細看完,樓主的意思是不是這樣的?(我不懂什么OS,操作系統)
假設每5MS中斷一次,
中斷一次,做第1件事,要在5MS內做完.做完空循環.
中斷第2次,做第2件事,要在5MS內做完.做完,做完空循環.
中斷第3次,做第3件事,要在5MS內做完.做完,做完空循環.
...
中斷第N次,做第N件事,要在5MS內做完.做完,清除中斷記數標記.做完空循環.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
我在沒中斷的系統中這樣做:
計數器如果等于1,做第1件事,完成剛好200US,然后記數器加1.
計數器如果等于2,做第2件事,完成剛好200US,然后記數器加1.
計數器如果等于1,做第3件事,完成剛好200US,然后記數器加1.
...
計數器如果等于N,做第N件事,完成剛好200US,然后記數器清0.然后記數器加1.
網友評論:雖然我的觀點和樓主不同,我支持能在主程序做的絕不用中斷。例如按鍵去抖就用主程序周期,反正去抖有50%甚至更大的偏差都沒關系。
不過還是支持一下樓主原創。
網友評論:IC庫存管理軟件第一品牌,您的最佳選擇
要用就用“易管理”
“易管理”庫存管理系統,輕松實現采購、銷售、詢價、客戶關系管理,銷售訂單合同等信息一體化管理,輕松實現庫存與貿易統一運行。這是在國內首次提出進銷存貿易一體化的平臺,這是領先全球的專享網絡平臺。
在激烈的市場競爭中,如何提升企業市場競爭力和自身形象,適應社會各方面發展的需求,以及如何應對新形勢、新挑戰?通過合理的企業信息化建設就可以讓你的企業插上騰飛的翅膀,它不但可以讓你的企業變得更高效,還可以讓您的企業具有更高的競爭力!
隨著信息技術的發展,決戰IC市場,信息在IC行業起著舉足輕重的作用,我們致力于專為IC行業的公司提供專業的全方位信息技術服務。
“易管理”,使您發現弊端,果斷解決
我們分析您的管理煩惱,為您尋求解決方案
想實時掌握公司的運作情況嗎?
想節約時間將更多精力用于提升業務競爭力嗎?
想隨時掌握客戶的銷售情況嗎?
想更有效的掌握庫存情況嗎?
想掌握復雜工作流程,從容知道處理方案嗎?
想排除加班處理繁雜財務工作的煩惱嗎??
企業在管理中遇到的問題
1、供應商庫存管理混亂!
2、商業秘密的供應商庫存信息給員工輕易帶走!
3、從前使用的庫存管理系統,麻煩又不好用!
4、分公司數據無法共享!
5、詢報價記錄沒有存底,潛在客戶無法抓!
。。。。。。。。。
我們擁有全國幾千客戶的優異口碑見證,幫助你實時準確的掌握公司庫存、客戶、財務,業務的狀況。
聯系人:陳明亮
電子郵件:cml@eguanli.com
電 話:+86-755-83910101839102028306712183067044
移動電話:13510964861
地 址:深圳市福田區深南中路6002號人民大廈5樓
網 址:http://www.iclook.cn
QQ:860736028
網友評論:想法不錯.
網友評論:還是比較贊成所長的簡約中斷
網友評論:板凳
網友評論:好像有處筆誤constFUNCTIONPTR[]tasks={
constFUNCTIONPTRtasks[]={
網友評論:不好理解,個人感覺:
時序的主次將完全顛倒;
而且系統可能變得很復雜;
是否放在中斷中CPU運行的時間基本一樣,低功耗從何而來?
主程序往往耗時都很長,時序豈不是要大亂?
中斷中多安排一些例行性的動作,如顯示,讀健,發聲等,這個是提倡的,但整個主程序搬過來,實在看不出什么好處。
網友評論:技術資料中心
匯集全球各大廠商的產品資料,從技術參數到應用筆記、解決方案、應用筆記等一應俱全,搜索方式靈活,各種技術資料相互關聯,資源集成度極高;同時,為了幫助工程師獲得更多新技術,ICBuy還將聯合各技術代理商開展在線技術培訓。
億芯網,簡稱ICBuy,立足于通過互聯網為電子行業的用戶提供元器件相關信息服務,內容包括技術資料中心、工程樣片零售商城、市場供貨信息查詢三個方面,為客戶提供從技術開發到小批量采購以及量產供應商的選擇等一系列產品和信息服務。
相關鏈接:http://www.icbuy.com/
網友評論:樓主實在是把一件簡單的事情復雜化了。如果能合理應用“狀態機”的概念,所謂的多任務處理就能輕松實現。
網友評論:不錯,努力學習中
網友評論:這樣能保證時鐘的準確?你能保證中斷里的任務能在這么短的時間內完成?假如你用了液晶,有刷寫液晶任務,他所耗用的時間早已經夠定時器中段多次了。用這種方式,只能是任務少,且耗時短。
假如使用了ps2接口,而此時正在定時器中斷中處理任務,當ps2設備有請求時,無法響應,應此會錯過這個響應。此類對響應要求苛刻的設備在這種系統中會很不可靠。
網友評論:雖然做過多年的單片機程序,還沒有想過原來可以這樣做,呵呵。。。
就像習慣了右手,卻忽視了左手也可以做一些事情,建議大家學習LZ變相思考的做法,沒準能想出更好的辦法
與諸君共勉
網友評論:記號
網友評論:只是很多具體的情況是任務不多不復雜,這時所有任務都可以在中斷中完成。。。
網友評論:感覺不是個很好的一個構架
當然了,比沒有構架好
網友評論:偶也是這么做的,就是有的觀點不敢茍同,個人認為,一個時間片已經足夠程序執行了,想想12M晶振,平均每條語句算他2us,10ms一個時間片,可以執行5000條語句,在狀態機的情況下并不是每條語句都執行到的,一般的項目一個狀態執行不可能5000條語句,好!執行完跳出我可以讓它休眠,降低耗電,更重要的是增加的抗干擾性!偶覺得開門狗拉回來沒用,如果開門狗溢出,重啟,需要利用狀態機來返回,返回到原來執行的狀態
網友評論:應用的場合要求實時性高,用個狀態機機制好點。事件驅動,我不認為中斷內做太多的動作,一般在中斷做個時間標志或者處理一些響應程度高的動作。沒有必要復雜化
網友評論:中斷程序應該做盡可能少的事情。
前后臺系統中,我喜歡這樣。
voidmain(void)
{
while(1)
{
Sleep();
SystemCounter++;
switch(status)
{
//各個任務......
}
}
}
網友評論:學習了。多謝LS
網友評論:不過用中斷要小心!
網友評論:超宏達科技是一家專業的電子元器件代理商(www.super-grand.net),主要是NXP、ATMEL、ST、FAIRCHILD、NS、MICROCHIP、Winbond、Rohm、JRC、SAMSUNG、IR、UTC的代理,公司強大的技術支持和電子商務平臺,可以解決電子元器件BOM表一站式打樣和在線詢價,詳細資料請登陸www.super-grand.net,電話:83219636傳真:83261186
網友評論:
voidmain(void)
{
ucharbuffer=SmartOption[0];//SmartOption
system_init();
receiver_init();
while(1)
{
keyscan_thread();
key_handle_therad();
minder_thread();
RF_encode_thread();
idle();
};
}
網友評論:有點操作系統的味道了