實現一個最簡單的嵌入式作業系統

來源:互聯網
上載者:User
實現一個最簡單的嵌入式作業系統(一)  

實現一個什麼都不能做的嵌入式作業系統

1.首先確定CPU,在這裡為了簡單,就選用嵌入式的CPU,比如ARM系列,之所以用RISC(簡單指令集)
類型的CPU,其方便之處是沒有實模式與保護模式之分,採用線性統一定址,也就是不需要進行段
頁式記憶體管理,還有就是晶片內部整合了一些常用外設控制器,比如乙太網路卡,串口等等,不需要像
在PC機的主板上那麼多外設晶片
2.確定要實現的模組和功能,為了簡單,只實現多任務調度(但有限制,比如最多不超過10),實
現中斷處理(不支援中斷優先順序),不進行動態SHELL互動,不實現動態模組載入,不實現fork之類
的動態進程派生和載入(也就是說要想在你的作業系統上加入使用者程式,只能靜態編譯進核心中;不
支援檔案系統,不支援網路,不支援PCI,USB,磁碟等外設(除了支援串口,呵呵,串口最簡單嘛),
不支援虛擬記憶體管理(也就是說多任務中的每個進程都可以訪問到任何地址,這樣做的話,一個程式
死了,那麼這個作業系統也就玩完了)
3.確定要使用的編譯器,這裡採用GCC,檔案採用ELF格式,當然,最終的檔案就是BIN格式,GCC和
LINUX有著緊密的聯絡,自己的作業系統,需要C庫支援和系統調用支援,所以需要自己去裁剪C庫,
自己去實現系統調用
4.實現步驟:首先是CPU選型,交叉編譯環境的建立,然後就是寫BOOTLOADER,寫作業系統

實現一個最簡單的嵌入式作業系統(二)  

如何?BOOTLOADER

1.之所以要實現一個專用的BOOTLOADER,一是為了更好的移植和自身的升級,二是為了方便作業系統的調試,當然,你完全可以將這部分所要實現的與作業系統相關的功能整合到作業系統中去
2.確定一個簡單的BOOTLOADER所要完成的功能:我們這裡只需要完成兩個主要功能,一是將作業系統載入到記憶體中去運行,二是將自己和作業系統核心固化到ROM儲存區(這裡的ROM可以是很多裝置,比如嵌入式晶片中的FLASH,PC機上的磁碟片,隨身碟,硬碟等)

3.BOOTLOADER的編寫:
第一步:要進行相關硬體的初使化,比如在at91rm9200這塊嵌入式板子上(以後都使用這一款晶片,主要是我對這款晶片比較熟悉,嘿嘿),大概要做接下來的幾方面的工作,其一:將CPU模式切換進系統模式,關閉系統中斷,關閉看門狗,根據具體情況進行記憶體區域對應,初始化記憶體控制區,包括所使用的記憶體條的相關參數,重新整理頻率等,其二:設定系統運行頻率,包括使用外部晶振,設定
CPU頻率,設定匯流排頻率,設定外部裝置所採用的頻率等。其三:設定系統中斷相關,包括定時器中斷,是否使用FIQ中斷,外部中斷等,還有就是中斷優先順序設定,這裡只實現兩個優先順序,只有時鐘中斷高一級,其它都一樣,而中斷向量初始化時都將這些中斷向量指向0x18處,並關閉這裡的所有中斷,如果板子還接有諸如FLASH裝置的話,還需要設定諸如FLASH相關操制寄存器,其四:需要關閉CACHE,到此為止,晶片相關內容就完成初始化了

第二步:中斷向量表,ARM的中斷與PC機晶片的中斷向量表有一點差異,嵌入式裝置為了簡單,當發生中斷時,由CPU直接跳入由0x0開始的一部分地區(ARM晶片自身決定了它中斷時就會跳入0x0開始的一片地區內,具體跳到哪個地址是由中斷的模式決定的,一般用到的就是複位中斷,FIQ,IRQ中斷,SWI中斷,指令異常中斷,資料異常中斷,預取指令異常中斷),而當CPU進入相應的由0x0開始的向量表中時,這就需要使用者自己編程接管中斷處理常式了,這就是需要使用者自己編寫中斷向量表,中斷向量表裡存放的就是一些跳轉指令,比如當CPU發生一個IRQ中斷時,就會自動跳入到0x18處,這裡就是使用者自己編寫的一個跳轉指令,假如使用者在此編寫了一條跳轉到0x20010000處的指令,那麼這個地址就是一個總的IRQ中斷處理入口,一個CPU可能有多個IRQ中斷,在這個總的入口處如何區分不同的中斷呢?就由使用者編程來決定了,具體實現請參見以後相關部分,中斷向量表的一般用一個vector.S檔案,當然,如何命名那是你自己的喜愛,但有一點需要聲明,那就是在連結時一定要將它定位在0x0處

第三步:設定堆棧,一般使用三個棧,一個是IRQ棧,一個是系統模式下的棧(系統模式下和使用者模式共用寄存器和記憶體空間,這主要是為了簡單),設定棧的目的主要是為了進行函數調用和局部變數的存放,不可能全用彙編,也不可能不用局部變數

第四步:將自己以後的程式碼片段和資料區段全部拷貝至記憶體,並將BSS段清零

第五步:進行串口的初始化(主要是為了與使用者互動,進行與PC機的檔案傳輸),FLASH的初始化這裡在FLASH中存放BOOT和核心),FLASH驅動的編寫(這裡的驅動有別於平常所說的驅動,由於FLASH不像SDRAM,只要設定了相關控制器之後就可以直接讀寫指定地址的資料,對FLASH的寫操作是一塊一塊資料進行,而不是一個位元組一個位元組地寫,具體請查閱相關資料)
第六步:等待一定的秒數,來接收使用者進行輸入,如果在指定的秒數內使用者未輸入任何字元,那麼
BOOT就開始在FLASH中的指定位置(可以由自己指定,這麼做主要是為了簡單)讀取核心的所有資料到記憶體中(具體是記憶體中的什麼位置由自己指定,也可以採用LINUX之類的做法,就是在記憶體的起始位置加上一個0x8000處),將跳轉到核心的第一條代碼處);如果使用者在指定的秒數內鍵入了字元(這主要是為了方便開發,如果開發定型之後完全可以不要這段代碼),那麼就在串口與使用者進行互動,接受使用者在串口輸入的命令,比如使用者要求下載檔案在FLASH中指定的位置等,具體內容可參考U-BOOT之類的開源項目到這裡為止,BOOT部分已完成,這個BOOT非常簡單,僅僅只是將PC機上傳下來的檔案固化到FLASH中,然後再將FLASH中的作業系統核心部分載入進記憶體中,並將CPU的控制權交給作業系統,下一頁開始講解如何寫一個最簡單的作業系統,呵,到現在才開始切入正題呢!!!!

