為了進(jìn)一步把單片機(jī)的潛能發(fā)揮到極限,我一直想寫個程序把單片機(jī)的所有資源都用光,但是如果依照單道程序順序執(zhí)行的方式,很難把MCU的CPU時間都充分利用,比如使用軟件延時函數(shù)實際上就是在無謂地消耗著CPU的時間什么事情都不做,因為CPU一直在循環(huán)等待著條件結(jié)束,這相當(dāng)于函數(shù)被阻塞了。
成功同時運行三個流水燈程序!太棒了!接下來在這個內(nèi)核的支持下你就可以創(chuàng)作你的應(yīng)用程序了,使用內(nèi)核提供的線程創(chuàng)建函數(shù)你可以創(chuàng)建N多個線程,當(dāng)然了,必須在內(nèi)存可接受的范圍內(nèi)。利用內(nèi)存分配函數(shù)你可以動態(tài)申請和釋放內(nèi)存了。再也不用為DELAY()這種浪費CPU效率的作法郁悶很久了。
為了更明顯地驗證這一點,你可以在WINDOWS下打開VC6.0或其他的C語言編譯器,寫段代碼如下:
#include <stdio.h>
void main(void)
{while(1) ;}
意思是讓CPU不做事情在等待,你猜,這句代碼會消耗掉多少CPU時間?
答案會根據(jù)不同機(jī)型而不同,如果是單核CPU的話,這句話會消耗掉CPU接近100%的時間!如果是雙核CPU,則只消耗掉50%左右,因為這段代碼只運行在其中一個核,另外一個核還可以做別的事情,截圖如下:
{while(1)
Sleep(100);
}
這段代碼實際上也是什么都不做,它不斷地調(diào)用Sleep()函數(shù),讓它延時100毫秒再醒來,然后繼續(xù)睡覺。現(xiàn)在你可以再打開任務(wù)管理器看一下CPU時間用了多少,答案是基本不用CPU時間。
為什么同樣地什么事情都不做,差別咋就這么大呢?這是因為使用了Sleep()這個函數(shù)是WINDOWS操作系統(tǒng)為你提供的,調(diào)用Sleep()之后 WINDOWS操作系統(tǒng)自動把你這個程序掛起了(就是暫時扔到一邊不管),然后讓CPU去執(zhí)行其他程序,等到時間到了,操作系統(tǒng)再把這段程序恢復(fù)繼續(xù)執(zhí)行,這樣的話CPU就可以得到充分地利用了,也就是說你可以在一塊CPU里面“同時”執(zhí)行多個任務(wù)而互不影響。ㄟ@里所說的“同時”并不是同時執(zhí)行,CPU每一時刻只能做一件事,但如果速度足夠快的話就可以讓人感到它是在同時執(zhí)行多項任務(wù)了)。是的,操作系統(tǒng)就是為了解決多任務(wù)執(zhí)行而生的。既然操作系統(tǒng)這么神奇,可不可以讓單片機(jī)也來爽一把呢?答案是肯定的。下面就介紹如何給單片機(jī)寫個操作系統(tǒng)!
/************************************************************************************/
工欲善其事,必先利其器,為了寫出操作系統(tǒng),必須得有一定的理論以及技術(shù)基礎(chǔ),
單片機(jī)方面的可以http://m.zg4o1577.cn 了解到,從下面是所需的材料:
單片機(jī)方面的可以http://m.zg4o1577.cn 了解到,從下面是所需的材料:
//1 C語言編程基礎(chǔ) :三斤
//2 數(shù)據(jù)結(jié)構(gòu)理論 :一斤八兩
//3 操作系統(tǒng)原理 :兩斤三兩八錢
//4 計算機(jī)組成原理以及單片機(jī)原理及應(yīng)用 :兩斤半
//5 匯編語言編程基礎(chǔ) :一斤四兩
//6 一份堅持的心 :多少斤自己掂量掂量,呵呵
/*************************************************************************************/
這么多怎么學(xué)?去哪學(xué)?下面是我個人推薦的書單,僅供參考:
1. C語言是必須要會的,而且要熟練,諸如”預(yù)編譯命令“你必須要懂,模塊化編程必須要熟悉,指針是C語言的一大精髓,在操作系統(tǒng)源碼里面指針是滿天飛的,所以得有足夠的理論基礎(chǔ),推薦國外的《C Primer Plus》 美國 Stephen Prata著,里面講的內(nèi)容由淺到深,語言引人入勝,大二開始看,現(xiàn)在還時不時地要回頭看,確實是一本不錯的好書:
另外,學(xué)會了C的基本語法之后你還得要會一點點編程技巧以及編程要注意的問題之類的,推薦有空的話多看看《C專家編程》和《C陷阱與缺陷》,這兩本書是C編程領(lǐng)域里面的經(jīng)典之作,相信看完你的功力會大有長進(jìn),但是還是要以經(jīng)常敲代碼為主:
2. 操作系統(tǒng)里面的數(shù)據(jù)組織形式都是以數(shù)據(jù)結(jié)構(gòu)的理論為基礎(chǔ)的,所以你得懂得數(shù)據(jù)結(jié)構(gòu)才能看懂里面的含義,但也不要求把數(shù)據(jù)結(jié)構(gòu)全精通,推薦嚴(yán)蔚敏版本的《數(shù)據(jù)結(jié)構(gòu)》,不過里面的算法都是用偽代碼寫出來的:
3. 有了編程基礎(chǔ)之后你還必須要懂得操作系統(tǒng)的基本原理,比如任務(wù)之間是怎么切換的,內(nèi)存是怎么管理的都得懂,推薦《操作系統(tǒng)-精髓與設(shè)計原理》
4. 匯編語言。為什么要學(xué)匯編?可能有些人會學(xué)得匯編難理解,而且現(xiàn)在C語言已經(jīng)可以很方便地編程了,所以不想學(xué)匯編,其實C語言再怎么方便強(qiáng)大,最后還是要通過編譯器轉(zhuǎn)換為匯編語言再由匯編轉(zhuǎn)換為機(jī)器碼,才能在機(jī)器中執(zhí)行。可以說,掌握了匯編之后你一定會對”代碼是怎么在CPU里面執(zhí)行的“這個哲學(xué)命題有進(jìn)一步的了解。另外,不學(xué)匯編你還真寫不出一個操作系統(tǒng)內(nèi)核來,因為操作系統(tǒng)的最低層是要直接操作CPU寄存器的,C語言無法實現(xiàn),只能用匯編寫出來,再由上層的C語言調(diào)用。匯編的書很多,這里就不介紹了,找一本去狂敲上個把星期就大概掌握了。
5. 另外你還要懂得計算機(jī)原理以及單片機(jī),其實單片機(jī)就是一臺閹割版的計算機(jī),你得對CPU寄存器,數(shù)據(jù)總線,地址總線,以及執(zhí)行方式這些有一定的了解才行,這方面的書也挺多的,不過介紹兩本個人覺得寫得挺好的書供課外閑讀,《編程卓越之道》1、2卷,這本書大體上介紹了高級語言是怎么樣在CPU里面執(zhí)行的,另外也對CPU內(nèi)部結(jié)構(gòu)做了一些介紹,比那些課內(nèi)教材寫得好,有空可以去看一下。
最后介紹一本《嵌入式實時操作系統(tǒng)UCOS II》,這本書介紹了UCOS II這個操作系統(tǒng)的內(nèi)部源代碼以及實現(xiàn)原理,我就是從這本書中學(xué)到了怎樣寫一個可以用的操作系統(tǒng)內(nèi)核。
/**************************************************************************************/
什么是操作系統(tǒng)?其實就是一個程序, 這個程序可以控制計算機(jī)的所有資源,對資源進(jìn)行分配,包括CPU時間,內(nèi)存,IO端口等,按一定規(guī)則分配給所需要的進(jìn)程(進(jìn)程?也就是一個程序,可以單獨執(zhí)行),并且自動控制讓CPU可以執(zhí)行多個互不相關(guān)的任務(wù),按照書中的介紹,一個操作系統(tǒng)需要具備四個要素:進(jìn)程調(diào)度、內(nèi)存管理、IO管理、文件管理。
那怎么樣可以讓CPU同時執(zhí)行多個任務(wù)呢?首先想象一下如果讓CPU執(zhí)行單道程序,它會從MAIN函數(shù)開始一直順序地執(zhí)行下去,CPU里面有一個叫PC的寄存器,也就是程序計數(shù)器,它永遠(yuǎn)指向下一條要執(zhí)行的指令的存放地址,因為大多數(shù)情況下指令都是逐條執(zhí)行的,所以PC寄存器也只是簡單地加一,所以大家都叫它”程序計數(shù)器“,從PC寄存器的特點也許我們可以做點文章?比如人為地讓PC寄存器指到另外一段程序的入口地址,那CPU不就自動地跑到另一段程序了么?哈哈。假如我們可以這樣做,那沒錯,CPU確定是跑到別人的領(lǐng)地去執(zhí)行代碼了,問題是:怎么樣讓它回來繼續(xù)執(zhí)行?換句話說,PC寄存器改變之后CPU 已經(jīng)不知道剛剛這段程序執(zhí)行到哪里了,亦即跑不回來了,就像斷了線的風(fēng)箏。呃。。這問題麻煩。。解決了這個問題就似乎有點苗頭了。。
好吧,我們來看看有一個很相似的問題,就是單片機(jī)在執(zhí)行代碼的時候,突然有一個中斷信號過來了,單片機(jī)馬上就屁顛屁顛地跑到中斷服務(wù)程序里面去執(zhí)行了,執(zhí)行完畢之后,奇怪!!它怎么還記得跑回來原來的地方。??OH NO .它是怎么辦到的。其實這里還要介紹另外一個寄存器叫SP的,即:STACK POINTER堆棧指針,這個指針指向一個內(nèi)存的地址,里面存放了一些數(shù)據(jù)。首先,單片機(jī)遇到中斷信號的時候,它就把當(dāng)前的PC寄存器的值保存到SP所指的地址,這就相當(dāng)于它記住了當(dāng)前執(zhí)行的地方,叫做斷點保護(hù),然后PC寄存器就指向中斷服務(wù)程序的地址,下一個時刻CPU就自動執(zhí)行中斷服務(wù)程序里面的代碼了,執(zhí)行完畢之后中斷服務(wù)程序調(diào)用了一個指令:RETI,這條指令叫返回指令,在函數(shù)結(jié)束之后調(diào)用,它會自動從SP指針指向的地址把值取出來放到PC寄存器里面,然后CPU就會自動回到之前斷掉的地方繼續(xù)執(zhí)行了!基于這個原理,我們可以回到上面的問題:首先,讓CPU把當(dāng)前的PC保存起來,然后把PC指向別段程序地址,CPU就跑到別人的領(lǐng)地去執(zhí)行了,執(zhí)行完了之后我們可以把SP指向的內(nèi)容放回PC,這樣調(diào)用RET指令之后,CPU就會回到原來的地方繼續(xù)執(zhí)行了!貌似這個問題完美地解決了。
可是還有一個關(guān)鍵的問題:CPU在執(zhí)行當(dāng)前代碼的時候 CPU里面所有的寄存器都保存的當(dāng)前這個程序所用到的值,比如做加法的時候用到PSW寄存器的進(jìn)位標(biāo)志位,如果此時切換到別的任務(wù),那再回到當(dāng)前程序的時候,這些值都會被改變,CPU會陷入混亂然后直接跑飛!解決這問題同樣要靠SP同學(xué),在切換任務(wù)的時候我們把所有寄存器依次入到SP指向的地址,稱為入棧操作,每次入棧SP指針的值都會加一或者減一,視不同CPU而定。而要恢復(fù)的時候,就從SP指向的地址依次把值取出來放回原來的地方,稱為彈棧操作。最后才彈出地址到PC寄存器,下一時刻,CPU自動跑到原來的地址繼續(xù)執(zhí)行,從CPU的角度看就像沒有發(fā)生任務(wù)切換一樣,一切依舊,繼續(xù)工作。如果CPU的執(zhí)行速度夠快,切換速度也夠快,這樣就可以給人感覺CPU同時在執(zhí)行很多任務(wù),這就是操作系統(tǒng)里面最基本的原理。
SO,解釋完原理,我們首先來就來實現(xiàn)簡單的任務(wù)切換,這里的難點就在于:執(zhí)行這一動作必須要操作CPU的寄存器,而C語言是無法實現(xiàn)的,這就是為什么要用到匯編的原因了,所有操作系統(tǒng)的最底層代碼都是用匯編語言實現(xiàn)的,否則根本無法實現(xiàn)任務(wù)切換。下面要介紹匯編里面的幾條相關(guān)指令。PS:雖然每種CPU的匯編都不同,但是基本原理還是相通的。
第一條:CALL。函數(shù)調(diào)用指令,當(dāng)我們要調(diào)用一個函數(shù)的時候,就會用到CALL這條指令,它執(zhí)行再從個動作,第一,先把當(dāng)前的PC值保存起來,即現(xiàn)場保護(hù),第二,把要調(diào)用的函數(shù)的入口地址送到PC,這樣,在下一時刻到來的時候,CPU就自動跳轉(zhuǎn)到特定的函數(shù)入口地址開始執(zhí)行了。
第二條:RET/RETI。當(dāng)一個函數(shù)執(zhí)行完畢的時候,需要返回到原來執(zhí)行的地方,這時候就要調(diào)用 RET指令(在中斷函數(shù)中返回的時候調(diào)用RETI指令)。它把SP指向的數(shù)據(jù),即上一次調(diào)用CALL時保存的那個地址原來到PC,這樣,當(dāng)下一時刻到來的時候,CPU就會跳回到原來的地方了。實際上函數(shù)調(diào)用過程就是這樣的,所以有時候一些簡單簡短的函數(shù)寧愿用#define宏定義寫出來,因為這樣寫出來就不用使用調(diào)用/返回過程,節(jié)省了時間。
第三/四條:PUSH/POP。這兩個指令是兩兄弟,即入棧及出棧。關(guān)于堆棧的特性說明一下:堆棧這種結(jié)構(gòu)的特性就是后進(jìn)先出,就像疊盤子一樣,最后疊上去的盤子會被最先取出,這種原理非常好用,想象一下函數(shù)嵌套的時候發(fā)生的一切,就是利用到這種思路。PUSH指令用到把寄存器的值保存起來,它會把值到保存到SP指針?biāo)傅牡胤健OP指令則把數(shù)據(jù)從SP所指的地址恢復(fù)到原來的寄存器中。
用這幾條指令,我們就可以寫出一個任務(wù)切換函數(shù)了,不過寫之前還要說明一下什么叫人工堆棧。其實上,一個程序在執(zhí)行的時候,它會用到一塊內(nèi)存空間用于保存各種變量,比如調(diào)用函數(shù)的時候這塊地方會用于保存地址以及寄存器,而在執(zhí)行一些復(fù)雜算法的時候,如果CPU的寄存器已經(jīng)用完了,這塊地方也會作為臨時中間變量的存放區(qū),另外,在向一個函數(shù)傳遞參數(shù)的時候,比如:printf(a,b,c,d,e....),如果參數(shù)過多,多余的參數(shù)也會先存放到這塊地方。所以說,這塊地方就像是這個程序的倉庫一樣,存放著要用的東西。如果是在單道程序中,顯然這樣用沒問題,但是如果是多道程序的話,問題就來了,因為如果所有任務(wù)共用那塊區(qū)域,那舊任務(wù)保存的東西就會被新任務(wù)所沖掉,CPU一下子就瘋掉了。。解決的辦法就是:每個任務(wù)都給它提供一塊專用的區(qū)域,這塊專用區(qū)域就叫人工堆棧,每個任務(wù)都不一樣,保證了不會相互沖突。
PS:因為51單片機(jī)的內(nèi)存太小,基本無法實現(xiàn)多任務(wù),實現(xiàn)了也不實用,所以硬件平臺我選用了AVR單片機(jī)ATMEGA16,有1KB內(nèi)存,應(yīng)該夠用了,花了兩天時間把AVR的匯編指令看了一遍
首先,當(dāng)需要切換任務(wù)的時候,要先把當(dāng)前的所有寄存器全部入棧,在AVR單片機(jī)中有32個通用寄存器R0-R31,還有PC指針,PSW程序狀態(tài)寄存器,這些都要入棧,所以需要的內(nèi)存挺多的,F(xiàn)在的編譯器都支持在線匯編,就是在C語言里面嵌入?yún)R編語言,方便得多,下面我宏定義了一組入棧操作:PUSH_REG(),里面是用PUSH指令把32個寄存器全部入棧
#define PUSH_REG() \
{_asm("PUSH R0\n\t" "PUSH R1\n\t" "PUSH R2\n\t" "PUSH R3\n\t" \
"PUSH R4\n\t" "PUSH R5\n\t" "PUSH R6\n\t" "PUSH R7\n\t" \
"PUSH R8\n\t" "PUSH R9\n\t" "PUSH R10\n\t" "PUSH R11\n\t" \
"PUSH R12\n\t" "PUSH R13\n\t" "PUSH R14\n\t" "PUSH R15\n\t" \
"PUSH R16\n\t" "PUSH R17\n\t" "PUSH R18\n\t" "PUSH R19\n\t" \
"PUSH R20\n\t" "PUSH R21\n\t" "PUSH R22\n\t" "PUSH R23\n\t" \
"PUSH R24\n\t" "PUSH R25\n\t" "PUSH R26\n\t" "PUSH R27\n\t" \
"PUSH R28\n\t" "PUSH R29\n\t" "PUSH R30\n\t" "PUSH R31\n\t" ); }
入完棧完接下來要保護(hù)當(dāng)前程序的SP指針,以便下次它要返回的時候能找到該人工堆棧的地址:
OS_LastThread->ThreadStackTop=(OS_DataType_ThreadStack *)SP;
這一句用C語言就可以實現(xiàn)了。
接下來關(guān)于當(dāng)前這段程序的現(xiàn)場算是保護(hù)好了,然后找到要切換到的任務(wù)的人工堆棧地址,把它賦給SP指針,如下:
SP=(uint16_t)OS_CurrentThread->ThreadStackTop;
出棧跟入棧的語法差不多,只是出棧順序要相反:
POP_REG();
接下來,要調(diào)用一條很重要的指令了。。〈肆钜怀,CPU就乖乖地切換任務(wù)了!
_asm("RET\n\t");
調(diào)用返回指令,它就從SP里面取出函數(shù)地址放到PC,注意他取出的是剛剛放入SP指向地址的函數(shù)入口,所以它會返回到新任務(wù)執(zhí)行。
就這樣,一個操作系統(tǒng)里面最核心的”任務(wù)調(diào)度器“的模型就這樣簡單地實現(xiàn)了,操作系統(tǒng)里面所作的跟任務(wù)切換有關(guān)的事情到最后都要調(diào)用到這個任務(wù)調(diào)度器,現(xiàn)在我們實現(xiàn)調(diào)度器了,相當(dāng)于成功了1/3,接下來的事情就是考慮在什么情況下調(diào)用這個調(diào)度器。
調(diào)度策略:實現(xiàn)了調(diào)度,還要繼續(xù)考慮調(diào)度策略,就是什么情況下需要調(diào)度哪些任務(wù)。調(diào)度策略分很多種,有興趣的可以去看那本《操作系統(tǒng)原理》,在我的源代碼里面使用了”搶占式優(yōu)先級調(diào)度+同一優(yōu)先級下時間片輪詢調(diào)度“的方法。
所謂搶占式優(yōu)先級調(diào)度是一種實時調(diào)度的方法,在實時操作系統(tǒng)中常用,這種方法的原理就是:操作系統(tǒng)在任何時候都要保證擁有最高優(yōu)先級的那個任務(wù)處于運行態(tài),比如此記在運行著優(yōu)先級為2的任務(wù),因為一些信號到達(dá),優(yōu)先級為1的那個任務(wù)解除了阻塞,處于就緒態(tài),這時操作系統(tǒng)就必須馬上停止任務(wù)2,切換到任務(wù)1,切換的這段時間需要越短越好。
而時間片輪詢即是讓每個任務(wù)都處于平等地位,然后給每個任務(wù)相同的時間片,當(dāng)一個任務(wù)的運行時間用完了,操作系統(tǒng)就馬上切換給下一個需要執(zhí)行的任務(wù),這種方法的實時性不高,但它確保了每個任務(wù)都有相同的執(zhí)行時間。
我把這兩種方法結(jié)合起來,首先設(shè)定了8個優(yōu)先級組,每個優(yōu)先級組下面都用單向鏈表把具有相同優(yōu)先級的任務(wù)連接起來。這樣的話首先操作系統(tǒng)會查找最高優(yōu)先級的那組,然后在組里面輪流執(zhí)行所有任務(wù)(和UCOS II相比這種做法更具有靈活性,因為UCOS II只有搶占式調(diào)度,這是UCOS II的硬傷。。)。我聲明了一個任務(wù)結(jié)構(gòu)體稱為線程控制塊,把關(guān)于該任務(wù)的所有狀態(tài)都放在一起:
/**
* @結(jié)構(gòu)體聲明
* @名稱 : OS_TCB , *pOS_TCB
* @成員 : 1. OS_DataType_ThreadStack *ThreadStackTop
* 線程人工堆棧棧頂指針
* 2. OS_DataType_ThreadStack *ThreadStackBottom
* 線程人工堆棧棧底指針
* 3. OS_DataType_ThreadStackSize ThreadStackSize
* 線程人工堆棧大小
* 4. OS_DataType_ThreadID ThreadID
* 線程ID號
* 5. OS_DataType_ThreadStatus ThreadStatus
* 線程運行狀態(tài)
* 6. OS_DataType_PSW PSW
* 記錄線程的程序狀態(tài)寄存器
* 7. struct _OS_TCB *Front
* 指向上一個線程控制塊的指針
* 8. struct _OS_TCB *Next
* 指向下一人線程控制塊的指針
* 9.struct _OS_TCB *CommWaitNext ;
* 指向線程通信控制塊的指針
* 10.struct _OS_TCB *TimeWaitNext ;
* 指向延時等待鏈表的指針
* 11.OS_DataType_PreemptionPriority Priority ;
* 任務(wù)優(yōu)先級
* 12.OS_DataType_TimeDelay TimeDelay ;
* 任務(wù)延時時間
* @描述 : 定義線程控制塊的成員
* @建立時間 : 2011-11-15
* @最近修改時間: 2011-11-17
*/
typedef struct _OS_TCB{
OS_DataType_ThreadStack *ThreadStackTop ;
OS_DataType_ThreadStack *ThreadStackBottom ;
OS_DataType_ThreadStackSize ThreadStackSize;
OS_DataType_ThreadID ThreadID ;
OS_DataType_ThreadStatus ThreadStatus ;
OS_DataType_PSW PSW ;
struct _OS_TCB *Front ;
struct _OS_TCB *Next ;
#if OS_COMMUNICATION_EN == ON
struct _OS_TCB *CommWaitNext ;
#endif
struct _OS_TCB *TimeWaitNext ;
OS_DataType_PreemptionPriority Priority ;
OS_DataType_TimeDelay TimeDelay ;
}OS_TCB,*pOS_TCB;
首先啟動系統(tǒng)的時候需要先創(chuàng)建任務(wù),任務(wù)被創(chuàng)建之后才可以得到執(zhí)行,使用如下函數(shù):
/**
* @名稱:線程創(chuàng)建函數(shù)
* @輸入?yún)?shù):1.pOS_TCB ThreadControlBlock 線程控制塊結(jié)構(gòu)體指針
* 2.void (*Thread)(void*) 線程函數(shù)入口地址,接受一個空指針形式的輸入?yún)?shù),無返回參數(shù)
* 3.void *Argument 需要傳遞給線程的參數(shù),空指針形式
* @建立時間 : 2011-11-18
* @最近修改時間: 2011-11-18
*/
void OS_ThreadCreate(pOS_TCB ThreadControlBlock,void (*Thread)(void *),void *Argument)
關(guān)于創(chuàng)建任務(wù)的大致描述就是:填定線程控制塊,把線程控制塊鏈到單向鏈表中,設(shè)置人工堆棧,細(xì)節(jié)很多,就不一一贅述了。
當(dāng)前版本只實現(xiàn)了輪詢調(diào)度,還沒加上搶占調(diào)度,使用下面的函數(shù)就可以啟動操作系統(tǒng)開始多線程任務(wù)!
/**
* @名稱 : 實時內(nèi)核引發(fā)函數(shù)
* @版本 : V 0.0
* @輸入?yún)?shù) : 無
* @輸出參數(shù) : 無
* @描述 : 在主函數(shù)中用于啟動,調(diào)用該函數(shù)后不會返回,直接切換到最高優(yōu)先級任務(wù)開始執(zhí)行
* @建立時間 : 2011-11-15
* @最近修改時間: 2011-11-15
*/
void OS_KernelStart(void)
{
OS_Status = OS_RUNNING ; //把內(nèi)核狀態(tài)設(shè)置為運行態(tài)
//取得第一個需要運行的任務(wù)
OS_CurrentThread = OS_TCB_PriorityGroup[pgm_read_byte(ThreadSearchTab + OS_PreemptionPriority)].OS_TCB_Current;
OS_LastThread = NULL ;
//SP指針指向該任務(wù)的棧頂
SP = (uint16_t)OS_CurrentThread->ThreadStackTop ;
//使用出棧操作
POP_REG();
//調(diào)用RET,調(diào)用之后開始執(zhí)行任務(wù),不會再返回到這里
_asm("RET\n\t");
}
怎樣實現(xiàn)時間片?答案是用定時器定時,每次定時器產(chǎn)生中斷的時候就轉(zhuǎn)換一次任務(wù),時基可以自己確定,一般來說時基越小的話會讓CPU花很多時間在切換任務(wù)上,降低了效率,時基大的話又使時間粒度變粗,會使一些程序得不到及時的執(zhí)行。我設(shè)定了每10MS中斷一次,就是說每一輪中每個線程都有10MS的執(zhí)行時間。具體算法不再贅述。
內(nèi)存管理策略
接下來要考慮怎樣管理內(nèi)存了!在PC里面編程的時候,如果需要開辟一個內(nèi)存空間,我們可以很容易地調(diào)用malloc()和free()來完成,但是在單片機(jī)里面卻行不通,因為要實現(xiàn)這兩個函數(shù)背后需要完成很多算法支持,從速度和空間上單片機(jī)都做不到。
在單片機(jī)里面如果你需要開辟內(nèi)存空間,你只有在編譯的時候就先定義好變量,無法動態(tài)申請,但是我們可以設(shè)計一個簡單的內(nèi)存管理策略來實現(xiàn)這種動態(tài)申請!原理就是在編譯的時候先向編譯器要一塊足夠大的內(nèi)存并且聲明為靜態(tài),然后把這塊空間交給內(nèi)存管理模塊來調(diào)用,內(nèi)存管理模塊負(fù)責(zé)分配這塊內(nèi)存,當(dāng)有任務(wù)要向它申請內(nèi)存的時候它就從里面拿出一塊交給任務(wù),而任務(wù)要釋放的時候就把該內(nèi)存空間交給內(nèi)存管理模塊來實現(xiàn)。
關(guān)于內(nèi)存管理也有很多種策略,在這里就不一一述說了,我在源代碼里面使用了一種簡單的隨機(jī)分配的方法,即有線程申請的時候就從當(dāng)前內(nèi)存塊的可用空間里拿出一塊來,然后在內(nèi)存頭加上一個專用的結(jié)構(gòu)體,把每個內(nèi)存塊都鏈接起來,這樣便于管理。當(dāng)線程釋放內(nèi)存的時候,就把內(nèi)存返回到內(nèi)存空間并跟其他空間的內(nèi)存塊合并起來等待線程再次調(diào)用。
/**
* @名稱 : 內(nèi)存塊申請函數(shù)
* @版本 : V 0.0
* @輸入?yún)?shù) : 1. OS_DataType_MemorySize MemorySize
需要申請內(nèi)存塊的大小
* @輸出參數(shù) : 1. void *
若申請成功,則返回可使用內(nèi)存塊首地址,否則返回NULL
* @描述 :
* @建立時間 : 2011-11-16
* @最近修改時間: 2011-11-16
*/
#if OS_MEMORY_EN
void *OS_MemoryMalloc(OS_DataType_MemorySize MemorySize)
{
pOS_MCB pmcb = OS_MCB_Head ;
pOS_MCB pmcb2 ;
MemorySize+=OS_MEMORY_BLOCK_SIZE ;
//進(jìn)入內(nèi)存搜索算法
while(1)
{
//檢測該內(nèi)存塊是否存在
if(pmcb==NULL)
{
return NULL ;
}
//如果存在則檢測該內(nèi)存塊的使用狀態(tài)
else if( (pmcb->Status==OS_MEMORY_STATUS_IDLE) && (pmcb->Size >= MemorySize) )
{
//如果可用內(nèi)存塊大小剛好等于需要申請的大小
//則立即分配
if(pmcb->Size == MemorySize)
{
pmcb->Status=OS_MEMORY_STATUS_USING ;
OS_MemoryIdleCount -= MemorySize ;
return (OS_DataType_Memory *)pmcb + OS_MEMORY_SIZE ;
}
//若可用內(nèi)存塊大小大于需要申請的大小
//則進(jìn)行分割操作
else
{
pmcb2=(pOS_MCB)( (OS_DataType_Memory *)pmcb + MemorySize );
pmcb2->Front=pmcb ;
pmcb2->Next=pmcb->Next ;
pmcb2->Status=OS_MEMORY_STATUS_IDLE ;
pmcb2->Size = pmcb->Size - MemorySize ;
pmcb->Status = OS_MEMORY_STATUS_USING ;
pmcb->Size = MemorySize ;
pmcb->Next=pmcb2;
OS_MemoryIdleCount -= MemorySize ;
return (OS_DataType_Memory *)pmcb+OS_MEMORY_BLOCK_SIZE ;
}
}
else
{
pmcb=pmcb->Next;
}
}
}
#endif
內(nèi)存釋放函數(shù):
/**
* @名稱 : 內(nèi)存塊釋放函數(shù)
* @版本 : V 0.0
* @輸入?yún)?shù) : 1. OS_DataType_MemorySize MemorySize
需要申請內(nèi)存塊的大小
* @輸出參數(shù) : 1. void *
若申請成功,則返回可使用內(nèi)存塊首地址,否則返回NULL
* @描述 :
* @建立時間 : 2011-11-16
* @最近修改時間: 2011-11-16
*/
#if OS_MEMORY_EN
void OS_MemoryFree(void *MCB)
{
pOS_MCB pmcb = (pOS_MCB)( (OS_DataType_Memory *)MCB - OS_MEMORY_BLOCK_SIZE );
//將當(dāng)前內(nèi)存塊設(shè)置為空閑狀態(tài)
pmcb->Status=OS_MEMORY_STATUS_IDLE ;
OS_MemoryIdleCount += pmcb->Size ;
//如果存在上一塊內(nèi)存塊,則進(jìn)入判斷
if(pmcb->Front!=NULL)
{
//如果上一塊內(nèi)存塊處于空閑狀態(tài),則進(jìn)行合并操作
if(pmcb->Front->Status == OS_MEMORY_STATUS_IDLE)
{
pmcb->Front->Size += pmcb->Size ;
pmcb->Front->Next = pmcb->Next ;
pmcb=pmcb->Front ;
OS_MemoryIdleCount += pmcb->Size ;
}
}
//如果存在下一塊內(nèi)存塊,則進(jìn)入判斷
if(pmcb->Next!=NULL)
{
//如果下一塊內(nèi)存塊處于空閑狀態(tài),則進(jìn)行合并操作
if(pmcb->Next->Status==OS_MEMORY_STATUS_IDLE)
{
pmcb->Size += pmcb->Next->Size ;
pmcb->Next = pmcb->Next->Next ;
OS_MemoryIdleCount += pmcb->Size ;
}
}
}
#endif
這種分配策略雖然實現(xiàn)簡單,但是缺點就是容易產(chǎn)生內(nèi)存碎片,即隨著時間推移,可用內(nèi)存會越來越碎片化,最后導(dǎo)致想要申請足夠大的內(nèi)存塊都沒辦法。。。
/********************************************************************************/
至此,一個簡單的單片機(jī)使用的操作系統(tǒng)模型就算完成了,應(yīng)用在AVR單片機(jī)中,下面進(jìn)入測試階段:
因為還沒有完成線程通信模塊還搶占式算法,所以目前只能執(zhí)行輪詢多任務(wù)操作。我寫了一個測試程序,就是創(chuàng)建三個流水燈程序
(是不是覺得寫個操作系統(tǒng)就用來跑流水燈太浪費了,哈哈),讓它們同時閃,在PROTEUS中仿真查看

