寫在前面:放寒假了終於有時間學習一下嵌入式作業系統的知識。一直想做嵌入式底層開發,但以前沒有接觸過這方面的知識,現在一邊學習一邊寫部落格,與大家分享一下自己的學習曆程。一直認為能夠自己編寫一個作業系統,才是真正的學會了作業系統的知識。所以選擇了陳旭武的《輕鬆自編小型嵌入式作業系統》。但是看了一部分後覺得,書中用拼音命名變數的習慣,已及作者編寫的作業系統極高的記憶體佔用率實在是讓人無力吐槽了。所以這本書平時翻一翻還可以,作為入門教材就不向大家推薦了。部落格內容也借鑒了書中比較優秀的一部分內容,說在前面。最簡單的任務調度以現代觀點而言,一個標準個人電腦的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 inputdo_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 intuint16;#include <reg52.h>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_Stack2void Task_0(); //任務0void Task_1(); //任務1void Task_2(); //任務2void 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個任務中順序執行了。