實現一個最簡單的嵌入式作業系統(三)  

如何?一個最簡單的作業系統

這裡為了簡單,就不考慮可移植性開求,不從BOOT部分來接收參數,也不對硬體進行檢測,
也不需要進行DATA段,程式碼片段的重定位。我只是讀了LINUX核心相關部分,並未自己去實現
一個作業系統,所以我以下所說的只是概念性的東西:

1.接管系統的中斷處理,由於BOOT部分的代碼決定了那個中斷向量表,從而決定了系統中斷
之後進入的記憶體位置,但BOOT並不知道作業系統的中斷處理函數位置所在啊,怎麼辦呢?
有幾種方法,其一是:如果你的板子可以重新對應地址,也就是可以將記憶體條所在的位置
重新對應成0x0開始,那麼在連結核心的時候,就將作業系統自己的中斷向量表定位在0x0處
並且在BOOTLOADER引導結束時就完成映射操作,並讓CPU跳轉到0x0處執行;如果沒有重映
射功能,我就不曉得怎麼辦了,不過我想到一個折衷的辦法,就是在BOOTLOADER啟動完成
時(也就是將CPU控制權交給作業系統核心時),重新改寫FLASH的0x0地區,就是將操作
系統的核心的中斷向量表寫入FLASH區的0x0處,比如,當一個IRQ發生時,CPU決定了會
跳入0x18(假設這裡FLASH佔用地址匯流排0x0至0x0fffffff,記憶體佔用0x20000000至0x2fffffff)
,而BOOTLOADER在最後將0x18處的代碼修改成了0x20000000加上0x18的地址處的代碼,而這個
地址就是核心的中斷向量表中的相關跳轉指令,就相當於跳轉進了核心所關聯的IRQ處理函數
的地址上去執行中斷處理函數了,而這樣的不好之處在於:當系統重新上電之後,BOOT的
中斷向量表已經被修改,除非BOOT本身不使用中斷,呵,在這樣簡單的系統中,BOOT是不
需要中斷功能的

