1. 程式人生 > >單片機多任務調度

單片機多任務調度

易懂 更新 這一 support display bit 狀況 fault timer

單片機多任務調度

mcu由於內部資源的限制,軟件設計有其特殊性,程序一般沒有復雜的算法以及數據結構,代碼量也不大, 通常不會使用OS (Operating System), 因為對於一個只有 若幹K ROM, 一百多byte RAM 的 mcu 來說,一個簡單OS 也會吃掉大部分的資源。

對於無 os 的系統,流行的設計是主程序(主循環 ) + (定時)中斷,這種結構雖然符合自然想法,不過卻有很多不利之處,首先是中斷可以在主程序的任何地方發生,隨意打斷主程序。其次主程序與中斷之間的耦合性(關聯度)較大,這種做法 使得主程序與中斷纏繞在一起,必須仔細處理以防不測。

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

(題外話:這種方法就不會有何處餵狗的說法,也沒有中斷是否應該盡可能的簡短的爭論了)

為了把主程序全部放入(定時)中斷中,必須把程序化分成一個個的模塊,即任務,每個任務完成一個特定的功能,例如掃描鍵盤並檢測按鍵。 設定一個合理的時基 (tick), 例如 5, 10 或 20 ms, 每次定時中斷,把所有任務執行一遍,為減少復雜性,一般不做動態調度(最多使用固定數組以簡化設計,做動態調度就接近 os 了),這實際上是一種無優先級時間片輪循的變種。來看看主程序的構成:

void main()

{

…. // Initialize

while (true) {

IDLE; //sleep

}

}

這裏的 IDLE 是一條sleep 指令,讓 mcu 進入低功耗模式。中斷程序的構成

void Timer_Interrupt()

{

SetTimer();

ResetStack();

Enable_Timer_Interrupt;

….

進入中斷後,首先重置Timer, 這主要針對8051, 8051 自動重裝分頻器只有 8-bit, 難以做到長時間定時;復位 stack ,即把stack 指針賦值為棧頂或棧底(對於 pic, TI DSP 等使用循環棧的 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

可以看到中斷把所有任務調用一遍,至於任務是否需要運行,由程序員自己控制。另一種做法是通過函數指針數組:

#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 是讓數組內容位於 code segment (ROM) 而非 data segment (RAM) 中,8051 中使用 code 作為 const 的替代品。

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

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

mov A, state

acall MultiJump

ajmp state0

ajmp state1

...

MultiJump: pop DPH

pop DPL

rl A

jmp @A+DPTR

還有一種方法是把函數指針數組(動態數組,鏈表更好,不過在 mcu 中不適用)放在 data segment 中,便於修改函數指針以運行不同的任務,這已經接近於動態調度了:

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)();

}

通過上面的手段,一個中斷驅動的框架形成了,下面的事情就是保證每個 tick 內所有任務的運行時間總和不能超過一個tick 的時間。為了做到這一點,必須把每個任務切分成一個個的時間片,每個 tick 內運行一片。這裏引入了狀態機 (state machine) 來實現切分。關於 state machine, 很多書中都有介紹, 這裏就不多說了。

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

狀態機的程序實現相當簡單,第一種方法是用 swich-case 實現:

void RunTaskN()

{

switch (state) {

case 0: state0(); break;

case 1: state1(); break;

case M: stateM(); break;

default:

state = 0;

}

}

另一種方法還是用更通用簡潔的函數指針數組:

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

一個小技巧是把第一個狀態 state0 設置為空狀態,即:

void state0() { }

這樣,state =0可以讓整個task 停止運行,如果需要投入運行,簡單的讓 state = 1 即可。

以下是一個鍵盤掃描的例子,這裏假設 tick = 20 ms, ScanKeyboard() 函數控制口線的輸出掃描,並檢測輸入轉換為鍵碼,利用每個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--; }

上面的鍵盤處理過程顯然比通常使用標誌去抖的程序簡潔清晰,而且沒有軟件延時去抖的困擾。以此類推,各個任務都可以劃分成一個個的state, 每個state 實際上占用不多的處理時間。某些任務可以劃分成若幹個子任務,每個子任務再劃分成若幹個狀態。

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

對於一些完全不能分割,必須獨占的任務來說,比如我以前一個低成本應用中紅外遙控器的軟件解碼任務,這時只能犧牲其他的任務了。兩種做法:一種是關閉中斷,完全的獨占;

void RunTaskN()

{

Disable_Interrupt;

Enable_Interrupt;

}

第二種,允許定時中斷發生,保證某些時基 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,那麽中斷正常返回到中斷點,繼續執行先前被中斷的任務,否則,復位 stack, 重新進行任務循環。這種狀況下,中斷處理過程極短,對獨占任務的影響也有限。

中斷驅動多任務配合狀態機的使用,我相信這是mcu 下無os 系統較好的設計結構。對於絕大多數 mcu 程序設計來說,可以極大的減輕程序結構的安排,無需過多的考慮各個任務之間的時間安排,而且可以讓程序簡潔易懂。缺點是,程序員必須花費一定的時間考慮如何切分任務。

下面是一段用 C 改寫的CD Player 中檢測 disc 是否存在的偽代碼,用以展示這種結構的設計技巧,原源代碼為Z8 mcu 匯編, 基於 Sony 的 DSP, Servo and RF 處理芯片, 通過送出命令字來控制主軸/滑板/聚焦/尋跡電機,並讀取狀態以及 CD 的sub Q 碼。這個處理任務只是一個大任務下用state machine切開的一個二級子任務,tick = 20 ms

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

state2() {

if (innerSwitch != ON) {

SendCommand(EnumCommand_SlidingMotorBackward);

timeout = MILLISECOND(10000)

state++; // 滑板電機向內運動, 直至觸及最內開關。

}

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; //本任務結束。

playProcess.state = 1; //啟動 play 任務

}

else if ((--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;

}

單片機多任務調度