簡單的51微控制器多工作業系統(C51)

liming0931發表於2018-08-12

 在網上看到這段程式碼,所以自己嘗試了,可以跑起來,但是沒有精確的定時功能,僅僅是任務的排程而已。

陣列中是11,而不是12。這裡寫錯了。。。

/*

簡單的多工作業系統

其實只有個任務排程切換,把說它是OS有點牽強,但它對於一些簡單的開發應用來說,
簡單也許就是最好的.盡情的擴充套件它吧.別忘了把你的成果分享給大家.

這是一個最簡單的OS,一切以執行效率為重,經測試,切換一次任務僅20個機器週期,
也就是在標準51(工作於12M晶振)上20uS.
而為速度作出的犧牲是,為了給每個任務都分配一個私有堆疊,而佔用了較多的記憶體.
作為補償,多工更容易安排程式邏輯,從而可以節省一些用於控制的變數.
任務槽越多,佔用記憶體越多,但任務也越好安排,以實際需求合理安排任務數目.
一般來說,4個已足夠.況且可以拿一個槽出來作為活動槽,換入換入一些臨時任務.

task_load(函式名,任務槽號)
裝載任務

os_start(任務槽號)
啟動任務表.引數必須指向一個裝載了的任務,否則系統會崩潰.

task_switch()
切換到其它任務


.編寫任務函式注意事項:
KEIL C編譯器是假定使用者使用單任務環境,所以在變數的使用上都未對多工進行處理,
編寫任務時應注意變數覆蓋和程式碼重入問題.

1.覆蓋:編譯器為了節省記憶體,會給兩個沒用呼叫關係的函式分配同一記憶體地址作為變數空間.
這在單任務下是很合理的,但對於多工來說,兩個程式會互相干擾對方.
解決的方法是:凡作用域內會跨越task_switch()的變數,都使用static前輟,
保證其地址空間分配時的唯一性.

2.重入:重入並不是多工下獨有的問題,在單任務時,函式遞迴同樣會導致重入,
即,一個函式的不同例項(或者叫作"複本")之間的變數覆蓋問題.
解決的方法是:使用reentrant函式後輟(例如:void function1() reentrant{...}).當然,根本的辦法還是避免重入,因為重入會帶來巨大的目的碼量,
並極大降低執行效率.

3.額外提醒一句,在本例中,任務函式必須為一個死迴圈.退出函式會導致系統崩潰.


.任務函式如果是用匯編寫成或內嵌彙編,切換任務時應該注意什麼問題?

由於KEIL C編譯器在處理函式呼叫時的約定規則為"子函式有可能修改任務暫存器",
因此編譯器在呼叫前已釋放所有暫存器,子函式無需考慮保護任何暫存器.
這對於寫慣彙編的人來說有點不習慣: 彙編習慣於在子程式中保護暫存器.
請注意一條原則:凡是需要跨越task_switch()的暫存器,全部需要保護(例如入棧).
根本解決辦法還是,不要讓暫存器跨越任務切換函式task_switch()
事實上這裡要補充一下,正如前所說,由於編譯器存在變數地址覆蓋優化,
因此凡是非靜態變數都不得跨越task_switch().


任務函式的書寫:
void 函式名(void)
{	//任務函式必須定義為無引數型
	while(1)
	{
		//任務函式不得返回,必須為死迴圈
		//....這裡寫任務處理程式碼

		task_switch();//每執行一段時間任務,就釋放CPU一下,
		讓別的任務有機會執行.
	}
}


任務裝載:
task_load(函式名,任務槽號)

裝載函式的動作可發生在任意時候,但通常是在main()中.要注意的是,
在本例中由於沒考慮任務換出,
所以在執行os_start()前必須將所有任務槽裝滿.之後可以隨意更換任務槽中的任務.

啟動任務排程器:
os_start(任務槽號)

呼叫該巨集後,將從引數指定的任務槽開始執行任務排程.
本例為每切換一次任務需額外開銷20個機器週期,用於遷移堆疊.
*/


#include <reg52.h>

sbit LED1 = P2 ^ 0;
sbit LED2 = P2 ^ 1;
void func1();
void func2();

/*============================以下為工作管理員程式碼============================*/

//任務槽個數.在本例中並未考慮任務換入換出,所以實際執行的任務有多少個,
//就定義多少個任務槽,不可多定義或少定義
#define MAX_TASKS 5

//任務的棧指標
unsigned char idata task_sp[MAX_TASKS];

//最大棧深.最低不得少於2個,保守值為12.
//預估方法:以2為基數,每增加一層函式呼叫,加2位元組.
//如果其間可能發生中斷,則還要再加上中斷需要的棧深.
//減小棧深的方法:1.儘量少巢狀子程式 2.調子程式前關中斷.
#define MAX_TASK_DEP 12

unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP];//任務堆疊.

unsigned char task_id;		//當前活動任務號


//任務切換函式(任務排程器)
void task_switch()
{
    task_sp[task_id] = SP;		//儲存當前任務的棧指標

    if (++task_id == MAX_TASKS)	//任務號切換到下一個任務
        task_id = 0;

    SP = task_sp[task_id];		//將系統的棧指標指向下個任務的私棧
}