2.這裡為了簡單,所以沒有使用分頁記憶體管理,就不需要建立頁表等操作,直接進行操作
系統的堆棧設定,同BOOT一樣的設定過程一樣,接著就進行BSS段清零操作,這裡的BSS段
是指作業系統自身的BSS段,與BOOT的BSS段是同一個含義只是用在了不同的地方了,接著
就跳入了MAIN函數

3.為了最大可能的簡單,採用靜態建立任務結構數組,比如只建立十個任務,那麼首先要
為這十個任務結構分配段記憶體,可以在堆上分配(這個分配的記憶體直到作業系統結束才會
被釋放,當然也可以指定一片作業系統的其它地方都用不到的記憶體地區,不過這樣寫的話
就有點外行的味道了,而符務結構數組的指標卻是全域變數,存放在BSS段或者DATA段),
由於在上一步中已經分配了一個系統堆棧,那麼我們這十個任務就分享這總體的堆棧地區
這裡的重點就是如果定義每個任務結構數組裡面的結構,可以參照LINUX的相關部分設計

4.中斷處理:在第一步中已經確定了CPU進行相關的幾類型的中斷跳轉地址,而相同類型
的中斷卻只有一個入口地址,這裡的中斷處理就會完成以幾個動作:
其一:入棧操作,包括所有寄存器入棧,至於這個棧,就是在第二步中所設定的IRQ棧,
其二:屏掉所有中斷,呵,這裡為了簡單起見,所以在處理中斷時不允許再次發生中斷
其三:讀取中斷相關的寄存器,判別是發生了什麼中斷,以至於跳進相關的中斷處理函
數中去執行(在這裡只包括兩種中斷,一是時鐘中斷,另一個是SWI中斷,也就是所謂
的系統調用時需要用到的)
其四:等待中斷處理完成,然後就開啟中斷並出棧,恢複現場,將CPU控制權交給被中斷
的代碼處
注意:
其一:在MIAN中必須首先確定整個系統有哪些需要處理的中斷,也就是有哪些中斷處理
函數,然後才編寫這裡的中斷處理函數
其二:本作業系統不處理虛擬記憶體,其至連CPU異常都不處理(一切都為了簡單),一旦
發生異常,系統就死機

5.對TIMER的實現,首先確定時間片,為了讓系統更穩定,而且我們不需要即時功能,盡
可能讓時間片設定長一點,比如我們讓一個任務運行20個時鐘滴答數,然後應根據系統
頻率來確定每個系統滴答所佔用的毫秒,這裡使用5毫秒讓系統定時器中斷一次,那麼就
需要寫時鐘寄存器,具體參閱晶片資料,計算下來,一個任務最大可能連續運行100毫秒
,注意:我們的作業系統不支援核心搶佔,同時只支援兩級中斷優先順序,就是只有時鐘
中斷的優先順序高一點,其它的優先順序都低一級,但是在中斷處理一節中卻屏掉了這個功能
因為一進入中斷處理,就禁止中斷,所以不管其它中斷優先順序有多高都沒有用的,這樣做
優點是簡單了,但不好之處顯而易見,特別在相關中斷處理函數如果進入了死迴圈,那麼
整個系統就死了,而且時間片也變得不準確了,反正都不用即時,也不需要即時鐘支援嘛
至於中斷優先順序設定請參閱晶片資料

