當(dāng)前位置:首頁 > 單片機(jī) > 單片機(jī)
[導(dǎo)讀]包括我在內(nèi)的很多人都對51使用操作系統(tǒng)呈悲觀態(tài)度,因?yàn)?1的片上資源太少.但對于很多要求不高的系統(tǒng)來說,使用操作系統(tǒng)可以使代碼變得更直觀,易于維護(hù),所以在51上仍有操作系統(tǒng)的生存機(jī)會.

 想了很久,要不要寫這篇文章?最后覺得對操作系統(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這樣的片子上用,占資源較多,唯有自已動手,以不變應(yīng)萬變,才能讓51也有操作系統(tǒng)可用.這篇貼子的目的,是教會大家如何現(xiàn)場寫一個OS,而不是給大家提供一個OS版本.提供的所有代碼,也都是示例代碼,所以不要因?yàn)樗鼪]什么功能就說LAJI之類的話.如果把功能寫全了,一來估計(jì)你也不想看了,二來也失去靈活性沒有價值了.

下面的貼一個示例出來,可以清楚的看到,OS本身只有不到10行源代碼,編譯后的目標(biāo)代碼60字節(jié),任務(wù)切換消耗為20個機(jī)器周期.相比之下,KEIL內(nèi)嵌的TINY51目標(biāo)代碼為800字節(jié),切換消耗100~700周期.唯一不足之處是,每個任務(wù)要占用掉十幾字節(jié)的堆棧,所以任務(wù)數(shù)不能太多,用在128B內(nèi)存的51里有點(diǎn)難度,但對于52來說問題不大.這套代碼在36M主頻的STC12C4052上實(shí)測,切換任務(wù)僅需2uS.

#include

#define MAX_TASKS 2 //任務(wù)槽個數(shù).必須和實(shí)際任務(wù)數(shù)一至

#define MAX_TASK_DEP 12 //最大棧深.最低不得少于2個,保守值為12.

unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP]; //任務(wù)堆棧.

unsigned char task_id; //當(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ā)生錯誤.

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()

{

//這里裝載了兩個任務(wù),因此在定義MAX_TASKS時也必須定義為2

task_load(task1, 0); //將task1函數(shù)裝入0號槽

task_load(task2, 1); //將task2函數(shù)裝入1號槽

os_start(0);

}

這樣一個簡單的多任務(wù)系統(tǒng)雖然不能稱得上真正的操作系統(tǒng),但只要你了解了它的原理,就能輕易地將它擴(kuò)展得非常強(qiáng)大,想知道要如何做嗎?

一.什么是操作系統(tǒng)?

人腦比較容易接受"類比"這種表達(dá)方式,我就用"公交系統(tǒng)"來類比"操作系統(tǒng)"吧.

當(dāng)我們要解決一個問題的時候,是用某種處理手段去完成它,這就是我們常說的"方法",計(jì)算機(jī)里叫"程序"(有時候也可以叫它"算法").

以出行為例,當(dāng)我們要從A地走到B地的時候,可以走著去,也可以飛著去,可以走直線,也可以繞彎路,只要能從A地到B地,都叫作方法.這種從A地到B的需求,相當(dāng)于計(jì)算機(jī)里的"任務(wù)",而實(shí)現(xiàn)從A地到B地的方法,叫作"任務(wù)處理流程"

很顯然,這些走法中,并不是每種都合理,有些傻子都會采用的,有些是傻子都不采會用的.用計(jì)算機(jī)的話來說就是,有的任務(wù)處理流程好,有的任務(wù)處理流程好,有的處理流程差.

可以歸納出這么幾種真正算得上方法的方法:

有些走法比較快速,適合于趕時間的人;有些走法比較省事,適合于懶人;有些走法比較便宜,適合于窮人.

用計(jì)算機(jī)的話說就是,有些省CPU,有些流程簡單,有些對系統(tǒng)資源要求低.

現(xiàn)在我們可以看到一個問題:

