uCOS-II中的任務切換機制


【@.1 函數周期與死循環】

 image

一般函數的生命周期很簡單,從開始調用函數起,直到函數返回,即結束。這樣一來就完成了這個函數的使命,它也就不再需要了。對於一般的函數就是這樣,但是回過頭想想,對於一個系統、OS、或者工業控制中的一個控制器重的系統個,函數返回是很輕易很隨便的就能返回嗎?返回就意味着函數結束,死亡,若是想系統這樣一個很大的函數,它的返回就意味着系統結束。因此,對於系統的函數返回有些時候我們不希望它返回,返回時是需要好好設計的,像嵌入式中的控制程序我們也並不需要它返回,直接關機就好了。因此,一個系統往往就是一個很大的循環,不停的掃描,而我們編程的時候對於這個死循環是需要好好設計的。考慮以下一個控制要求,

@.按鍵控制電機啟、停、正轉反轉,並每秒發送CAN報文報告當前情況。

我們可以有多種方法實現這一要求:

image

方法一:每次在循環體重掃描當前按鍵的電平,從而進入對應的控制電機函數,如果所有電平都沒有信號則直接進入下一個循環。發送CAN報文就直接用一個定時中斷。這樣的好處就是編程簡單直白,每次循環進入不同的電機控制函數,壞處很明顯,一定要等待到下一個循環才能進入其他的電機控制函數,每次循環的時間不好控制,不管你用函數指針還是if/else來判斷,每次循環一定要等待電機動作結束才能進入下一個循環。

image

方法二:改用外部中斷來處理按鍵。僅當按鍵按下時觸發外部中斷,從而控制響應的電機進行操作。這樣的好處就是循環體簡單,可以僅僅就是一個計數器加一,所有控制都等中斷來實現。但這樣帶來的問題也很明顯,就是中斷嵌套問題。比如當電機正轉時按下停止按鈕,這時由於是在中斷中,停止按鈕是否真的能夠得到響應?這就涉及到中斷嵌套問題,並不見得所有CPU都能支持中斷嵌套,我的這一篇文章對中斷嵌套問題進行了一個討論。

image

方法三:采用RTOS的思想,加入任務調度系統。每次任務調度系統就是一個小小的循環,對於各個任務進行輪詢,當這次輪詢發現某任務是已經就緒的優先級最高的任務,則交給CPU處理,所有中斷與任務,任務與任務之間有通訊機制可以交換信息。當然實際上並不僅僅只在任務調度器輪詢時才進行任務的切換,實際上的操作比這個復雜一些,我的這篇文章就想以uCOS-II為例討論RTOS的任務調度系統是怎樣執行的。

【@.2 uCOS-II中的任務調度】

回到前面的方法一,二,我們將這種任務稱作前后台任務。

image

后台就是指程序的大循環,程序一定要等到前台任務(可以說中斷或某個功能函數)返回才能繼續運行下去。而在RTOS中每個任務都有自己的控制塊指向改任務,由任務調度器來決定這個時候該運行哪個任務。

 image

所有這些任務控制快(TCB)構成一個雙向鏈表,每個TCB中都有一些控制字,比如一個指向堆棧的指針(*sp),一個表明當前任務狀態的位(State),指明任務被掛起等待的超時時間(dly),任務的優先級(Prio),指向事件控制塊的指針(*Event,事件機制后面會討論)。一個全局的任務就續表記錄了當前任務是否就緒,任務調度器就靠查詢任務就緒表尋找到就緒任務中優先級最高的一個任務。

每一個任務都是一個死循環,並且必須在循環內調用系統函數來釋放CPU控制權,比如調用系統的延時函數OSTimeDly()延時一段時間,這個時候系統就會知道這個任務被延時了,延時時間記錄在TCB中的超時時間dly中,任務調度器將其他任務予以運行。

實際的任務調度有兩種機制