6.進程調度的實現,也就是do_timer函數(時鐘中斷處理函數),有一個全域變數指標,
指向的就是當前任務結構數組(或者鏈表),當時鐘中斷時,就進入此函數中,首先判斷
任務結構體中的時間片是否用完,如未用完,就減一,然後退出中斷,讓CPU繼續運行當
前的任結構,若用完了時間片,就重設時間片,並重新尋找任何結構數組中的下一個等待
啟動並執行任務,若找到了,就切換至新的任務,至於如何切換,請見下一頁描述,如果未找
到就切換到IDLE任務(類似於LINUX,呵呵,所有的處理就是模仿LINUX,由於本人水平太
差,所就不能自創一招),注意:為了簡單,所以沒有實現任務優先順序,也未實現任務
休眠等,也就是說只要靜態地決定了有十個任務,這十個任務就按先後順序一個一個執行
而且每個任務都不允許結束,就是說在每個進程中的最後一句代碼都必須用死迴圈,不然
的話系統就跑飛了),還有一點,進程不支援訊號,沒有休眠與喚醒操作,這個CPU就是
不停地在運行,呵呵,反正CPU又不是人,所以不需要人權的哈!!!這種調度是不是簡
單得不能再簡單了?????!!!!

7.串口不使用中斷,這就是最大可能的降低難度,串口使用論詢的方式來實現讀寫(當
然是阻塞的方式了哦,而且只有寫,不允許讀,因為讀的時候需要涉及到採用中斷方式,
因為輪詢方式有個不好的地方,那就是正在讀的時候,這裡有可能當前進程的時間片用
完了,系統切換到另一個進程,這裡你在PC機的串口輸入的資料就丟棄了,唉,又是為
了簡單嘛)

8,最後一步就是MIAN函數的最後一部分,將本進程當作IDLE進程(相當於修改任務結構
數組中的資料),開啟中斷,將當前進程加入一段死迴圈,以免它退出去。

9.編譯你的BOOTLOADER,KERNEL,並燒寫至FLASH,反覆調試

10.至此將你的at91rm9200(或者是其它相類似的晶片)的串口接上PC機,開啟超級終端,
開啟板子電源,說不定你的作業系統就列印出了"hello,world"了!!!一個最簡單的操作
系統就出來了

下一頁是具體的功能模組實現

實現一個最簡單的嵌入式作業系統(四)  

任務結構數組(或鏈表)的實現

我們的任務結構就採用鏈表形式吧,但其長度是限定了的,頭指標是一個全域指標變數(
指標變數是一個無符號整型指標,其指標本身所在的地址是在BSS段,但其指向的內容是分
配在堆上的一片記憶體),分配核心記憶體的函數就用kmalloc吧,kmalloc函數需要自己編寫
呵,為了簡單,這個函數只接受一個參數,就是所需分配大小,這個函數做得很簡單,首先
有一個全域針指,它在初始化時指向了整個堆的起始位置,並且固定大小,就是所謂的核心
堆棧,在核心堆棧之後就是使用者堆棧,由於總共有十個任務,當然不包括核心本身的任務,
所以整個堆棧就平均分成十一部分,注意:在所有任務初始化完成之後,還有一個步驟就是
將核心這個任務移到使用者態,相當於要將自己的任務結構的堆棧指標修改一下就行了),
判斷大小是否超出了核心堆的可分配範圍,還有一點,需要維護核心堆和其它任務的堆,
需要進行分塊,並且有一個全域的記憶體使用量標識,就用數組吧,簡單,0表示相應的記憶體
部分未佔用,1就表示佔用,對應的kfree就相當於把標誌置0),
對於記憶體的維護,比較複雜,為了簡單,就定為4K,並且不能進行大於四K的記憶體申請,因為
大於4K之後,由於沒有虛擬位址的概念,就不能實現堆上的連續分配地址,當然在棧上分配
是可以大於4K的,棧是由編譯器和CPU所決定了的

