1 引言
FreeRTOS操作系統是一個源碼公開的免費的嵌入式實時操作系統,具有可移植、可裁減、調度策略靈活的特點,可以方便地移植到各種體系結構的微處理器上運行,其最新版本為5.2.0版。
作為一個輕量級的操作系統,FreeRTOS提供的功能包括:任務管理、時間管理、信號量、消息隊列、內存管理、記錄功能等,可基本滿足較小系統的需要。FreeRTOS內核支持優先級調度算法,每個任務可根據重要程度的不同被賦予一定的優先級,CPU總是讓處于就緒態的、優先級最高的任務先運行。FreeRTOS內核同時支持輪換調度算法,系統允許不同的任務使用相同的優先級,在沒有更高優先級任務就緒的情況下,同一優先級的任務共享CPU的使用時間。在任務的組織實現方面,FreeRTOS內核支持傳統的實現:各任務擁有各自的堆棧,支持完全的搶占式調度。FreeRTOS內核同時支持各任務共享同一個堆棧,使RAM的需求進一步減小。但正因如此,該方式的使用受到相對嚴格的限制。
本文移植的硬件平臺是由恩智浦公司生產的基于ARM7TDMI核的微處理器LPC2292。開發調試平臺是ARM ADS 1.2。
2 啟動代碼的編寫
啟動代碼是芯片復位后進入C語言的main()函數前執行的一段代碼,主要是為運行C語言程序提供基本的運行環境,如初始化存儲系統等。為了能夠進行系統初始化,采用一個匯編文件作為啟動代碼是常見的做法。初始化代碼所完成的操作與具體的硬件平臺相關,但一般包括如下內容:(1)初始化異常向量表;(2)初始化存儲器系統;(3)初始化堆棧;(4)初始化有特殊要求的端口、設備;(5)初始化應用程序的運行環境;(6)改變處理器的運行模式;(7)調用主應用程序。
需要注意的是,在對處理器每個模式的堆棧指針寄存器進行初始化的時候,用戶模式下的堆棧寄存器必須最后進行初始化。因為在用戶模式下,不能用MSR指令從用戶模式切換到其它的特權模式,所以如果在其它特權模式的堆棧指針被初始化之前切換到用戶模式,就無法對特權模式下的堆棧指針進行初始化。
第二個需要注意的是,程序使用編譯器分配的空間作為堆棧,而不是按照通常的做法把堆棧分配到RAM的頂端。這樣做有兩個好處:(1)不必知道RAM頂端的位置,移植更加方便。(2)編譯器給出的占用RAM空間的大小就是實際占用的大小,便于控制RAM的分配。
3 FreeRTOS的移植
本次一直主要集中在3個文件里面:portmacro.h,port.c,port.s。其中portmacro.h主要包含于編譯器相關的數據類型的定義、堆棧類型的定義以及幾個宏定義和函數說明。而port.c中則包含與移植有關的C函數,包括堆棧的初始化函數、任務調度器啟動函數、臨界區的進入與退出、時鐘中斷服務程序等。port.s中則包含與移植有關的匯編語言函數,包括上下文切換、開/關中斷、任務切換等。移植中關鍵的功能模塊實現如下文所述。
3.1開/關中斷的實現
FreeRTOS使用函數portDISABLE_INTERRUPTS() 和portENABLE_INTERRUPTS( )分別實現關中斷和開中斷。這些代碼與處理器有關,需要進行移植。在ARM處理器核中,關中斷和開中斷是通過改變程序狀態寄存器CPSR中的相應控制位來實現的。開中斷的匯編語言函數portENABLE_INTERRUPTS()的代碼如下:
portENABLE_INTERRUPTS
STMDB SP!, {R0} ;/* 把R0壓入棧 */
MRS R0, CPSR ;/* 讀取狀態寄存器到R0 */
BIC R0, R0, #0xC0 ; /* 允許IRQ、FIQ中斷 */
MSR CPSR_cxsf, R0 ; /* 回寫R0的值到狀態寄存器 */
LDMIA SP!, {R0} ;/* R0出棧 */
BX LR ;/* 函數返回 */
用類似的方法可以實現關中斷函數portDISABLE_INTERRUPTS()。
3.2 臨界區的進入與退出
代碼的臨界段也稱為臨界區,指處理時不可分割的代碼。一旦這部分代碼開始執行,則不允許任何中斷打斷。開中斷和關中斷可以保護臨界代碼段,保證FreeRTOS的臨界代碼不會被多個任務和中斷服務程序同時訪問,避免造成共享數據的不一致性。為了保護臨界區的資源,在進入臨界區之前須關中斷,而臨界區代碼執行完畢后,要立即開中斷。臨界區的退出函數vPortExitCritical()代碼如下:
void vPortExitCritical( void )
{
if( ulCriticalNesting > portNO_CRITICAL_NESTING )
{
ulCriticalNesting--; /* 中斷嵌套計數器ulCriticalNesting自減一*/
/* 如果中斷嵌套層數為零的話,則需要開中斷. */
if( ulCriticalNesting == portNO_CRITICAL_NESTING )
{
/* 調用函數portENABLE_INTERRUPTS()來開中斷. */
portENABLE_INTERRUPTS();
}
}
}
用類似的方法可以實現臨界區的進入函數vPortEnterCritical( )。
3.3 堆棧的初始化
任務創建函數xTaskCreate()通過調用堆棧的初始化函數pxPortInitialiseStack()來初始化任務的棧結構;實際是定義了任務堆棧上下文(context)的內容。所有的寄存器都保存到堆棧中,堆?雌饋砭拖裰袛鄤偘l生過一樣。上下文的保存格式如圖1所示。
在堆棧的上下文中,PC存放任務執行的第一條指令,LR保存的是任務的返回地址,SP保存的是任務的堆棧地址。R0存放的是傳遞給任務的參數。CPSR存放的是任務運行時處理器的初始狀態。CriticalNesting存放的是中斷嵌套計數器ulCriticalNesting的值。任務堆棧的上下文保存結構與任務切換的實現緊密相關,所以我們在設計上下文保存結構的時候,要重點考慮實現任務切換的便捷性。
PC
LR
SP
R12
.........
R1
R0
CPSR
CriticalNesting
圖1 上下文的保存結構
portSTACK_TYPE *pxPortInitialiseStack( portSTACK_TYPE *pxTopOfStack,
pdTASK_CODE pxCode, void *pvParameters )
{
portSTACK_TYPE *pxOriginalTOS;
pxOriginalTOS = pxTopOfStack;
*pxTopOfStack = ( portSTACK_TYPE ) pxCode + portINSTRUCTION_SIZE; /* PC */
*(--pxTopOfStack) = ( portSTACK_TYPE ) 0xaaaaaaaa; /* R14 */
*(--pxTopOfStack) = ( portSTACK_TYPE ) pxOriginalTOS; /* SP */
*(--pxTopOfStack) = ( portSTACK_TYPE ) 0x12121212; /* R12 */
.................................................
*(--pxTopOfStack) = ( portSTACK_TYPE ) pvParameters; /* R0 */
*(--pxTopOfStack) = ( portSTACK_TYPE ) portINITIAL_SPSR; /* CPSR */
*(--pxTopOfStack) = portNO_CRITICAL_SECTION_NESTING; /* ulCriticalNesting */
return pxTopOfStack;
}
3.4 上下文的保存與恢復
本次移植分別定義了兩個宏來實現上下文的保存和恢復,分別是:portSAVE_CONTEXT()和portRESTORE_CONTEXT()。portSAVE_CONTEXT()宏首先設置R0指向任務的堆棧,接著保存任務的返回地址,然后再保存其他的寄存器和CPSR,以及中斷嵌套計數器。最后把新的棧頂保存在當前的任務控制塊里面。其宏定義如下:
MACRO
portSAVE_CONTEXT
STMDB SP!, {R0} ; 設置R0指向任務堆棧.
STMDB SP, {SP}^
SUB SP, SP, #4
LDMIA SP!, {R0}
STMDB R0!, {LR}
MOV LR, R0
LDMIA SP!, {R0}
STMDB LR, {R0-LR}^ ;把R0-LR寄存器壓入任務堆棧
SUB LR, LR, #60
MRS R0, SPSR ;把SPSR壓入任務堆棧
STMDB LR!, {R0}
LDR R0, =ulCriticalNesting ;把中斷嵌套計數器壓入任務堆棧
LDR R0, [R0]
STMDB LR!, {R0}
LDR R1, =pxCurrentTCB ; 存儲當前的任務堆棧棧頂到任務控制塊中
LDR R0, [R1]
STR LR, [R0]
MEND
利用類似的方法可以實現portRESTORE_CONTEXT()宏,需要注意的是,該宏要嚴格按照保存上下文的相反的順序恢復上下文。
3.5 啟動任務調度
操作系統初始化之后,就可以開啟系統時鐘,運行系統內第1個最高優先級的就緒任務。對于第1個執行的任務,不需要進行上下文切換,而只要恢復上下文即可。第一個任務的執行是通過調用匯編函數portRESTORE_CONTEXT()恢復上下文來實現的。啟動任務調度程序代碼如下:
portBASE_TYPE xPortStartScheduler( void )
{
prvSetupTimerInterrupt(); /* 設置并啟動Timer0. */
vPortStartFirstTask(); /* 啟動第一個任務. */
return 0;
}
3.6 任務切換的實現
程序中portYIELD()函數的調用,會進行一次任務級的上下文切換。在本次移植中,使用軟件中斷指令SWI是處理器進入管理模式和ARM指令狀態,并使用功能0實現portYIELD()的功能。portYIELD()(功能號0)最終使用程序vPortYieldProcessor實現。vPortYieldProcessor的匯編代碼如下:
vPortYieldProcessor
ADD LR, LR, #4
portSAVE_CONTEXT ; 保存上下文 (宏)
LDR R0, =vTaskSwitchContext ; 選擇就緒的優先級最高優先級的任務
MOV LR, PC
BX R0
portRESTORE_CONTEXT ; 恢復上下文 (宏)
3.7 時鐘中斷服務的實現
當時鐘中斷到來的時,處理器跳轉到相應的時鐘中斷服務程序,時鐘中斷服務程序主要調用vTaskIncrementTick()函數,該處理函數主要處理跟系統時鐘相關的工作,如將時鐘節拍數加1,檢查是否有更高優先級的任務就緒等等。系統時鐘中斷服務程序由vTickISR()函數實現。包括以下步驟:(1)保存上下文。(2)調用vTaskIncrementTick()函數,如果系統的調度策略配置為可搶占調度,則查找最高優先級的就緒任務。(3)清除中斷源,并通知中斷控制器中斷結束。(4)恢復上下文。vTickISR()函數的代碼如下:
void vTickISR( void )
{
portSAVE_CONTEXT(); /* 需要保存上下文 */
/* 增加xTickCount值,檢查新的xTickCount值是否引起一個延遲周期過期,這個函數調用可導致一個任務變成準備運行. */
vTaskIncrementTick();
/*如果是系統配置為可搶占式調度,則檢查是否要上下文切換。如果喚醒的任務比已經中斷的任務有更高優先級,就需要切換 */
#if configUSE_PREEMPTION == 1
vTaskSwitchContext();
#endif
T0IR= portTIMER_MATCH_ISR_BIT; /* 清除中斷源*/
VICVectAddr = portCLEAR_VIC_INTERRUPT; /* 通知中斷控制器中斷結束*/
/*恢復上下文.如果發生了上下文切換,這將恢復要繼續運行的任務的上下文 */
portRESTORE_CONTEXT();
}
4結論
本文設計并實現了FreeRTOS 5.2.0操作系統到ARM7處理器上的移植。移植程序在福州大學工業控制研究所自行研制的ZD100終端上實現,經該環境的多任務運行結果表明,系統穩定可靠。同時,移植的方法在同類ARM架構的處理器上具有較強的通用性。
參考文獻
[1] Richard Barry. Creating a New FreeRTOS.org Port[EB/OL].
http://www.freertos.org/FreeRTOS-porting-guide.html.2008.1-1
[2] NXP Semiconductors. LPC2292 USER MANUAL[Z]. 2004.14-77
[3] 劉濱,王琦,劉麗麗. 嵌入式操作系統FreeRTOS的原理與實現[J]. 單片機與嵌入式系統應用, 2005.7 : 8-11.
[4] ARM Limited. ARM Developer Suite(Version 1.2).Developer Guide[Z].1999.43-51
[5] 周立功. ARM嵌入式系統基礎教程[M].北京:北京航空航天大學出版社.2004.30-281
作者簡介
黃鵬程,男,1984年出生,福建莆田市人,福州大學數學與計算機科學學院碩士研究生,主要研究的方向:嵌入式系統。<