1.中斷級任務調度:任何中斷返回時必須調用一個系統函數OSIntExit(void),進行一次任務調度。比如一般任務調度器通常是一個定時中斷,比如1000次每秒,成為OS時鍾。每次OS時鍾要返回時都會調用OSInitExit()進行任務切換,運行當前就緒的優先級最高任務。一般這個OS時鍾的優先級很低,為整個系統優先級倒數第二低(倒數第一低的是Idle Task,空閑任務),因此實際上這個OS時鍾最終並不會返回。

2.任務級任務調度:在任務運行中執行一次OS的特定函數,比如前面提到的OSTimeDly(),此函數在返回之前會執行一次任務調度。

因此總的任務調度次數會遠遠高於OS時鍾的輪詢頻率的。

我們下面舉例來說明這兩種調度機制:

【@.3 中斷級任務調度】

image

當OS時鍾的定時中斷來臨時(比如1000次每秒),會進入OS時鍾的服務函數,它會做大致一下工作,遞增一個全局的計數器OSTIme,遍歷所有OSTCB鏈表,將其中的dly超時時間遞減,若等於0,則在就緒列表中置位。最后這個中斷要返回時會調用OSInitExit(),它會找到在任務就緒表中處於就緒狀態的最高優先級的任務,並且調用OSInitCtxSw(),即上下文切換,將之前找到的任務放在任務堆棧中的寄存器推入到CPU的R0~R15,CPSR中,最后結束。這里的這個OSInitCtxSw(),上下文切換函數是實際上跟CPU打交道的函數,也是移植uCOS-II需要編寫的函數。

任何中斷返回時都必須調用這個OSInitExit(),不管是外部中斷,定時器中斷,串口中斷。當然,我們可以在實際編寫中將這一中斷過程編寫一個統一模板,調用中斷服務實際函數。這是后話了。

【@.4 任務級任務調度】

image

這種調度方式實際上豐富了任務之間的通訊機制以及任務的控制,也是最需要理解的一種方式。主要的流程跟中斷級方式類似,不過是需要在任務中調用一些特定的系統函數。實際的任務切換函數是OSSched(),會調用實際的上下文切換函數OS_TASK_SW()。這個函數往往跟中斷級的上下文切換函數是一樣的,我們移植時只需要編寫一個函數即可。下面以幾個具體的函數為例進行講解:

1.OSTaskResume/ OSTaskSuspend

image

首先是一個比較好理解的OSTaskSuspend()和OSTaskResume(),直接控制任務的掛起和恢復。對於圖中的TaskA,在任務運行到某處調用OSTaskSuspend()之后,會掛起就緒表中優先級為prio的任務x(優先級也就是這個函數的傳入參數)。之后調用OSSched,進行任務切換。若掛起的是自己,即調用OSTaskSuspend時傳入的優先級是自己的,則自身被掛起,函數不返回;若掛起的是別的任務,那么別的任務就直接被掛起,當前函數返回。

另一個任務TaskB中調用OSTaskResume(),將首先在置位表中置位優先級為prio的任務,之后交給OSSched()進行任務切換,之后就是一個優先級的判斷問題,若恢復的任務比自己任務高,那么自己被掛起,高優先級任務運行;若被恢復的任務優先級低於自己,則函數自己函數返回,繼續運行,直到TaskB運行到其他函數釋放CPU控制權。所以像TaskB這樣的任務中還需要包含一個其他的函數,比如OSTimeDly來釋放CPU。

最后,若恢復的任務是自己,很明顯是沒有意義的。

2.OSSemPost/OSSemPend

在通訊機制上uCOS-II利用事件管理塊(ECB)統一進行處理,其中信號量是最基本的一種方式,可以起到共享資源單獨訪問和任務間同步的功能。

 image