如果全世界所有的資源給你一個人用(單任務(wù)獨(dú)占全部資源),那最適合你需求的方法就是好方法.但事實(shí)上要外出的人很多,例如10個人(10個任務(wù)),卻只有1輛車(1套資源),這叫作"資源爭用".

如果每個人都要使用最適合他需求的方法,那司機(jī)就只好給他們一人跑一趟了,而在任一時刻里,車上只有一個乘客.這叫作"順序執(zhí)行",我們可以看到這種方法對系統(tǒng)資源的浪費(fèi)是嚴(yán)重的.

如果我們沒有法力將1臺車變成10臺車來送這10個人,就只好制定一些機(jī)制和約定,讓1臺車看起來像10臺車,來解決這個問題的辦法想必大家都知道,那就是制定公交線路.

最簡單的辦法是將所有旅客需要走的起點(diǎn)與終點(diǎn)串成一條線,車在這條線上開,乘客則自已決定上下車.這就是最簡單的公交線路.它很差勁,但起碼解決客人們對車爭用.對應(yīng)到計(jì)算機(jī)里,就是把所有任務(wù)的代碼混在一起執(zhí)行.

這樣做既不優(yōu)異雅,也沒效率,于是司機(jī)想了個辦法,把這些客戶叫到一起商量,將所有客人出行的起點(diǎn)與終點(diǎn)羅列出來,統(tǒng)計(jì)這些線路的使用頻度,然后制定出公交線路:有些路線可以合并起來成為一條線路,而那些不能合并的路線,則另行開辟行車車次,這叫作"任務(wù)定義".另外,對于人多路線,車次排多點(diǎn),時間上也優(yōu)先安排,這叫作"任務(wù)優(yōu)先級".

經(jīng)過這樣的安排后,雖然仍只有一輛車,但運(yùn)載能力卻大多了.這套車次/路線的按排,就是一套"公交系統(tǒng)".哈,知道什么叫操作系統(tǒng)了吧?它也就是這么樣的一種約定.

操作系統(tǒng):

我們先回過頭歸納一下:

汽車 系統(tǒng)資源.主要指的是CPU,當(dāng)然還有其它,比如內(nèi)存,定時器,中斷源等.

客戶出行 任務(wù)

正在走的路線 進(jìn)程

一個一個的運(yùn)送旅客 順序執(zhí)行

同時運(yùn)送所有旅客 多任務(wù)并行

按不同的使用頻度制定路線并優(yōu)先跑較繁忙的路線 任務(wù)優(yōu)先級

計(jì)算機(jī)內(nèi)有各種資源,單從硬件上說,就有CPU,內(nèi)存,定時器,中斷源,I/O端口等.而且還會派生出來很多軟件資源,例如消息池.

操作系統(tǒng)的存在,就是為了讓這些資源能被合理地分配.

最后我們來總結(jié)一下,所謂操作系統(tǒng),以我們目前權(quán)宜的理解就是:為"解決計(jì)算機(jī)資源爭用而制定出的一種約定".

二.51上的操作系統(tǒng)

對于一個操作系統(tǒng)來說,最重要的莫過于并行多任務(wù).在這里要澄清一下,不要拿當(dāng)年的DOS來說事,時代不同了.況且當(dāng)年IBM和小比爾著急將PC搬上市,所以才抄襲PLM(好象是叫這個名吧?記不太清)搞了個今天看來很"粗制濫造"的DOS出來.看看當(dāng)時真正的操作系統(tǒng)---UNIX,它還在紙上時就已經(jīng)是多任務(wù)的了.

對于我們PC來說,要實(shí)現(xiàn)多任務(wù)并不是什么問題,但換到MCU卻很頭痛:

1.系統(tǒng)資源少

在PC上,CPU主頻以G為單位,內(nèi)存以GB為單位,而MCU的主頻通常只有十幾M,內(nèi)存則是Byts.在這么少的資源上同時運(yùn)行多個任務(wù),就意味著操作系統(tǒng)必須盡可能的少占用硬件資源.

