專注電子技術學習與研究
當前位置:單片機教程網(wǎng) >> MCU設計實例 >> 瀏覽文章

DIY:給單片機寫個實時操作系統(tǒng)內核!

作者:佚名   來源:本站原創(chuàng)   點擊數(shù):  更新時間:2012年11月06日   【字體:
  為了進一步把單片機的潛能發(fā)揮到極限,我一直想寫個程序把單片機的所有資源都用光,但是如果依照單道程序順序執(zhí)行的方式,很難把MCU的CPU時間都充分利用,比如使用軟件延時函數(shù)實際上就是在無謂地消耗著CPU的時間什么事情都不做,因為CPU一直在循環(huán)等待著條件結束,這相當于函數(shù)被阻塞了。

 
為了更明顯地驗證這一點,你可以在WINDOWS下打開VC6.0或其他的C語言編譯器,寫段代碼如下:

 
#include <stdio.h>
void main(void) 
{while(1) ;}

 
意思是讓CPU不做事情在等待,你猜,這句代碼會消耗掉多少CPU時間?

 
答案會根據(jù)不同機型而不同,如果是單核CPU的話,這句話會消耗掉CPU接近100%的時間!如果是雙核CPU,則只消耗掉50%左右,因為這段代碼只運行在其中一個核,另外一個核還可以做別的事情,截圖如下:


 

 然后你可以測試下面這幾句代碼:

#include <stdio.h>

#include <windows.h>

void main(void)
{while(1)
Sleep(100);
}

 
這段代碼實際上也是什么都不做,它不斷地調用Sleep()函數(shù),讓它延時100毫秒再醒來,然后繼續(xù)睡覺。現(xiàn)在你可以再打開任務管理器看一下CPU時間用了多少,答案是基本不用CPU時間!!

 
為什么同樣地什么事情都不做,差別咋就這么大呢?這是因為使用了Sleep()這個函數(shù)是WINDOWS操作系統(tǒng)為你提供的,調用Sleep()之后 WINDOWS操作系統(tǒng)自動把你這個程序掛起了(就是暫時扔到一邊不管),然后讓CPU去執(zhí)行其他程序,等到時間到了,操作系統(tǒng)再把這段程序恢復繼續(xù)執(zhí)行,這樣的話CPU就可以得到充分地利用了,也就是說你可以在一塊CPU里面“同時”執(zhí)行多個任務而互不影響。ㄟ@里所說的“同時”并不是同時執(zhí)行,CPU每一時刻只能做一件事,但如果速度足夠快的話就可以讓人感到它是在同時執(zhí)行多項任務了)。是的,操作系統(tǒng)就是為了解決多任務執(zhí)行而生的。既然操作系統(tǒng)這么神奇,可不可以讓單片機也來爽一把呢?答案是肯定的。下面就介紹如何給單片機寫個操作系統(tǒng)!

 
/************************************************************************************/
工欲善其事,必先利其器,為了寫出操作系統(tǒng),必須得有一定的理論以及技術基礎,
單片機方面的可以http://www.torrancerestoration.com 了解到,從下面是所需的材料:
//1   C語言編程基礎        :三斤
//2   數(shù)據(jù)結構理論           :一斤八兩
//3   操作系統(tǒng)原理           :兩斤三兩八錢
//4   計算機組成原理以及單片機原理及應用  :兩斤半
//5   匯編語言編程基礎    :一斤四兩
//6   一份堅持的心            :多少斤自己掂量掂量,呵呵
/*************************************************************************************/

 
這么多怎么學?去哪學?下面是我個人推薦的書單,僅供參考:

 
1. C語言是必須要會的,而且要熟練,諸如”預編譯命令“你必須要懂,模塊化編程必須要熟悉,指針是C語言的一大精髓,在操作系統(tǒng)源碼里面指針是滿天飛的,所以得有足夠的理論基礎,推薦國外的《C Primer Plus》 美國 Stephen Prata著,里面講的內容由淺到深,語言引人入勝,大二開始看,現(xiàn)在還時不時地要回頭看,確實是一本不錯的好書:

 

 

另外,學會了C的基本語法之后你還得要會一點點編程技巧以及編程要注意的問題之類的,推薦有空的話多看看《C專家編程》和《C陷阱與缺陷》,這兩本書是C編程領域里面的經(jīng)典之作,相信看完你的功力會大有長進,但是還是要以經(jīng)常敲代碼為主:

 

 

 

2. 操作系統(tǒng)里面的數(shù)據(jù)組織形式都是以數(shù)據(jù)結構的理論為基礎的,所以你得懂得數(shù)據(jù)結構才能看懂里面的含義,但也不要求把數(shù)據(jù)結構全精通,推薦嚴蔚敏版本的《數(shù)據(jù)結構》,不過里面的算法都是用偽代碼寫出來的:

 

 

3. 有了編程基礎之后你還必須要懂得操作系統(tǒng)的基本原理,比如任務之間是怎么切換的,內存是怎么管理的都得懂,推薦《操作系統(tǒng)-精髓與設計原理》

 

 

 

4. 匯編語言。為什么要學匯編?可能有些人會學得匯編難理解,而且現(xiàn)在C語言已經(jīng)可以很方便地編程了,所以不想學匯編,其實C語言再怎么方便強大,最后還是要通過編譯器轉換為匯編語言再由匯編轉換為機器碼,才能在機器中執(zhí)行?梢哉f,掌握了匯編之后你一定會對”代碼是怎么在CPU里面執(zhí)行的“這個哲學命題有進一步的了解。另外,不學匯編你還真寫不出一個操作系統(tǒng)內核來,因為操作系統(tǒng)的最低層是要直接操作CPU寄存器的,C語言無法實現(xiàn),只能用匯編寫出來,再由上層的C語言調用。匯編的書很多,這里就不介紹了,找一本去狂敲上個把星期就大概掌握了。

 


 

5. 另外你還要懂得計算機原理以及單片機,其實單片機就是一臺閹割版的計算機,你得對CPU寄存器,數(shù)據(jù)總線,地址總線,以及執(zhí)行方式這些有一定的了解才行,這方面的書也挺多的,不過介紹兩本個人覺得寫得挺好的書供課外閑讀,《編程卓越之道》1、2卷,這本書大體上介紹了高級語言是怎么樣在CPU里面執(zhí)行的,另外也對CPU內部結構做了一些介紹,比那些課內教材寫得好,有空可以去看一下。

 

 

 

 


 

 

最后介紹一本《嵌入式實時操作系統(tǒng)UCOS II》,這本書介紹了UCOS II這個操作系統(tǒng)的內部源代碼以及實現(xiàn)原理,我就是從這本書中學到了怎樣寫一個可以用的操作系統(tǒng)內核。

 

 

書單推薦完畢,下面進入重點~~~~~~~~~~~~~~~~~

 
/**************************************************************************************/
什么是操作系統(tǒng)?其實就是一個程序, 這個程序可以控制計算機的所有資源,對資源進行分配,包括CPU時間,內存,IO端口等,按一定規(guī)則分配給所需要的進程(進程?也就是一個程序,可以單獨執(zhí)行),并且自動控制讓CPU可以執(zhí)行多個互不相關的任務,按照書中的介紹,一個操作系統(tǒng)需要具備四個要素:進程調度、內存管理、IO管理、文件管理。

 
那怎么樣可以讓CPU同時執(zhí)行多個任務呢?首先想象一下如果讓CPU執(zhí)行單道程序,它會從MAIN函數(shù)開始一直順序地執(zhí)行下去,CPU里面有一個叫PC的寄存器,也就是程序計數(shù)器,它永遠指向下一條要執(zhí)行的指令的存放地址,因為大多數(shù)情況下指令都是逐條執(zhí)行的,所以PC寄存器也只是簡單地加一,所以大家都叫它”程序計數(shù)器“,從PC寄存器的特點也許我們可以做點文章?比如人為地讓PC寄存器指到另外一段程序的入口地址,那CPU不就自動地跑到另一段程序了么?哈哈。假如我們可以這樣做,那沒錯,CPU確定是跑到別人的領地去執(zhí)行代碼了,問題是:怎么樣讓它回來繼續(xù)執(zhí)行?換句話說,PC寄存器改變之后CPU 已經(jīng)不知道剛剛這段程序執(zhí)行到哪里了,亦即跑不回來了,就像斷了線的風箏。呃。。這問題麻煩。。解決了這個問題就似乎有點苗頭了。。

 
好吧,我們來看看有一個很相似的問題,就是單片機在執(zhí)行代碼的時候,突然有一個中斷信號過來了,單片機馬上就屁顛屁顛地跑到中斷服務程序里面去執(zhí)行了,執(zhí)行完畢之后,奇怪!它怎么還記得跑回來原來的地方。。?OH NO .它是怎么辦到的。其實這里還要介紹另外一個寄存器叫SP的,即:STACK POINTER堆棧指針,這個指針指向一個內存的地址,里面存放了一些數(shù)據(jù)。首先,單片機遇到中斷信號的時候,它就把當前的PC寄存器的值保存到SP所指的地址,這就相當于它記住了當前執(zhí)行的地方,叫做斷點保護,然后PC寄存器就指向中斷服務程序的地址,下一個時刻CPU就自動執(zhí)行中斷服務程序里面的代碼了,執(zhí)行完畢之后中斷服務程序調用了一個指令:RETI,這條指令叫返回指令,在函數(shù)結束之后調用,它會自動從SP指針指向的地址把值取出來放到PC寄存器里面,然后CPU就會自動回到之前斷掉的地方繼續(xù)執(zhí)行了!基于這個原理,我們可以回到上面的問題:首先,讓CPU把當前的PC保存起來,然后把PC指向別段程序地址,CPU就跑到別人的領地去執(zhí)行了,執(zhí)行完了之后我們可以把SP指向的內容放回PC,這樣調用RET指令之后,CPU就會回到原來的地方繼續(xù)執(zhí)行了!!貌似這個問題完美地解決了!

 
可是還有一個關鍵的問題:CPU在執(zhí)行當前代碼的時候 CPU里面所有的寄存器都保存的當前這個程序所用到的值,比如做加法的時候用到PSW寄存器的進位標志位,如果此時切換到別的任務,那再回到當前程序的時候,這些值都會被改變,CPU會陷入混亂然后直接跑飛!!解決這問題同樣要靠SP同學,在切換任務的時候我們把所有寄存器依次入到SP指向的地址,稱為入棧操作,每次入棧SP指針的值都會加一或者減一,視不同CPU而定。而要恢復的時候,就從SP指向的地址依次把值取出來放回原來的地方,稱為彈棧操作。最后才彈出地址到PC寄存器,下一時刻,CPU自動跑到原來的地址繼續(xù)執(zhí)行,從CPU的角度看就像沒有發(fā)生任務切換一樣,一切依舊,繼續(xù)工作。如果CPU的執(zhí)行速度夠快,切換速度也夠快,這樣就可以給人感覺CPU同時在執(zhí)行很多任務,這就是操作系統(tǒng)里面最基本的原理。

 
SO,解釋完原理,我們首先來就來實現(xiàn)簡單的任務切換,這里的難點就在于:執(zhí)行這一動作必須要操作CPU的寄存器,而C語言是無法實現(xiàn)的,這就是為什么要用到匯編的原因了,所有操作系統(tǒng)的最底層代碼都是用匯編語言實現(xiàn)的,否則根本無法實現(xiàn)任務切換。下面要介紹匯編里面的幾條相關指令。PS:雖然每種CPU的匯編都不同,但是基本原理還是相通的。
第一條:CALL。函數(shù)調用指令,當我們要調用一個函數(shù)的時候,就會用到CALL這條指令,它執(zhí)行再從個動作,第一,先把當前的PC值保存起來,即現(xiàn)場保護,第二,把要調用的函數(shù)的入口地址送到PC,這樣,在下一時刻到來的時候,CPU就自動跳轉到特定的函數(shù)入口地址開始執(zhí)行了。
第二條:RET/RETI。當一個函數(shù)執(zhí)行完畢的時候,需要返回到原來執(zhí)行的地方,這時候就要調用 RET指令(在中斷函數(shù)中返回的時候調用RETI指令)。它把SP指向的數(shù)據(jù),即上一次調用CALL時保存的那個地址原來到PC,這樣,當下一時刻到來的時候,CPU就會跳回到原來的地方了。實際上函數(shù)調用過程就是這樣的,所以有時候一些簡單簡短的函數(shù)寧愿用#define宏定義寫出來,因為這樣寫出來就不用使用調用/返回過程,節(jié)省了時間。
第三/四條:PUSH/POP。這兩個指令是兩兄弟,即入棧及出棧。關于堆棧的特性說明一下:堆棧這種結構的特性就是后進先出,就像疊盤子一樣,最后疊上去的盤子會被最先取出,這種原理非常好用,想象一下函數(shù)嵌套的時候發(fā)生的一切,就是利用到這種思路。PUSH指令用到把寄存器的值保存起來,它會把值到保存到SP指針所指的地方。POP指令則把數(shù)據(jù)從SP所指的地址恢復到原來的寄存器中。

 
用這幾條指令,我們就可以寫出一個任務切換函數(shù)了,不過寫之前還要說明一下什么叫人工堆棧。其實上,一個程序在執(zhí)行的時候,它會用到一塊內存空間用于保存各種變量,比如調用函數(shù)的時候這塊地方會用于保存地址以及寄存器,而在執(zhí)行一些復雜算法的時候,如果CPU的寄存器已經(jīng)用完了,這塊地方也會作為臨時中間變量的存放區(qū),另外,在向一個函數(shù)傳遞參數(shù)的時候,比如:printf(a,b,c,d,e....),如果參數(shù)過多,多余的參數(shù)也會先存放到這塊地方。所以說,這塊地方就像是這個程序的倉庫一樣,存放著要用的東西。如果是在單道程序中,顯然這樣用沒問題,但是如果是多道程序的話,問題就來了,因為如果所有任務共用那塊區(qū)域,那舊任務保存的東西就會被新任務所沖掉,CPU一下子就瘋掉了。。解決的辦法就是:每個任務都給它提供一塊專用的區(qū)域,這塊專用區(qū)域就叫人工堆棧,每個任務都不一樣,保證了不會相互沖突。

 
PS:因為51單片機的內存太小,基本無法實現(xiàn)多任務,實現(xiàn)了也不實用,所以硬件平臺我選用了AVR單片機ATMEGA16,有1KB內存,應該夠用了,花了兩天時間把AVR的匯編指令看了一遍

 

 

