




版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報或認(rèn)領(lǐng)
文檔簡介
1、http:/www.ourde前言 想了很久,要不要寫這篇文章?最后覺得對操作系統(tǒng)感興趣的人還是很多,寫吧.我不一定能造出玉,但我可以拋出磚. 包括我在內(nèi)的很多人都對51使用操作系統(tǒng)呈悲觀態(tài)度,因為51的片上資源太少.但對于很多要求不高的系統(tǒng)來說,使用操作系統(tǒng)可以使代碼變得更直觀,易于維護(hù),所以在51上仍有操作系統(tǒng)的生存機(jī)會. 流行的uCos,Tiny51等,其實都不適合在2051這樣的片子上用,占資源較多,唯有自已動手,以不變應(yīng)萬變,才能讓51也有操作系統(tǒng)可用.這篇貼子的目的,是教會大家如何現(xiàn)場寫一個OS,而不是給大家提供一個OS版本.提供的所有代碼,也都是示例代碼,所以不要因為它沒什么功能
2、就說LAJI之類的話.如果把功能寫全了,一來估計你也不想看了,二來也失去靈活性沒有價值了. 下面的貼一個示例出來,可以清楚的看到,OS本身只有不到10行源代碼,編譯后的目標(biāo)代碼60字節(jié),任務(wù)切換消耗為20個機(jī)器周期.相比之下,KEIL內(nèi)嵌的TINY51目標(biāo)代碼為800字節(jié),切換消耗100700周期.唯一不足之處是,每個任務(wù)要占用掉十幾字節(jié)的堆棧,所以任務(wù)數(shù)不能太多,用在128B內(nèi)存的51里有點難度,但對于52來說問題不大.這套代碼在36M主頻的STC12C4052上實測,切換任務(wù)僅需2uS. #include #defineMAX_TASKS2/任務(wù)槽個數(shù).必須和實際任務(wù)數(shù)一至 #define
3、MAX_TASK_DEP12/最大棧深.最低不得少于2個,保守值為12. unsignedcharidatatask_stackMAX_TASKSMAX_TASK_DEP;/任務(wù)堆棧. unsignedchartask_id;/當(dāng)前活動任務(wù)號 /任務(wù)切換函數(shù)(任務(wù)調(diào)度器) voidtask_switch() task_sptask_id=SP; if(+task_id=MAX_TASKS) task_id=0; SP=task_sptask_id; /任務(wù)裝入函數(shù).將指定的函數(shù)(參數(shù)1)裝入指定(參數(shù)2)的任務(wù)槽中.如果該槽中原來就有任務(wù),則原任務(wù)丟失,但系統(tǒng)本身不會發(fā)生錯誤. voidtas
4、k_load(unsignedintfn,unsignedchartid) task_sptid=task_stacktid+1; task_stacktid0=(unsignedint)fn&0xff; task_stacktid1=(unsignedint)fn8; /從指定的任務(wù)開始運行任務(wù)調(diào)度.調(diào)用該宏后,將永不返回. #defineos_start(tid)task_id=tid,SP=task_sptid;return; /*=以下為測試代碼=*/ voidtask1() staticunsignedchari; while(1) i+; task_switch();/編譯后在這里
5、打上斷點 voidtask2() staticunsignedcharj; while(1) j+=2; task_switch();/編譯后在這里打上斷點 voidmain() /這里裝載了兩個任務(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á)方式,我就用
6、公交系統(tǒng)來類比操作系統(tǒng)吧. 當(dāng)我們要解決一個問題的時候,是用某種處理手段去完成它,這就是我們常說的方法,計算機(jī)里叫程序(有時候也可以叫它算法). 以出行為例,當(dāng)我們要從A地走到B地的時候,可以走著去,也可以飛著去,可以走直線,也可以繞彎路,只要能從A地到B地,都叫作方法.這種從A地到B的需求,相當(dāng)于計算機(jī)里的任務(wù),而實現(xiàn)從A地到B地的方法,叫作任務(wù)處理流程 很顯然,這些走法中,并不是每種都合理,有些傻子都會采用的,有些是傻子都不采會用的.用計算機(jī)的話來說就是,有的任務(wù)處理流程好,有的任務(wù)處理流程好,有的處理流程差. 可以歸納出這么幾種真正算得上方法的方法: 有些走法比較快速,適合于趕時間的人;
7、有些走法比較省事,適合于懶人;有些走法比較便宜,適合于窮人. 用計算機(jī)的話說就是,有些省CPU,有些流程簡單,有些對系統(tǒng)資源要求低. 現(xiàn)在我們可以看到一個問題: 如果全世界所有的資源給你一個人用(單任務(wù)獨占全部資源),那最適合你需求的方法就是好方法.但事實上要外出的人很多,例如10個人(10個任務(wù)),卻只有1輛車(1套資源),這叫作資源爭用. 如果每個人都要使用最適合他需求的方法,那司機(jī)就只好給他們一人跑一趟了,而在任一時刻里,車上只有一個乘客.這叫作順序執(zhí)行,我們可以看到這種方法對系統(tǒng)資源的浪費是嚴(yán)重的. 如果我們沒有法力將1臺車變成10臺車來送這10個人,就只好制定一些機(jī)制和約定,讓1臺車
8、看起來像10臺車,來解決這個問題的辦法想必大家都知道,那就是制定公交線路. 最簡單的辦法是將所有旅客需要走的起點與終點串成一條線,車在這條線上開,乘客則自已決定上下車.這就是最簡單的公交線路.它很差勁,但起碼解決客人們對車爭用.對應(yīng)到計算機(jī)里,就是把所有任務(wù)的代碼混在一起執(zhí)行. 這樣做既不優(yōu)異雅,也沒效率,于是司機(jī)想了個辦法,把這些客戶叫到一起商量,將所有客人出行的起點與終點羅列出來,統(tǒng)計這些線路的使用頻度,然后制定出公交線路:有些路線可以合并起來成為一條線路,而那些不能合并的路線,則另行開辟行車車次,這叫作任務(wù)定義.另外,對于人多路線,車次排多點,時間上也優(yōu)先安排,這叫作任務(wù)優(yōu)先級. 經(jīng)過這
9、樣的安排后,雖然仍只有一輛車,但運載能力卻大多了.這套車次/路線的按排,就是一套公交系統(tǒng).哈,知道什么叫操作系統(tǒng)了吧?它也就是這么樣的一種約定. 操作系統(tǒng): 我們先回過頭歸納一下: 汽車系統(tǒng)資源.主要指的是CPU,當(dāng)然還有其它,比如內(nèi)存,定時器,中斷源等. 客戶出行任務(wù) 正在走的路線進(jìn)程 一個一個的運送旅客順序執(zhí)行 同時運送所有旅客多任務(wù)并行 按不同的使用頻度制定路線并優(yōu)先跑較繁忙的路線任務(wù)優(yōu)先級 計算機(jī)內(nèi)有各種資源,單從硬件上說,就有CPU,內(nèi)存,定時器,中斷源,I/O端口等.而且還會派生出來很多軟件資源,例如消息池. 操作系統(tǒng)的存在,就是為了讓這些資源能被合理地分配. 最后我們來總結(jié)一下,
10、所謂操作系統(tǒng),以我們目前權(quán)宜的理解就是:為解決計算機(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來說,要實現(xiàn)多任務(wù)并不是什么問題,但換到MCU卻很頭痛: 1.系統(tǒng)資源少 在PC上,CPU主頻以G為單位,內(nèi)存以GB為單位,而MCU的主頻通常只有十幾M,內(nèi)存則是Byts.在這么少的資源上同
11、時運行多個任務(wù),就意味著操作系統(tǒng)必須盡可能的少占用硬件資源. 2.任務(wù)實時性要求高 PC并不需要太關(guān)心實時性,因為PC上幾乎所有的實時任務(wù)都被專門的硬件所接管,例如所有的聲卡網(wǎng)卡顯示上都內(nèi)置有DSP以及大量的緩存.CPU只需坐在那里指手劃腳告訴這些板卡如何應(yīng)付實時信息就行了. 而MCU不同,實時信息是靠CPU來處理的,緩存也非常有限,甚至沒有緩存.一旦信息到達(dá),CPU必須在極短的時間內(nèi)響應(yīng),否則信息就會丟失. 就拿串口通信來舉例,在標(biāo)準(zhǔn)的PC架構(gòu)里,巨大的內(nèi)存允許將信息保存足夠長的時間.而對于MCU來說內(nèi)存有限,例如51僅有128字節(jié)內(nèi)存,還要扣除掉寄存器組占用掉的832個字節(jié),所以通常都僅用
12、幾個字節(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).其實我對這種說法并不完全接受,否則也沒有這篇文章了. 我的看法是
13、,51不適合采用通用操作系統(tǒng).所謂通用操作系統(tǒng)就是,不論你是什么樣的應(yīng)用需求,也不管你用什么芯片,只要你是51,通通用同一個操作系統(tǒng). 這種想法對于PC來說沒問題,對于嵌入式來說也不錯,對AVR來說還湊合,而對于51這種貧窮型的MCU來說,不行. 怎樣行?量體裁衣,現(xiàn)場根據(jù)需求構(gòu)建一個操作系統(tǒng)出來! 看到這里,估計很多人要翻白眼了,大體上兩種: 1.操作系統(tǒng)那么復(fù)雜,說造就造,當(dāng)自已是神了? 2.操作系統(tǒng)那么復(fù)雜,現(xiàn)場造一個會不會出BUG? 哈哈,看清楚了?問題出在復(fù)雜上面,如果操作系統(tǒng)不復(fù)雜,問題不就解決了? 事實上,很多人對操作系統(tǒng)的理解是片面的,操作系統(tǒng)不一定要做得很復(fù)雜很全面,就算僅個
14、多任務(wù)并行管理能力,你也可以稱它操作系統(tǒng). 只要你對多任務(wù)并行的原理有所了解,就不難現(xiàn)場寫一個出來,而一旦你做到了這一點,為各任務(wù)間安排通信約定,使之發(fā)展成一個為你的應(yīng)用系統(tǒng)量身定做的操作系統(tǒng)也就不難了. 為了加深對操作系統(tǒng)的理解,可以看一看這份PPT,讓你充分了解一個并行多任務(wù)是如何一步步從順序流程演變過來的.里面還提到了很多人都在用的狀態(tài)機(jī),你會發(fā)現(xiàn)操作系統(tǒng)跟狀態(tài)機(jī)從原理上其實是多么相似.會用狀態(tài)機(jī)寫程序,都能寫出操作系統(tǒng).三.我的第一個操作系統(tǒng) 直接進(jìn)入主題,先貼一個操作系統(tǒng)的示范出來.大家可以看到,原來操作系統(tǒng)可以做得么簡單. 當(dāng)然,這里要申明一下,這玩意兒其實算不上真正的操作系統(tǒng),它
15、除了并行多任務(wù)并行外根本沒有別的功能.但凡事都從簡單開始,搞懂了它,就能根據(jù)應(yīng)用需求,將它擴(kuò)展成一個真正的操作系統(tǒng). 好了,代碼來了. 將下面的代碼直接放到KEIL里編譯,在每個task?()函數(shù)的task_switch();那里打上斷點,就可以看到它們的確是同時在執(zhí)行的. #include #defineMAX_TASKS2/任務(wù)槽個數(shù).必須和實際任務(wù)數(shù)一至 #defineMAX_TASK_DEP12/最大棧深.最低不得少于2個,保守值為12. unsignedcharidatatask_stackMAX_TASKSMAX_TASK_DEP;/任務(wù)堆棧. unsignedchartask_i
16、d;/當(dāng)前活動任務(wù)號 /任務(wù)切換函數(shù)(任務(wù)調(diào)度器) voidtask_switch() task_sptask_id=SP; if(+task_id=MAX_TASKS) task_id=0; SP=task_sptask_id; /任務(wù)裝入函數(shù).將指定的函數(shù)(參數(shù)1)裝入指定(參數(shù)2)的任務(wù)槽中.如果該槽中原來就有任務(wù),則原任務(wù)丟失,但系統(tǒng)本身不會發(fā)生錯誤. voidtask_load(unsignedintfn,unsignedchartid) task_sptid=task_stacktid+1; task_stacktid0=(unsignedint)fn&0xff; task_sta
17、cktid1=(unsignedint)fn8; /從指定的任務(wù)開始運行任務(wù)調(diào)度.調(diào)用該宏后,將永不返回. #defineos_start(tid)task_id=tid,SP=task_sptid;return; /*=以下為測試代碼=*/ voidtask1() staticunsignedchari; while(1) i+; task_switch();/編譯后在這里打上斷點 voidtask2() staticunsignedcharj; while(1) j+=2; task_switch();/編譯后在這里打上斷點 voidmain() /這里裝載了兩個任務(wù),因此在定義MAX_T
18、ASKS時也必須定義為2 task_load(task1,0);/將task1函數(shù)裝入0號槽 task_load(task2,1);/將task2函數(shù)裝入1號槽 os_start(0); 限于篇幅我已經(jīng)將代碼作了簡化,并刪掉了大部分注釋,大家可以直接下載源碼包,里面完整的注解,并帶KEIL工程文件,斷點也打好了,直接按ctrl+f5就行了. 現(xiàn)在來看看這個多任務(wù)系統(tǒng)的原理: 這個多任務(wù)系統(tǒng)準(zhǔn)確來說,叫作協(xié)同式多任務(wù). 所謂協(xié)同式,指的是當(dāng)一個任務(wù)持續(xù)運行而不釋放資源時,其它任務(wù)是沒有任何機(jī)會和方式獲得運行機(jī)會,除非該任務(wù)主動釋放CPU. 在本例里,釋放CPU是靠task_switch()來完成
19、的.task_switch()函數(shù)是一個很特殊的函數(shù),我們可以稱它為任務(wù)切換器. 要清楚任務(wù)是如何切換的,首先要回顧一下堆棧的相關(guān)知識. 有個很簡單的問題,因為它太簡單了,所以相信大家都沒留意過: 我們知道,不論是CALL還是JMP,都是將當(dāng)前的程序流打斷,請問CALL和JMP的區(qū)別是什么? 你會說:CALL可以RET,JMP不行.沒錯,但原因是啥呢?為啥CALL過去的就可以用RET跳回來,JMP過去的就不能用RET來跳回呢? 很顯然,CALL通過某種方法保存了打斷前的某些信息,而在返回斷點前執(zhí)行的RET指令,就是用于取回這些信息. 不用多說,大家都知道,某些信息就是PC指針,而某種方法就是壓
20、棧. 很幸運,在51里,堆棧及堆棧指針都是可被任意修改的,只要你不怕死.那么假如在執(zhí)行RET前將堆棧修改一下會如何?往下看: 當(dāng)程序執(zhí)行CALL后,在子程序里將堆棧剛才壓入的斷點地址清除掉,并將一個函數(shù)的地址壓入,那么執(zhí)行完RET后,程序就跳到這個函數(shù)去了. 事實上,只要我們在RET前將堆棧改掉,就能將程序跳到任務(wù)地方去,而不限于CALL里壓入的地址. 重點來了. 首先我們得為每個任務(wù)單獨開一塊內(nèi)存,這塊內(nèi)存專用于作為對應(yīng)的任務(wù)的堆棧,想將CPU交給哪個任務(wù),只需將棧指針指向誰內(nèi)存塊就行了. 接下來我們構(gòu)造一個這樣的函數(shù): 當(dāng)任務(wù)調(diào)用該函數(shù)時,將當(dāng)前的堆棧指針保存一個變量里,并換上另一個任務(wù)的
21、堆棧指針.這就是任務(wù)調(diào)度器了. OK了,現(xiàn)在我們只要正確的填充好這幾個堆棧的原始內(nèi)容,再調(diào)用這個函數(shù),這個任務(wù)調(diào)度就能運行起來了. 那么這幾個堆棧里的原始內(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)存塊就是私棧,要想同時運行幾少個任務(wù)就得分配多少塊.而每個子內(nèi)存塊若干字節(jié)就是棧深.
22、記住,每調(diào)一層子程序需要2字節(jié).如果不考慮中斷,4層調(diào)用深度,也就是8字節(jié)棧深應(yīng)該差不多了. unsignedcharidatatask_stackMAX_TASKSMAX_TASK_DEP 當(dāng)然,還有件事不能忘,就是堆指針的保存處.不然光有堆棧怎么知道應(yīng)該從哪個地址取數(shù)據(jù)啊 unsignedcharidatatask_spMAX_TASKS 上面兩項用于裝任務(wù)信息的區(qū)域,我們給它個概念叫任務(wù)槽.有些人叫它任務(wù)堆,我覺得還是槽比較直觀 對了,還有任務(wù)號.不然怎么知道當(dāng)前運行的是哪個任務(wù)呢? unsignedchartask_id 當(dāng)前運行存放在1號槽的任務(wù)時,這個值就是1,運行2號槽的任務(wù)時,
23、這個值就是2. 2.構(gòu)造任務(wù)調(diào)度函函數(shù): voidtask_switch() task_sptask_id=SP;/保存當(dāng)前任務(wù)的棧指針 if(+task_id=MAX_TASKS)/任務(wù)號切換到下一個任務(wù) task_id=0; SP=task_sptask_id;/將系統(tǒng)的棧指針指向下個任務(wù)的私棧. 3.裝載任務(wù): 將各任務(wù)的函數(shù)地址的低字節(jié)和高字節(jié)分別入在 task_stack任務(wù)號0和task_stack任務(wù)號1中: 為了便于使用,寫一個函數(shù):task_load(函數(shù)名,任務(wù)號) voidtask_load(unsignedintfn,unsignedchartid) task_spti
24、d=task_stacktid+1; task_stacktid0=(unsignedint)fn&0xff; task_stacktid1=(unsignedint)fn8; 4.啟動任務(wù)調(diào)度器: 將棧指針指向任意一個任務(wù)的私棧,執(zhí)行RET指令.注意,這可很有學(xué)問的哦,沒玩過堆棧的人腦子有點轉(zhuǎn)不彎:這一RET,RET到哪去了?嘿嘿,別忘了在RET前已經(jīng)將堆棧指針指向一個函數(shù)的入口了.你別把RET看成RET,你把它看成是另一種類型的JMP就好理解了. SP=task_sp任務(wù)號; return; 做完這4件事后,任務(wù)并行執(zhí)行就開始了.你可以象寫普通函數(shù)一個寫任務(wù)函數(shù),只需(目前可以這么說)注意
25、在適當(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ī)方式實現(xiàn)的多任務(wù)系統(tǒng)來說,其實效率還沒這么高-caseswitch和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)適用于實時性要求較高而內(nèi)存需求不大
26、的應(yīng)用場合,我在運行于36M主頻的STC12C4052上實測了一把,切換一個任務(wù)不到3微秒. 下回我們講講用KEIL寫多任務(wù)函數(shù)時要注意的事項. 下下回我們講講如何增強(qiáng)這個多任務(wù)系統(tǒng),跑步進(jìn)入操作系統(tǒng)時代.四.用KEIL寫多任務(wù)系統(tǒng)的技巧與注意事項 C51編譯器很多,KEIL是其中比較流行的一種.我列出的所有例子都必須在KEIL中使用.為何?不是因為KEIL好所以用它(當(dāng)然它的確很棒),而是因為這里面用到了KEIL的一些特性,如果換到其它編譯器下,通過編譯的倒不是問題,但運行起來可能是堆棧錯位,上下文丟失等各種要命的錯誤,因為每種編譯器的特性并不相同.所以在這里先說清楚這一點. 但是,我開頭已
27、經(jīng)說了,這套帖子的主要目的是闡述原理,只要你能把這幾個例子消化掉,那么也能夠自已動手寫出適合其它編譯器的OS. 好了,說說KEIL的特性吧,先看下面的函數(shù): sbitsigl=P17; voidfunc1() registerchardatai; i=5; do sigl=!sigl; while(-i); 你會說,這個函數(shù)沒什么特別的嘛!呵呵,別著急,你將它編譯了,然后展開匯編代碼再看看: 193:voidfunc1() 194:registerchardatai; 195:i=5; C:0x00C37F05MOVR7,#0x05 196:do 197:sigl=!sigl; C:0x00C
28、5B297CPLsigl(0x90.7) 198:while(-i); C:0x00C7DFFCDJNZR7,C:00C5 199: C:0x00C922RET 看清楚了沒?這個函數(shù)里用到了R7,卻沒有對R7進(jìn)行保護(hù)! 有人會跳起來了:這有什么值得奇怪的,因為上層函數(shù)里沒用到R7啊.呵呵,你說的沒錯,但只說對了一半:事實上,KEIL編譯器里作了約定,在調(diào)子函數(shù)前會盡可能釋放掉所有寄存器.通常性況下,除了中斷函數(shù)外,其它函數(shù)里都可以任意修改所有寄存器而無需先壓棧保護(hù)(其實并不是這樣,但現(xiàn)在暫時這樣認(rèn)為,飯要一口一口吃嘛,我很快會說到的). 這個特性有什么用呢?有!當(dāng)我們調(diào)用任務(wù)切換函數(shù)時,要保護(hù)
29、的對象里可以把所有的寄存器排除掉了,就是說,只需要保護(hù)堆棧即可! 現(xiàn)在我們回過頭來看看之前例子里的任務(wù)切換函數(shù): voidtask_switch() task_sptask_id=SP;/保存當(dāng)前任務(wù)的棧指針 if(+task_id=MAX_TASKS)/任務(wù)號切換到下一個任務(wù) task_id=0; SP=task_sptask_id;/將系統(tǒng)的棧指針指向下個任務(wù)的私棧. 看到?jīng)],一個寄存器也沒保護(hù),展開匯編看看,的確沒保護(hù)寄存器. 好了,現(xiàn)在要給大家潑冷水了,看下面兩個函數(shù): voidfunc1() registerchardatai; i=5; do sigl=!sigl; while(-
30、i); voidfunc2() registerchardatai; i=5; do func1(); while(-i); 父函數(shù)fun2()里調(diào)用func1(),展開匯編代碼看看: 193:voidfunc1() 194:registerchardatai; 195:i=5; C:0x00C37F05MOVR7,#0x05 196:do 197:sigl=!sigl; C:0x00C5B297CPLsigl(0x90.7) 198:while(-i); C:0x00C7DFFCDJNZR7,C:00C5 199: C:0x00C922RET 200:voidfunc2() 201:regi
31、sterchardatai; 202:i=5; C:0x00CA7E05MOVR6,#0x05 203:do 204:func1(); C:0x00CC11C3ACALLfunc1(C:00C3) 205:while(-i); C:0x00CEDEFCDJNZR6,C:00CC 206: C:0x00D022RET 看清楚沒?函數(shù)func2()里的變量使用了寄存器R6,而在func1和func2里都沒保護(hù). 聽到這里,你可能又要跳一跳了:func1()里并沒有用到R6,干嘛要保護(hù)?沒錯,但編譯器是怎么知道func1()沒用到R6的呢?是從調(diào)用關(guān)系里推測出來的. 一點都沒錯,KEIL會根據(jù)函數(shù)間
32、的直接調(diào)用關(guān)系為各函數(shù)分配寄存器,既不用保護(hù),又不會沖突,KEIL好棒哦!等一下,先別高興,換到多任務(wù)的環(huán)境里再試試: voidfunc1() registerchardatai; i=5; do sigl=!sigl; while(-i); voidfunc2() registerchardatai; i=5; do sigl=!sigl; while(-i); 展開匯編代碼看看: 193:voidfunc1() 194:registerchardatai; 195:i=5; C:0x00C37F05MOVR7,#0x05 196:do 197:sigl=!sigl; C:0x00C5B29
33、7CPLsigl(0x90.7) 198:while(-i); C:0x00C7DFFCDJNZR7,C:00C5 199: C:0x00C922RET 200:voidfunc2() 201:registerchardatai; 202:i=5; C:0x00CA7F05MOVR7,#0x05 203:do 204:sigl=!sigl; C:0x00CCB297CPLsigl(0x90.7) 205:while(-i); C:0x00CEDFFCDJNZR7,C:00CC 206: C:0x00D022RET 看到了吧?哈哈,這回神仙也算不出來了.因為兩個函數(shù)沒有了直接調(diào)用的關(guān)系,所以編譯
34、器認(rèn)為它們之間不會產(chǎn)生沖突,結(jié)果分配了一對互相沖突的寄存器,當(dāng)任務(wù)從func1()切換到func2()時,func1()中的寄存器內(nèi)容就給破壞掉了.大家可以試著去編譯一下下面的程序: sbitsigl=P17; voidfunc1() registerchardatai; i=5; do sigl=!sigl; task_switch(); while(-i); voidfunc2() registerchardatai; i=5; do sigl=!sigl; task_switch(); while(-i); 我們這里只是示例,所以仍可以通過手工分配不同的寄存器避免寄存器沖突,但在真實的應(yīng)
35、用中,由于任務(wù)間的切換是非常隨機(jī)的,我們無法預(yù)知某個時刻哪個寄存器不會沖突,所以分配不同寄存器的方法不可取.那么,要怎么辦呢? 這樣就行了: sbitsigl=P17; voidfunc1() staticchardatai; while(1) i=5; do sigl=!sigl; task_switch(); while(-i); voidfunc2() staticchardatai; while(1) i=5; do sigl=!sigl; task_switch(); while(-i); 將兩個函數(shù)中的變量通通改成靜態(tài)就行了.還可以這么做: sbitsigl=P17; voidfu
36、nc1() registerchardatai; while(1) i=5; do sigl=!sigl; while(-i); task_switch(); voidfunc2() registerchardatai; 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)于變量覆蓋. 變量分兩種,一種是全局變量,一種是局部變量(在這里,寄存器變
37、量算到局部變量里). 對于全局變量,每個變量都會分配到單獨的地址. 而對于局部變量,KEIL會做一個覆蓋優(yōu)化,即沒有直接調(diào)用關(guān)系的函數(shù)的變量共用空間.由于不是同時使用,所以不會沖突,這對內(nèi)存小的51來說,是好事. 但現(xiàn)在我們進(jìn)入多任務(wù)的世界了,這就意味著兩個沒有直接調(diào)用關(guān)系的函數(shù)其實是并列執(zhí)行的,空間不能共用了.怎么辦呢?一種笨辦法是關(guān)掉覆蓋優(yōu)化功能.呵呵,的確很笨. 比較簡單易行一個解決辦法是,不關(guān)閉覆蓋優(yōu)化,但將那些在作用域內(nèi)需要跨越任務(wù)(換句話說就是在變量用完前會調(diào)用task_switch()函數(shù)的)變量通通改成靜態(tài)(static)即可.這里要對初學(xué)者提一下,靜態(tài)你可以理解為全局,因為它
38、的地址空間一直保留,但它又不是全局,它只能在定義它的那個花括號對里訪問. 靜態(tài)變量有個副作用,就是即使函數(shù)退出了,仍會占著內(nèi)存.所以寫任務(wù)函數(shù)的時候,盡量在變量作用域結(jié)束后才切換任務(wù),除非這個變量的作用域很長(時間上長),會影響到其它任務(wù)的實時性.只有在這種情況下才考慮在變量作用域內(nèi)跨越任務(wù),并將變量申明為靜態(tài). 事實上,只要編程思路比較清析,很少有變量需要跨越任務(wù)的.就是說,靜態(tài)變量并不多. 說完了覆蓋我們再說說重入. 所謂重入,就是一個函數(shù)在同一時刻有兩個不同的進(jìn)程復(fù)本.對初學(xué)者來說可能不好理解,我舉個例子吧: 有一個函數(shù)在主程序會被調(diào)用,在中斷里也會被調(diào)用,假如正當(dāng)在主程序里調(diào)用時,中斷
39、發(fā)生了,會發(fā)生什么情況? voidfunc1() staticchardatai; 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ā)生重入,看下面的例子: voidfunc1() . delay(); . voidfunc2() . delay(); . voiddelay() staticunsignedchari;/注意這里是申明為static,不申明static的話會發(fā)生覆蓋
40、問題.而申明為static會發(fā)生重入問題.麻煩啊 for(i=0;i10;i+) task_switch(); 兩個并行執(zhí)行的任務(wù)都調(diào)用了delay(),這就叫重入.問題在于重入后的兩個復(fù)本都依賴變量i來控制循環(huán),而該變量跨越了任務(wù),這樣,兩個任務(wù)都會修改i值了. 重入只能以防為主,就是說盡量不要讓重入發(fā)生,比如將代碼改成下面的樣子: #definedelay()staticunsignedchari;for(i=0;i10;i+)task_switch();/i仍定義為static,但實際上已經(jīng)不是同一個函數(shù)了,所以分配的地址不同. voidfunc1() . delay(); . void
41、func2() . delay(); . 用宏來代替函數(shù),就意味著每個調(diào)用處都是一個獨立的代碼復(fù)本,那么兩個delay實際使用的內(nèi)存地址也就不同了,重入問題消失. 但這種方法帶來的問題是,每調(diào)用一次delay(),都會產(chǎn)生一個delay的目標(biāo)代碼,如果delay的代碼很多,那就會造成大量的rom空間占用.有其它辦法沒? 本人所知有限,只有最后一招了: voiddelay()reentrant unsignedchari; for(i=0;i10;i+) task_switch(); 加入reentrant申明后,該函數(shù)就可以支持重入.但小心使用,申明為重入后,函數(shù)效率極低! 最后附帶說下中斷.
42、因為沒太多可說的,就不單獨開章了. 中斷跟普通的寫法沒什么區(qū)別,只不過在目前所示例的多任務(wù)系統(tǒng)里因為有堆棧的壓力,所以要使用using來減少對堆棧的使用(順便提下,也不要調(diào)用子函數(shù),同樣是為了減輕堆棧壓力) 用using,必須用#pragmaNOAREGS關(guān)閉掉絕對寄存器訪問,如果中斷里非要調(diào)用函數(shù),連同函數(shù)也要放在#pragmaNOAREGS的作用域內(nèi).如例所示: #pragmaSAVE #pragmaNOAREGS/使用using時必須將絕對寄存器訪問關(guān)閉 voidclock_timer(void)interrupt1using1/使用using是為了減輕堆棧的壓力 #pragmaREST
43、ORE 改成上面的寫法后,中斷固定占用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)生重入覆蓋.使用重入申明解決 五.向操作系統(tǒng)邁進(jìn) 源代碼打包ourdev_385493.rar(文件大小:39K)(原文件名:aos.rar) 先下載示例代碼.用KEIL打開它,但先別急著看,回這里來. 前面所說的例子中,除了多任務(wù)并行執(zhí)行能力外,沒有其它功能,這對于一個極簡單的系統(tǒng)來說是夠用的,但如果系統(tǒng)稍復(fù)雜一點,例如: 1.某任務(wù)中需要延時 2.某任務(wù)中需要等待,直至某事務(wù)處理完. 3.任務(wù)并非一開始就全部裝入,隨著處理流程的展開,在不同的時刻裝入不同的任務(wù).任務(wù)具有生命周期,事務(wù)處理完畢后,希望將任務(wù)結(jié)束并清除. 這里就是操作系統(tǒng)的幾個
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 新課標(biāo)視野下中小學(xué)教師跨學(xué)科素養(yǎng)的構(gòu)成與提升研究
- 內(nèi)蒙古建筑職業(yè)技術(shù)學(xué)院《影像后期制作》2023-2024學(xué)年第一學(xué)期期末試卷
- 昆明學(xué)院《女性文學(xué)研究》2023-2024學(xué)年第一學(xué)期期末試卷
- 湖南工業(yè)職業(yè)技術(shù)學(xué)院《外科護(hù)理學(xué)實訓(xùn)》2023-2024學(xué)年第一學(xué)期期末試卷
- 長江藝術(shù)工程職業(yè)學(xué)院《人體素描》2023-2024學(xué)年第一學(xué)期期末試卷
- 培訓(xùn)需求申請
- 廣東白云學(xué)院《高級英語寫作2》2023-2024學(xué)年第一學(xué)期期末試卷
- 常州幼兒師范高等??茖W(xué)校《社會調(diào)查與數(shù)據(jù)應(yīng)用》2023-2024學(xué)年第一學(xué)期期末試卷
- 溫州理工學(xué)院《高級英語(1)》2023-2024學(xué)年第一學(xué)期期末試卷
- 廣東碧桂園職業(yè)學(xué)院《中國文學(xué)批評專題》2023-2024學(xué)年第一學(xué)期期末試卷
- 電機(jī)學(xué)知到智慧樹章節(jié)測試課后答案2024年秋東北電力大學(xué)
- 凈水器安裝協(xié)議書
- 2019北師大版高中英語單詞表全7冊
- 2024年借貸擔(dān)保合同
- 寧夏固原市第六中學(xué)2022-2023學(xué)年七年級上學(xué)期分班考試語文檢測卷
- 《電工與電子技術(shù)基礎(chǔ)(第4版)》中職全套教學(xué)課件
- 北師大版生物八年級下冊23章1節(jié)生物的生存依賴一定的環(huán)境(44張)課件-課件
- 2024年工業(yè)廢水處理工(高級)技能鑒定考試題庫-下(多選、判斷題)
- 2023年全國職業(yè)院校技能大賽-聲樂、器樂表演大賽賽項規(guī)程
- NB∕SH∕T 0001-2019 電纜瀝青標(biāo)準(zhǔn)規(guī)范
- DL∕T 1688-2017 氣體絕緣金屬封閉開關(guān)設(shè)備狀態(tài)評價導(dǎo)則
評論
0/150
提交評論