2.任務(wù)實(shí)時性要求高

PC并不需要太關(guān)心實(shí)時性,因?yàn)镻C上幾乎所有的實(shí)時任務(wù)都被專門的硬件所接管,例如所有的聲卡網(wǎng)卡顯示上都內(nèi)置有DSP以及大量的緩存.CPU只需坐在那里指手劃腳告訴這些板卡如何應(yīng)付實(shí)時信息就行了.

而MCU不同,實(shí)時信息是靠CPU來處理的,緩存也非常有限,甚至沒有緩存.一旦信息到達(dá),CPU必須在極短的時間內(nèi)響應(yīng),否則信息就會丟失.

就拿串口通信來舉例,在標(biāo)準(zhǔn)的PC架構(gòu)里,巨大的內(nèi)存允許將信息保存足夠長的時間.而對于MCU來說內(nèi)存有限,例如51僅有128字節(jié)內(nèi)存,還要扣除掉寄存器組占用掉的8~32個字節(jié),所以通常都僅用幾個字節(jié)來緩沖.當(dāng)然,你可以將數(shù)據(jù)的接收與處理的過程合并,但對于一個操作系統(tǒng)來說,不推薦這么做.

假定以115200bps通信速率向MCU傳數(shù)據(jù),則每個字節(jié)的傳送時間約為9uS,假定緩存為8字節(jié),則串口處理任務(wù)必須在70uS內(nèi)響應(yīng).

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

可用于MCU的操作系統(tǒng)很多,但適合51(這里的51專指無擴(kuò)展內(nèi)存的51)幾乎沒有.前陣子見過一個"圈圈操作系統(tǒng)",那是我所見過的操作系統(tǒng)里最輕量的,但仍有改進(jìn)的余地.

很多人認(rèn)為,51根本不適合使用操作系統(tǒng).其實(shí)我對這種說法并不完全接受,否則也沒有這篇文章了.

我的看法是,51不適合采用"通用操作系統(tǒng)".所謂通用操作系統(tǒng)就是,不論你是什么樣的應(yīng)用需求,也不管你用什么芯片,只要你是51,通通用同一個操作系統(tǒng).

這種想法對于PC來說沒問題,對于嵌入式來說也不錯,對AVR來說還湊合,而對于51這種"貧窮型"的MCU來說,不行.

怎樣行?量體裁衣,現(xiàn)場根據(jù)需求構(gòu)建一個操作系統(tǒng)出來!

看到這里,估計(jì)很多人要翻白眼了,大體上兩種:

1.操作系統(tǒng)那么復(fù)雜,說造就造,當(dāng)自已是神了?

2.操作系統(tǒng)那么復(fù)雜,現(xiàn)場造一個會不會出BUG?

哈哈,看清楚了?問題出在"復(fù)雜"上面,如果操作系統(tǒng)不復(fù)雜,問題不就解決了?

事實(shí)上,很多人對操作系統(tǒng)的理解是片面的,操作系統(tǒng)不一定要做得很復(fù)雜很全面,就算僅個多任務(wù)并行管理能力,你也可以稱它操作系統(tǒng).

只要你對多任務(wù)并行的原理有所了解,就不難現(xiàn)場寫一個出來,而一旦你做到了這一點(diǎn),為各任務(wù)間安排通信約定,使之發(fā)展成一個為你的應(yīng)用系統(tǒng)量身定做的操作系統(tǒng)也就不難了.