首先,當需要切換任務的時候,要先把當前的所有寄存器全部入棧,在AVR單片機中有32個通用寄存器R0-R31,還有PC指針,PSW程序狀態(tà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"    ); }
入完棧完接下來要保護當前程序的SP指針,以便下次它要返回的時候能找到該人工堆棧的地址:
OS_LastThread->ThreadStackTop=(OS_DataType_ThreadStack *)SP;
 
這一句用C語言就可以實現(xiàn)了。
接下來關于當前這段程序的現(xiàn)場算是保護好了,然后找到要切換到的任務的人工堆棧地址,把它賦給SP指針,如下:
SP=(uint16_t)OS_CurrentThread->ThreadStackTop;
 
出棧跟入棧的語法差不多,只是出棧順序要相反:
POP_REG();
接下來,要調用一條很重要的指令了!。〈肆钜怀,CPU就乖乖地切換任務了!
_asm("RET\n\t");
 
調用返回指令,它就從SP里面取出函數(shù)地址放到PC,注意他取出的是剛剛放入SP指向地址的函數(shù)入口,所以它會返回到新任務執(zhí)行。
就這樣,一個操作系統(tǒng)里面最核心的”任務調度器“的模型就這樣簡單地實現(xiàn)了,操作系統(tǒng)里面所作的跟任務切換有關的事情到最后都要調用到這個任務調度器,現(xiàn)在我們實現(xiàn)調度器了,相當于成功了1/3,接下來的事情就是考慮在什么情況下調用這個調度器。

 
調度策略:實現(xiàn)了調度,還要繼續(xù)考慮調度策略,就是什么情況下需要調度哪些任務。調度策略分很多種,有興趣的可以去看那本《操作系統(tǒng)原理》,在我的源代碼里面使用了”搶占式優(yōu)先級調度+同一優(yōu)先級下時間片輪詢調度“的方法。
所謂搶占式優(yōu)先級調度是一種實時調度的方法,在實時操作系統(tǒng)中常用,這種方法的原理就是:操作系統(tǒng)在任何時候都要保證擁有最高優(yōu)先級的那個任務處于運行態(tài),比如此記在運行著優(yōu)先級為2的任務,因為一些信號到達,優(yōu)先級為1的那個任務解除了阻塞,處于就緒態(tài),這時操作系統(tǒng)就必須馬上停止任務2,切換到任務1,切換的這段時間需要越短越好。
而時間片輪詢即是讓每個任務都處于平等地位,然后給每個任務相同的時間片,當一個任務的運行時間用完了,操作系統(tǒng)就馬上切換給下一個需要執(zhí)行的任務,這種方法的實時性不高,但它確保了每個任務都有相同的執(zhí)行時間。
我把這兩種方法結合起來,首先設定了8個優(yōu)先級組,每個優(yōu)先級組下面都用單向鏈表把具有相同優(yōu)先級的任務連接起來。這樣的話首先操作系統(tǒng)會查找最高優(yōu)先級的那組,然后在組里面輪流執(zhí)行所有任務(和UCOS II相比這種做法更具有靈活性,因為UCOS II只有搶占式調度,這是UCOS II的硬傷。。)。我聲明了一個任務結構體稱為線程控制塊,把關于該任務的所有狀態(tài)都放在一起:
/** 
*  @結構體聲明
*  @名稱        : 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 ;
*                                   任務優(yōu)先級
*                          12.OS_DataType_TimeDelay           TimeDelay ;
*                                  任務延時時間
*  @描述        : 定義線程控制塊的成員
*  @建立時間    : 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)建任務,任務被創(chuàng)建之后才可以得到執(zhí)行,使用如下函數(shù):
/**
*  @名稱:線程創(chuàng)建函數(shù)
*  @輸入?yún)?shù):1.pOS_TCB ThreadControlBlock      線程控制塊結構體指針
*          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)
關于創(chuàng)建任務的大致描述就是:填定線程控制塊,把線程控制塊鏈到單向鏈表中,設置人工堆棧,細節(jié)很多,就不一一贅述了。

 
當前版本只實現(xiàn)了輪詢調度,還沒加上搶占調度,使用下面的函數(shù)就可以啟動操作系統(tǒng)開始多線程任務!
/** 
*  @名稱        : 實時內核引發(fā)函數(shù)
*  @版本        : V 0.0
*  @輸入?yún)?shù)    : 無
*  @輸出參數(shù)    : 無
*  @描述        : 在主函數(shù)中用于啟動,調用該函數(shù)后不會返回,直接切換到最高優(yōu)先級任務開始執(zhí)行
*  @建立時間    : 2011-11-15
*  @最近修改時間: 2011-11-15
*/
void OS_KernelStart(void)
{
OS_Status = OS_RUNNING ;    //把內核狀態(tài)設置為運行態(tài)

 
                                                         //取得第一個需要運行的任務
OS_CurrentThread = OS_TCB_PriorityGroup[pgm_read_byte(ThreadSearchTab + OS_PreemptionPriority)].OS_TCB_Current;
OS_LastThread = NULL ;         
                                                         //SP指針指向該任務的棧頂
SP = (uint16_t)OS_CurrentThread->ThreadStackTop ;
                                                        //使用出棧操作
POP_REG();
                                                        //調用RET,調用之后開始執(zhí)行任務,不會再返回到這里
_asm("RET\n\t");
}

 
怎樣實現(xiàn)時間片?答案是用定時器定時,每次定時器產(chǎn)生中斷的時候就轉換一次任務,時基可以自己確定,一般來說時基越小的話會讓CPU花很多時間在切換任務上,降低了效率,時基大的話又使時間粒度變粗,會使一些程序得不到及時的執(zhí)行。我設定了每10MS中斷一次,就是說每一輪中每個線程都有10MS的執(zhí)行時間。具體算法不再贅述。

 
內存管理策略
接下來要考慮怎樣管理內存了!在PC里面編程的時候,如果需要開辟一個內存空間,我們可以很容易地調用malloc()和free()來完成,但是在單片機里面卻行不通,因為要實現(xiàn)這兩個函數(shù)背后需要完成很多算法支持,從速度和空間上單片機都做不到。
在單片機里面如果你需要開辟內存空間,你只有在編譯的時候就先定義好變量,無法動態(tài)申請,但是我們可以設計一個簡單的內存管理策略來實現(xiàn)這種動態(tài)申請!原理就是在編譯的時候先向編譯器要一塊足夠大的內存并且聲明為靜態(tài),然后把這塊空間交給內存管理模塊來調用,內存管理模塊負責分配這塊內存,當有任務要向它申請內存的時候它就從里面拿出一塊交給任務,而任務要釋放的時候就把該內存空間交給內存管理模塊來實現(xiàn)。
關于內存管理也有很多種策略,在這里就不一一述說了,我在源代碼里面使用了一種簡單的隨機分配的方法,即有線程申請的時候就從當前內存塊的可用空間里拿出一塊來,然后在內存頭加上一個專用的結構體,把每個內存塊都鏈接起來,這樣便于管理。當線程釋放內存的時候,就把內存返回到內存空間并跟其他空間的內存塊合并起來等待線程再次調用。
/** 
*  @名稱           : 內存塊申請函數(shù)
*  @版本           : V 0.0
*  @輸入?yún)?shù)    : 1. OS_DataType_MemorySize MemorySize
                              需要申請內存塊的大小
*  @輸出參數(shù)    : 1. void *
                             若申請成功,則返回可使用內存塊首地址,否則返回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 ;
 
//進入內存搜索算法
while(1)
{
//檢測該內存塊是否存在
if(pmcb==NULL)
{
return NULL ;
}
   
//如果存在則檢測該內存塊的使用狀態(tài)
else if( (pmcb->Status==OS_MEMORY_STATUS_IDLE) && (pmcb->Size >= MemorySize) )
{
//如果可用內存塊大小剛好等于需要申請的大小
//則立即分配
if(pmcb->Size == MemorySize)
{
pmcb->Status=OS_MEMORY_STATUS_USING ;
OS_MemoryIdleCount -= MemorySize ;
return (OS_DataType_Memory *)pmcb + OS_MEMORY_SIZE ;
}
 
//若可用內存塊大小大于需要申請的大小
//則進行分割操作
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

 
內存釋放函數(shù):
/** 
*  @名稱           : 內存塊釋放函數(shù)
*  @版本           : V 0.0
*  @輸入?yún)?shù)    : 1. OS_DataType_MemorySize MemorySize
                              需要申請內存塊的大小
*  @輸出參數(shù)    : 1. void *
                              若申請成功,則返回可使用內存塊首地址,否則返回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 );
 
//將當前內存塊設置為空閑狀態(tài)
pmcb->Status=OS_MEMORY_STATUS_IDLE ;
OS_MemoryIdleCount += pmcb->Size ;
 
//如果存在上一塊內存塊,則進入判斷
if(pmcb->Front!=NULL)
{
//如果上一塊內存塊處于空閑狀態(tài),則進行合并操作
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 ;
}
}
//如果存在下一塊內存塊,則進入判斷
if(pmcb->Next!=NULL)
{
//如果下一塊內存塊處于空閑狀態(tài),則進行合并操作
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)生內存碎片,即隨著時間推移,可用內存會越來越碎片化,最后導致想要申請足夠大的內存塊都沒辦法。。。

 
/********************************************************************************/
至此,一個簡單的單片機使用的操作系統(tǒng)模型就算完成了,應用在AVR單片機中,下面進入測試階段:
因為還沒有完成線程通信模塊還搶占式算法,所以目前只能執(zhí)行輪詢多任務操作。我寫了一個測試程序,就是創(chuàng)建三個流水燈程序(是不是覺得寫個操作系統(tǒng)就用來跑流水燈太浪費了,哈哈),讓它們同時閃,在PROTEUS中仿真查看
在AVR STUDIO5開發(fā)環(huán)境中編寫,代碼如下:
#include "includes.h"
#include "OS_core.h"

 
#define STACK_SIZE 80  //定義每個任務的人工堆棧大小

 
//定義三個任務各自的人工堆棧
uint8_t Test1Stack[STACK_SIZE];
uint8_t Test2Stack[STACK_SIZE];
uint8_t Test3Stack[STACK_SIZE];

 
//定義三個任務各自的線程控制塊
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)建任務
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();

 
      //啟動內核 
OS_KernelStart();

 
      //正常的話程序永遠不會執(zhí)行到這里。!
while(1);
}

 

 
OK,開始調試咯!打開PROTEUS連線,LOAD程序,然后運行。。。。

 

 

成功同時運行三個流水燈程序!太棒了!接下來在這個內核的支持下你就可以創(chuàng)作你的應用程序了,使用內核提供的線程創(chuàng)建函數(shù)你可以創(chuàng)建N多個線程,當然了,必須在內存可接受的范圍內。利用內存分配函數(shù)你可以動態(tài)申請和釋放內存了。再也不用為DELAY()這種浪費CPU效率的作法郁悶很久了。
 
上面所說的所有代碼都開源,想看的同學發(fā)郵件到我EMAIL: wfm2012@126.com 索要
下次有空再作一些應用范例來玩玩
關閉窗口

相關文章