//任務裝入函式.將指定的函式(引數1)裝入指定(引數2)的任務槽中.
//如果該槽中原來就有任務,則原任務丟失,但系統本身不會發生錯誤.
//將各任務的函式地址的低位元組和高位元組分別入在
//task_stack[任務號][0]和task_stack[任務號][1]中
void task_load(unsigned int fn, unsigned char tid)
{
    //task_sp[tid] = task_stack[tid][1];
    task_sp[tid] = task_stack[tid] + 1;
    task_stack[tid][0] = (unsigned int)fn & 0xff;
    task_stack[tid][1] = (unsigned int)fn >> 8;
}

//從指定的任務開始執行任務排程.呼叫該巨集後,將永不返回.
#define os_start(tid) {task_id = tid,SP = task_sp[tid];return;}




/*============================以下為測試程式碼============================*/


unsigned char stra[3], strb[3];//用於記憶體塊複製測試的陣列.


//測試任務:複製記憶體塊.每複製一個位元組釋放CPU一次
void task1()
{
    //每複製一個位元組釋放CPU一次,控制迴圈的變數必須考慮覆蓋
    static unsigned char i;//如果將這個變數前的static去掉,會發生什麼事?
    i = 0;

    while (1) //任務必須為死迴圈,不得退出函式,否則系統會崩潰
    {
        stra[i] = strb[i];
        if (++i == sizeof(stra))
            i = 0;

        //變數i在這裡跨越了task_switch(),因此它必須定義為靜態(static),
        //否則它將會被其它程式修改,因為在另一個程式裡也會用到該變數所佔用的地址.
        task_switch();//釋放CPU一會兒,讓其它程式有機會執行.如果去掉該行,則別的程式永遠不會被呼叫到
    }
}

//測試任務:複製記憶體塊.每複製一個位元組釋放CPU一次.
void task2()
{
    //每複製一個位元組釋放CPU一次,控制迴圈的變數必須考慮覆蓋
    static unsigned char i;//如果將這個變數前的static去掉,將會發生覆蓋問題.
    //task1()和task2()會被編譯器分配到同一個記憶體地址上,當兩個任務同時執行時,i的值就會被兩個任務改來改去
    i = 0;

    while (1) //任務必須為死迴圈,不得退出函式,否則系統會崩潰
    {
        stra[i] = strb[i];
        if (++i == sizeof(stra))
            i = 0;

        //變數i在這裡跨越了task_switch(),因此它必須定義為靜態(static),
        //否則它將會被其它程式修改,因為在另一個程式裡也會用到該變數所佔用的地址.
        task_switch();//釋放CPU一會兒,讓其它程式有機會執行.如果去掉該行,則別的程式永遠不會被呼叫到
    }
}

//測試任務:複製記憶體塊.複製完所有位元組後釋放CPU一次.
void task3()
{
    //複製全部位元組後才釋放CPU,控制迴圈的變數不須考慮覆蓋
    unsigned char i;//這個變數前不需要加static,
    //因為在它的作用域內並沒有釋放過CPU

    while (1) //任務必須為死迴圈,不得退出函式,否則系統會崩潰
    {
        i = sizeof(stra);
        do
        {
            stra[i-1] = strb[i-1];
        }
        while (--i);

        //變數i在這裡已完成它的使命,所以無需定義為靜態.
        //你甚至可以定義為暫存器型(regiter)
        task_switch();//釋放CPU一會兒,讓其它程式有機會執行.如果去掉該行,
        //則別的程式永遠不會被呼叫到
    }
}



/*
my first task
*/

void func1()
{
    static unsigned char data i;
    i = 0;

    while (1)
    {
        //25ms
        if (i < 250)
        {
            i++;
        }
        if (i >= 250)
        {
            LED1 = ~LED1;
            i = 0;
        }
        task_switch();
    }
}

//經過模擬計算得j=10,即等待1ms
void func2()
{
    static unsigned int data j;
    j = 0;

    while (1)
    {
        //65ms
        if (j < 650)
        {
            j++;
        }
        if (j >= 650)
        {
            LED2 = ~LED2;
            j = 0;
        }
        task_switch();
    }
}

/*
keyscan task

void task3()
{
	while(1)
	{
        i = 5;
        do
		{
                sigl = !sigl;
        }while(--i);
        task_switch();
    }
}

*/

/*
void func1()
{
    register char data i;
    while(1)
	{
        i = 5;
        do
		{
                sigl = !sigl;
        }while(--i);
        task_switch();
    }
}
void func2()
{
    register char data i;
    while(1)
	{
        i = 5;
        do
		{
                sigl = !sigl;
        }while(--i);
        task_switch();
    }
}
*/

void main()
{
    //在這個示例裡並沒有考慮任務的換入換出,所以任務槽必須全部用完,否則系統會崩潰.
    //這裡裝載了三個任務,因此在定義MAX_TASKS時也必須定義為5
    task_load(task1, 0);//將task1函式裝入0號槽
    task_load(task2, 1);//將task2函式裝入1號槽
    task_load(task3, 2);//將task3函式裝入2號槽
    task_load(func1, 3);//將task3函式裝入3號槽
    task_load(func2, 4);//將task3函式裝入4號槽

    os_start(0);//啟動任務排程,並從0號槽開始執行.引數改為1,則首先執行1號槽.
    //呼叫該巨集後,程式流將永不再返回main(),也就是說,該語句行之後的所有語句都不被執行到.
}

 原文如下所示 

給51DIY超輕量級多工作業系統  2009-05-30 2149


想了很久,要不要寫這篇文章最後覺得對作業系統感興趣的人還是很多,寫吧.我不一定能造出玉,但我可以丟擲磚. 

包括我在內的很多人都對51使用作業系統呈悲觀態度,因為51的片上資源太少.但對於很多要求不高的系統來說,使用作業系統可以使程式碼變得更直觀,易於維護,所以在51上仍有作業系統的生存機會. 

流行的uCos,Tiny51等,其實都不適合在2051這樣的片子上用,佔資源較多,唯有自已動手,以不變應萬變,才能讓51也有作業系統可用.這篇貼子的目的,是教會大家如何現場寫一個OS,而不是給大家提供一個OS版本.提供的所有程式碼,也都是示例程式碼,所以不要因為它沒什麼功能就說LAJI之類的話.如果把功能寫全了,一來估計你也不想看了,二來也失去靈活性沒有價值了. 


下面的貼一個示例出來,可以清楚的看到,OS本身只有不到10行原始碼,編譯後的目的碼60位元組,任務切換消耗為20個機器週期.相比之下,KEIL內嵌的TINY51目的碼為800位元組,切換消耗100~700週期.唯一不足之處是,每個任務要佔用掉十幾位元組的堆疊,所以任務數不能太多,用在128B記憶體的51裡有點難度,但對於52來說問題不大.這套程式碼在36M主頻的STC12C4052上實測,切換任務僅需2uS. 


#include reg51.h  

#define MAX_TASKS 2       任務槽個數.必須和實際任務數一至  
#define MAX_TASK_DEP 12   最大棧深.最低不得少於2個,保守值為12.  
unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP];任務堆疊.  
unsigned char task_id;    當前活動任務號  


任務切換函式(任務排程器)  
void task_switch()
{  
        task_sp[task_id] = SP;  

        if(++task_id == MAX_TASKS)  
                task_id = 0;  

        SP = task_sp[task_id];  
}  

任務裝入函式.將指定的函式(引數1)裝入指定(引數2)的任務槽中.如果該槽中原來就有任務,則原任務丟失,但系統本身不會發生錯誤.  
void task_load(unsigned int fn, unsigned char tid)
{  
        task_sp[tid] = task_stack[tid] + 1;  
        task_stack[tid][0] = (unsigned int)fn & 0xff;  
        task_stack[tid][1] = (unsigned int)fn  8;  
}  

從指定的任務開始執行任務排程.呼叫該巨集後,將永不返回.  
#define os_start(tid) {task_id = tid,SP = task_sp[tid];return;}  




============================以下為測試程式碼============================  

void task1()
{  
        static unsigned char i;  
        while(1)
        {  
                i++;  
                task_switch();編譯後在這裡打上斷點  
        }  
}  

void task2()
{  
        static unsigned char j;  
        while(1)
        {  
                j+=2;  
                task_switch();編譯後在這裡打上斷點  
        }  
}  

void main()
{  
        這裡裝載了兩個任務,因此在定義MAX_TASKS時也必須定義為2  
        task_load(task1, 0);將task1函式裝入0號槽  
        task_load(task2, 1);將task2函式裝入1號槽  
        os_start(0);  
}  




這樣一個簡單的多工系統雖然不能稱得上真正的作業系統,但只要你瞭解了它的原理,就能輕易地將它擴充套件得非常強大,想知道要如何做嗎

 一.什麼是作業系統 



人腦比較容易接受類比這種表達方式,我就用公交系統來類比作業系統吧. 

當我們要解決一個問題的時候,是用某種處理手段去完成它,這就是我們常說的方法,計算機裡叫程式(有時候也可以叫它演算法). 
以出行為例,當我們要從A地走到B地的時候,可以走著去,也可以飛著去,可以走直線,也可以繞彎路,只要能從A地到B地,都叫作方法.這種從A地到B的需求,相當於計算機裡的任務,而實現從A地到B地的方法,叫作任務處理流程 

很顯然,這些走法中,並不是每種都合理,有些傻子都會採用的,有些是傻子都不採會用的.用計算機的話來說就是,有的任務處理流程好,有的任務處理流程好,有的處理流程差. 
可以歸納出這麼幾種真正算得上方法的方法 
有些走法比較快速,適合於趕時間的人;有些走法比較省事,適合於懶人;有些走法比較便宜,適合於窮人. 
用計算機的話說就是,有些省CPU,有些流程簡單,有些對系統資源要求低. 

現在我們可以看到一個問題 
如果全世界所有的資源給你一個人用(單任務獨佔全部資源),那最適合你需求的方法就是好方法.但事實上要外出的人很多,例如10個人(10個任務),卻只有1輛車(1套資源),這叫作資源爭用. 
如果每個人都要使用最適合他需求的方法,那司機就只好給他們一人跑一趟了,而在任一時刻裡,車上只有一個乘客.這叫作順序執行,我們可以看到這種方法對系統資源的浪費是嚴重的. 
如果我們沒有法力將1臺車變成10臺車來送這10個人,就只好制定一些機制和約定,讓1臺車看起來像10臺車,來解決這個問題的辦法想必大家都知道,那就是制定公交線路. 
最簡單的辦法是將所有旅客需要走的起點與終點串成一條線,車在這條線上開,乘客則自已決定上下車.這就是最簡單的公交線路.它很差勁,但起碼解決客人們對車爭用.對應到計算機裡,就是把所有任務的程式碼混在一起執行. 
這樣做既不優異雅,也沒效率,於是司機想了個辦法,把這些客戶叫到一起商量,將所有客人出行的起點與終點羅列出來,統計這些線路的使用頻度,然後制定出公交線路有些路線可以合併起來成為一條線路,而那些不能合併的路線,則另行開闢行車車次,這叫作任務定義.另外,對於人多路線,車次排多點,時間上也優先安排,這叫作任務優先順序. 
經過這樣的安排後,雖然仍只有一輛車,但運載能力卻大多了.這套車次路線的按排,就是一套公交系統.哈,知道什麼叫作業系統了吧它也就是這麼樣的一種約定. 