為了加深對操作系統(tǒng)的理解,可以看一看<<演變>>這份PPT,讓你充分了解一個并行多任務(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

我的第一個操作系統(tǒng)

直接進(jìn)入主題,先貼一個操作系統(tǒng)的示范出來.大家可以看到,原來操作系統(tǒng)可以做得么簡單.

當(dāng)然,這里要申明一下,這玩意兒其實(shí)算不上真正的操作系統(tǒng),它除了并行多任務(wù)并行外根本沒有別的功能.但凡事都從簡單開始,搞懂了它,就能根據(jù)應(yīng)用需求,將它擴(kuò)展成一個真正的操作系統(tǒng).

好了,代碼來了.

將下面的代碼直接放到KEIL里編譯,在每個task?()函數(shù)的"task_switch();"那里打上斷點(diǎn),就可以看到它們的確是"同時"在執(zhí)行的.

#include

#define MAX_TASKS 2 //任務(wù)槽個數(shù).必須和實(shí)際任務(wù)數(shù)一至

#define MAX_TASK_DEP 12 //最大棧深.最低不得少于2個,保守值為12.

unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP];//任務(wù)堆棧.

unsigned char task_id; //當(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ā)生錯誤.

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()

{

//這里裝載了兩個任務(wù),因此在定義MAX_TASKS時也必須定義為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)在來看看這個多任務(wù)系統(tǒng)的原理:

這個多任務(wù)系統(tǒng)準(zhǔn)確來說,叫作"協(xié)同式多任務(wù)".

所謂"協(xié)同式",指的是當(dāng)一個任務(wù)持續(xù)運(yùn)行而不釋放資源時,其它任務(wù)是沒有任何機(jī)會和方式獲得運(yùn)行機(jī)會,除非該任務(wù)主動釋放CPU.

在本例里,釋放CPU是靠task_switch()來完成的.task_switch()函數(shù)是一個很特殊的函數(shù),我們可以稱它為"任務(wù)切換器".

要清楚任務(wù)是如何切換的,首先要回顧一下堆棧的相關(guān)知識.

有個很簡單的問題,因?yàn)樗唵瘟?所以相信大家都沒留意過:

我們知道,不論是CALL還是JMP,都是將當(dāng)前的程序流打斷,請問CALL和JMP的區(qū)別是什么?

你會說:CALL可以RET,JMP不行.沒錯,但原因是啥呢?為啥CALL過去的就可以用RET跳回來,JMP過去的就不能用RET來跳回呢?

很顯然,CALL通過某種方法保存了打斷前的某些信息,而在返回?cái)帱c(diǎn)前執(zhí)行的RET指令,就是用于取回這些信息.

不用多說,大家都知道,"某些信息"就是PC指針,而"某種方法"就是壓棧.

很幸運(yùn),在51里,堆棧及堆棧指針都是可被任意修改的,只要你不怕死.那么假如在執(zhí)行RET前將堆棧修改一下會如何?往下看:

當(dāng)程序執(zhí)行CALL后,在子程序里將堆棧剛才壓入的斷點(diǎn)地址清除掉,并將一個函數(shù)的地址壓入,那么執(zhí)行完RET后,程序就跳到這個函數(shù)去了.

事實(shí)上,只要我們在RET前將堆棧改掉,就能將程序跳到任務(wù)地方去,而不限于CALL里壓入的地址.

重點(diǎn)來了......

首先我們得為每個任務(wù)單獨(dú)開一塊內(nèi)存,這塊內(nèi)存專用于作為對應(yīng)的任務(wù)的堆棧,想將CPU交給哪個任務(wù),只需將棧指針指向誰內(nèi)存塊就行了.

接下來我們構(gòu)造一個這樣的函數(shù):

當(dāng)任務(wù)調(diào)用該函數(shù)時,將當(dāng)前的堆棧指針保存一個變量里,并換上另一個任務(wù)的堆棧指針.這就是任務(wù)調(diào)度器了.

OK了,現(xiàn)在我們只要正確的填充好這幾個堆棧的原始內(nèi)容,再調(diào)用這個函數(shù),這個任務(wù)調(diào)度就能運(yùn)行起來了.

那么這幾個堆棧里的原始內(nèi)容是哪里來的呢?這就是"任務(wù)裝載"函數(shù)要干的事了.