任務結構包括:
1.所剩的時間片
2.本任務所指向的程式碼片段記憶體位址,這裡也就是函數入口地址
3.本任務所指向的資料區段地址,這裡的資料區段被包含進了整個核心中,所以並沒有用,作為保留
4.本任務的函數體是否存在,也就是否會被調度
5.本任務所使用的棧指標
6.本任務所使用的堆積指標
7.本任務的標識,用0代表是IDLE,1代表是其它進程
8.所有寄存器的值
9.當前PC值,初始化時被置成了函數入口地址

首先講解一下任務數組結構的初始化:
將先定義一個全域指標,然後將此指標強制轉換為一個任務結構指標,並通過kmalloc函在核心所
佔用的堆(前而講過核心的堆的起始就是整個堆的起始)上去分配十個任務結構所佔的記憶體,這裡
是絕不會超過4K的並且為這十個任務結構賦值,將第一個任務置為IDLE,時間片為20,程式碼片段記憶體位址為main函數的的地址,資料區段地址忽略,函數體存在,可以被調度,棧指標指向的位置根據以下來計算:
假定每個給每個任務可使用的堆棧設定為64K,而整個堆的起始位置是0x20030000,那麼第一個堆積指標所指向的就是0x20030000,棧就是0x20030000+64K的位置,第二個以後就以此類推
注意:在初始化任務結構之前,不允許系統使用堆,但可以使用棧,那麼核心任務棧部分就分成了
兩個,在未進行調度之前,棧就是上一頁中第二步中所設的棧,那麼上一頁設定堆棧的時候就得注
意必須將堆棧空間設成十個64K再加上在本步驟使用以前的最大可能所需的棧空間

再講解一下任務切換時所要做的事情:
進入整個中斷處理入口時,會將所有寄存器推入IRQ棧之中,並把值拷貝到當前任務結構相應的欄位當中,並取出被中斷的進程的當前PC值存入當前任務結構中的相應欄位中,接下就判別中斷類型,以進入相應的中斷處理函數,這裡就會進入do_timer函數中,以下就是進入此函數之後的流程:
核心中還有一個全域指標,就是當前任務指標,它本身也是在系統BSS段中,它的定義如上一步中的那個全域指標一樣,當由系統時鐘中斷之後,就取出這個全域指標,上一步初始化完成之後,還會把這個指標指向第一個任務結構所在位置,也就是0x20030000處,那麼就取出這個任務結構中的時間片欄位,判斷其是否為0,若為0,就進行以下的操作:儲存使用者態下的棧指標至當前任務結構,儲存堆積指標,並將搜尋一下可以被調度的任務結構,並將此任務結構賦給當前任務指標,置需要進行任務切換標識,此標識同樣是一個全域變數,但它是被賦了初值,會放在整個系統的DATA段中,返回do_timer函數。若不為0,就進行以下操作:
將時間片減一,返回do_timer函數接下來判斷任務切換標識,若為0,則進行以下操作:
不需要進行任務切換,所有寄存器出棧(這裡的棧指的是IRQ棧),重新開啟中斷,切換到使用者模式,載入當前任務結構中的當前PC值欄位,以退出中斷處理常式若此標識為1,則執行以下操作:
就需要進行任務切換,讓所有寄存器出棧(這裡的棧指的是IRQ棧),將當前任務結構中的所有寄
存器的值恢複到相應寄存器中,將使用者態下的棧指標恢複至當前任務結構棧指標,將堆積指標恢複至
當前任務結構堆積指標,並把需要進行任務切換標識恢複為0,重新開啟中斷,切換到使用者模式,任務切換是通過載入PC值來實現的,也就是通過載入當前任務結構中的當前PC值欄位,以退出中斷處理常式