在AVR STUDIO5開發(fā)環(huán)境中編寫,代碼如下:
#include "includes.h"
#include "OS_core.h"
#define STACK_SIZE 80 //定義每個任務(wù)的人工堆棧大小
//定義三個任務(wù)各自的人工堆棧
uint8_t Test1Stack[STACK_SIZE];
uint8_t Test2Stack[STACK_SIZE];
uint8_t Test3Stack[STACK_SIZE];
//定義三個任務(wù)各自的線程控制塊
OS_TCB Task1;
OS_TCB Task2;
OS_TCB Task3;
//線程1讓PB口閃爍
void Test1(void *p)
{
uint8_t i;
DDRB=0XFF;
PORTB=0xff;
SREG|=0X80;
while(1)
{
for(i=0;i<8;i++) PORTB=1<<i;
}
}
//線程2讓PC口閃爍
void Test2(void *p)
{
uint8_t i;
DDRC=0xff;
PORTC=0XFF;
SREG|=0X80 ;
while(1)
{
for(i=0;i<8;i++) PORTC=1<<i;
}
}
//線程3讓PD口閃爍
void Test3(void *p)
{
uint8_t i;
DDRD=0XFF;
PORTD=0xff;
SREG|=0X80;
while(1)
{
for(i=0;i<8;i++) PORTD=1<<i;
}
}
//MAIN函數(shù)
int main(void)
{
uint8_t i = 0x77;
//初始化操作系統(tǒng)
OS_Init();
//初始化線程控制塊并創(chuàng)建任務(wù)
OS_ThreadInit(&Task1,Test1Stack,STACK_SIZE,5,0);
OS_ThreadCreate(&Task1,Test1,&i);
OS_ThreadInit(&Task3,Test3Stack,STACK_SIZE,5,0);
OS_ThreadCreate(&Task3,Test3,&i);
OS_ThreadInit(&Task2,Test2Stack,STACK_SIZE,5,0);
OS_ThreadCreate(&Task2,Test2,&i);
//初始化定時器
OS_TimerInit();
//啟動內(nèi)核
OS_KernelStart();
//正常的話程序永遠(yuǎn)不會執(zhí)行到這里!。
while(1);
}
OK,開始調(diào)試咯!打開PROTEUS連線,LOAD程序,然后運行。。。。

下次有空再作一些應(yīng)用范例來玩玩