找回密碼
 立即注冊(cè)

QQ登錄

只需一步,快速開(kāi)始

搜索
查看: 3013|回復(fù): 0
打印 上一主題 下一主題
收起左側(cè)

中斷多任務(wù)+狀態(tài)機(jī) 單片機(jī)軟件結(jié)構(gòu)設(shè)計(jì)

[復(fù)制鏈接]
跳轉(zhuǎn)到指定樓層
樓主
ID:109555 發(fā)表于 2016-3-18 16:16 | 只看該作者 回帖獎(jiǎng)勵(lì) |倒序?yàn)g覽 |閱讀模式
mcu由于內(nèi)部資源的限制,軟件設(shè)計(jì)有其特殊性,程序一般沒(méi)有復(fù)雜的算法以及數(shù)據(jù)結(jié)構(gòu),代碼量也不大, 通常不會(huì)使用 OS (Operating System),  因?yàn)閷?duì)于一個(gè)只有 若干K ROM, 一百多byte RAM mcu 來(lái)說(shuō),一個(gè)簡(jiǎn)單OS  也會(huì)吃掉大部分的資源。

 

對(duì)于無(wú) os 的系統(tǒng),流行的設(shè)計(jì)是主程序(主循環(huán) ) + (定時(shí))中斷,這種結(jié)構(gòu)雖然符合自然想法,不過(guò)卻有很多不利之處,首先是中斷可以在主程序的任何地方發(fā)生,隨意打斷主程序。其次主程序與中斷之間的耦合性(關(guān)聯(lián)度)較大,這種做法 使得主程序與中斷纏繞在一起,必須仔細(xì)處理以防不測(cè)。

 

那么換一種思路,如果把主程序全部放入(定時(shí))中斷中會(huì)怎么樣?這么做至少可以立即看到幾個(gè)好處: 系統(tǒng)可以處于低功耗的休眠狀態(tài),將由中斷喚醒進(jìn)入主程序; 如果程序跑飛,則中斷可以拉回;沒(méi)有了主從之分(其他中斷另計(jì)),程序易于模塊化。

 

(題外話:這種方法就不會(huì)有何處喂狗的說(shuō)法,也沒(méi)有中斷是否應(yīng)該盡可能的簡(jiǎn)短的爭(zhēng)論了)

 

為了把主程序全部放入(定時(shí))中斷中,必須把程序化分成一個(gè)個(gè)的模塊,即任務(wù),每個(gè)任務(wù)完成一個(gè)特定的功能,例如掃描鍵盤(pán)并檢測(cè)按鍵。 設(shè)定一個(gè)合理的時(shí)基 (tick), 例如  5, 10 20 ms,  每次定時(shí)中斷,把所有任務(wù)執(zhí)行一遍,為減少?gòu)?fù)雜性,一般不做動(dòng)態(tài)調(diào)度(最多使用固定數(shù)組以簡(jiǎn)化設(shè)計(jì),做動(dòng)態(tài)調(diào)度就接近 os 了),這實(shí)際上是一種無(wú)優(yōu)先級(jí)時(shí)間片輪循的變種。來(lái)看看主程序的構(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, 這主要針對(duì)8051, 8051 自動(dòng)重裝分頻器只有 8-bit, 難以做到長(zhǎng)時(shí)間定時(shí);復(fù)位 stack ,即把stack 指針賦值為棧頂或棧底(對(duì)于 pic TI DSP 等使用循環(huán)棧的 mcu 來(lái)說(shuō),則無(wú)此必要),用以表示與過(guò)去決裂,而且不準(zhǔn)備返回到中斷點(diǎn),保證不會(huì)保留程序在跑飛時(shí)stack 中的遺體。Enable_Timer_Interrupt 也主要是針對(duì)80518051 由于中斷控制較弱,只有兩級(jí)中斷優(yōu)先級(jí),而且使用了如果中斷程序不用 reti 返回,則不能響應(yīng)同級(jí)中斷這種偷懶方法,所以對(duì)于 8051, 必須調(diào)用一次 reti 來(lái)開(kāi)放中斷:

 

                 _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)行,由程序員自己控制。另一種做法是通過(guò)函數(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<CountOfArray (tasks), i++)

                                (*tasks[i])();

         while (1) IDLE

}

 

 

使用const 是讓數(shù)組內(nèi)容位于 code segment ROM) 而非 data segment (RAM) 中,8051 中使用 code 作為 const 的替代品。

 

(題外話:關(guān)于函數(shù)指針賦值時(shí)是否需要取地址操作符 & 的問(wèn)題,與數(shù)組名一樣,取決于 compiler. 對(duì)于熟悉匯編的人來(lái)說(shuō),函數(shù)名和數(shù)組名都是常數(shù)地址,無(wú)需也不能取地址。對(duì)于不熟悉匯編的人來(lái)說(shuō),用 & 取地址是理所當(dāng)然的事情。Visual C++ 2005對(duì)此兩者都支持)

 

