新聞詳情
嵌入式開發內核結構瀏覽數:17次
![]() 第3 本章給出μC/OS-Ⅱ的主要結構概貌。讀者將學習以下一些內容; ?μC/OS-Ⅱ是怎樣處理臨界段代碼的; ?什么是任務,怎樣把用戶的任務交給μC/OS-Ⅱ; ?任務是怎樣調度的; ?應用程序CPU的利用率是多少,μC/OS-Ⅱ是怎樣知道的; ?怎樣寫中斷服務子程序; ?什么是時鐘節拍,μC/OS-Ⅱ是怎樣處理時鐘節拍的; ?μC/OS-Ⅱ是怎樣初始化的,以及 ?怎樣啟動多任務; 本章還描述以下函數,這些服務于應用程序: ?OS_ENTER_CRITICAL() 和 OS_EXIT_CRITICAL(), ?OSInit(), ?OSStart(), ?OSIntEnter() 和 OSIntExit(), ?OSSchedLock() 和 OSSchedUnlock(), 以及 ?OSVersion(). 和其它內核一樣,μC/OS-Ⅱ為了處理臨界段代碼需要關中斷,處理完畢后再開中斷。這使得μC/OS-Ⅱ能夠避免同時有其它任務或中斷服務進入臨界段代碼。關中斷的時間是實時內核開發商應提供的最重要的指標之一,因為這個指標影響用戶系統對實時事件的響應性。μC/OS-Ⅱ努力使關中斷時間降至最短,但就使用μC/OS-Ⅱ而言,關中斷的時間很大程度上取決于微處理器的架構以及編譯器所生成的代碼質量。 微處理器一般都有關中斷/開中斷指令,用戶使用的C語言編譯器必須有某種機制能夠在C中直接實現關中斷/開中斷地操作。某些C編譯器允許在用戶的C源代碼中插入匯編語言的語句。這使得插入微處理器指令來關中斷/開中斷很容易實現。而有的編譯器把從C語言中關中斷/開中斷放在語言的擴展部分。μC/OS-Ⅱ定義兩個宏(macros)來關中斷和開中斷,以便避開不同C編譯器廠商選擇不同的方法來處理關中斷和開中斷。μC/OS-Ⅱ中的這兩個宏調用分別是:OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()。因為這兩個宏的定義取決于所用的微處理器,故在文件OS_CPU.H中可以找到相應宏定義。每種微處理器都有自己的OS_CPU.H文件。 4. 一個任務通常是一個無限的循環[L3.1(2)],如程序清單3.1所示。一個任務看起來像其它C的函數一樣,有函數返回類型,有形式參數變量,但是任務是絕不會返回的。故返回參數必須定義成void[L3.1(1)]。
不同的是,當任務完成以后,任務可以自我刪除,如清單L3.2所示。注意任務代碼并非真的刪除了,μC/OS-Ⅱ只是簡單地不再理會這個任務了,這個任務的代碼也不會再運行,如果任務調用了OSTaskDel(),這個任務絕不會返回什么。
形式參數變量[L3.1(1)]是由用戶代碼在第一次執行的時候帶入的。請注意,該變量的類型是一個指向void的指針。這是為了允許用戶應用程序傳遞任何類型的數據給任務。這個指針好比一輛萬能的車子,如果需要的話,可以運載一個變量的地址,或一個結構,甚至是一個函數的地址。也可以建立許多相同的任務,所有任務都使用同一個函數(或者說是同一個任務代碼程序), 見第一章的例1。例如,用戶可以將四個串行口安排成每個串行口都是一個單獨的任務,而每個任務的代碼實際上是相同的。并不需要將代碼復制四次,用戶可以建立一個任務,向這個任務傳入一個指向某數據結構的指針變量,這個數據結構定義串行口的參數(波特率、I/O口地址、中斷向量號等)。 μC/OS-Ⅱ可以管理多達64個任務,但目前版本的μC/OS-Ⅱ有兩個任務已經被系統占用了。作者保留了優先級為0、1、2、3、OS_LOWEST_PRIO-3、OS_LOWEST_PRI0-2,OS_LOWEST_PRI0-1以及OS_LOWEST_PRI0這8個任務以被將來使用。OS_LOWEST_PRI0是作為定義的常數在OS_CFG.H文件中用定義常數語句#define constant定義的。因此用戶可以有多達56個應用任務。必須給每個任務賦以不同的優先級,優先級可以從0到OS_LOWEST_PR10-2。優先級號越低,任務的優先級越高。μC/OS-Ⅱ總是運行進入就緒態的優先級最高的任務。目前版本的μC/OS-Ⅱ中,任務的優先級號就是任務編號(ID)。優先級號(或任務的ID號)也被一些內核服務函數調用,如改變優先級函數OSTaskChangePrio(),以及任務刪除函數OSTaskDel()。 為了使μC/OS-Ⅱ能管理用戶任務,用戶必須在建立一個任務的時候,將任務的起始地址與其它參數一起傳給下面兩個函數中的一個:OSTastCreat或OSTaskCreatExt()。OSTaskCreateExt()是OSTaskCreate()的擴展,擴展了一些附加的功能。,這兩個函數的解釋見第四章,任務管理。 4. 圖3.1是μC/OS-Ⅱ控制下的任務狀態轉換圖。在任一給定的時刻,任務的狀態一定是在這五種狀態之一。 睡眠態(DORMANT)指任務駐留在程序空間之中,還沒有交給μC/OS-Ⅱ管理,(見程序清單L3.1或L3.2)。把任務交給μC/OS-Ⅱ是通過調用下述兩個函數之一:OSTaskCreate()或OSTaskCreateExt()。當任務一旦建立,這個任務就進入就緒態準備運行。任務的建立可以是在多任務運行開始之前,也可以是動態地被一個運行著的任務建立。如果一個任務是被另一個任務建立的,而這個任務的優先級高于建立它的那個任務,則這個剛剛建立的任務將立即得到CPU的控制權。一個任務可以通過調用OSTaskDel()返回到睡眠態,或通過調用該函數讓另一個任務進入睡眠態。 調用OSStart()可以啟動多任務。OSStart()函數運行進入就緒態的優先級最高的任務。就緒的任務只有當所有優先級高于這個任務的任務轉為等待狀態,或者是被刪除了,才能進入運行態。 圖3.1 任務的狀態 正在運行的任務可以通過調用兩個函數之一將自身延遲一段時間,這兩個函數是OSTimeDly()或OSTimeDlyHMSM()。這個任務于是進入等待狀態,等待這段時間過去,下一個優先級最高的、并進入了就緒態的任務立刻被賦予了CPU的控制權。等待的時間過去以后,系統服務函數OSTimeTick()使延遲了的任務進入就緒態(見3.10節,時鐘節拍)。 正在運行的任務期待某一事件的發生時也要等待,手段是調用以下3個函數之一:OSSemPend(),OSMboxPend(),或OSQPend()。調用后任務進入了等待狀態(WAITING)。當任務因等待事件被掛起(Pend),下一個優先級最高的任務立即得到了CPU的控制權。當事件發生了,被掛起的任務進入就緒態。事件發生的報告可能來自另一個任務,也可能來自中斷服務子程序。 正在運行的任務是可以被中斷的,除非該任務將中斷關了,或者μC/OS-Ⅱ將中斷關了。被中斷了的任務就進入了中斷服務態(ISR)。響應中斷時,正在執行的任務被掛起,中斷服務子程序控制了CPU的使用權。中斷服務子程序可能會報告一個或多個事件的發生,而使一個或多個任務進入就緒態。在這種情況下,從中斷服務子程序返回之前,μC/OS-Ⅱ要判定,被中斷的任務是否還是就緒態任務中優先級最高的。如果中斷服務子程序使一個優先級更高的任務進入了就緒態,則新進入就緒態的這個優先級更高的任務將得以運行,否則原來被中斷了的任務才能繼續運行。 當所有的任務都在等待事件發生或等待延遲時間結束,μC/OS-Ⅱ執行空閑任務(idle task),執行OSTaskIdle()函數。 4. 一旦任務建立了,任務控制塊OS_TCBs將被賦值(程序清單3.3)。任務控制塊是一個數據結構,當任務的CPU使用權被剝奪時,μC/OS-Ⅱ用它來保存該任務的狀態。當任務重新得到CPU使用權時,任務控制塊能確保任務從當時被中斷的那一點絲毫不差地繼續執行。OS_TCBs全部駐留在RAM中。讀者將會注意到筆者在組織這個數據結構時,考慮到了各成員的邏輯分組。任務建立的時候,OS_TCBs就被初始化了(見第四章 任務管理)。
.OSTCBStkPtr是指向當前任務棧頂的指針。μC/OS-Ⅱ允許每個任務有自己的棧,尤為重要的是,每個任務的棧的容量可以是任意的。有些商業內核要求所有任務棧的容量都一樣,除非用戶寫一個復雜的接口函數來改變之。這種限制浪費了RAM,當各任務需要的棧空間不同時,也得按任務中預期棧容量需求最多的來分配棧空間。OSTCBStkPtr是OS_TCB數據結構中唯一的一個能用匯編語言來處置的變量(在任務切換段的代碼Context-switching code之中,)把OSTCBStkPtr放在數據結構的最前面,使得從匯編語言中處理這個變量時較為容易。 .OSTCBExtPtr 指向用戶定義的任務控制塊擴展。用戶可以擴展任務控制塊而不必修改μC/OS-Ⅱ的源代碼。.OSTCBExtPtr只在函數OstaskCreateExt()中使用,故使用時要將OS_TASK_CREAT_EN設為1,以允許建立任務函數的擴展。例如用戶可以建立一個數據結構,這個數據結構包含每個任務的名字,或跟蹤某個任務的執行時間,或者跟蹤切換到某個任務的次數(見例3)。注意,筆者將這個擴展指針變量放在緊跟著堆棧指針的位置,為的是當用戶需要在匯編語言中處理這個變量時,從數據結構的頭上算偏移量比較方便。 .OSTCBStkBottom是指向任務棧底的指針。如果微處理器的棧指針是遞減的,即棧存儲器從高地址向低地址方向分配,則OSTCBStkBottom指向任務使用的棧空間的最低地址。類似地,如果微處理器的棧是從低地址向高地址遞增型的,則OSTCBStkBottom指向任務可以使用的棧空間的最高地址。函數OSTaskStkChk()要用到變量OSTCBStkBottom,在運行中檢驗棧空間的使用情況。用戶可以用它來確定任務實際需要的棧空間。這個功能只有當用戶在任務建立時允許使用OSTaskCreateExt()函數時才能實現。這就要求用戶將OS_TASK_CREATE_EXT_EN設為1,以便允許該功能。 .OSTCBStkSize存有棧中可容納的指針元數目而不是用字節(Byte)表示的棧容量總數。也就是說,如果棧中可以保存1,000個入口地址,每個地址寬度是32位的,則實際棧容量是4,000字節。同樣是1,000個入口地址,如果每個地址寬度是16位的,則總棧容量只有2,000字節。在函數OSStakChk()中要調用OSTCBStkSize。同理,若使用該函數的話,要將OS_TASK_CREAT_EXT_EN設為1。 .OSTCBOpt把“選擇項”傳給OSTaskCreateExt(),只有在用戶將OS_TASK_CREATE_EXT_EN設為1時,這個變量才有效。μC/OS-Ⅱ目前只支持3個選擇項(見uCOS_II.H):OS_TASK_OTP_STK_CHK, OS_TASK_OPT_STK_CLR和OS_TASK_OPT_SAVE_FP。 OS_TASK_OTP_STK_CHK 用于告知TaskCreateExt(),在任務建立的時候任務棧檢驗功能得到了允許。OS_TASK_OPT_STK_CLR表示任務建立的時候任務棧要清零。只有在用戶需要有棧檢驗功能時,才需要將棧清零。如果不定義OS_TASK_OPT_STK_CLR,而后又建立、刪除了任務,棧檢驗功能報告的棧使用情況將是錯誤的。如果任務一旦建立就決不會被刪除,而用戶初始化時,已將RAM清過零,則OS_TASK_OPT_STK_CLR不需要再定義,這可以節約程序執行時間。傳遞了OS_TASK_OPT_STK_CLR將增加TaskCreateExt()函數的執行時間,因為要將棧空間清零。棧容量越大,清零花的時間越長。最后一個選擇項OS_TASK_OPT_SAVE_FP通知TaskCreateExt(),任務要做浮點運算。如果微處理器有硬件的浮點協處理器,則所建立的任務在做任務調度切換時,浮點寄存器的內容要保存。 .OSTCBId用于存儲任務的識別碼。這個變量現在沒有使用,留給將來擴展用。 .OSTCBNext和.OSTCBPrev用于任務控制塊OS_TCBs的雙重鏈接,該鏈表在時鐘節拍函數OSTimeTick()中使用,用于刷新各個任務的任務延遲變量.OSTCBDly,每個任務的任務控制塊OS_TCB在任務建立的時候被鏈接到鏈表中,在任務刪除的時候從鏈表中被刪除。雙重連接的鏈表使得任一成員都能被快速插入或刪除。 .OSTCBEventPtr是指向事件控制塊的指針,后面的章節中會有所描述(見第6章 任務間通訊與同步)。 .OSTCBMsg是指向傳給任務的消息的指針。用法將在后面的章節中提到(見第6章任務間通訊與同步)。 .OSTCBDly當需要把任務延時若干時鐘節拍時要用到這個變量,或者需要把任務掛起一段時間以等待某事件的發生,這種等待是有超時限制的。在這種情況下,這個變量保存的是任務允許等待事件發生的最多時鐘節拍數。如果這個變量為0,表示任務不延時,或者表示等待事件發生的時間沒有限制。 .OSTCBStat是任務的狀態字。當.OSTCBStat為0,任務進入就緒態。可以給.OSTCBStat賦其它的值,在文件uCOS_II.H中有關于這個值的描述。 .OSTCBPrio是任務優先級。高優先級任務的.OSTCBPrio值小。也就是說,這個值越小,任務的優先級越高。 .OSTCBX, .OSTCBY, .OSTCBBitX和 .OSTCBBitY用于加速任務進入就緒態的過程或進入等待事件發生狀態的過程(避免在運行中去計算這些值)。這些值是在任務建立時算好的,或者是在改變任務優先級時算出的。這些值的算法見程序清單L3.4。
.OSTCBDelReq是一個布爾量,用于表示該任務是否需要刪除,用法將在后面的章節中描述(見第4章 任務管理) 應用程序中可以有的最多任務數(OS_MAX_TASKS)是在文件OS_CFG.H中定義的。這個最多任務數也是μC/OS-Ⅱ分配給用戶程序的最多任務控制塊OS_TCBs的數目。將OS_MAX_TASKS的數目設置為用戶應用程序實際需要的任務數可以減小RAM的需求量。所有的任務控制塊OS_TCBs都是放在任務控制塊列表數組OSTCBTbl[]中的。請注意,μC/OS-Ⅱ分配給系統任務OS_N_SYS_TASKS若干個任務控制塊,見文件μC/OS-Ⅱ.H,供其內部使用。目前,一個用于空閑任務,另一個用于任務統計(如果OS_TASK_STAT_EN是設為1的)。在μC/OS-Ⅱ初始化的時候,如圖3.2所示,所有任務控制塊OS_TCBs被鏈接成單向空任務鏈表。當任務一旦建立,空任務控制塊指針OSTCBFreeList指向的任務控制塊便賦給了該任務,然后OSTCBFreeList的值調整為指向下鏈表中下一個空的任務控制塊。一旦任務被刪除,任務控制塊就還給空任務鏈表。 圖3.2 空任務列表 每個任務被賦予不同的優先級等級,從0級到最低優先級OS_LOWEST_PR1O,包括0和OS_LOWEST_PR1O在內(見文件OS_CFG.H)。當μC/OS-Ⅱ初始化的時候,最低優先級OS_LOWEST_PR1O總是被賦給空閑任務idle task。注意,最多任務數目OS_MAX_TASKS和最低優先級數是沒有關系的。用戶應用程序可以只有10個任務,而仍然可以有32個優先級的級別(如果用戶將最低優先級數設為31的話)。 每個任務的就緒態標志都放入就緒表中的,就緒表中有兩個變量OSRedyGrp和OSRdyTbl[]。在OSRdyGrp中,任務按優先級分組,8個任務為一組。OSRdyGrp中的每一位表示8組任務中每一組中是否有進入就緒態的任務。任務進入就緒態時,就緒表OSRdyTbl[]中的相應元素的相應位也置位。就緒表OSRdyTbl[]數組的大小取決于OS_LOWEST_PR1O(見文件OS_CFG.H)。當用戶的應用程序中任務數目比較少時,減少OS_LOWEST_PR1O的值可以降低μC/OS-Ⅱ對RAM(數據空間)的需求量。 為確定下次該哪個優先級的任務運行了,內核調度器總是將OS_LOWEST_PR1O在就緒表中相應字節的相應位置1。OSRdyGrp和OSRdyTbl[]之間的關系見圖3.3,是按以下規則給出的: 當OSRdyTbl[0]中的任何一位是1時,OSRdyGrp的第0位置1, 當OSRdyTbl[1]中的任何一位是1時,OSRdyGrp的第1位置1, 當OSRdyTbl[2]中的任何一位是1時,OSRdyGrp的第2位置1, 當OSRdyTbl[3]中的任何一位是1時,OSRdyGrp的第3位置1, 當OSRdyTbl[4]中的任何一位是1時,OSRdyGrp的第4位置1, 當OSRdyTbl[5]中的任何一位是1時,OSRdyGrp的第5位置1, 當OSRdyTbl[6]中的任何一位是1時,OSRdyGrp的第6位置1, 當OSRdyTbl[7]中的任何一位是1時,OSRdyGrp的第7位置1, 程序清單3.5中的代碼用于將任務放入就緒表。Prio是任務的優先級。
讀者可以看出,任務優先級的低三位用于確定任務在總就緒表OSRdyTbl[]中的所在位。接下去的三位用于確定是在OSRdyTbl[]數組的第幾個元素。OSMapTbl[]是在ROM中的(見文件OS_CORE.C)屏蔽字,用于限制OSRdyTbl[]數組的元素下標在0到7之間,見表3.1 圖3.3μC/OS-Ⅱ就緒表 如果一個任務被刪除了,則用程序清單3.6中的代碼做求反處理。
以上代碼將就緒任務表數組OSRdyTbl[]中相應元素的相應位清零,而對于OSRdyGrp,只有當被刪除任務所在任務組中全組任務一個都沒有進入就緒態時,才將相應位清零。也就是說OSRdyTbl[prio>>3]所有的位都是零時,OSRdyGrp的相應位才清零。為了找到那個進入就緒態的優先級最高的任務,并不需要從OSRdyTbl[0]開始掃描整個就緒任務表,只需要查另外一張表,即優先級判定表OSUnMapTbl([256])(見文件OS_CORE.C)。OSRdyTbl[]中每個字節的8位代表這一組的8個任務哪些進入就緒態了,低位的優先級高于高位。利用這個字節為下標來查OSUnMapTbl這張表,返回的字節就是該組任務中就緒態任務中優先級最高的那個任務所在的位置。這個返回值在0到7之間。確定進入就緒態的優先級最高的任務是用以下代碼完成的,如程序清單L3.7所示。
例如,如果OSRdyGrp的值為二進制01101000,查OSUnMapTbl[OSRdyGrp]得到的值是3,它相應于OSRdyGrp中的第3位bit3,這里假設最右邊的一位是第0位bit0。類似地,如果OSRdyTbl[3]的值是二進制11100100,則OSUnMapTbl[OSRdyTbc[3]]的值是2,即第2位。于是任務的優先級Prio就等于26(3*8+2)。利用這個優先級的值。查任務控制塊優先級表OSTCBPrioTbl[],得到指向相應任務的任務控制塊OS_TCB的工作就完成了。 μC/OS-Ⅱ總是運行進入就緒態任務中優先級最高的那一個。確定哪個任務優先級最高,下面該哪個任務運行了的工作是由調度器(Scheduler)完成的。任務級的調度是由函數OSSched()完成的。中斷級的調度是由另一個函數OSIntExt()完成的,這個函數將在以后描述。OSSched()的代碼如程序清單L3.8所示。
μC/OS-Ⅱ任務調度所花的時間是常數,與應用程序中建立的任務數無關。如程序清單中[L3.8(1)]條件語句的條件不滿足,任務調度函數OSSched()將退出,不做任務調度。這個條件是:如果在中斷服務子程序中調用OSSched(),此時中斷嵌套層數OSIntNesting>0,或者由于用戶至少調用了一次給任務調度上鎖函數OSSchedLock(),使OSLockNesting>0。如果不是在中斷服務子程序調用OSSched(),并且任務調度是允許的,即沒有上鎖,則任務調度函數將找出那個進入就緒態且優先級最高的任務[L3.8(2)],進入就緒態的任務在就緒任務表中有相應的位置位。一旦找到那個優先級最高的任務,OSSched()檢驗這個優先級最高的任務是不是當前正在運行的任務,以此來避免不必要的任務調度[L3.8(3)]。注意,在μC/OS中曾經是先得到OSTCBHighRdy然后和OSTCBCur做比較。因為這個比較是兩個指針型變量的比較,在8位和一些16位微處理器中這種比較相對較慢。而在μC/OS-Ⅱ中是兩個整數的比較。并且,除非用戶實際需要做任務切換,在查任務控制塊優先級表OSTCBPrioTbl[]時,不需要用指針變量來查OSTCBHighRdy。綜合這兩項改進,即用整數比較代替指針的比較和當需要任務切換時再查表,使得μC/OS-Ⅱ比μC/OS在8位和一些16位微處理器上要更快一些。 為實現任務切換,OSTCBHighRdy必須指向優先級最高的那個任務控制塊OS_TCB,這是通過將以OSPrioHighRdy為下標的OSTCBPrioTbl[]數組中的那個元素賦給OSTCBHighRdy來實現的[L3.8(4)]。接著,統計計數器OSCtxSwCtr加1,以跟蹤任務切換次數[L3.8(5)]。最后宏調用OS_TASK_SW()來完成實際上的任務切換[L3.8(6)]。 任務切換很簡單,由以下兩步完成,將被掛起任務的微處理器寄存器推入堆棧,然后將較高優先級的任務的寄存器值從棧中恢復到寄存器中。在μC/OS-Ⅱ中,就緒任務的棧結構總是看起來跟剛剛發生過中斷一樣,所有微處理器的寄存器都保存在棧中。換句話說,μC/OS-Ⅱ運行就緒態的任務所要做的一切,只是恢復所有的CPU寄存器并運行中斷返回指令。為了做任務切換,運行OS_TASK_SW(),人為模仿了一次中斷。多數微處理器有軟中斷指令或者陷阱指令TRAP來實現上述操作。中斷服務子程序或陷阱處理(Trap hardler),也稱作事故處理(exception handler),必須提供中斷向量給匯編語言函數OSCtxSw()。OSCtxSw()除了需要OS_TCBHighRdy指向即將被掛起的任務,還需要讓當前任務控制塊OSTCBCur指向即將被掛起的任務,參見第8章,移植μC/OS-Ⅱ,有關于OSCtxSw()的更詳盡的解釋。 OSSched()的所有代碼都屬臨界段代碼。在尋找進入就緒態的優先級最高的任務過程中,為防止中斷服務子程序把一個或幾個任務的就緒位置位,中斷是被關掉的。為縮短切換時間,OSSched()全部代碼都可以用匯編語言寫。為增加可讀性,可移植性和將匯編語言代碼最少化,OSSched()是用C寫的。 4. 給調度器上鎖函數OSSchedlock()(程序清單L3.9)用于禁止任務調度,直到任務完成后調用給調度器開鎖函數OSSchedUnlock()為止,(程序清單L3.10)。調用OSSchedlock()的任務保持對CPU的控制權,盡管有個優先級更高的任務進入了就緒態。然而,此時中斷是可以被識別的,中斷服務也能得到(假設中斷是開著的)。OSSchedlock()和OSSchedUnlock()必須成對使用。變量OSLockNesting跟蹤OSSchedLock()函數被調用的次數,以允許嵌套的函數包含臨界段代碼,這段代碼其它任務不得干預。μC/OS-Ⅱ允許嵌套深度達255層。當OSLockNesting等于零時,調度重新得到允許。函數OSSchedLock()和OSSchedUnlock()的使用要非常謹慎,因為它們影響μC/OS-Ⅱ對任務的正常管理。 當OSLockNesting減到零的時候,OSSchedUnlock()調用OSSched[L3.10(2)]。OSSchedUnlock()是被某任務調用的,在調度器上鎖的期間,可能有什么事件發生了并使一個更高優先級的任務進入就緒態。 調用OSSchedLock()以后,用戶的應用程序不得使用任何能將現行任務掛起的系統調用。也就是說,用戶程序不得調用OSMboxPend()、OSQPend()、OSSemPend()、OSTaskSuspend(OS_PR1O_SELF)、OSTimeDly()或OSTimeDlyHMSM(),直到OSLockNesting回零為止。因為調度器上了鎖,用戶就鎖住了系統,任何其它任務都不能運行。 當低優先級的任務要發消息給多任務的郵箱、消息隊列、信號量時(見第6章 任務間通訊和同步),用戶不希望高優先級的任務在郵箱、隊列和信號量沒有得到消息之前就取得了CPU的控制權,此時,用戶可以使用禁止調度器函數。
μC/OS-Ⅱ總是建立一個空閑任務,這個任務在沒有其它任務進入就緒態時投入運行。這個空閑任務[OSTaskIdle()]永遠設為最低優先級,即OS_LOWEST_PRI0。空閑任務OSTaskIdle()什么也不做,只是在不停地給一個32位的名叫OSIdleCtr的計數器加1,統計任務(見3.08節,統計任務)使用這個計數器以確定現行應用軟件實際消耗的CPU時間。程序清單L3.11是空閑任務的代碼。在計數器加1前后,中斷是先關掉再開啟的,因為8位以及大多數16位微處理器的32位加1需要多條指令,要防止高優先級的任務或中斷服務子程序從中打入。空閑任務不可能被應用軟件刪除。
4. μC/OS-Ⅱ有一個提供運行時間統計的任務。這個任務叫做OSTaskStat(),如果用戶將系統定義常數OS_TASK_STAT_EN(見文件OS_CFG.H)設為1,這個任務就會建立。一旦得到了允許,OSTaskStat()每秒鐘運行一次(見文件OS_CORE.C),計算當前的CPU利用率。換句話說,OSTaskStat()告訴用戶應用程序使用了多少CPU時間,用百分比表示,這個值放在一個有符號8位整數OSCPUsage中,精讀度是1個百分點。 如果用戶應用程序打算使用統計任務,用戶必須在初始化時建立一個唯一的任務,在這個任務中調用OSStatInit()(見文件OS_CORE.C)。換句話說,在調用系統啟動函數OSStart()之前,用戶初始代碼必須先建立一個任務,在這個任務中調用系統統計初始化函數OSStatInit(),然后再建立應用程序中的其它任務。程序清單L3.12是統計任務的示意性代碼。
因為用戶的應用程序必須先建立一個起始任務[TaskStart()],當主程序main()調用系統啟動函數OSStcnt()的時候,μC/OS-Ⅱ只有3個要管理的任務:TaskStart()、OSTaskIdle()和OSTaskStat()。請注意,任務TaskStart()的名稱是無所謂的,叫什么名字都可以。因為μC/OS-Ⅱ已經將空閑任務的優先級設為最低,即OS_LOWEST_PR10,統計任務的優先級設為次低,OS_LOWEST_PR10-1。啟動任務TaskStart()總是優先級最高的任務。 圖F3.4解釋初始化統計任務時的流程。用戶必須首先調用的是μC/OS-Ⅱ中的系統初始化函數OSInit(),該函數初始化μC/OS-Ⅱ[圖F3.4(2)]。有的處理器(例如Motorola的MC68HC11),不需要“設置”中斷向量,中斷向量已經在ROM中有了。用戶必須調用OSTaskCreat()或者OSTaskCreatExt()以建立TaskStart()[圖F3.4(3)]。進入多任務的條件準備好了以后,調用系統啟動函數OSStart()。這個函數將使TaskStart()開始執行,因為TaskStart()是優先級最高的任務[圖F3.4(4)]]。 圖F3.4統計任務的初始化 TaskStart()負責初始化和啟動時鐘節拍[圖F3.4(5)]。在這里啟動時鐘節拍是必要的,因為用戶不會希望在多任務還沒有開始時就接收到時鐘節拍中斷。接下去TaskStart()調用統計初始化函數OSStatInit()[圖F3.4(6)]。統計初始化函數OSStatInit()決定在沒有其它應用任務運行時,空閑計數器(OSIdleCtr)的計數有多快。奔騰II微處理器以333MHz運行時,加1操作可以使該計數器的值達到每秒15,000,000次。OSIdleCtr的值離32位計數器的溢出極限值4,294,967,296還差得遠。微處理器越來越快,用戶要注意這里可能會是將來的一個潛在問題。 系統統計初始化任務函數OSStatInit()調用延遲函數OSTimeDly()將自身延時2個時鐘節拍以停止自身的運行[圖F3.4(7)]。這是為了使OSStatInit()與時鐘節拍同步。μC/OS-Ⅱ然后選下一個優先級最高的進入就緒態的任務運行,這恰好是統計任務OSTaskStat()。讀者會在后面讀到OSTaskStat()的代碼,但粗看一下,OSTaskStat()所要做的第一件事就是查看統計任務就緒標志是否為“假”,如果是的話,也要延時兩個時鐘節拍[圖F3.4(8)]。一定會是這樣,因為標志OSStatRdy已被OSInit()函數初始化為“假”,所以實際上DSTaskStat也將自己推入休眠態(Sleep)兩個時鐘節拍[圖F3.4(9)]。于是任務切換到空閑任務,OSTaskIdle()開始運行,這是唯一一個就緒態任務了。CPU處在空閑任務OSTaskIdle中,直到TaskStart()的延遲兩個時鐘節拍完成[圖3.4(10)]。兩個時鐘節拍之后,TaskStart()恢復運行[圖F3.4(11)]。 在執行OSStartInit()時,空閑計數器OSIdleCtr被清零[圖F3.4(12)]。然后,OSStatInit()將自身延時整整一秒[圖F3.4(13)]。因為沒有其它進入就緒態的任務,OSTaskIdle()又獲得了CPU的控制權[圖F3.4(14)]。一秒鐘以后,TaskStart()繼續運行,還是在OSStatInit()中,空閑計數器將1秒鐘內計數的值存入空閑計數器最大值OSIdleCtrMax中[圖F3.4(15)]。 OSStarInit()將統計任務就緒標志OSStatRdy設為“真”[圖F3.4(16)],以此來允許兩個時鐘節拍以后OSTaskStat()開始計算CPU的利用率。 統計任務的初始化函數OSStatInit()的代碼如程序清單 L3.13所示。
統計任務OSStat()的代碼程序清單L3.14所示。在前面一段中,已經討論了為什么要等待統計任務就緒標志OSStatRdy[L3.14(1)]。這個任務每秒執行一次,以確定所有應用程序中的任務消耗了多少CPU時間。當用戶的應用程序代碼加入以后,運行空閑任務的CPU時間就少了,OSIdleCtr就不會像原來什么任務都不運行時有那么多計數。要知道,OSIdleCtr的最大計數值是OSStatInit()在初始化時保存在計數器最大值OSIdleCtrMax中的。CPU利用率(表達式[3.1])是保存在變量OSCPUsage[L3.14(2)]中的: [3.1]表達式 Need to typeset the equation. 一旦上述計算完成,OSTaskStat()調用任務統計外界接入函數OSTaskStatHook() [L3.14(3)],這是一個用戶可定義的函數,這個函數能使統計任務得到擴展。這樣,用戶可以計算并顯示所有任務總的執行時間,每個任務執行時間的百分比以及其它信息(參見1.09節例3)。
μC/OS中,中斷服務子程序要用匯編語言來寫。然而,如果用戶使用的C語言編譯器支持在線匯編語言的話,用戶可以直接將中斷服務子程序代碼放在C語言的程序文件中。中斷服務子程序的示意碼如程序清單L3.15所示。
用戶代碼應該將全部CPU寄存器推入當前任務棧[L3.15(1)]。注意,有些微處理器,例如Motorola68020(及68020以上的微處理器),做中斷服務時使用另外的堆棧。 μC/OS-Ⅱ可以用在這類微處理器中,當任務切換時,寄存器是保存在被中斷了的那個任務的棧中的。 μC/OS-Ⅱ需要知道用戶在做中斷服務,故用戶應該調用OSIntEnter(),或者將全程變量OSIntNesting[L3.15(2)]直接加1,如果用戶使用的微處理器有存儲器直接加1的單條指令的話。如果用戶使用的微處理器沒有這樣的指令,必須先將OSIntNesting讀入寄存器,再將寄存器加1,然后再寫回到變量OSIatNesting中去,就不如調用OSIatEnter()。OSIntNesting是共享資源。OSIntEnter()把上述三條指令用開中斷、關中斷保護起來,以保證處理OSIntNesting時的排它性。直接給OSIntNesting加1比調用OSIntEnter()快得多,可能時,直接加1更好。要當心的是,在有些情況下,從OSIntEnter()返回時,會把中斷開了。遇到這種情況,在調用OSIntEnter()之前要先清中斷源,否則,中斷將連續反復打入,用戶應用程序就會崩潰! 上述兩步完成以后,用戶可以開始服務于叫中斷的設備了[L3.15(3)]。這一段完全取決于應用。μC/OS-Ⅱ允許中斷嵌套,因為μC/OS-Ⅱ跟蹤嵌套層數OSIntNesting。然而,為允許中斷嵌套,在多數情況下,用戶應在開中斷之前先清中斷源。 調用脫離中斷函數OSIntExit()[L3.15(4)]標志著中斷服務子程序的終結,OSIntExit()將中斷嵌套層數計數器減1。當嵌套計數器減到零時,所有中斷,包括嵌套的中斷就都完成了,此時μC/OS-Ⅱ要判定有沒有優先級較高的任務被中斷服務子程序(或任一嵌套的中斷)喚醒了。如果有優先級高的任務進入了就緒態,μC/OS-Ⅱ就返回到那個高優先級的任務,OSIntExit()返回到調用點[L3.15(5)]。保存的寄存器的值是在這時恢復的,然后是執行中斷返回指令[L3.16(6)]。注意,如果調度被禁止了(OSIntNesting>0),μC/OS-Ⅱ將被返回到被中斷了的任務。 以上描述的詳細解釋如圖F3.5所示。中斷來到了[F3.5(1)]但還不能被被CPU識別,也許是因為中斷被μC/OS-Ⅱ或用戶應用程序關了,或者是因為CPU還沒執行完當前指令。一旦CPU響應了這個中斷[F3.5(2)],CPU的中斷向量(至少大多數微處理器是如此)跳轉到中斷服務子程序[F3.5(3)]。如上所述,中斷服務子程序保存CPU寄存器(也叫做 CPU context)[F3.5(4)],一旦做完,用戶中斷服務子程序通知μC/OS-Ⅱ進入中斷服務子程序了,辦法是調用OSIntEnter()或者給OSIntNesting直接加1[F3.5(5)]。然后用戶中斷服務代碼開始執行[F3.5(6)]。用戶中斷服務中做的事要盡可能地少,要把大部分工作留給任務去做。中斷服務子程序通知某任務去做事的手段是調用以下函數之一:OSMboxPost(),OSQPost(),OSQPostFront(),OSSemPost()。中斷發生并由上述函數發出消息時,接收消息的任務可能是,也可能不是掛起在郵箱、隊列或信號量上的任務。用戶中斷服務完成以后,要調用OSIntExit()[F3.5(7)]。從時序圖上可以看出,對被中斷了的任務說來,如果沒有高優先級的任務被中斷服務子程序激活而進入就緒態,OSIntExit()只占用很短的運行時間。進而,在這種情況下,CPU寄存器只是簡單地恢復[F3.5(8)]并執行中斷返回指令[F3.5(9)]。如果中斷服務子程序使一個高優先級的任務進入了就緒態,則OSIntExit()將占用較長的運行時間,因為這時要做任務切換[F3.5(10)]。新任務的寄存器內容要恢復并執行中斷返回指令[F3.5(12)]。 圖3.5 中斷服務 進入中斷函數OSIntEnter()的代碼如程序清單L3.16所示,從中斷服務中退出函數OSIntExit()的代碼如程序清單L3.17所示。如前所述,OSIntEnter()所做的事是非常少的。
OSIntExit()看起來非常像OSSched()。但有三點不同。第一點,OSIntExit()使中斷嵌套層數減1[L3.17(2)]而調度函數OSSched()的調度條件是:中斷嵌套層數計數器和鎖定嵌套計數器(OSLockNesting)二者都必須是零。第二個不同點是,OSRdyTbl[]所需的檢索值Y是保存在全程變量OSIntExitY中的[L3.17(3)]。這是為了避免在任務棧中安排局部變量。這個變量在哪兒和中斷任務切換函數OSIntCtxSw()有關,(見9.04.03節,中斷任務切換函數)。最后一點,如果需要做任務切換,OSIntExit()將調用OSIntCtxSw()[L3.17(4)]而不是調用OS_TASK_SW(),正像在OSSched()函數中那樣。 調用中斷切換函數OSIntCtxSw()而不調用任務切換函數OS_TASK_SW(),有兩個原因,首先是,如程序清單中L3.5(1)和圖F3.6(1)所示,一半的工作,即CPU寄存器入棧的工作已經做完了。第二個原因是,在中斷服務子程序中調用OSIntExit()時,將返回地址推入了堆棧[L3.15(4)和F3.6(2)]。OSIntExit()中的進入臨界段函數OS_ENTER_CRITICAL()或許將CPU的狀態字也推入了堆棧L3.7(1)和F3.6(3)。這取決于中斷是怎么被關掉的(見第8章移植μC/OS-Ⅱ)。最后,調用OSIntCtxSw()時的返回地址又被推入了堆棧[L3.17(4)和F3.1(4)],除了棧中不相關的部分,當任務掛起時,棧結構應該與μC/OS-Ⅱ所規定的完全一致。OSIntCtxSw()只需要對棧指針做簡單的調整,如圖F3.6(5)所示。換句話說,調整棧結構要保證所有掛起任務的棧結構看起來是一樣的。 圖3.6中斷中的任務切換函數OSIntCtxSw()調整棧結構 有的微處理器,像Motorola 68HC11中斷發生時CPU寄存器是自動入棧的,且要想允許中斷嵌套的話,在中斷服務子程序中要重新開中斷,這可以視作一個優點。確實,如果用戶中斷服務子程序執行得非常快,用戶不需要通知任務自身進入了中斷服務,只要不在中斷服務期間開中斷,也不需要調用OSIntEnter()或OSIntNesting加1。程序清單L3。18中的示意代碼表示這種情況。一個任務和這個中斷服務子程序通訊的唯一方法是通過全程變量。
4. μC/OS需要用戶提供周期性信號源,用于實現時間延時和確認超時。節拍率應在每秒10次到100次之間,或者說10到100Hz。時鐘節拍率越高,系統的額外負荷就越重。時鐘節拍的實際頻率取決于用戶應用程序的精度。時鐘節拍源可以是專門的硬件定時器,也可以是來自50/60Hz交流電源的信號。 用戶必須在多任務系統啟動以后再開啟時鐘節拍器,也就是在調用OSStart()之后。換句話說,在調用OSStart()之后做的第一件事是初始化定時器中斷。通常,容易犯的錯誤是將允許時鐘節拍器中斷放在系統初始化函數OSInit()之后,在調啟動多任務系統啟動函數OSStart()之前,如程序清單L3.19所示。
這里潛在地危險是,時鐘節拍中斷有可能在μC/OS-Ⅱ啟動第一個任務之前發生,此時μC/OS-Ⅱ是處在一種不確定的狀態之中,用戶應用程序有可能會崩潰。 μC/OS-Ⅱ中的時鐘節拍服務是通過在中斷服務子程序中調用OSTimeTick()實現的。時鐘節拍中斷服從所有前面章節中描述的規則。時鐘節拍中斷服務子程序的示意代碼如程序清單L3.20所示。這段代碼必須用匯編語言編寫,因為在C語言里不能直接處理CPU的寄存器。
時鐘節拍函數OSTimeTick()的代碼如程序清單3.21所示。OSTimtick()以調用可由用戶定義的時鐘節拍外連函數OSTimTickHook()開始,這個外連函數可以將時鐘節拍函數OSTimtick()予以擴展[L3.2(1)]。筆者決定首先調用OSTimTickHook()是打算在時鐘節拍中斷服務一開始就給用戶一個可以做點兒什么的機會,因為用戶可能會有一些時間要求苛刻的工作要做。OSTimtick()中量大的工作是給每個用戶任務控制塊OS_TCB中的時間延時項OSTCBDly減1(如果該項不為零的話)。OSTimTick()從OSTCBList開始,沿著OS_TCB鏈表做,一直做到空閑任務[L3.21(3)]。當某任務的任務控制塊中的時間延時項OSTCBDly減到了零,這個任務就進入了就緒態[L3.21(5)]。而確切被任務掛起的函數OSTaskSuspend()掛起的任務則不會進入就緒態[L3.21(4)]。OSTimTick()的執行時間直接與應用程序中建立了多少個任務成正比。
OSTimeTick()還通過調用OSTime()[L3.21(7)]累加從開機以來的時間,用的是一個無符號32位變量。注意,在給OSTime加1之前使用了關中斷,因為多數微處理器給32位數加1的操作都得使用多條指令。 中斷服務子程序似乎就得寫這么長,如果用戶不喜歡將中斷服務程序寫這么長,可以從任務級調用OSTimeTick(),如程序清單L3.22所示。要想這么做,得建立一個高于應用程序中所有其它任務優先級的任務。時鐘節拍中斷服務子程序利用信號量或郵箱發信號給這個高優先級的任務。
用戶當然需要先建立一個郵箱(初始化成NULL)用于發信號給上述任何告知時鐘節拍中斷已經發生了(程序清單L3.23)。
在調用μC/OS-Ⅱ的任何其它服務之前,μC/OS-Ⅱ要求用戶首先調用系統初始化函數OSIint()。OSIint()初始化μC/OS-Ⅱ所有的變量和數據結構(見OS_CORE.C)。 OSInit()建立空閑任務idle task,這個任務總是處于就緒態的。空閑任務OSTaskIdle()的優先級總是設成最低,即OS_LOWEST_PRIO。如果統計任務允許OS_TASK_STAT_EN和任務建立擴展允許都設為1,則OSInit()還得建立統計任務OSTaskStat()并且讓其進入就緒態。OSTaskStat的優先級總是設為OS_LOWEST_PRIO-1。 圖F3.7表示調用OSInit()之后,一些μC/OS-Ⅱ變量和數據結構之間的關系。其解釋是基于以下假設的: ?在文件OS_CFG.H中,OS_TASK_STAT_EN是設為1的。 ?在文件OS_CFG.H中,OS_LOWEST_PRIO是設為63的。 ?在文件OS_CFG.H中, 最多任務數OS_MAX_TASKS是設成大于2的。 以上兩個任務的任務控制塊(OS_TCBs)是用雙向鏈表鏈接在一起的。OSTCBList指向這個鏈表的起始處。當建立一個任務時,這個任務總是被放在這個鏈表的起始處。換句話說,OSTCBList總是指向最后建立的那個任務。鏈的終點指向空字符NULL(也就是零)。 因為這兩個任務都處在就緒態,在就緒任務表OSRdyTbl[]中的相應位是設為1的。還有,因為這兩個任務的相應位是在OSRdyTbl[]的同一行上,即屬同一組,故OSRdyGrp中只有1位是設為1的。 μC/OS-Ⅱ還初始化了4個空數據結構緩沖區,如圖F3.8所示。每個緩沖區都是單向鏈表,允許μC/OS-Ⅱ從緩沖區中迅速得到或釋放一個緩沖區中的元素。注意,空任務控制塊在空緩沖區中的數目取決于最多任務數OS_MAX_TASKS,這個最多任務數是在OS_CFG.H文件中定義的。μC/OS-Ⅱ自動安排總的系統任務數OS_N_SYS_TASKS(見文件μC/OS-Ⅱ.H)。控制塊OS_TCB的數目也就自動確定了。當然,包括足夠的任務控制塊分配給統計任務和空閑任務。指向空事件表OSEventFreeList和空隊列表OSFreeList的指針將在第6章,任務間通訊與同步中討論。指向空存儲區的指針表OSMemFreeList將在第7章存儲管理中討論。 多任務的啟動是用戶通過調用OSStart()實現的。然而,啟動μC/OS-Ⅱ之前,用戶至少要建立一個應用任務,如程序清單L3.24所示。
圖3.7 調用OSInit()之后的數據結構 圖3.8 空緩沖區 OSStart()的代碼如程序清單L3.25所示。當調用OSStart()時,OSStart()從任務就緒表中找出那個用戶建立的優先級最高任務的任務控制塊[L3.25(1)]。然后,OSStart()調用高優先級就緒任務啟動函數OSStartHighRdy()[L3,25(2)],(見匯編語言文件OS_CPU_A.ASM),這個文件與選擇的微處理器有關。實質上,函數OSStartHighRdy()是將任務棧中保存的值彈回到CPU寄存器中,然后執行一條中斷返回指令,中斷返回指令強制執行該任務代碼。見9.04.01節,高優先級就緒任務啟動函數OSStartHighRdy()。那一節詳細介紹對于80x86微處理器是怎么做的。注意,OSStartHighRdy()將永遠不返回到OSStart()。
多任務啟動以后變量與數據結構中的內容如圖F3.9所示。這里筆者假設用戶建立的任務優先級為6,注意,OSTaskCtr指出已經建立了3個任務。OSRunning已設為“真”,指出多任務已經開始,OSPrioCur和OSPrioHighRdy存放的是用戶應用任務的優先級,OSTCBCur和OSTCBHighRdy二者都指向用戶任務的任務控制塊。 應用程序調用OSVersion()[程序清單L3.26]可以得到當前μC/OS-Ⅱ的版本號。OSVersion()函數返回版本號值乘以100。換言之,200表示版本號2.00。
為找到μC/OS-Ⅱ的最新版本以及如何做版本升級,用戶可以與出版商聯系,或者查看μC/OS-Ⅱ得正式網站WWW. uCOS-II.COM 圖3.9調用OSStart()以后的變量與數據結構 讀者或許注意到有4個OS_CORE.C中的函數沒有在本章中提到。這4個函數是OSEventWaitListInit(),OSEventTaskRdy(),OSEventTaskWait(),OSEventTO()。這幾個函數是放在文件OS_CORE.C中的,而對如何使用這個函數的解釋見第6章,任務間的通訊與同步。
|