作業系統 


我們先回過頭歸納一下 
汽車                                            系統資源.主要指的是CPU,當然還有其它,比如記憶體,定時器,中斷源等. 
客戶出行                                        任務 
正在走的路線                                    程式 
一個一個的運送旅客                              順序執行 
同時運送所有旅客                                多工並行 
按不同的使用頻度制定路線並優先跑較繁忙的路線    任務優先順序 


計算機內有各種資源,單從硬體上說,就有CPU,記憶體,定時器,中斷源,IO埠等.而且還會派生出來很多軟體資源,例如訊息池. 
作業系統的存在,就是為了讓這些資源能被合理地分配. 
最後我們來總結一下,所謂作業系統,以我們目前權宜的理解就是為解決計算機資源爭用而制定出的一種約定. 
 二.51上的作業系統 

對於一個作業系統來說,最重要的莫過於並行多工.在這裡要澄清一下,不要拿當年的DOS來說事,時代不同了.況且當年IBM和小比爾著急將PC搬上市,所以才抄襲PLM(好象是叫這個名吧記不太清)搞了個今天看來很粗製濫造的DOS出來.看看當時真正的作業系統---UNIX,它還在紙上時就已經是多工的了. 

對於我們PC來說,要實現多工並不是什麼問題,但換到MCU卻很頭痛 

1.系統資源少 
在PC上,CPU主頻以G為單位,記憶體以GB為單位,而MCU的主頻通常只有十幾M,記憶體則是Byts.在這麼少的資源上同時執行多個任務,就意味著作業系統必須儘可能的少佔用硬體資源. 
2.任務實時性要求高 
PC並不需要太關心實時性,因為PC上幾乎所有的實時任務都被專門的硬體所接管,例如所有的音效卡網路卡顯示上都內建有DSP以及大量的快取.CPU只需坐在那裡指手劃腳告訴這些板卡如何應付實時資訊就行了. 
而MCU不同,實時資訊是靠CPU來處理的,快取也非常有限,甚至沒有快取.一旦資訊到達,CPU必須在極短的時間內響應,否則資訊就會丟失. 
就拿串列埠通訊來舉例,在標準的PC架構裡,巨大的記憶體允許將資訊儲存足夠長的時間.而對於MCU來說記憶體有限,例如51僅有128位元組記憶體,還要扣除掉暫存器組佔用掉的8~32個位元組,所以通常都僅用幾個位元組來緩衝.當然,你可以將資料的接收與處理的過程合併,但對於一個作業系統來說,不推薦這麼做. 
假定以115200bps通訊速率向MCU傳資料,則每個位元組的傳送時間約為9uS,假定快取為8位元組,則串列埠處理任務必須在70uS內響應. 


這兩個問題都指向了同一種解決思路作業系統必須輕量輕量再輕量,最好是不佔資源(那當然是做夢啦). 

可用於MCU的作業系統很多,但適合51(這裡的51專指無擴充套件記憶體的51)幾乎沒有.前陣子見過一個圈圈作業系統,那是我所見過的作業系統裡最輕量的,但仍有改進的餘地. 

很多人認為,51根本不適合使用作業系統.其實我對這種說法並不完全接受,否則也沒有這篇文章了. 
我的看法是,51不適合採用通用作業系統.所謂通用作業系統就是,不論你是什麼樣的應用需求,也不管你用什麼晶片,只要你是51,通通用同一個作業系統. 

這種想法對於PC來說沒問題,對於嵌入式來說也不錯,對AVR來說還湊合,而對於51這種貧窮型的MCU來說,不行. 
怎樣行量體裁衣,現場根據需求構建一個作業系統出來! 

看到這裡,估計很多人要翻白眼了,大體上兩種 
1.作業系統那麼複雜,說造就造,當自已是神了 
2.作業系統那麼複雜,現場造一個會不會出BUG 
哈哈,看清楚了問題出在複雜上面,如果作業系統不復雜,問題不就解決了 

事實上,很多人對作業系統的理解是片面的,作業系統不一定要做得很複雜很全面,就算幾個多工並行管理能力,你也可以稱它作業系統. 
只要你對多工並行的原理有所瞭解,就不難現場寫一個出來,而一旦你做到了這一點,為各任務間安排通訊約定,使之發展成一個為你的應用系統量身定做的作業系統也就不難了. 

為了加深對作業系統的理解,可以看一看演變這份PPT,讓你充分了解一個並行多工是如何一步步從順序流程演變過來的.裡面還提到了很多人都在用的狀態機,你會發現作業系統跟狀態機從原理上其實是多麼相似.會用狀態機寫程式,都能寫出作業系統. 
 三.我的第一個作業系統 


直接進入主題,先貼一個作業系統的示範出來.大家可以看到,原來作業系統可以做得麼簡單. 
當然,這裡要申明一下,這玩意兒其實算不上真正的作業系統,它除了並行多工並行外根本沒有別的功能.但凡事都從簡單開始,搞懂了它,就能根據應用需求,將它擴充套件成一個真正的作業系統. 