這種方法在匯編下表現(xiàn)為散轉(zhuǎn), 一個(gè)小技巧是利用 stack 獲取跳轉(zhuǎn)表入口:

           

                                    mov                A, state

                                             acall                MultiJump

                                             ajmp               state0

                                             ajmp               state1

                                    ...

 

MultiJump:                  pop                DPH

                                 pop                DPL

                                 rl                    A

                                 jmp                @A+DPTR

 

 

還有一種方法是把函數(shù)指針數(shù)組(動(dòng)態(tài)數(shù)組,鏈表更好,不過(guò)在 mcu 中不適用)放在 data segment 中,便于修改函數(shù)指針以運(yùn)行不同的任務(wù),這已經(jīng)接近于動(dò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)();

                }

 

 

通過(guò)上面的手段,一個(gè)中斷驅(qū)動(dòng)的框架形成了,下面的事情就是保證每個(gè) tick 內(nèi)所有任務(wù)的運(yùn)行時(shí)間總和不能超過(guò)一個(gè) tick 的時(shí)間。為了做到這一點(diǎn),必須把每個(gè)任務(wù)切分成一個(gè)個(gè)的時(shí)間片,每個(gè) tick 內(nèi)運(yùn)行一片。這里引入了狀態(tài)機(jī) (state machine) 來(lái)實(shí)現(xiàn)切分。關(guān)于 state machine,  很多書(shū)中都有介紹, 這里就不多說(shuō)了。

 

(題外話:實(shí)踐升華出理論,理論再作用于實(shí)踐。我很長(zhǎng)時(shí)間不知道我一直沿用的方法就是state machine,直到學(xué)習(xí)UML/C++,書(shū)中介紹 tachniques for identifying dynamic behvior,方才豁然開(kāi)朗。功夫在詩(shī)外,掌握 C++, 甚至C# JAVA, 對(duì)理解嵌入式程序設(shè)計(jì),會(huì)有莫大的幫助)

 

狀態(tài)機(jī)的程序?qū)崿F(xiàn)相當(dāng)簡(jiǎn)單,第一種方法是用 swich-case 實(shí)現(xiàn):

 

            void RunTaskN()

                {

                switch (state) {

                                case 0: state0(); break;

                                case 1: state1(); break;

                               

                                case M: stateM(); break;

                                default:

                                                state = 0;

                }

}

 

另一種方法還是用更通用簡(jiǎn)潔的函數(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

 

一個(gè)小技巧是把第一個(gè)狀態(tài) state0 設(shè)置為空狀態(tài),即:

 

                void state0() { }

 

這樣,state =0可以讓整個(gè)task 停止運(yùn)行,如果需要投入運(yùn)行,簡(jiǎn)單的讓 state = 1 即可。

 

以下是一個(gè)鍵盤(pán)掃描的例子,這里假設(shè) tick = 20 ms, ScanKeyboard() 函數(shù)控制口線的輸出掃描,并檢測(cè)輸入轉(zhuǎn)換為鍵碼,利用每個(gè)state 之間 20 ms 的間隔去抖動(dòng)。

 

                enum EnumKey {

EnumKey_NoKey =  0,

    };

                struct StructKey {

                                int                keyValue;

                                bool                keyPressed;

    } ;

 

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--; }

 

 

上面的鍵盤(pán)處理過(guò)程顯然比通常使用標(biāo)志去抖的程序簡(jiǎn)潔清晰,而且沒(méi)有軟件延時(shí)去抖的困擾。以此類推,各個(gè)任務(wù)都可以劃分成一個(gè)個(gè)的state, 每個(gè)state 實(shí)際上占用不多的處理時(shí)間。某些任務(wù)可以劃分成若干個(gè)子任務(wù),每個(gè)子任務(wù)再劃分成若干個(gè)狀態(tài)。

 