在啟動任務(wù)調(diào)度前將各個任務(wù)函數(shù)的入口地址放在上面所說的"任務(wù)專用的內(nèi)存塊"里就行了!對了,順便說一下,這個"任務(wù)專用的內(nèi)存塊"叫作"私棧",私棧的意思就是說,每個任務(wù)的堆棧都是私有的,每個任務(wù)都有一個自已的堆棧.

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

1.分配若干個內(nèi)存塊,每個內(nèi)存塊為若干字節(jié):

這里所說的"若干個內(nèi)存塊"就是私棧,要想同時運(yùn)行幾少個任務(wù)就得分配多少塊.而"每個子內(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)該從哪個地址取數(shù)據(jù)啊

unsigned char idata task_sp[MAX_TASKS]

上面兩項(xiàng)用于裝任務(wù)信息的區(qū)域,我們給它個概念叫"任務(wù)槽".有些人叫它"任務(wù)堆",我覺得還是"槽"比較直觀

對了,還有任務(wù)號.不然怎么知道當(dāng)前運(yùn)行的是哪個任務(wù)呢?

unsigned char task_id

當(dāng)前運(yùn)行存放在1號槽的任務(wù)時,這個值就是1,運(yùn)行2號槽的任務(wù)時,這個值就是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ù)號切換到下一個任務(wù)

task_id = 0;

SP = task_sp[task_id]; //將系統(tǒng)的棧指針指向下個任務(wù)的私棧.

}

3.裝載任務(wù):

將各任務(wù)的函數(shù)地址的低字節(jié)和高字節(jié)分別入在

task_stack[任務(wù)號][0]和task_stack[任務(wù)號][1]中:

為了便于使用,寫一個函數(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.啟動任務(wù)調(diào)度器:

將棧指針指向任意一個任務(wù)的私棧,執(zhí)行RET指令.注意,這可很有學(xué)問的哦,沒玩過堆棧的人腦子有點(diǎn)轉(zhuǎn)不彎:這一RET,RET到哪去了?嘿嘿,別忘了在RET前已經(jīng)將堆棧指針指向一個函數(shù)的入口了.你別把RET看成RET,你把它看成是另一種類型的JMP就好理解了.

SP = task_sp[任務(wù)號];

return;

做完這4件事后,任務(wù)"并行"執(zhí)行就開始了.你可以象寫普通函數(shù)一個寫任務(wù)函數(shù),只需(目前可以這么說)注意在適當(dāng)?shù)臅r候(例如以前調(diào)延時的地方)調(diào)用一下task_switch(),以讓出CPU控制權(quán)給別的任務(wù)就行了.

最后說下效率問題.

這個多任務(wù)系統(tǒng)的開銷是每次切換消耗20個機(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".那個值沒意義,堆棧沒算進(jìn)去.關(guān)于比較省內(nèi)存多任務(wù)機(jī)制,我將來會說到.

概括來說,這個多任務(wù)系統(tǒng)適用于實(shí)時性要求較高而內(nèi)存需求不大的應(yīng)用場合,我在運(yùn)行于36M主頻的STC12C4052上實(shí)測了一把,切換一個任務(wù)不到3微秒.

下回我們講講用KEIL寫多任務(wù)函數(shù)時要注意的事項(xiàng).

下下回我們講講如何增強(qiáng)這個多任務(wù)系統(tǒng),跑步進(jìn)入操作系統(tǒng)時代.

四.用KEIL寫多任務(wù)系統(tǒng)的技巧與注意事項(xiàng)

C51編譯器很多,KEIL是其中比較流行的一種.我列出的所有例子都必須在KEIL中使用.為何?不是因?yàn)镵EIL好所以用它(當(dāng)然它的確很棒),而是因?yàn)檫@里面用到了KEIL的一些特性,如果換到其它編譯器下,通過編譯的倒不是問題,但運(yùn)行起來可能是堆棧錯位,上下文丟失等各種要命的錯誤,因?yàn)槊糠N編譯器的特性并不相同.所以在這里先說清楚這一點(diǎn).

