51單片機(jī)多任務(wù)操作系統(tǒng)的原理與實(shí)現(xiàn)
//任務(wù)切換函數(shù)(任務(wù)調(diào)度器)
本文引用地址:http://cafeforensic.com/article/201701/342566.htmvoid task_switch()
{
task_sp[task_id] = SP;
if(++task_id == MAX_TASKS)
task_id = 0;
SP = task_sp[task_id];
}
//任務(wù)裝入函數(shù).將指定的函數(shù)(參數(shù)1)裝入指定(參數(shù)2)的任務(wù)槽中.如果該槽中原來(lái)就有任務(wù),則原任務(wù)丟失,但系統(tǒng)本身不會(huì)發(fā)生錯(cuò)誤.
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;
}
//從指定的任務(wù)開(kāi)始運(yùn)行任務(wù)調(diào)度.調(diào)用該宏后,將永不返回.
#define os_start(tid) {task_id = tid,SP = task_sp[tid];return;}
/*==================以下為測(cè)試代碼=====================*/
void task1()
{
static unsigned char i;
while(1){
i++;
task_switch();//編譯后在這里打上斷點(diǎn)
}
}
void task2()
{
static unsigned char j;
while(1){
j+=2;
task_switch();//編譯后在這里打上斷點(diǎn)
}
}
void main()
{
//這里裝載了兩個(gè)任務(wù),因此在定義MAX_TASKS時(shí)也必須定義為2
task_load(task1, 0);//將task1函數(shù)裝入0號(hào)槽
task_load(task2, 1);//將task2函數(shù)裝入1號(hào)槽
os_start(0);
}
限于篇幅我已經(jīng)將代碼作了簡(jiǎn)化,并刪掉了大部分注釋,大家可以直接下載源碼包,里面完整的注解,并帶KEIL工程文件,斷點(diǎn)也打好了,直接按ctrl+f5就行了.
現(xiàn)在來(lái)看看這個(gè)多任務(wù)系統(tǒng)的原理:
這個(gè)多任務(wù)系統(tǒng)準(zhǔn)確來(lái)說(shuō),叫作"協(xié)同式多任務(wù)".
所謂"協(xié)同式",指的是當(dāng)一個(gè)任務(wù)持續(xù)運(yùn)行而不釋放資源時(shí),其它任務(wù)是沒(méi)有任何機(jī)會(huì)和方式獲得運(yùn)行機(jī)會(huì),除非該任務(wù)主動(dòng)釋放CPU.
在本例里,釋放CPU是靠task_switch()來(lái)完成的.task_switch()函數(shù)是一個(gè)很特殊的函數(shù),我們可以稱它為"任務(wù)切換器".
要清楚任務(wù)是如何切換的,首先要回顧一下堆棧的相關(guān)知識(shí).
有個(gè)很簡(jiǎn)單的問(wèn)題,因?yàn)樗?jiǎn)單了,所以相信大家都沒(méi)留意過(guò):
我們知道,不論是CALL還是JMP,都是將當(dāng)前的程序流打斷,請(qǐng)問(wèn)CALL和JMP的區(qū)別是什么?
你會(huì)說(shuō):CALL可以RET,JMP不行.沒(méi)錯(cuò),但原因是啥呢?為啥CALL過(guò)去的就可以用RET跳回來(lái),JMP過(guò)去的就不能用RET來(lái)跳回呢?
很顯然,CALL通過(guò)某種方法保存了打斷前的某些信息,而在返回?cái)帱c(diǎn)前執(zhí)行的RET指令,就是用于取回這些信息.
不用多說(shuō),大家都知道,"某些信息"就是PC指針,而"某種方法"就是壓棧.
很幸運(yùn),在51里,堆棧及堆棧指針都是可被任意修改的,只要你不怕死.那么假如在執(zhí)行RET前將堆棧修改一下會(huì)如何?往下看:
當(dāng)程序執(zhí)行CALL后,在子程序里將堆棧剛才壓入的斷點(diǎn)地址清除掉,并將一個(gè)函數(shù)的地址壓入,那么執(zhí)行完RET后,程序就跳到這個(gè)函數(shù)去了.
事實(shí)上,只要我們?cè)赗ET前將堆棧改掉,就能將程序跳到任務(wù)地方去,而不限于CALL里壓入的地址.
重點(diǎn)來(lái)了......
首先我們得為每個(gè)任務(wù)單獨(dú)開(kāi)一塊內(nèi)存,這塊內(nèi)存專用于作為對(duì)應(yīng)的任務(wù)的堆棧,想將CPU交給哪個(gè)任務(wù),只需將棧指針指向誰(shuí)內(nèi)存塊就行了.
接下來(lái)我們構(gòu)造一個(gè)這樣的函數(shù):
當(dāng)任務(wù)調(diào)用該函數(shù)時(shí),將當(dāng)前的堆棧指針保存一個(gè)變量里,并換上另一個(gè)任務(wù)的堆棧指針.這就是任務(wù)調(diào)度器了.
OK了,現(xiàn)在我們只要正確的填充好這幾個(gè)堆棧的原始內(nèi)容,再調(diào)用這個(gè)函數(shù),這個(gè)任務(wù)調(diào)度就能運(yùn)行起來(lái)了.
那么這幾個(gè)堆棧里的原始內(nèi)容是哪里來(lái)的呢?這就是"任務(wù)裝載"函數(shù)要干的事了.
在啟動(dòng)任務(wù)調(diào)度前將各個(gè)任務(wù)函數(shù)的入口地址放在上面所說(shuō)的"任務(wù)專用的內(nèi)存塊"里就行了!對(duì)了,順便說(shuō)一下,這個(gè)"任務(wù)專用的內(nèi)存塊"叫作"私棧",私棧的意思就是說(shuō),每個(gè)任務(wù)的堆棧都是私有的,每個(gè)任務(wù)都有一個(gè)自已的堆棧.
話都說(shuō)到這份上了,相信大家也明白要怎么做了:
1.分配若干個(gè)內(nèi)存塊,每個(gè)內(nèi)存塊為若干字節(jié):
這里所說(shuō)的"若干個(gè)內(nèi)存塊"就是私棧,要想同時(shí)運(yùn)行幾少個(gè)任務(wù)就得分配多少塊.而"每個(gè)子內(nèi)存塊若干字節(jié)"就是棧深.記住,每調(diào)一層子程序需要2字節(jié).如果不考慮中斷,4層調(diào)用深度,也就是8字節(jié)棧深應(yīng)該差不多了.
unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP]
當(dāng)然,還有件事不能忘,就是堆指針的保存處.不然光有堆棧怎么知道應(yīng)該從哪個(gè)地址取數(shù)據(jù)啊
unsigned char idata task_sp[MAX_TASKS]
上面兩項(xiàng)用于裝任務(wù)信息的區(qū)域,我們給它個(gè)概念叫"任務(wù)槽".有些人叫它"任務(wù)堆",我覺(jué)得還是"槽"比較直觀
對(duì)了,還有任務(wù)號(hào).不然怎么知道當(dāng)前運(yùn)行的是哪個(gè)任務(wù)呢?
unsigned char task_id
當(dāng)前運(yùn)行存放在1號(hào)槽的任務(wù)時(shí),這個(gè)值就是1,運(yùn)行2號(hào)槽的任務(wù)時(shí),這個(gè)值就是2....
2.構(gòu)造任務(wù)調(diào)度函函數(shù):
void task_switch()
{
task_sp[task_id] = SP; //保存當(dāng)前任務(wù)的棧指針
if(++task_id == MAX_TASKS) //任務(wù)號(hào)切換到下一個(gè)任務(wù)
task_id = 0;
SP = task_sp[task_id]; //將系統(tǒng)的棧指針指向下個(gè)任務(wù)的私棧.
}
3.裝載任務(wù):
將各任務(wù)的函數(shù)地址的低字節(jié)和高字節(jié)分別入在
task_stack[任務(wù)號(hào)][0]和task_stack[任務(wù)號(hào)][1]中:
為了便于使用,寫(xiě)一個(gè)函數(shù): task_load(函數(shù)名, 任務(wù)號(hào))
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.啟動(dòng)任務(wù)調(diào)度器:
將棧指針指向任意一個(gè)任務(wù)的私棧,執(zhí)行RET指令.注意,這可很有學(xué)問(wèn)的哦,沒(méi)玩過(guò)堆棧的人腦子有點(diǎn)轉(zhuǎn)不彎:這一RET,RET到哪去了?嘿嘿,別忘了在RET前已經(jīng)將堆棧指針指向一個(gè)函數(shù)的入口了.你別把RET看成RET,你把它看成是另一種類型的JMP就好理解了.
SP = task_sp[任務(wù)號(hào)];
return;
做完這4件事后,任務(wù)"并行"執(zhí)行就開(kāi)始了.你可以象寫(xiě)普通函數(shù)一個(gè)寫(xiě)任務(wù)函數(shù),只需(目前可以這么說(shuō))注意在適當(dāng)?shù)臅r(shí)候(例如以前調(diào)延時(shí)的地方)調(diào)用一下task_switch(),以讓出CPU控制權(quán)給別的任務(wù)就行了.
最后說(shuō)下效率問(wèn)題.
這個(gè)多任務(wù)系統(tǒng)的開(kāi)銷是每次切換消耗20個(gè)機(jī)器周期(CALL和RET都算在內(nèi)了),貴嗎?不算貴,對(duì)于很多用狀態(tài)機(jī)方式實(shí)現(xiàn)的多任務(wù)系統(tǒng)來(lái)說(shuō),其實(shí)效率還沒(méi)這么高--- case switch和if()可不像你想像中那么便宜.
關(guān)于內(nèi)存的消耗我要說(shuō)的是,當(dāng)然不能否認(rèn)這種多任務(wù)機(jī)制的確很占內(nèi)存.但建議大家不要老盯著編譯器下面的那行字"DATA = XXXbyte".那個(gè)值沒(méi)意義,堆棧沒(méi)算進(jìn)去.關(guān)于比較省內(nèi)存多任務(wù)機(jī)制,我將來(lái)會(huì)說(shuō)到.
概括來(lái)說(shuō),這個(gè)多任務(wù)系統(tǒng)適用于實(shí)時(shí)性要求較高而內(nèi)存需求不大的應(yīng)用場(chǎng)合,我在運(yùn)行于36M主頻的STC12C4052上實(shí)測(cè)了一把,切換一個(gè)任務(wù)不到3微秒.
下回我們講講用KEIL寫(xiě)多任務(wù)函數(shù)時(shí)要注意的事項(xiàng).
下下回我們講講如何增強(qiáng)這個(gè)多任務(wù)系統(tǒng),跑步進(jìn)入操作系統(tǒng)時(shí)代.
四.用KEIL寫(xiě)多任務(wù)系統(tǒng)的技巧與注意事項(xiàng)
C51編譯器很多,KEIL是其中比較流行的一種.我列出的所有例子都必須在KEIL中使用.為何?不是因?yàn)镵EIL好所以用它(當(dāng)然它的確很棒),而是因?yàn)檫@里面用到了KEIL的一些特性,如果換到其它編譯器下,通過(guò)編譯的倒不是問(wèn)題,但運(yùn)行起來(lái)可能是堆棧錯(cuò)位,上下文丟失等各種要命的錯(cuò)誤,因?yàn)槊糠N編譯器的特性并不相同.所以在這里先說(shuō)清楚這一點(diǎn).
但是,我開(kāi)頭已經(jīng)說(shuō)了,這套帖子的主要目的是闡述原理,只要你能把這幾個(gè)例子消化掉,那么也能夠自已動(dòng)手寫(xiě)出適合其它編譯器的OS.
好了,說(shuō)說(shuō)KEIL的特性吧,先看下面的函數(shù):
sbit sigl = P1^7;
void func1()
{
register char data i;
i = 5;
do{
sigl = !sigl;
}while(--i);
}
你會(huì)說(shuō),這個(gè)函數(shù)沒(méi)什么特別的嘛!呵呵,別著急,你將它編譯了,然后展開(kāi)匯編代碼再看看:
193: void func1(){
194: register char data i;
195: i = 5;
C:0x00C3 7F05 MOV R7,#0x05
196: do{
197: sigl = !sigl;
C:0x00C5 B297 CPL sigl(0x90.7)
198: }while(--i);
C:0x00C7 DFFC DJNZ R7,C:00C5
199: }
C:0x00C9 22 RET
看清楚了沒(méi)?這個(gè)函數(shù)里用到了R7,卻沒(méi)有對(duì)R7進(jìn)行保護(hù)!
有人會(huì)跳起來(lái)了:這有什么值得奇怪的,因?yàn)樯蠈雍瘮?shù)里沒(méi)用到R7啊.呵呵,你說(shuō)的沒(méi)錯(cuò),但只說(shuō)對(duì)了一半:事實(shí)上,KEIL編譯器里作了約定,在調(diào)子函數(shù)前會(huì)盡可能釋放掉所有寄存器.通常性況下,除了中斷函數(shù)外,其它函數(shù)里都可以任意修改所有寄存器而無(wú)需先壓棧保護(hù)(其實(shí)并不是這樣,但現(xiàn)在暫時(shí)這樣認(rèn)為,飯要一口一口吃嘛,我很快會(huì)說(shuō)到的).
這個(gè)特性有什么用呢?有!當(dāng)我們調(diào)用任務(wù)切換函數(shù)時(shí),要保護(hù)的對(duì)象里可以把所有的寄存器排除掉了,就是說(shuō),只需要保護(hù)堆棧即可!
現(xiàn)在我們回過(guò)頭來(lái)看看之前例子里的任務(wù)切換函數(shù):
void task_switch()
{
task_sp[task_id] = SP; //保存當(dāng)前任務(wù)的棧指針
if(++task_id == MAX_TASKS) //任務(wù)號(hào)切換到下一個(gè)任務(wù)
task_id = 0;
SP = task_sp[task_id]; //將系統(tǒng)的棧指針指向下個(gè)任務(wù)的私棧.
}
看到?jīng)],一個(gè)寄存器也沒(méi)保護(hù),展開(kāi)匯編看看,的確沒(méi)保護(hù)寄存器.
評(píng)論