亚洲春色中文字幕久久久-三上亚,一吻二脱三床四吻胸,国产真实伦对白视频全集,在线毛片观看,精品成品入口黄网,国产毛aⅴ片久久久,亚洲AV色香蕉一区二区三区老师,萧皇后A级艳片,色情日本视频更新,99久久亚洲精品日本无码

 找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

搜索
查看: 3820|回復(fù): 1
收起左側(cè)

中斷驅(qū)動多任務(wù)-單片機(MCU) 下的一種軟件設(shè)計結(jié)構(gòu)

[復(fù)制鏈接]
ID:108531 發(fā)表于 2016-3-12 18:19 | 顯示全部樓層 |閱讀模式
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)度)較大,這種做法 使得主程序與中斷纏繞在一起,必須仔細處理以防不測。

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

(題外話:這種方法就不會有何處喂狗的說法,也沒有中斷是否應(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 進入低功耗模式。中斷程序的構(gòu)成

                void Timer_Interrupt()
                {
                                 SetTimer();
                                 ResetStack();
                                 Enable_Timer_Interrupt;
                                 ….


進入中斷后,首先重置Timer, 這主要針對8051, 8051 自動重裝分頻器只有 8-bit, 難以做到長時間定時;復(fù)位 stack ,即把stack 指針賦值為棧頂或棧底(對于 pic, TI DSP 等使用循環(huán)棧的 mcu 來說,則無此必要),用以表示與過去決裂,而且不準備返回到中斷點,保證不會保留程序在跑飛時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ù)是否需要運行,由程序員自己控制。另一種做法是通過函數(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)();
         while (1) IDLE;
}

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

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

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

mov                A, state
                                             acall                MultiJump
                                             ajmp               state0
                                             ajmp               state1
                                    ...

MultiJump:                  pop                DPH
                                 pop                DPL
                                 rl                    A
                                 jmp                @A+DPTR

還有一種方法是把函數(shù)指針數(shù)組(動態(tài)數(shù)組,鏈表更好,不過在 mcu 中不適用)放在 data segment 中,便于修改函數(shù)指針以運行不同的任務(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);
                          if (pFunc != NULL)
                                      (*pFunc)();
                }


通過上面的手段,一個中斷驅(qū)動的框架形成了,下面的事情就是保證每個 tick 內(nèi)所有任務(wù)的運行時間總和不能超過一個 tick 的時間。為了做到這一點,必須把每個任務(wù)切分成一個個的時間片,每個 tick 內(nèi)運行一片。這里引入了狀態(tài)機 (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)機的程序?qū)崿F(xiàn)相當簡單,第一種方法是用 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 停止運行,如果需要投入運行,簡單的讓 state = 1 即可。

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

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


上面的鍵盤處理過程顯然比通常使用標志去抖的程序簡潔清晰,而且沒有軟件延時去抖的困擾。以此類推,各個任務(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<CountOfArray (tasks), i++)
                                                                (*tasks)();
            while (1) IDLE;
        }
        else
                watchDogCounter--;           
    }

只要watchDogCounter 不為 0,那么中斷正常返回到中斷點,繼續(xù)執(zhí)行先前被中斷的任務(wù),否則,復(fù)位 stack, 重新進行任務(wù)循環(huán)。這種狀況下,中斷處理過程極短,對獨占任務(wù)的影響也有限。
中斷驅(qū)動多任務(wù)配合狀態(tài)機的使用,我相信這是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 處理芯片, 通過送出命令字來控制主軸/滑板/聚焦/尋跡電機,并讀取狀態(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++;                // 滑板電機向內(nèi)運動, 直至觸及最內(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電機停止時間  
                                state++;
                }
}
    }
                state4() { if ((--timeout) == 0) state++; }                  //等待電機完全停止
                state5() {  
SendCommand(EnumCommand_SlidingMotorForward);
timeout = MILLISECOND(2000);  
state++;
}                // 滑板電機向外運動,脫離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電機停止時間  
                                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;
    }

回復(fù)

使用道具 舉報

ID:72251 發(fā)表于 2016-5-6 23:23 | 顯示全部樓層
樓主大哥是高手!
回復(fù)

使用道具 舉報

您需要登錄后才可以回帖 登錄 | 立即注冊

本版積分規(guī)則

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

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

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