但是,我開頭已經(jīng)說了,這套帖子的主要目的是闡述原理,只要你能把這幾個例子消化掉,那么也能夠自已動手寫出適合其它編譯器的OS.

好了,說說KEIL的特性吧,先看下面的函數(shù):

sbit sigl = P1^7;

void func1()

{

register char data i;

i = 5;

do{

sigl = !sigl;

}while(--i);

}

你會說,這個函數(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

看清楚了沒?這個函數(shù)里用到了R7,卻沒有對R7進(jìn)行保護(hù)!

有人會跳起來了:這有什么值得奇怪的,因?yàn)樯蠈雍瘮?shù)里沒用到R7啊.呵呵,你說的沒錯,但只說對了一半:事實(shí)上,KEIL編譯器里作了約定,在調(diào)子函數(shù)前會盡可能釋放掉所有寄存器.通常性況下,除了中斷函數(shù)外,其它函數(shù)里都可以任意修改所有寄存器而無需先壓棧保護(hù)(其實(shí)并不是這樣,但現(xiàn)在暫時這樣認(rèn)為,飯要一口一口吃嘛,我很快會說到的).

這個特性有什么用呢?有!當(dāng)我們調(diào)用任務(wù)切換函數(shù)時,要保護(hù)的對象里可以把所有的寄存器排除掉了,就是說,只需要保護(hù)堆棧即可!

現(xiàn)在我們回過頭來看看之前例子里的任務(wù)切換函數(shù):

void task_switch()

{

task_sp[task_id] = SP; //保存當(dāng)前任務(wù)的棧指針

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

task_id = 0;

SP = task_sp[task_id]; //將系統(tǒng)的棧指針指向下個任務(wù)的私棧.

}

看到?jīng)],一個寄存器也沒保護(hù),展開匯編看看,的確沒保護(hù)寄存器.

好了,現(xiàn)在要給大家潑冷水了,看下面兩個函數(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ù)?沒錯,但編譯器是怎么知道func1()沒用到R6的呢?是從調(diào)用關(guān)系里推測出來的.

一點(diǎn)都沒錯,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)閮蓚€函數(shù)沒有了直接調(diào)用的關(guān)系,所以編譯器認(rèn)為它們之間不會產(chǎn)生沖突,結(jié)果分配了一對互相沖突的寄存器,當(dāng)任務(wù)從func1()切換到func2()時,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ù)知某個時刻哪個寄存器不會沖突,所以分配不同寄存器的方法不可取.那么,要怎么辦呢?

這樣就行了:

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);

}

}

將兩個函數(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ù).此時雖然兩個任務(wù)仍然會互相破壞對方的寄存器內(nèi)容,但對方已經(jīng)不關(guān)心寄存器里的內(nèi)容了.

以上所說的,就是"變量覆蓋"的問題.現(xiàn)在我們系統(tǒng)地說說關(guān)于"變量覆蓋".

變量分兩種,一種是全局變量,一種是局部變量(在這里,寄存器變量算到局部變量里).

對于全局變量,每個變量都會分配到單獨(dú)的地址.

而對于局部變量,KEIL會做一個"覆蓋優(yōu)化",即沒有直接調(diào)用關(guān)系的函數(shù)的變量共用空間.由于不是同時使用,所以不會沖突,這對內(nèi)存小的51來說,是好事.

但現(xiàn)在我們進(jìn)入多任務(wù)的世界了,這就意味著兩個沒有直接調(diào)用關(guān)系的函數(shù)其實(shí)是并列執(zhí)行的,空間不能共用了.怎么辦呢?一種笨辦法是關(guān)掉覆蓋優(yōu)化功能.呵呵,的確很笨.

