51單片機(jī)多任務(wù)操作系統(tǒng)的原理與實(shí)現(xiàn)
想了很久,要不要寫這篇文章?最后覺得對操作系統(tǒng)感興趣的人還是很多,寫吧.我不一定能造出玉,但我可以拋出磚.
包括我在內(nèi)的很多人都對51使用操作系統(tǒng)呈悲觀態(tài)度,因?yàn)?1的片上資源太少.但對于很多要求不高的系統(tǒng)來說,使用操作系統(tǒng)可以使代碼變得更直觀,易于維護(hù),所以在51上仍有操作系統(tǒng)的生存機(jī)會.
流行的uCos,Tiny51等,其實(shí)都不適合在2051這樣的片子上用,占資源較多,唯有自已動(dòng)手,以不變應(yīng)萬變,才能讓51也有操作系統(tǒng)可用.這篇貼子的目的,是教會大家如何現(xiàn)場寫一個(gè)OS,而不是給大家提供一個(gè)OS版本.提供的所有代碼,也都是示例代碼,所以不要因?yàn)樗鼪]什么功能就說LAJI之類的話.如果把功能寫全了,一來估計(jì)你也不想看了,二來也失去靈活性沒有價(jià)值了.
下面的貼一個(gè)示例出來,可以清楚的看到,OS本身只有不到10行源代碼,編譯后的目標(biāo)代碼60字節(jié),任務(wù)切換消耗為20個(gè)機(jī)器周期.相比之下,KEIL內(nèi)嵌的TINY51目標(biāo)代碼為800字節(jié),切換消耗100~700周期.唯一不足之處是,每個(gè)任務(wù)要占用掉十幾字節(jié)的堆棧,所以任務(wù)數(shù)不能太多,用在128B內(nèi)存的51里有點(diǎn)難度,但對于52來說問題不大.這套代碼在36M主頻的STC12C4052上實(shí)測,切換任務(wù)僅需2uS.
#include
#define MAX_TASKS 2 //任務(wù)槽個(gè)數(shù).必須和實(shí)際任務(wù)數(shù)一至
#define MAX_TASK_DEP 12 //最大棧深.最低不得少于2個(gè),保守值為12.
unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP]; //任務(wù)堆棧.
unsigned char task_id; //當(dāng)前活動(dòng)任務(wù)號 //任務(wù)切換函數(shù)(任務(wù)調(diào)度器)
void 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ù)槽中.如果該槽中原來就有任務(wù),則原任務(wù)丟失,但系統(tǒng)本身不會發(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ù)開始運(yùn)行任務(wù)調(diào)度.調(diào)用該宏后,將永不返回.
#define os_start(tid) {task_id = tid,SP = task_sp[tid];return;}
/*======================以下為測試代碼======================*/ 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號槽
task_load(task2, 1); //將task2函數(shù)裝入1號槽
os_start(0);
}
這樣一個(gè)簡單的多任務(wù)系統(tǒng)雖然不能稱得上真正的操作系統(tǒng),但只要你了解了它的原理,就能輕易地將它擴(kuò)展得非常強(qiáng)大,想知道要如何做嗎?
一.什么是操作系統(tǒng)?
人腦比較容易接受"類比"這種表達(dá)方式,我就用"公交系統(tǒng)"來類比"操作系統(tǒng)"吧.
當(dāng)我們要解決一個(gè)問題的時(shí)候,是用某種處理手段去完成它,這就是我們常說的"方法",計(jì)算機(jī)里叫"程序"(有時(shí)候也可以叫它"算法").
以出行為例,當(dāng)我們要從A地走到B地的時(shí)候,可以走著去,也可以飛著去,可以走直線,也可以繞彎路,只要能從A地到B地,都叫作方法.這種從A地到B的需求,相當(dāng)于計(jì)算機(jī)里的"任務(wù)",而實(shí)現(xiàn)從A地到B地的方法,叫作"任務(wù)處理流程"
很顯然,這些走法中,并不是每種都合理,有些傻子都會采用的,有些是傻子都不采會用的.用計(jì)算機(jī)的話來說就是,有的任務(wù)處理流程好,有的任務(wù)處理流程好,有的處理流程差.
可以歸納出這么幾種真正算得上方法的方法:
有些走法比較快速,適合于趕時(shí)間的人;有些走法比較省事,適合于懶人;有些走法比較便宜,適合于窮人.
用計(jì)算機(jī)的話說就是,有些省CPU,有些流程簡單,有些對系統(tǒng)資源要求低.
現(xiàn)在我們可以看到一個(gè)問題:
如果全世界所有的資源給你一個(gè)人用(單任務(wù)獨(dú)占全部資源),那最適合你需求的方法就是好方法.但事實(shí)上要外出的人很多,例如10個(gè)人(10個(gè)任務(wù)),卻只有1輛車(1套資源),這叫作"資源爭用".
如果每個(gè)人都要使用最適合他需求的方法,那司機(jī)就只好給他們一人跑一趟了,而在任一時(shí)刻里,車上只有一個(gè)乘客.這叫作"順序執(zhí)行",我們可以看到這種方法對系統(tǒng)資源的浪費(fèi)是嚴(yán)重的.
如果我們沒有法力將1臺車變成10臺車來送這10個(gè)人,就只好制定一些機(jī)制和約定,讓1臺車看起來像10臺車,來解決這個(gè)問題的辦法想必大家都知道,那就是制定公交線路.
最簡單的辦法是將所有旅客需要走的起點(diǎn)與終點(diǎn)串成一條線,車在這條線上開,乘客則自已決定上下車.這就是最簡單的公交線路.它很差勁,但起碼解決客人們對車爭用.對應(yīng)到計(jì)算機(jī)里,就是把所有任務(wù)的代碼混在一起執(zhí)行.
這樣做既不優(yōu)異雅,也沒效率,于是司機(jī)想了個(gè)辦法,把這些客戶叫到一起商量,將所有客人出行的起點(diǎn)與終點(diǎn)羅列出來,統(tǒng)計(jì)這些線路的使用頻度,然后制定出公交線路:有些路線可以合并起來成為一條線路,而那些不能合并的路線,則另行開辟行車車次,這叫作"任務(wù)定義".另外,對于人多路線,車次排多點(diǎn),時(shí)間上也優(yōu)先安排,這叫作"任務(wù)優(yōu)先級".
經(jīng)過這樣的安排后,雖然仍只有一輛車,但運(yùn)載能力卻大多了.這套車次/路線的按排,就是一套"公交系統(tǒng)".哈,知道什么叫操作系統(tǒng)了吧?它也就是這么樣的一種約定.
操作系統(tǒng):
我們先回過頭歸納一下:
汽車 系統(tǒng)資源.主要指的是CPU,當(dāng)然還有其它,比如內(nèi)存,定時(shí)器,中斷源等.
客戶出行 任務(wù)
正在走的路線 進(jìn)程
一個(gè)一個(gè)的運(yùn)送旅客 順序執(zhí)行
同時(shí)運(yùn)送所有旅客 多任務(wù)并行
按不同的使用頻度制定路線并優(yōu)先跑較繁忙的路線 任務(wù)優(yōu)先級
計(jì)算機(jī)內(nèi)有各種資源,單從硬件上說,就有CPU,內(nèi)存,定時(shí)器,中斷源,I/O端口等.而且還會派生出來很多軟件資源,例如消息池.
操作系統(tǒng)的存在,就是為了讓這些資源能被合理地分配.
最后我們來總結(jié)一下,所謂操作系統(tǒng),以我們目前權(quán)宜的理解就是:為"解決計(jì)算機(jī)資源爭用而制定出的一種約定".
二.51上的操作系統(tǒng)
對于一個(gè)操作系統(tǒng)來說,最重要的莫過于并行多任務(wù).在這里要澄清一下,不要拿當(dāng)年的DOS來說事,時(shí)代不同了.況且當(dāng)年IBM和小比爾著急將PC搬上市,所以才抄襲PLM(好象是叫這個(gè)名吧?記不太清)搞了個(gè)今天看來很"粗制濫造"的DOS出來.看看當(dāng)時(shí)真正的操作系統(tǒng)---UNIX,它還在紙上時(shí)就已經(jīng)是多任務(wù)的了.
對于我們PC來說,要實(shí)現(xiàn)多任務(wù)并不是什么問題,但換到MCU卻很頭痛:
1.系統(tǒng)資源少
在PC上,CPU主頻以G為單位,內(nèi)存以GB為單位,而MCU的主頻通常只有十幾M,內(nèi)存則是Byts.在這么少的資源上同時(shí)運(yùn)行多個(gè)任務(wù),就意味著操作系統(tǒng)必須盡可能的少占用硬件資源.
2.任務(wù)實(shí)時(shí)性要求高
PC并不需要太關(guān)心實(shí)時(shí)性,因?yàn)镻C上幾乎所有的實(shí)時(shí)任務(wù)都被專門的硬件所接管,例如所有的聲卡網(wǎng)卡顯示上都內(nèi)置有DSP以及大量的緩存.CPU只需坐在那里指手劃腳告訴這些板卡如何應(yīng)付實(shí)時(shí)信息就行了.
而MCU不同,實(shí)時(shí)信息是靠CPU來處理的,緩存也非常有限,甚至沒有緩存.一旦信息到達(dá),CPU必須在極短的時(shí)間內(nèi)響應(yīng),否則信息就會丟失.
就拿串口通信來舉例,在標(biāo)準(zhǔn)的PC架構(gòu)里,巨大的內(nèi)存允許將信息保存足夠長的時(shí)間.而對于MCU來說內(nèi)存有限,例如51僅有128字節(jié)內(nèi)存,還要扣除掉寄存器組占用掉的8~32個(gè)字節(jié),所以通常都僅用幾個(gè)字節(jié)來緩沖.當(dāng)然,你可以將數(shù)據(jù)的接收與處理的過程合并,但對于一個(gè)操作系統(tǒng)來說,不推薦這么做.
假定以115200bps通信速率向MCU傳數(shù)據(jù),則每個(gè)字節(jié)的傳送時(shí)間約為9uS,假定緩存為8字節(jié),則串口處理任務(wù)必須在70uS內(nèi)響應(yīng).
這兩個(gè)問題都指向了同一種解決思路:操作系統(tǒng)必須輕量輕量再輕量,最好是不占資源(那當(dāng)然是做夢啦).
可用于MCU的操作系統(tǒng)很多,但適合51(這里的51專指無擴(kuò)展內(nèi)存的51)幾乎沒有.前陣子見過一個(gè)"圈圈操作系統(tǒng)",那是我所見過的操作系統(tǒng)里最輕量的,但仍有改進(jìn)的余地.
很多人認(rèn)為,51根本不適合使用操作系統(tǒng).其實(shí)我對這種說法并不完全接受,否則也沒有這篇文章了.
我的看法是,51不適合采用"通用操作系統(tǒng)".所謂通用操作系統(tǒng)就是,不論你是什么樣的應(yīng)用需求,也不管你用什么芯片,只要你是51,通通用同一個(gè)操作系統(tǒng).
這種想法對于PC來說沒問題,對于嵌入式來說也不錯(cuò),對AVR來說還湊合,而對于51這種"貧窮型"的MCU來說,不行.
怎樣行?量體裁衣,現(xiàn)場根據(jù)需求構(gòu)建一個(gè)操作系統(tǒng)出來!
看到這里,估計(jì)很多人要翻白眼了,大體上兩種:
1.操作系統(tǒng)那么復(fù)雜,說造就造,當(dāng)自已是神了?
2.操作系統(tǒng)那么復(fù)雜,現(xiàn)場造一個(gè)會不會出BUG?
哈哈,看清楚了?問題出在"復(fù)雜"上面,如果操作系統(tǒng)不復(fù)雜,問題不就解決了?
事實(shí)上,很多人對操作系統(tǒng)的理解是片面的,操作系統(tǒng)不一定要做得很復(fù)雜很全面,就算僅個(gè)多任務(wù)并行管理能力,你也可以稱它操作系統(tǒng).
只要你對多任務(wù)并行的原理有所了解,就不難現(xiàn)場寫一個(gè)出來,而一旦你做到了這一點(diǎn),為各任務(wù)間安排通信約定,使之發(fā)展成一個(gè)為你的應(yīng)用系統(tǒng)量身定做的操作系統(tǒng)也就不難了.
為了加深對操作系統(tǒng)的理解,可以看一看<<演變>>這份PPT,讓你充分了解一個(gè)并行多任務(wù)是如何一步步從順序流程演變過來的.里面還提到了很多人都在用的"狀態(tài)機(jī)",你會發(fā)現(xiàn)操作系統(tǒng)跟狀態(tài)機(jī)從原理上其實(shí)是多么相似.會用狀態(tài)機(jī)寫程序,都能寫出操作系統(tǒng).
三file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_image001.gif
我的第一個(gè)操作系統(tǒng)
直接進(jìn)入主題,先貼一個(gè)操作系統(tǒng)的示范出來.大家可以看到,原來操作系統(tǒng)可以做得么簡單.
當(dāng)然,這里要申明一下,這玩意兒其實(shí)算不上真正的操作系統(tǒng),它除了并行多任務(wù)并行外根本沒有別的功能.但凡事都從簡單開始,搞懂了它,就能根據(jù)應(yīng)用需求,將它擴(kuò)展成一個(gè)真正的操作系統(tǒng).
好了,代碼來了.
將下面的代碼直接放到KEIL里編譯,在每個(gè)task?()函數(shù)的"task_switch();"那里打上斷點(diǎn),就可以看到它們的確是"同時(shí)"在執(zhí)行的.
#include
#define MAX_TASKS 2 //任務(wù)槽個(gè)數(shù).必須和實(shí)際任務(wù)數(shù)一至
#define MAX_TASK_DEP 12 //最大棧深.最低不得少于2個(gè),保守值為12.
unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP];//任務(wù)堆棧.
unsigned char task_id; //當(dāng)前活動(dòng)任務(wù)號
//任務(wù)切換函數(shù)(任務(wù)調(diào)度器)
void 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ù)槽中.如果該槽中原來就有任務(wù),則原任務(wù)丟失,但系統(tǒng)本身不會發(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ù)開始運(yùn)行任務(wù)調(diào)度.調(diào)用該宏后,將永不返回.
#define os_start(tid) {task_id = tid,SP = task_sp[tid];return;}
/*==================以下為測試代碼=====================*/
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號槽
task_load(task2, 1);//將task2函數(shù)裝入1號槽
os_start(0);
}
限于篇幅我已經(jīng)將代碼作了簡化,并刪掉了大部分注釋,大家可以直接下載源碼包,里面完整的注解,并帶KEIL工程文件,斷點(diǎn)也打好了,直接按ctrl+f5就行了.
現(xiàn)在來看看這個(gè)多任務(wù)系統(tǒng)的原理:
這個(gè)多任務(wù)系統(tǒng)準(zhǔn)確來說,叫作"協(xié)同式多任務(wù)".
所謂"協(xié)同式",指的是當(dāng)一個(gè)任務(wù)持續(xù)運(yùn)行而不釋放資源時(shí),其它任務(wù)是沒有任何機(jī)會和方式獲得運(yùn)行機(jī)會,除非該任務(wù)主動(dòng)釋放CPU.
在本例里,釋放CPU是靠task_switch()來完成的.task_switch()函數(shù)是一個(gè)很特殊的函數(shù),我們可以稱它為"任務(wù)切換器".
要清楚任務(wù)是如何切換的,首先要回顧一下堆棧的相關(guān)知識.
有個(gè)很簡單的問題,因?yàn)樗唵瘟?所以相信大家都沒留意過:
我們知道,不論是CALL還是JMP,都是將當(dāng)前的程序流打斷,請問CALL和JMP的區(qū)別是什么?
你會說:CALL可以RET,JMP不行.沒錯(cuò),但原因是啥呢?為啥CALL過去的就可以用RET跳回來,JMP過去的就不能用RET來跳回呢?
很顯然,CALL通過某種方法保存了打斷前的某些信息,而在返回?cái)帱c(diǎn)前執(zhí)行的RET指令,就是用于取回這些信息.
不用多說,大家都知道,"某些信息"就是PC指針,而"某種方法"就是壓棧.
很幸運(yùn),在51里,堆棧及堆棧指針都是可被任意修改的,只要你不怕死.那么假如在執(zhí)行RET前將堆棧修改一下會如何?往下看:
當(dāng)程序執(zhí)行CALL后,在子程序里將堆棧剛才壓入的斷點(diǎn)地址清除掉,并將一個(gè)函數(shù)的地址壓入,那么執(zhí)行完RET后,程序就跳到這個(gè)函數(shù)去了.
事實(shí)上,只要我們在RET前將堆棧改掉,就能將程序跳到任務(wù)地方去,而不限于CALL里壓入的地址.
重點(diǎn)來了......
首先我們得為每個(gè)任務(wù)單獨(dú)開一塊內(nèi)存,這塊內(nèi)存專用于作為對應(yīng)的任務(wù)的堆棧,想將CPU交給哪個(gè)任務(wù),只需將棧指針指向誰內(nè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)行起來了.
那么這幾個(gè)堆棧里的原始內(nèi)容是哪里來的呢?這就是"任務(wù)裝載"函數(shù)要干的事了.
在啟動(dòng)任務(wù)調(diào)度前將各個(gè)任務(wù)函數(shù)的入口地址放在上面所說的"任務(wù)專用的內(nèi)存塊"里就行了!對了,順便說一下,這個(gè)"任務(wù)專用的內(nèi)存塊"叫作"私棧",私棧的意思就是說,每個(gè)任務(wù)的堆棧都是私有的,每個(gè)任務(wù)都有一個(gè)自已的堆棧.
話都說到這份上了,相信大家也明白要怎么做了:
1.分配若干個(gè)內(nèi)存塊,每個(gè)內(nèi)存塊為若干字節(jié):
這里所說的"若干個(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ù)堆",我覺得還是"槽"比較直觀
對了,還有任務(wù)號.不然怎么知道當(dāng)前運(yùn)行的是哪個(gè)任務(wù)呢?
unsigned char task_id
當(dāng)前運(yùn)行存放在1號槽的任務(wù)時(shí),這個(gè)值就是1,運(yùn)行2號槽的任務(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ù)號切換到下一個(gè)任務(wù)
task_id = 0;
SP = task_sp[task_id]; //將系統(tǒng)的棧指針指向下個(gè)任務(wù)的私棧.
}
3.裝載任務(wù):
將各任務(wù)的函數(shù)地址的低字節(jié)和高字節(jié)分別入在
task_stack[任務(wù)號][0]和task_stack[任務(wù)號][1]中:
為了便于使用,寫一個(gè)函數(shù): task_load(函數(shù)名, 任務(wù)號)
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é)問的哦,沒玩過堆棧的人腦子有點(diǎn)轉(zhuǎn)不彎:這一RET,RET到哪去了?嘿嘿,別忘了在RET前已經(jīng)將堆棧指針指向一個(gè)函數(shù)的入口了.你別把RET看成RET,你把它看成是另一種類型的JMP就好理解了.
SP = task_sp[任務(wù)號];
return;
做完這4件事后,任務(wù)"并行"執(zhí)行就開始了.你可以象寫普通函數(shù)一個(gè)寫任務(wù)函數(shù),只需(目前可以這么說)注意在適當(dāng)?shù)臅r(shí)候(例如以前調(diào)延時(shí)的地方)調(diào)用一下task_switch(),以讓出CPU控制權(quán)給別的任務(wù)就行了.
最后說下效率問題.
這個(gè)多任務(wù)系統(tǒng)的開銷是每次切換消耗20個(gè)機(jī)器周期(CALL和RET都算在內(nèi)了),貴嗎?不算貴,對于很多用狀態(tài)機(jī)方式實(shí)現(xiàn)的多任務(wù)系統(tǒng)來說,其實(shí)效率還沒這么高--- case switch和if()可不像你想像中那么便宜.
關(guān)于內(nèi)存的消耗我要說的是,當(dāng)然不能否認(rèn)這種多任務(wù)機(jī)制的確很占內(nèi)存.但建議大家不要老盯著編譯器下面的那行字"DATA = XXXbyte".那個(gè)值沒意義,堆棧沒算進(jìn)去.關(guān)于比較省內(nèi)存多任務(wù)機(jī)制,我將來會說到.
概括來說,這個(gè)多任務(wù)系統(tǒng)適用于實(shí)時(shí)性要求較高而內(nèi)存需求不大的應(yīng)用場合,我在運(yùn)行于36M主頻的STC12C4052上實(shí)測了一把,切換一個(gè)任務(wù)不到3微秒.
下回我們講講用KEIL寫多任務(wù)函數(shù)時(shí)要注意的事項(xiàng).
下下回我們講講如何增強(qiáng)這個(gè)多任務(wù)系統(tǒng),跑步進(jìn)入操作系統(tǒng)時(shí)代.
四.用KEIL寫多任務(wù)系統(tǒng)的技巧與注意事項(xiàng)
C51編譯器很多,KEIL是其中比較流行的一種.我列出的所有例子都必須在KEIL中使用.為何?不是因?yàn)镵EIL好所以用它(當(dāng)然它的確很棒),而是因?yàn)檫@里面用到了KEIL的一些特性,如果換到其它編譯器下,通過編譯的倒不是問題,但運(yùn)行起來可能是堆棧錯(cuò)位,上下文丟失等各種要命的錯(cuò)誤,因?yàn)槊糠N編譯器的特性并不相同.所以在這里先說清楚這一點(diǎn).
但是,我開頭已經(jīng)說了,這套帖子的主要目的是闡述原理,只要你能把這幾個(gè)例子消化掉,那么也能夠自已動(dòng)手寫出適合其它編譯器的OS.
好了,說說KEIL的特性吧,先看下面的函數(shù):
sbit sigl = P1^7;
void func1()
{
register char data i;
i = 5;
do{
sigl = !sigl;
}while(--i);
}
你會說,這個(gè)函數(shù)沒什么特別的嘛!呵呵,別著急,你將它編譯了,然后展開匯編代碼再看看:
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
看清楚了沒?這個(gè)函數(shù)里用到了R7,卻沒有對R7進(jìn)行保護(hù)!
有人會跳起來了:這有什么值得奇怪的,因?yàn)樯蠈雍瘮?shù)里沒用到R7啊.呵呵,你說的沒錯(cuò),但只說對了一半:事實(shí)上,KEIL編譯器里作了約定,在調(diào)子函數(shù)前會盡可能釋放掉所有寄存器.通常性況下,除了中斷函數(shù)外,其它函數(shù)里都可以任意修改所有寄存器而無需先壓棧保護(hù)(其實(shí)并不是這樣,但現(xiàn)在暫時(shí)這樣認(rèn)為,飯要一口一口吃嘛,我很快會說到的).
這個(gè)特性有什么用呢?有!當(dāng)我們調(diào)用任務(wù)切換函數(shù)時(shí),要保護(hù)的對象里可以把所有的寄存器排除掉了,就是說,只需要保護(hù)堆棧即可!
現(xiàn)在我們回過頭來看看之前例子里的任務(wù)切換函數(shù):
void task_switch()
{
task_sp[task_id] = SP; //保存當(dāng)前任務(wù)的棧指針
if(++task_id == MAX_TASKS) //任務(wù)號切換到下一個(gè)任務(wù)
task_id = 0;
SP = task_sp[task_id]; //將系統(tǒng)的棧指針指向下個(gè)任務(wù)的私棧.
}
看到?jīng)],一個(gè)寄存器也沒保護(hù),展開匯編看看,的確沒保護(hù)寄存器.
好了,現(xiàn)在要給大家潑冷水了,看下面兩個(gè)函數(shù):
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);
}
父函數(shù)fun2()里調(diào)用func1(),展開匯編代碼看看:
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
200: void func2(){
201: register char data i;
202: i = 5;
C:0x00CA 7E05 MOV R6,#0x05
203: do{
204: func1();
C:0x00CC 11C3 ACALL func1(C:00C3)
205: }while(--i);
C:0x00CE DEFC DJNZ R6,C:00CC
206: }
C:0x00D0 22 RET
看清楚沒?函數(shù)func2()里的變量使用了寄存器R6,而在func1和func2里都沒保護(hù).
聽到這里,你可能又要跳一跳了:func1()里并沒有用到R6,干嘛要保護(hù)?沒錯(cuò),但編譯器是怎么知道func1()沒用到R6的呢?是從調(diào)用關(guān)系里推測出來的.
一點(diǎn)都沒錯(cuò),KEIL會根據(jù)函數(shù)間的直接調(diào)用關(guān)系為各函數(shù)分配寄存器,既不用保護(hù),又不會沖突,KEIL好棒哦!!等一下,先別高興,換到多任務(wù)的環(huán)境里再試試:
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;
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
200: void func2(){
201: register char data i;
202: i = 5;
C:0x00CA 7F05 MOV R7,#0x05
203: do{
204: sigl = !sigl;
C:0x00CC B297 CPL sigl(0x90.7)
205: }while(--i);
C:0x00CE DFFC DJNZ R7,C:00CC
206: }
C:0x00D0 22 RET
看到了吧?哈哈,這回神仙也算不出來了.因?yàn)閮蓚(gè)函數(shù)沒有了直接調(diào)用的關(guān)系,所以編譯器認(rèn)為它們之間不會產(chǎn)生沖突,結(jié)果分配了一對互相沖突的寄存器,當(dāng)任務(wù)從func1()切換到func2()時(shí),func1()中的寄存器內(nèi)容就給破壞掉了.大家可以試著去編譯一下下面的程序:
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);
}
我們這里只是示例,所以仍可以通過手工分配不同的寄存器避免寄存器沖突,但在真實(shí)的應(yīng)用中,由于任務(wù)間的切換是非常隨機(jī)的,我們無法預(yù)知某個(gè)時(shí)刻哪個(gè)寄存器不會沖突,所以分配不同寄存器的方法不可取.那么,要怎么辦呢?
這樣就行了:
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);
}
}
將兩個(gè)函數(shù)中的變量通通改成靜態(tà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();
}
}
即,在變量的作用域內(nèi)不切換任務(wù),等變量用完了,再切換任務(wù).此時(shí)雖然兩個(gè)任務(wù)仍然會互相破壞對方的寄存器內(nèi)容,但對方已經(jīng)不關(guān)心寄存器里的內(nèi)容了.
以上所說的,就是"變量覆蓋"的問題.現(xiàn)在我們系統(tǒng)地說說關(guān)于"變量覆蓋".
變量分兩種,一種是全局變量,一種是局部變量(在這里,寄存器變量算到局部變量里).
對于全局變量,每個(gè)變量都會分配到單獨(dú)的地址.
而對于局部變量,KEIL會做一個(gè)"覆蓋優(yōu)化",即沒有直接調(diào)用關(guān)系的函數(shù)的變量共用空間.由于不是同時(shí)使用,所以不會沖突,這對內(nèi)存小的51來說,是好事.
但現(xiàn)在我們進(jìn)入多任務(wù)的世界了,這就意味著兩個(gè)沒有直接調(diào)用關(guān)系的函數(shù)其實(shí)是并列執(zhí)行的,空間不能共用了.怎么辦呢?一種笨辦法是關(guān)掉覆蓋優(yōu)化功能.呵呵,的確很笨.
比較簡單易行一個(gè)解決辦法是,不關(guān)閉覆蓋優(yōu)化,但將那些在作用域內(nèi)需要跨越任務(wù)(換句話說就是在變量用完前會調(diào)用task_switch()函數(shù)的)變量通通改成靜態(tài)(static)即可.這里要對初學(xué)者提一下,"靜態(tài)"你可以理解為"全局",因?yàn)樗牡刂房臻g一直保留,但它又不是全局,它只能在定義它的那個(gè)花括號對{}里訪問.
靜態(tài)變量有個(gè)副作用,就是即使函數(shù)退出了,仍會占著內(nèi)存.所以寫任務(wù)函數(shù)的時(shí)候,盡量在變量作用域結(jié)束后才切換任務(wù),除非這個(gè)變量的作用域很長(時(shí)間上長),會影響到其它任務(wù)的實(shí)時(shí)性.只有在這種情況下才考慮在變量作用域內(nèi)跨越任務(wù),并將變量申明為靜態(tài).
事實(shí)上,只要編程思路比較清析,很少有變量需要跨越任務(wù)的.就是說,靜態(tài)變量并不多.
說完了"覆蓋"我們再說說"重入".
所謂重入,就是一個(gè)函數(shù)在同一時(shí)刻有兩個(gè)不同的進(jìn)程復(fù)本.對初學(xué)者來說可能不好理解,我舉個(gè)例子吧:
有一個(gè)函數(shù)在主程序會被調(diào)用,在中斷里也會被調(diào)用,假如正當(dāng)在主程序里調(diào)用時(shí),中斷發(fā)生了,會發(fā)生什么情況?
void func1()
{
static char data i;
i = 5;
do{
sigl = !sigl;
}while(--i);
}
假定func1()正執(zhí)行到i=3時(shí),中斷發(fā)生,一旦中斷調(diào)用到func1()時(shí),i的值就被破壞了,當(dāng)中斷結(jié)束后,i == 0.
以上說的是在傳統(tǒng)的單任務(wù)系統(tǒng)中,所以重入的機(jī)率不是很大.但在多任務(wù)系統(tǒng)中,很容易發(fā)生重入,看下面的例子:
void func1()
{
....
delay();
....
}
void func2()
{
....
delay();
....
}
void delay()
{
static unsigned char i;//注意這里是申明為static,不申明static的話會發(fā)生覆蓋問題.而申明為static會發(fā)生重入問題.麻煩啊
for(i=0;i<10;i++)
task_switch();
}
兩個(gè)并行執(zhí)行的任務(wù)都調(diào)用了delay(),這就叫重入.問題在于重入后的兩個(gè)復(fù)本都依賴變量i來控制循環(huán),而該變量跨越了任務(wù),這樣,兩個(gè)任務(wù)都會修改i值了.
重入只能以防為主,就是說盡量不要讓重入發(fā)生,比如將代碼改成下面的樣子:
#define delay() {static unsigned char i; for(i=0;i<10;i++) task_switch();}//i仍定義為static,但實(shí)際上已經(jīng)不是同一個(gè)函數(shù)了,所以分配的地址不同.
void func1()
{
....
delay();
....
}
void func2()
{
....
delay();
....
}
用宏來代替函數(shù),就意味著每個(gè)調(diào)用處都是一個(gè)獨(dú)立的代碼復(fù)本,那么兩個(gè)delay實(shí)際使用的內(nèi)存地址也就不同了,重入問題消失.
但這種方法帶來的問題是,每調(diào)用一次delay(),都會產(chǎn)生一個(gè)delay的目標(biāo)代碼,如果delay的代碼很多,那就會造成大量的rom空間占用.有其它辦法沒?
本人所知有限,只有最后一招了:
void delay() reentrant
{
unsigned char i;
for(i=0;i<10;i++)
task_switch();
}
加入reentrant申明后,該函數(shù)就可以支持重入.但小心使用,申明為重入后,函數(shù)效率極低!
最后附帶說下中斷.因?yàn)闆]太多可說的,就不單獨(dú)開章了.
中斷跟普通的寫法沒什么區(qū)別,只不過在目前所示例的多任務(wù)系統(tǒng)里因?yàn)橛卸褩5膲毫?所以要使用using來減少對堆棧的使用(順便提下,也不要調(diào)用子函數(shù),同樣是為了減輕堆棧壓力)
用using,必須用#pragma NOAREGS關(guān)閉掉絕對寄存器訪問,如果中斷里非要調(diào)用函數(shù),連同函數(shù)也要放在#pragma NOAREGS的作用域內(nèi).如例所示:
#pragma SAVE
#pragma NOAREGS //使用using時(shí)必須將絕對寄存器訪問關(guān)閉
void clock_timer(void) interrupt 1 using 1 //使用using是為了減輕堆棧的壓力
}
#pragma RESTORE
改成上面的寫法后,中斷固定占用4個(gè)字節(jié)堆棧.就是說,如果你在不用中斷時(shí)任務(wù)棧深定為8的話,現(xiàn)在就要定為8+4 = 12了.
另外說句廢話,中斷里處理的事一定要少,做個(gè)標(biāo)記就行了,剩下的事交給對應(yīng)的任務(wù)去處理.
現(xiàn)在小結(jié)一下:
切換任務(wù)時(shí)要保證沒有寄存器跨越任務(wù),否則產(chǎn)生任務(wù)間寄存器覆蓋. 使用靜態(tài)變量解決
切換任務(wù)時(shí)要保證沒有變量跨越任務(wù),否則產(chǎn)生任務(wù)間地址空間(變量)覆蓋. 使用靜態(tài)變量解決
兩個(gè)不同的任務(wù)不要調(diào)用同時(shí)調(diào)用同一個(gè)函數(shù),否則產(chǎn)生重入覆蓋. 使用重入申明解決
編輯:admin 最后修改時(shí)間:2018-05-18