(題外話:對(duì)于常數(shù)類型,建議使用 enum 分類組織,避免使用大量 #define 定義常數(shù))

 

對(duì)于一些完全不能分割,必須獨(dú)占的任務(wù)來(lái)說(shuō),比如我以前一個(gè)低成本應(yīng)用中紅外遙控器的軟件解碼任務(wù),這時(shí)只能犧牲其他的任務(wù)了。兩種做法:一種是關(guān)閉中斷,完全的獨(dú)占;

 

            void RunTaskN()

    {

                Disable_Interrupt;

               

                Enable_Interrupt;

    }          

 

第二種,允許定時(shí)中斷發(fā)生,保證某些時(shí)基 register 得以更新;

 

                void Timer_Interrupt()

                {

                                SetTimer();

                                Enable_Timer_Interrupt;

                                UpdateTimingRegisters();

                                if (watchDogCounter = 0) {

                                               ResetStack();

                                                for (i=0; i<CountOfArray (tasks), i++)

                                                                (*tasks[i])();

            while (1) IDLE;

        }

        else

                watchDogCounter--;          

    }

 

只要watchDogCounter 不為 0,那么中斷正常返回到中斷點(diǎn),繼續(xù)執(zhí)行先前被中斷的任務(wù),否則,復(fù)位 stack, 重新進(jìn)行任務(wù)循環(huán)。這種狀況下,中斷處理過(guò)程極短,對(duì)獨(dú)占任務(wù)的影響也有限。

 

中斷驅(qū)動(dòng)多任務(wù)配合狀態(tài)機(jī)的使用,我相信這是mcu 下無(wú)os 系統(tǒng)較好的設(shè)計(jì)結(jié)構(gòu)。對(duì)于絕大多數(shù) mcu 程序設(shè)計(jì)來(lái)說(shuō),可以極大的減輕程序結(jié)構(gòu)的安排,無(wú)需過(guò)多的考慮各個(gè)任務(wù)之間的時(shí)間安排,而且可以讓程序簡(jiǎn)潔易懂。缺點(diǎn)是,程序員必須花費(fèi)一定的時(shí)間考慮如何切分任務(wù)。

 

下面是一段用 C 改寫(xiě)的CD Player 中檢測(cè) disc 是否存在的偽代碼,用以展示這種結(jié)構(gòu)的設(shè)計(jì)技巧,原源代碼為Z8 mcu 匯編, 基于 Sony DSP, Servo and RF 處理芯片, 通過(guò)送出命令字來(lái)控制主軸/滑板/聚焦/尋跡電機(jī),并讀取狀態(tài)以及 CD sub Q 碼。這個(gè)處理任務(wù)只是一個(gè)大任務(wù)下用state machine切開(kāi)的一個(gè)二級(jí)子任務(wù),tick = 20 ms。

           

                state1() { InitializeMotor(); state++; }

                state2() { 

if (innerSwitch != ON) {

SendCommand(EnumCommand_SlidingMotorBackward);

timeout = MILLISECOND(10000); 

state++;                // 滑板電機(jī)向內(nèi)運(yùn)動(dòng), 直至觸及最內(nèi)開(kā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 超時(shí)錯(cuò)誤,

        }

        else {

                if (innerSwitch == ON) {

                                                        SendCommand(EnumCommand _SlidingMotorStop)

                                timeout = MILLISECOND(200);                  // 200ms電機(jī)停止時(shí)間 

                                state++;

                }

 

}

    }

                state4() { if ((--timeout) == 0) state++; }                  //等待電機(jī)完全停止

                state5() { 

SendCommand(EnumCommand_SlidingMotorForward);

timeout = MILLISECOND(2000); 

state++;

}                // 滑板電機(jī)向外運(yùn)動(dòng),脫離inner switch

 

                state6() {

                                if ((--timeout) == 0) {    

                                                SendCommand(EnumCommand_SlidingMotorStop)

                                                systemErrorCode = EnumErrorCode_InnerSwitch;

                                                state = 0;              // 2 s 超時(shí)錯(cuò)誤,

}

else {

                if (innerSwitch == OFF) {

                                                        SendCommand(EnumCommand_SlidingMotorStop)

                                timeout = MILLISECOND(200);                  // 200ms電機(jī)停止時(shí)間 

                                state++;

                }

}

                }

                state7() { state4(); } 

                state8() { LaserOn(); state++; retryCounter = 3;}                 //打開(kāi)激光器

                state9() {

SendCommand(FocusUp);

state++; 

timeout = MILLISECOND(2000)

    }                  //光頭上舉,檢測(cè)聚焦過(guò)零 3 次,判斷cd 是否存在

               

                state10() {

                                if (FocusCrossZero)  {

                                                systemStatus.Disc = EnumStatus_DiscExist;   

                                                SendCommand(EnumCommand_AutoFocusOn);    //cd, 打開(kāi)自動(dòng)聚焦。

                                    state = 0;                             //本任務(wù)結(jié)束。

                                    playProcess.state = 1;                //啟動(dòng) play 任務(wù)

                                }

                                else if ((--timeout) == 0) {

                                                SendCommand(EnumCommand_ FocusClose);                  //光頭聚焦復(fù)位

                                                if ((--retryCounter) == 0) {

                                                                systemStatus.Disc = EnumStatus_Nodisc;       //無(wú)盤(pán)

                                                                displayProcess.state = EnumDisplayState_NoDisc;  //顯示閃爍的無(wú)盤(pán)  

                                                                LaserOff();

                                                                state = 0;                //任務(wù)停止

            }

            else

                            state--;                                 //再試               

        }

                }

 

    stateStop() {

                SendCommand(EnumCommand_SlidingMotorStop);

    SendCommand(EnumCommand_FocusClose); 

    state = 0;

    }

評(píng)分

參與人數(shù) 1黑幣 +100 收起 理由
admin + 100 共享資料的黑幣獎(jiǎng)勵(lì)!

查看全部評(píng)分

分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏1 分享淘帖 頂 踩
回復(fù)

使用道具 舉報(bào)

本版積分規(guī)則

手機(jī)版|小黑屋|51黑電子論壇 |51黑電子論壇6群 QQ 管理員QQ:125739409;技術(shù)交流QQ群281945664

Powered by 單片機(jī)教程網(wǎng)

快速回復(fù) 返回頂部 返回列表