比較簡單易行一個解決辦法是,不關(guān)閉覆蓋優(yōu)化,但將那些在作用域內(nèi)需要跨越任務(wù)(換句話說就是在變量用完前會調(diào)用task_switch()函數(shù)的)變量通通改成靜態(tài)(static)即可.這里要對初學(xué)者提一下,"靜態(tài)"你可以理解為"全局",因?yàn)樗牡刂房臻g一直保留,但它又不是全局,它只能在定義它的那個花括號對{}里訪問.

靜態(tài)變量有個副作用,就是即使函數(shù)退出了,仍會占著內(nèi)存.所以寫任務(wù)函數(shù)的時候,盡量在變量作用域結(jié)束后才切換任務(wù),除非這個變量的作用域很長(時間上長),會影響到其它任務(wù)的實(shí)時性.只有在這種情況下才考慮在變量作用域內(nèi)跨越任務(wù),并將變量申明為靜態(tài).

事實(shí)上,只要編程思路比較清析,很少有變量需要跨越任務(wù)的.就是說,靜態(tài)變量并不多.

說完了"覆蓋"我們再說說"重入".

所謂重入,就是一個函數(shù)在同一時刻有兩個不同的進(jìn)程復(fù)本.對初學(xué)者來說可能不好理解,我舉個例子吧:

有一個函數(shù)在主程序會被調(diào)用,在中斷里也會被調(diào)用,假如正當(dāng)在主程序里調(diào)用時,中斷發(fā)生了,會發(fā)生什么情況?

void func1()

{

static char data i;

i = 5;

do{

sigl = !sigl;

}while(--i);

}

假定func1()正執(zhí)行到i=3時,中斷發(fā)生,一旦中斷調(diào)用到func1()時,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();

}

兩個并行執(zhí)行的任務(wù)都調(diào)用了delay(),這就叫重入.問題在于重入后的兩個復(fù)本都依賴變量i來控制循環(huán),而該變量跨越了任務(wù),這樣,兩個任務(wù)都會修改i值了.

重入只能以防為主,就是說盡量不要讓重入發(fā)生,比如將代碼改成下面的樣子:

#define delay() {static unsigned char i; for(i=0;i<10;i++) task_switch();}//i仍定義為static,但實(shí)際上已經(jīng)不是同一個函數(shù)了,所以分配的地址不同.

void func1()

{

....

delay();

....

}

void func2()

{

....

delay();

....

}

用宏來代替函數(shù),就意味著每個調(diào)用處都是一個獨(dú)立的代碼復(fù)本,那么兩個delay實(shí)際使用的內(nèi)存地址也就不同了,重入問題消失.

但這種方法帶來的問題是,每調(diào)用一次delay(),都會產(chǎn)生一個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時必須將絕對寄存器訪問關(guān)閉

void clock_timer(void) interrupt 1 using 1 //使用using是為了減輕堆棧的壓力

}

#pragma RESTORE

改成上面的寫法后,中斷固定占用4個字節(jié)堆棧.就是說,如果你在不用中斷時任務(wù)棧深定為8的話,現(xiàn)在就要定為8+4 = 12了.

另外說句廢話,中斷里處理的事一定要少,做個標(biāo)記就行了,剩下的事交給對應(yīng)的任務(wù)去處理.

現(xiàn)在小結(jié)一下:

切換任務(wù)時要保證沒有寄存器跨越任務(wù),否則產(chǎn)生任務(wù)間寄存器覆蓋. 使用靜態(tài)變量解決

切換任務(wù)時要保證沒有變量跨越任務(wù),否則產(chǎn)生任務(wù)間地址空間(變量)覆蓋. 使用靜態(tài)變量解決

 

兩個不同的任務(wù)不要調(diào)用同時調(diào)用同一個函數(shù),否則產(chǎn)生重入覆蓋. 使用重入申明解決

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術(shù)解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關(guān)鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風(fēng)險(xiǎn),如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報(bào)道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競爭力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競爭優(yōu)勢...

關(guān)鍵字: 通信 BSP 電信運(yùn)營商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術(shù)學(xué)會聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(shù)(集團(tuán))股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