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