好了,程式碼來了. 
將下面的程式碼直接放到KEIL裡編譯,在每個task()函式的task_switch();那裡打上斷點,就可以看到它們的確是同時在執行的. 


#include reg51.h 

#define MAX_TASKS 2       任務槽個數.必須和實際任務數一至 
#define MAX_TASK_DEP 12   最大棧深.最低不得少於2個,保守值為12. 
unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP];任務堆疊. 
unsigned char task_id;    當前活動任務號 


任務切換函式(任務排程器) 
void task_switch()
{ 
        task_sp[task_id] = SP; 

        if(++task_id == MAX_TASKS) 
                task_id = 0; 

        SP = task_sp[task_id]; 
} 

任務裝入函式.將指定的函式(引數1)裝入指定(引數2)的任務槽中.如果該槽中原來就有任務,則原任務丟失,但系統本身不會發生錯誤. 
void task_load(unsigned int fn, unsigned char tid)
{ 
	u 
	task_stack[tid][0] = (unsigned int)fn & 0xff; 
	task_stack[tid][1] = (unsigned int)fn  8; 
} 

從指定的任務開始執行任務排程.呼叫該巨集後,將永不返回. 
#define os_start(tid) {task_id = tid,SP = task_sp[tid];return;} 




============================以下為測試程式碼============================ 

void task1()
{ 
        static unsigned char i; 
        while(1)
        { 
                i++; 
                task_switch();編譯後在這裡打上斷點 
        } 
} 

void task2()
{ 
        static unsigned char j; 
        while(1)
        { 
                j+=2; 
                task_switch();編譯後在這裡打上斷點 
        } 
} 

void main()
{ 
        這裡裝載了兩個任務,因此在定義MAX_TASKS時也必須定義為2 
        task_load(task1, 0);將task1函式裝入0號槽 
        task_load(task2, 1);將task2函式裝入1號槽 
        os_start(0); 
} 



限於篇幅我已經將程式碼作了簡化,並刪掉了大部分註釋,大家可以直接下載原始碼包,裡面完整的註解,並帶KEIL工程檔案,斷點也打好了,直接按ctrl+f5就行了. 




現在來看看這個多工系統的原理 

這個多工系統準確來說,叫作協同式多工. 
所謂協同式,指的是當一個任務持續執行而不釋放資源時,其它任務是沒有任何機會和方式獲得執行機會,除非該任務主動釋放CPU. 
在本例裡,釋放CPU是靠task_switch()來完成的.task_switch()函式是一個很特殊的函式,我們可以稱它為任務切換器. 
要清楚任務是如何切換的,首先要回顧一下堆疊的相關知識. 

有個很簡單的問題,因為它太簡單了,所以相信大家都沒留意過 
我們知道,不論是CALL還是JMP,都是將當前的程式流打斷,請問CALL和JMP的區別是什麼 
你會說CALL可以RET,JMP不行.沒錯,但原因是啥呢,為啥CALL過去的就可以用RET跳回來,JMP過去的就不能用RET來跳回呢 

很顯然,CALL通過某種方法儲存了打斷前的某些資訊,而在返回斷點前執行的RET指令,就是用於取回這些資訊. 
不用多說,大家都知道,某些資訊就是PC指標,而某種方法就是壓棧. 
很幸運,在51裡,堆疊及堆疊指標都是可被任意修改的,只要你不怕死.那麼假如在執行RET前將堆疊修改一下會如何往下看 
當程式執行CALL後,在子程式裡將堆疊剛才壓入的斷點地址清除掉,並將一個函式的地址壓入,那麼執行完RET後,程式就跳到這個函式去了. 
事實上,只要我們在RET前將堆疊改掉,就能將程式跳到任務地方去,而不限於CALL裡壓入的地址. 

重點來了...... 
首先我們得為每個任務單獨開一塊記憶體,這塊記憶體專用於作為對應的任務的堆疊,想將CPU交給哪個任務,只需將棧指標指向誰記憶體塊就行了. 接下來我們構造一個這樣的函式 

當任務呼叫該函式時,將當前的堆疊指標儲存一個變數裡,並換上另一個任務的堆疊指標.這就是任務排程器了. 

OK了,現在我們只要正確的填充好這幾個堆疊的原始內容,再呼叫這個函式,這個任務排程就能執行起來了. 
那麼這幾個堆疊裡的原始內容是哪裡來的呢這就是任務裝載函式要乾的事了. 

在啟動任務排程前將各個任務函式的入口地址放在上面所說的任務專用的記憶體塊裡就行了!對了,順便說一下,這個任務專用的記憶體塊叫作私棧,私棧的意思就是說,每個任務的堆疊都是私有的,每個任務都有一個自已的堆疊. 

話都說到這份上了,相信大家也明白要怎麼做了 

1.分配若干個記憶體塊,每個記憶體塊為若干位元組 
這裡所說的若干個記憶體塊就是私棧,要想同時執行幾少個任務就得分配多少塊.而每個子記憶體塊若干位元組就是棧深.記住,每調一層子程式需要2位元組.如果不考慮中斷,4層呼叫深度,也就是8位元組棧深應該差不多了. 

unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP] 

當然,還有件事不能忘,就是堆指標的儲存處.不然光有堆疊怎麼知道應該從哪個地址取資料啊 
unsigned char idata task_sp[MAX_TASKS] 

上面兩項用於裝任務資訊的區域,我們給它個概念叫任務槽.有些人叫它任務堆,我覺得還是槽比較直觀 

對了,還有任務號.不然怎麼知道當前執行的是哪個任務呢 
unsigned char task_id 
當前執行存放在1號槽的任務時,這個值就是1,執行2號槽的任務時,這個值就是2.... 

2.構造任務排程函函式 
void task_switch()
{ 
        task_sp[task_id] = SP;儲存當前任務的棧指標 

        if(++task_id == MAX_TASKS)任務號切換到下一個任務 
                task_id = 0; 

        SP = task_sp[task_id];將系統的棧指標指向下個任務的私棧. 
} 


3.裝載任務 
將各任務的函式地址的低位元組和高位元組分別入在 
task_stack[任務號][0]和task_stack[任務號][1]中 

為了便於使用,寫一個函式  task_load(函式名, 任務號) 

void task_load(unsigned int fn, unsigned char tid)
{ 
        task_sp[tid] = task_stack[tid] + 1; 
        task_stack[tid][0] = (unsigned int)fn & 0xff; 
        task_stack[tid][1] = (unsigned int)fn  8; 
} 

4.啟動任務排程器 
將棧指標指向任意一個任務的私棧,執行RET指令.注意,這可很有學問的哦,沒玩過堆疊的人腦子有點轉不彎這一RET,RET到哪去了嘿嘿,別忘了在RET前已經將堆疊指標指向一個函式的入口了.你別把RET看成RET,你把它看成是另一種型別的JMP就好理解了. 

SP = task_sp[任務號]; 
return; 

做完這4件事後,任務並行執行就開始了.你可以象寫普通函式一個寫任務函式,只需(目前可以這麼說)注意在適當的時候(例如以前調延時的地方)呼叫一下task_switch(),以讓出CPU控制權給別的任務就行了. 


最後說下效率問題. 
這個多工系統的開銷是每次切換消耗20個機器週期(CALL和RET都算在內了),貴嗎不算貴,對於很多用狀態機方式實現的多工系統來說,其實效率還沒這麼高--- case switch和if()可不像你想像中那麼便宜. 

關於記憶體的消耗我要說的是,當然不能否認這種多工機制的確很佔記憶體.但建議大家不要老盯著編譯器下面的那行字DATA = XXXbyte.那個值沒意義,堆疊沒算進去.關於比較省記憶體多工機制,我將來會說到. 
概括來說,這個多工系統適用於實時性要求較高而記憶體需求不大的應用場合,我在執行於36M主頻的STC12C4052上實測了一把,切換一個任務不到3微秒. 


下回我們講講用KEIL寫多工函式時要注意的事項. 
下下回我們講講如何增強這個多工系統,跑步進入作業系統時代.

四.用KEIL寫多工系統的技巧與注意事項 

C51編譯器很多,KEIL是其中比較流行的一種.我列出的所有例子都必須在KEIL中使用.為何,不是因為KEIL好所以用它(當然它的確很棒),而是因為這裡面用到了KEIL的一些特性,如果換到其它編譯器下,通過編譯的倒不是問題,但執行起來可能是堆疊錯位,上下文丟失等各種要命的錯誤,因為每種編譯器的特性並不相同.所以在這裡先說清楚這一點. 
但是,我開頭已經說了,這套帖子的主要目的是闡述原理,只要你能把這幾個例子消化掉,那麼也能夠自已動手寫出適合其它編譯器的OS. 

好了,說說KEIL的特性吧,先看下面的函式 

sbit sigl = P1^7; 
void func1()
{ 
        register char data i; 
        i = 5; 
        do{ 
                sigl = !sigl; 
        }while(--i); 
} 

你會說,這個函式沒什麼特別的嘛!呵呵,彆著急,你將它編譯了,然後展開彙編程式碼再看看 

   193 void func1(){  
   194         register char data i;  
   195         i = 5;  
C0x00C3    7F05     MOV      R7,#0x05 
   196         do{  
   197                 sigl = !sigl;  
C0x00C5    B297     CPL      sigl(0x90.7) 
   198         }while(--i);  
C0x00C7    DFFC     DJNZ     R7,C00C5 
   199 }  
C0x00C9    22       RET       

看清楚了沒這個函式裡用到了R7,卻沒有對R7進行保護! 
有人會跳起來了這有什麼值得奇怪的,因為上層函式裡沒用到R7啊.呵呵,你說的沒錯,但只說對了一半事實上,KEIL編譯器裡作了約定,在調子函式前會盡可能釋放掉所有暫存器.通常性況下,除了中斷函式外,其它函式裡都可以任意修改所有暫存器而無需先壓棧保護(其實並不是這樣,但現在暫時這樣認為,飯要一口一口吃嘛,我很快會說到的). 
這個特性有什麼用呢有!當我們呼叫任務切換函式時,要保護的物件裡可以把所有的暫存器排除掉了,就是說,只需要保護堆疊即可! 

現在我們回過頭來看看之前例子裡的任務切換函式 

void task_switch()
{ 
        task_sp[task_id] = SP;儲存當前任務的棧指標 

        if(++task_id == MAX_TASKS)任務號切換到下一個任務 
                task_id = 0; 

        SP = task_sp[task_id];將系統的棧指標指向下個任務的私棧. 
} 