系統調用的實現

本系統是完全可以不實現系統調用的,因為沒有實現核心態和使用者態的保護,完全可以不實現
自己的C庫,所有的函數都像kmalloc之類的實現一樣,在核心中直接寫函數原型,但為了以後
擴充,還是說一下系統調用,這裡以malloc系統調用來實現

首先說明還有一個堆積指標(前面在kmalloc時有一個堆積指標,不過那個堆積指標是為核心任務,中
斷處理所提供),這裡這個堆積指標是用於使用者態的,它在系統初始化完成之前會賦上初值,其初
值就是第一個任務結構所使用的堆的起始位置,也就是在核心所使用的堆加上64K的位置
函數庫中的malloc函數實現步驟如下:
1.首先檢測申請大小是否超出了4K,若超出4K,就返回錯誤
2.進行系統調用(這裡用_syscall1,並只傳遞一個參數(所需分配大小)
系統調用函數_syscall1的實現:
1.將寄存器壓入堆棧(這裡的棧指向就是當前任務的棧)
2.將系統調用號1放至R0,參數放入R1
3.發出SWI指令以產生SWI中斷(就是所說的非強制中斷,陷阱)
此時系統發生中斷,會進入SWI中斷處理入口,下面說一下SWI入口函數的實現
1.取出R0的值,判斷其值,進入相應的分支處理程式碼片段
2.在此進入_malloc處理程式碼片段,取出R1的值,然後再得到前面所說的當前堆積指標,並申請對應數
據塊大小,置用於記憶體佔用標識的相應欄位,將當前堆積指標放入R0,移動當前堆積指標,改變當前任
務結構的堆積指標,切換到使用者態,返回SWI中斷系統調用_syscall1的返回處理:
為了簡單,在從核心態返回使用者態時,不再進行任務的重新調度,所以上面的步驟就相對簡單
1.當從SWI中斷返回後,系統就運行在了使用者態,此時取出R0的值,並賦值給需要申請記憶體的指標
2.在使用者態彈出寄存器,返回到上一層函數
malloc函數的返回,此時malloc函數直接返回指標就行了,整個malloc的流程就結束了,其它的系
統調用同這個過程類似

到此為止,這個作業系統初步實現了,但好像什麼事情都不能做,如果讓它支援串口中斷的話,或許可以做那麼一點點事情,比如像單片機那樣的功能,整個系統的痛點就是中斷處理和任務切換,在本例中,由於ARM不支援像0x86那樣的CPU級的保護模式,所以進行任務切換的時候,就得自己通過載入PC值的方法來實現,呵,因為我想不到更好的辦法,但這個辦法有一個不好解決的地方,就是寄存器入棧和出棧的保護,在進入中斷時,必須保護寄存器,但如果需要進行重新調度,就得從中斷環境切換到進程上下文中,如何從中斷環境切換到進程上下文呢??我在這裡所採用的方法很笨拙:
1.首先讓寄存器入棧
2.讓寄存器儲存至當前任務結構數組,被中斷掉的進程的PC值儲存至任務結構
3.處理timer中斷
4.如果進行任務切換,尋找下一個可調度的進程,然後把當前任務結構指下剛搜尋到
的任務結構,讓寄存器出棧,恢複當前任務結構裡的值到寄存器,恢複堆棧指標,切換到使用者態,通過載入當前任務結構的PC值來恢複被掛起的進程這裡在中斷上下文中使用了任務結構,這在LINUX上好像是不這樣用的,中斷上下文和進程上下文是兩個不同的概念,中斷上下文中不能訪問進程上下文裡的任務結構,我實在想不出有什麼辦法來實現進程調度了,所以請看到我這則文章的人提出好一點的方法

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.