以現代觀點而言,壹個標準個人電腦的OS應該提供以下的功能:
進程管理(Processing management)
內存管理(Memory management)
文件系統(File system)
網絡通訊(Networking)
安全機制(Security)
用戶界面(User interface)
驅動程序(Device drivers)
但壹個最簡易的嵌入式操作系統,所包含的可以少很多。最簡單的操作系統,通常都是圍繞著進程管理展開的。所以,現在可以嘗試下壹個最簡單的“操作系統”,只能做簡單地進行人工任務調度。為了簡單起見,使用最簡單的AT89S52運行程序:內存小的數的清字節數,外設只有幾個IO,結構簡單,很方便操作系統的編寫。
1.裸跑的任務和操作系統中的任務
相信大家都很熟悉,用單片機裸跑,程序壹般都寫成如下壹個大的while死循環:
void main (void)
{
while (1) /* repeat forever */
{
do_something();
}
}
或者又像:
void main (void)
{
while (1) /* repeat forever */
{
do_something1();
do_something2(); //Catch data input
do_something3();
.
.
.
}
}
這裏每壹個函數完成壹個獨立的操作或者任務,這些函數(也可以叫任務)以壹定的順序執行,壹個接著壹個。這裏的任務切換,單純就是執行完壹個,再執行另壹個。不斷循環。
但是,壹旦增加更多的任務,那麽執行的順序就變成了壹個問題。在以上的例子中,壹旦函數do_something1()運行了太長的時間,那麽主循環就需要很長的時間才可以執行到do_something2()。如果do_something2()是接收輸入數據的函數,數據就很有可能丟失。當然,我們也可以在循環中插入更多的do_something2()函數調用,或者把do_something1()拆分成幾個比較小的部分。但這就比較考驗編程者功力了,如果任務太多,編寫程序將成為壹個相當復雜的問題。
這時,壹個幫助妳分配各個任務運行時間的操作系統就很有必要了。在操作系統中,任務壹般形如:
void check_serial_io_task (void) _task_ 1
{
/* This task checks for serial I/O */
}
void process_serial_cmds_task (void) _task_ 2
{
/* This task processes serial commands */
}
void check_kbd_io_task (void) _task_ 3
{
/* This task checks for keyboard I/O */
}
任務之間的切換已經交給操作系統完成了,熟悉的main函數和while(1)壹般已經隱去不見了。
2.如何做任務切換
還是說單片機裸跑,裸跑時,把C語言文件編譯成匯編,可以看到,是用CALL指令去調壹個任務函數,執行完畢後,用RET退出。但是這樣的方法用在切換頻繁的操作系統中,就無疑不適合了,因為我們無法做到預知什麽時候退出,即調用RET。
任務切換,看起來很玄,實際上說白了,就是改變程序指針PC的值。前邊寫的_task_ 1,_task_ 2,編譯以後,都存儲在ROM中。把PC指向這段ROM,他就執行了,想切換另壹個任務,就用PC指向那個任務。就這麽簡單。這樣說,是不是就是PC=壹個地址就可以了?不行,因為絕大多數單片機,是不允許給PC寄存器直接賦值的。那樣寫,編譯器會報錯的。壹般操作系統,都用以下方法改變PC的值:
unsigned char Task_Stack1[3];
Task_Stack1[1] = (uint16) Task_1;
Task_Stack1[2] = (uint16) Task_1 >> 8;
SP = Task_Stack1+2;
}//編譯成RET
PC的值不能直接改變,但是可以變通,通過其他方式改變PC的值。壹個函數執行完畢,總是要改變PC的。這是,PC是如何改變的呢?函數執行前,PC被壓入了堆棧中。函數結束,要調用的是RET指令,也就是PC出棧。壓在堆棧中的原始PC值,這時從堆棧中彈出,程序又回到了原來的位置。這裏就是模仿這壹過程:模擬壹個堆棧的結構,把要執行的函數入口地址(C語言中的函數名)裝入其中,把SP指向這個自己創建的堆棧棧頂。壹個RET指令,就將[SP]和[SP-1]彈到PC中了。就這樣,PC改變到了要執行的函數入口地址,開始執行目標函數。(AT89s52的PC為16位,壓到堆棧中是兩個字節)
3.壹個最簡單的人工調度系統
應用上面的思想,寫壹個最簡單的3任務人工調度系統。代碼如下:
typedef unsigned char uint8;
typedef unsigned int uint16;
#include
sbit led0 = P0^0;
sbit led1 = P0^1;
sbit led2 = P0^2;
uint8 Cur_TaskID; //當前運行的任務號
uint8 Task_Stack0[10]; //0號任務的堆棧
uint8 Task_Stack1[10];
uint8 Task_Stack2[10];
uint8 Task_StackSP[3]; //3個堆棧的棧頂指針
//Task_StackSP[0] -> Task_Stack0
//Task_StackSP[1] -> Task_Stack1
//Task_StackSP[2] -> Task_Stack2
void Task_0(); //任務0
void Task_1(); //任務1
void Task_2(); //任務2
void Task_Scheduling(uint8 Task_ID); //任務調度
void main (void)
{
Task_Stack0[1] = (uint16) Task_0; //按照小端模式,任務函數入口地址裝入任務堆棧
Task_Stack0[2] = (uint16) Task_0 >> 8;
Task_Stack1[1] = (uint16) Task_1;
Task_Stack1[2] = (uint16) Task_1 >> 8;
Task_Stack2[1] = (uint16) Task_2;
Task_Stack2[2] = (uint16) Task_2 >> 8;
Task_StackSP[0] = Task_Stack0;
Task_StackSP[0] += 2; //剛入棧兩個元素。這裏取得棧頂地址,即Task_Stack0[2]
Task_StackSP[1] = Task_Stack1;
Task_StackSP[1] += 2;
Task_StackSP[2] = Task_Stack2;
Task_StackSP[2] += 2;
Cur_TaskID = 0;
SP = Task_StackSP[0]; //SP取得0號任務的棧頂地址
}//利用main的返回指令RET,使PC取得0號任務入口地址
//任務調度函數
void Task_Scheduling(uint8 Task_ID)
{
Task_StackSP[Cur_TaskID] = SP;
Cur_TaskID = Task_ID;
SP = Task_StackSP[Cur_TaskID];
}
//0號任務函數
void Task_0()
{
while(1)
{
led0 = 0;
Task_Scheduling(1);
}
}
//1號任務函數
void Task_1()
{
while(1)
{
led1 = 0;
Task_Scheduling(2);
}
}
//2號任務函數
void Task_2()
{
while(1)
{
led2 = 0;
Task_Scheduling(0);
}
}
代碼要做的,就是3個任務的順序執行。任務調度函數Task_Scheduling的思想也即如前面所述。在Keil中可以運行代碼,可以看到,程序在3個任務中順序執行了。