看到沒,一個暫存器也沒保護,展開彙編看看,的確沒保護暫存器. 


好了,現在要給大家潑冷水了,看下面兩個函式 

void func1()
{ 
        register char data i; 
        i = 5; 
        do
        { 
                sigl = !sigl; 
        }while(--i); 
} 
void func2()
{ 
        register char data i; 
        i = 5; 
        do
        { 
                func1(); 
        }while(--i); 
} 

父函式fun2()裡呼叫func1(),展開彙編程式碼看看 
   193 void func1(){  
   194         register char data i;  
   195         i = 5;  
C0x00C3    7F05     MOV      R7,#0x05 
   196         do{  
   197                 sigl = !sigl;  
C0x00C5    B297     CPL      sigl(0x90.7) 
   198         }while(--i);  
C0x00C7    DFFC     DJNZ     R7,C00C5 
   199 }  
C0x00C9    22       RET       
   200 void func2(){  
   201         register char data i;  
   202         i = 5;  
C0x00CA    7E05     MOV      R6,#0x05 
   203         do{  
   204                 func1();  
C0x00CC    11C3     ACALL    func1(C00C3) 
   205         }while(--i);  
C0x00CE    DEFC     DJNZ     R6,C00CC 
   206 }  
C0x00D0    22       RET       

看清楚沒函式func2()裡的變數使用了暫存器R6,而在func1和func2裡都沒保護. 
聽到這裡,你可能又要跳一跳了func1()裡並沒有用到R6,幹嘛要保護沒錯,但編譯器是怎麼知道func1()沒用到R6的呢是從呼叫關係裡推測出來的.一點都沒錯,KEIL會根據函式間的直接呼叫關係為各函式分配暫存器,既不用保護,又不會衝突,KEIL好棒哦!!等一下,先別高興,換到多工的環境裡再試試 

void func1(){ 
        register char data i; 
        i = 5; 
        do{ 
                sigl = !sigl; 
        }while(--i); 
} 
void func2(){ 
        register char data i; 
        i = 5; 
        do{ 
                sigl = !sigl; 
        }while(--i); 
} 

展開彙編程式碼看看 

   193 void func1(){  
   194         register char data i;  
   195         i = 5;  
C0x00C3    7F05     MOV      R7,#0x05 
   196         do{  
   197                 sigl = !sigl;  
C0x00C5    B297     CPL      sigl(0x90.7) 
   198         }while(--i);  
C0x00C7    DFFC     DJNZ     R7,C00C5 
   199 }  
C0x00C9    22       RET       
   200 void func2(){  
   201         register char data i;  
   202         i = 5;  
C0x00CA    7F05     MOV      R7,#0x05 
   203         do{  
   204                 sigl = !sigl;  
C0x00CC    B297     CPL      sigl(0x90.7) 
   205         }while(--i);  
C0x00CE    DFFC     DJNZ     R7,C00CC 
   206 }  
C0x00D0    22       RET       


看到了吧哈哈,這回神仙也算不出來了.因為兩個函式沒有了直接呼叫的關係,所以編譯器認為它們之間不會產生衝突,結果分配了一對互相沖突的暫存器,當任務從func1()切換到func2()時,func1()中的暫存器內容就給破壞掉了.大家可以試著去編譯一下下面的程式 

sbit sigl = P1^7; 
void func1(){ 
        register char data i; 
        i = 5; 
        do{ 
                sigl = !sigl; 
                task_switch(); 
        }while(--i); 
} 
void func2(){ 
        register char data i; 
        i = 5; 
        do{ 
                sigl = !sigl; 
                task_switch(); 
        }while(--i); 
} 

我們這裡只是示例,所以仍可以通過手工分配不同的暫存器避免暫存器衝突,但在真實的應用中,由於任務間的切換是非常隨機的,我們無法預知某個時刻哪個暫存器不會衝突,所以分配不同暫存器的方法不可取.那麼,要怎麼辦呢 
這樣就行了 

sbit sigl = P1^7; 
void func1(){ 
        static char data i; 
        while(1){ 
                i = 5; 
                do{ 
                        sigl = !sigl; 
                        task_switch(); 
                }while(--i); 
        } 
} 
void func2(){ 
        static char data i; 
        while(1){ 
                i = 5; 
                do{ 
                        sigl = !sigl; 
                        task_switch(); 
                }while(--i); 
        } 
} 

將兩個函式中的變數通通改成靜態就行了.還可以這麼做 

sbit sigl = P1^7; 
void func1(){ 
        register char data i; 
        while(1){ 
                i = 5; 
                do{ 
                        sigl = !sigl; 
                }while(--i); 
                task_switch(); 
        } 
} 
void func2(){ 
        register char data i; 
        while(1){ 
                i = 5; 
                do{ 
                        sigl = !sigl; 
                }while(--i); 
                task_switch(); 
        } 
} 

即,在變數的作用域內不切換任務,等變數用完了,再切換任務.此時雖然兩個任務仍然會互相破壞對方的暫存器內容,但對方已經不關心暫存器裡的內容了. 

以上所說的,就是變數覆蓋的問題.現在我們系統地說說關於變數覆蓋. 

變數分兩種,一種是全域性變數,一種是區域性變數(在這裡,暫存器變數算到區域性變數裡). 
對於全域性變數,每個變數都會分配到單獨的地址. 
而對於區域性變數,KEIL會做一個覆蓋優化,即沒有直接呼叫關係的函式的變數共用空間.由於不是同時使用,所以不會衝突,這對記憶體小的51來說,是好事. 
但現在我們進入多工的世界了,這就意味著兩個沒有直接呼叫關係的函式其實是並列執行的,空間不能共用了.怎麼辦呢一種笨辦法是關掉覆蓋優化功能.呵呵,的確很笨. 

比較簡單易行一個解決辦法是,不關閉覆蓋優化,但將那些在作用域內需要跨越任務(換句話說就是在變數用完前會呼叫task_switch()函式的)變數通通改成靜態(static)即可.這裡要對初學者提一下,靜態你可以理解為全域性,因為它的地址空間一直保留,但它又不是全域性,它只能在定義它的那個花括號對{}裡訪問. 
靜態變數有個副作用,就是即使函式退出了,仍會佔著記憶體.所以寫任務函式的時候,儘量在變數作用域結束後才切換任務,除非這個變數的作用域很長(時間上長),會影響到其它任務的實時性.只有在這種情況下才考慮在變數作用域內跨越任務,並將變數申明為靜態. 
事實上,只要程式設計思路比較清析,很少有變數需要跨越任務的.就是說,靜態變數並不多. 

說完了覆蓋我們再說說重入. 
所謂重入,就是一個函式在同一時刻有兩個不同的程式複本.對初學者來說可能不好理解,我舉個例子吧 
有一個函式在主程式會被呼叫,在中斷裡也會被呼叫,假如正當在主程式裡呼叫時,中斷髮生了,會發生什麼情況 

void func1()
{ 
        static char data i; 
        i = 5; 
        do
        { 
                sigl = !sigl; 
        }while(--i); 
} 

假定func1()正執行到i=3時,中斷髮生,一旦中斷呼叫到func1()時,i的值就被破壞了,當中斷結束後,i == 0. 

以上說的是在傳統的單任務系統中,所以重入的機率不是很大.但在多工系統中,很容易發生重入,看下面的例子 
void func1(){ 
.... 
delay(); 
.... 
} 
void func2(){ 
.... 
delay(); 
.... 
} 
void delay(){ 
        static unsigned char i;注意這裡是申明為static,不申明static的話會發生覆蓋問題.而申明為static會發生重入問題.麻煩啊 
        for(i=0;i10;i++) 
                task_switch(); 
} 

兩個並行執行的任務都呼叫了delay(),這就叫重入.問題在於重入後的兩個複本都依賴變數i來控制迴圈,而該變數跨越了任務,這樣,兩個任務都會修改i值了. 
重入只能以防為主,就是說盡量不要讓重入發生,比如將程式碼改成下面的樣子 
#define delay() 
{static unsigned char i; for(i=0;i10;i++) task_switch();}
i仍定義為static,但實際上已經不是同一個函式了,所以分配的地址不同. 
void func1()
{ 
.... 
delay(); 
.... 
} 
void func2()
{ 
.... 
delay(); 
.... 
} 

用巨集來代替函式,就意味著每個呼叫處都是一個獨立的程式碼複本,那麼兩個delay實際使用的記憶體地址也就不同了,重入問題消失. 
但這種方法帶來的問題是,每呼叫一次delay(),都會產生一個delay的目的碼,如果delay的程式碼很多,那就會造成大量的rom空間佔用.有其它辦法沒 

本人所知有限,只有最後一招了 
void delay() reentrant
{ 
        unsigned char i; 
        for(i=0;i<10;i++) 
                task_switch(); 
} 
加入reentrant申明後,該函式就可以支援重入.但小心使用,申明為重入後,函式效率極低! 



最後附帶說下中斷.因為沒太多可說的,就不單獨開章了. 
中斷跟普通的寫法沒什麼區別,只不過在目前所示例的多工系統裡因為有堆疊的壓力,所以要使用using來減少對堆疊的使用(順便提下,也不要呼叫子函式,同樣是為了減輕堆疊壓力) 
用using,必須用#pragma NOAREGS關閉掉絕對暫存器訪問,如果中斷裡非要呼叫函式,連同函式也要放在#pragma NOAREGS的作用域內.如例所示 

#pragma SAVE 
#pragma NOAREGS  使用using時必須將絕對暫存器訪問關閉 
void clock_timer(void) interrupt 1 using 1 使用using是為了減輕堆疊的壓力 
} 
#pragma RESTORE 

改成上面的寫法後,中斷固定佔用4個位元組堆疊.就是說,如果你在不用中斷時任務棧深定為8的話,現在就要定為8+4 = 12了. 
另外說句廢話,中斷裡處理的事一定要少,做個標記就行了,剩下的事交給對應的任務去處理. 



現在小結一下 

切換任務時要保證沒有暫存器跨越任務,否則產生任務間暫存器覆蓋.        使用靜態變數解決 
切換任務時要保證沒有變數跨越任務,否則產生任務間地址空間(變數)覆蓋.  使用靜態變數解決 
兩個不同的任務不要同時呼叫同一個函式,否則產生重入覆蓋.          使用重入申明解決 

 

其中os_start()函式可以這麼寫

//從指定的任務開始執行任務排程.呼叫該巨集後,將永不返回.
//#define os_start(tid) {task_id = tid,SP = task_sp[tid];return;}
void os_start(char tid)
{
    task_id = tid;
    SP = task_sp[tid];
    //return;
}

 

相關文章