我們建立一個信號量,類型為OS_EVENT的指針,其內部包含了一個事件等待表,記錄着等待該事件(也就是Sem自身)的所有任務,當某一任務等待此信號量時,在它的事件等待表上相應優先級置一。若任務TaskA調用了OSSemPend()等待一個信號量,此函數會調用OS_EventTaskWait,一個內部的事件等待函數。圖中表明了此函數的流程,首先會建立任務A的TCB與此信號量Sem之間的鏈接,即TCB中的Event指針指向Sem。之后清楚任務就緒表中的相應位,再將Sem中的事件等待表置一,OS_EventTaskWait之后會調用OSSched進行實際的上下問切換,此時由於之前的一些列操作將導致這個時候任務會被掛起,只能等待其他任務(或等待超時)將其重新恢復成就緒狀態。

調用OSSemPost時的流程如下

 image

OSSemPost會調用OS_EventTaskRdy,圖中可以看出這個函數的流程,首先從Sem的等待事件列表中尋找到等待該事件的優先級最高的任務。可能同時有多個任務在等待此信號量,假如其中優先級最高的尋找結果就是前面的TaskA,則接下來將切斷此任務的TCB與ECB的聯系,並且在任務就續表中置位此任務。OS_EventTaskRdy返回之后將進入OSSched,將根據前面的一些列操作進行任務調度,當然,若發送信號量后接收方(TaskA)的優先級大於發送方(TaskB),則TaskB自身被掛起,運行TaskA;反之,TaskB繼續運行。

這種信號量的發送/接收機制適用於任務同步,共享資源的獨占,比如:

image

上圖中所示,當TaskA調用Pend接收到信號量Sem時執行foo函數,此時TaskB在Pend處,不管怎么調度也不會執行foo函數,直到TaskA運行到Post處時才有可能執行下去。這樣就防止了同一時間有兩個任務同時處理共享資源(這里是函數foo())。

image

另一種情況如上圖示,Post一方由一個任務或一個中斷服務函數調用,接受的一方由另一個任務調用Pend。只有當發送方Post這個信號量之后,接收方Pend處才有可能繼續往下運行。

注意:這點跟OSTaskResume恢復一個任務很像,但是還是有區別,一旦Resume一個任務這個任務就一直運行下去了,但是Post一個信號量之后含有Pend的任務執行一次,之后到進入任務的下個循環繼續Pend,只有等新的一次Post了之后才能繼續運行。

3.OSFlagPost/OSFlagPend

信號量集的發送和接受,也是一種多信號的集體通訊方式,這里不詳細討論,只舉例簡單說明其作用

image

圖中所示TaskA和TaskB共同控制TaskTarget,只有當TaskA,B都發送了信號之后,TaskTarget才得以運行。實際上這種信號集的機制還可以有其他的邏輯關系可以配置,比如發送方中任意一個發送任務之后就可以讓接收方運行。

【@.5 任務調度總結】

先簡單的利用前面的內容,回過頭來看文章一開頭的任務,搭建一個完整的電機控制系統。這里我們利用uCOS-II中的通訊機制進行設計。

例:帶按鍵去抖的電機控制任務:

image

四個外部中斷控制電機的啟停,正轉反轉。外部中斷服務函數中Post一個信號量給一個專門的電機調度任務,電機調度任務之后Post一個信號量給按鍵去抖任務,此任務會利用OSTimeDly函數延時一段時間,比如說20ms,再次判斷剛才按鍵的電平值,若任然有按鍵值則說明不是抖動,可以控制電機,之后利用Resume,恢復一個電機轉動的實際任務。

Post信號量啟用一個任務跟Resume恢復一個任務是區別很大的,最大的區別是,Post信號量之后只能讓接收方運行一次,必須不停的Post才能不停的運行,而Resume一個任務之后,被恢復的任務將一直運行,只能用Suspend將其再次掛起。

下圖表明了任務狀態之前切換的關系,調用哪種函數可以進行任務切換。uCOS-II就是通過這些直接或間接調用的系統函數進行任務切換的。

image

@.[FIN]      @.date->Mar 28, 2013      @.author->apollius


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM