FreeRTOS——任務基礎知識

Winter_wzy發表於2018-11-29

        RTOS 系統的核心就是任務管理,FreeRTOS 也不例外,而且大多數學習RTOS 系統的工程師或者學生主要就是為了使用RTOS 的多工處理功能,初步上手RTOS 系統首先必須掌握的也是任務的建立、刪除、掛起和恢復等操作,由此可見任務管理的重要性。本章分為如下幾部分:
5.1 什麼是多工系統
5.2 FreeRTOS 任務與協程
5.3 初次使用
5.3 任務狀態
5.4 任務優先順序
5.5 任務實現
5.6 任務控制塊
5.7 任務堆疊

5.1 什麼是多工系統?
回想一下我們以前在使用51、AVR、STM32 微控制器裸機(未使用系統)的時候一般都是在main 函式裡面用while(1)做一個大迴圈來完成所有的處理,即應用程式是一個無限的迴圈,迴圈中呼叫相應的函式完成所需的處理。有時候我們也需要中斷中完成一些處理。相對於多工系統而言,這個就是單任務系統,也稱作前後臺系統,中斷服務函式作為前臺程式,大迴圈while(1)作為後臺程式,如圖5.1.1 所示:

                                                    

前後臺系統的實時性差,前後臺系統各個任務(應用程式)都是排隊等著輪流執行,不管你這個程式現在有多緊急,沒輪到你就只能等著!相當於所有任務(應用程式)的優先順序都是一樣的。但是前後臺系統簡單啊,資源消耗也少啊!在稍微大一點的嵌入式應用中前後臺系統就明顯力不從心了,此時就需要多工系統出馬了。多工系統會把一個大問題(應用)“分而治之”,把大問題劃分成很多個小問題,逐步的把小問題解決掉,大問題也就隨之解決了,這些小問題可以單獨的作為一個小任務來處理。這些小任務是併發處理的,注意,並不是說同一時刻一起執行很多個任務,而是由於每個任務執行的時間很短,導致看起來像是同一時刻執行了很多個任務一樣。多個任務帶來了一個新的問題,究竟哪個任務先執行,哪個任務後執行呢?完成這個功能的東西在RTOS 系統中叫做任務排程器。不同的系統其任務排程器的實現方法也不同,比如FreeRTOS 是一個搶佔式的實時多工系統,那麼其任務排程器也是搶佔式的,執行過程如圖5.1.2 所示:

                                                      

在圖5.1.2 中,高優先順序的任務可以打斷低優先順序任務的執行而取得CPU 的使用權,這樣就保證了那些緊急任務的執行。這樣我們就可以為那些對實時性要求高的任務設定一個很高的優先順序,比如自動駕駛中的障礙物檢測任務等。高優先順序的任務執行完成以後重新把CPU 的使用權歸還給低優先順序的任務,這個就是搶佔式多工系統的基本原理。

5.2 FreeRTOS 任務與協程

再FreeRTOS 中應用既可以使用任務,也可以使用協程(Co-Routine),或者兩者混合使用。但是任務和協程使用不同的API 函式,因此不能通過佇列(或訊號量)將資料從任務傳送給協程,反之亦然。協程是為那些資源很少的MCU 準備的,其開銷很小,但是FreeRTOS 官方已經不打算再更新協程了,所以本教程只講解任務。

5.2.1 任務(Task)的特性
在使用RTOS 的時候一個實時應用可以作為一個獨立的任務。每個任務都有自己的執行環境,不依賴於系統中其他的任務或者RTOS 排程器。任何一個時間點只能有一個任務執行,具體執行哪個任務是由RTOS 排程器來決定的,RTOS 排程器因此就會重複的開啟、關閉每個任務。任務不需要了解RTOS 排程器的具體行為,RTOS 排程器的職責是確保當一個任務開始執行的時候其上下文環境(暫存器值,堆疊內容等)和任務上一次退出的時候相同。為了做到這一點,每個任務都必須有個堆疊,當任務切換的時候將上下文環境儲存在堆疊中,這樣當任務再次執行的時候就可以從堆疊中取出上下文環境,任務恢復執行。

任務特性:
1、簡單。
2、沒有使用限制。
3、支援搶佔
4、支援優先順序
5、每個任務都擁有堆疊導致了RAM 使用量增大。

6、如果使用搶佔的話的必須仔細的考慮重入的問題。

5.2.2 協程(Co-routine)的特性
協程是為那些資源很少的MCU 而做的,但是隨著MCU 的飛速發展,效能越來越強大,現在協程幾乎很少用到了!但是FreeRTOS 目前還沒有把協程移除的計劃,但是FreeRTOS 是絕對不會再更新和維護協程了,因此協程大家瞭解一下就行了。在概念上協程和任務是相似的,但是有如下根本上的不同:
1、堆疊使用
所有的協程使用同一個堆疊(如果是任務的話每個任務都有自己的堆疊),這樣就比使用任
務消耗更少的RAM。
2、排程器和優先順序
協程使用合作式的排程器,但是可以在使用搶佔式的排程器中使用協程。
3、巨集實現
協程是通過巨集定義來實現的。
4、使用限制

為了降低對RAM 的消耗做了很多的限制。

5.3 任務狀態
FreeRTOS 中的任務永遠處於下面幾個狀態中的某一個:
● 執行態
當一個任務正在執行時,那麼就說這個任務處於執行態,處於執行態的任務就是當前正在使用處理器的任務。如果使用的是單核處理器的話那麼不管在任何時刻永遠都只有一個任務處於執行態。
● 就緒態
處於就緒態的任務是那些已經準備就緒(這些任務沒有被阻塞或者掛起),可以執行的任務,但是處於就緒態的任務還沒有執行,因為有一個同優先順序或者更高優先順序的任務正在執行!
● 阻塞態
如果一個任務當前正在等待某個外部事件的話就說它處於阻塞態,比如說如果某個任務呼叫了函式vTaskDelay()的話就會進入阻塞態,直到延時週期完成。任務在等待佇列、訊號量、事件組、通知或互斥訊號量的時候也會進入阻塞態。任務進入阻塞態會有一個超時時間,當超過這個超時時間任務就會退出阻塞態,即使所等待的事件還沒有來臨!
● 掛起態
像阻塞態一樣,任務進入掛起態以後也不能被排程器呼叫進入執行態,但是進入掛起態的任務沒有超時時間。任務進入和退出掛起態通過呼叫函式vTaskSuspend()和xTaskResume()。任務狀態之間的轉換如圖5.4.1 所示:

                                                          

5.4 任務優先順序

每個任務都可以分配一個從0~(configMAX_PRIORITIES-1) 的優先順序,configMAX_PRIORITIES 在檔案FreeRTOSConfig.h 中有定義,前面我們講解FreeRTOS 系統配置的時候已經講過了。如果所使用的硬體平臺支援類似計算前導零這樣的指令(可以通過該指令選擇下一個要執行的任務, Cortex-M 處理器是支援該指令的) , 並且巨集configUSE_PORT_OPTIMISED_TASK_SELECTION 也設定為了1 , 那麼巨集configMAX_PRIORITIES 不能超過32 ! 也就是優先順序不能超過32 級。其他情況下巨集configMAX_PRIORITIES 可以為任意值,但是考慮到RAM 的消耗,巨集configMAX_PRIORITIES最好設定為一個滿足應用的最小值。優先順序數字越低表示任務的優先順序越低,0 的優先順序最低,configMAX_PRIORITIES-1 的優先順序最高。空閒任務的優先順序最低,為0。FreeRTOS 排程器確保處於就緒態或執行態的高優先順序的任務獲取處理器使用權,換句話說就是處於就緒態的最高優先順序的任務才會執行。當巨集configUSE_TIME_SLICING 定義為1 的時候多個任務可以共用一個優先順序,數量不限。預設情況下巨集configUSE_TIME_SLICING 在檔案FreeRTOS.h 中已經定義為1。此時處於就緒態的優先順序相同的任務就會使用時間片輪轉排程器獲取執行時間。

5.5 任務實現
在使用FreeRTOS 的過程中,我們要使用函式xTaskCreate()或xTaskCreateStatic()來建立任務,這兩個函式的第一個引數pxTaskCode,就是這個任務的任務函式。什麼是任務函式?任務函式就是完成本任務工作的函式。我這個任務要幹嘛?要做什麼?要完成什麼樣的功能都是在這個任務函式中實現的。 比如我要做個任務,這個任務要點個流水燈,那麼這個流水燈的程式就是任務函式中實現的。FreeRTOS 官方給出的任務函式模板如下:

void vATaskFunction(void *pvParameters) (1)
{
for( ; ; ) (2)
{
--任務應用程式-- (3)
vTaskDelay(); (4)
}
/* 不能從任務函式中返回或者退出, 從任務函式中返回或退出的話就會呼叫
configASSERT(),前提是你定義了configASSERT()。如果一定要從任務函式中退出的話那一定
要呼叫函式vTaskDelete(NULL)來刪除此任務。*/
vTaskDelete(NULL); (5)
}
(1)、任務函式本質也是函式,所以肯定有任務名什麼的,不過這裡我們要注意:任務函式的返回型別一定要為void 型別,也就是無返回值,而且任務的引數也是void 指標型別的!任務函式名可以根據實際情況定義。
(2)、任務的具體執行過程是一個大迴圈,for(; ; )就代表一個迴圈,作用和while(1)一樣,筆者習慣用while(1)。
(3)、迴圈裡面就是真正的任務程式碼了,此任務具體要乾的活就在這裡實現!
(4)、FreeRTOS 的延時函式,此處不一定要用延時函式,其他只要能讓FreeRTOS 發生任務切換的API 函式都可以,比如請求訊號量、佇列等,甚至直接呼叫任務排程器。只不過最常用的就是FreeRTOS 的延時函式。
(5)、任務函式一般不允許跳出迴圈,如果一定要跳出迴圈的話在跳出迴圈以後一定要呼叫函式vTaskDelete(NULL)刪除此任務!FreeRTOS 的任務函式和UCOS 的任務函式模式基本相同的,不止FreeRTOS,其他RTOS的任務函式基本也是這種方式的。

5.6 任務控制塊
FreeRTOS 的每個任務都有一些屬性需要儲存,FreeRTOS 把這些屬性集合到一起用一個結構體來表示,這個結構體叫做任務控制塊:TCB_t,在使用函式xTaskCreate()建立任務的時候就會自動的給每個任務分配一個任務控制塊。在老版本的FreeRTOS 中任務控制塊叫做tskTCB,新版本重新命名為TCB_t,但是本質上還是tskTCB,本教程後面提到任務控制塊的話均用TCB_t表示,此結構體在檔案tasks.c 中有定義,如下:

typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; //任務堆疊棧頂
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGS xMPUSettings; //MPU 相關設定
#endif
ListItem_t xStateListItem; //狀態列表項
ListItem_t xEventListItem; //事件列表項
UBaseType_t uxPriority; //任務優先順序
StackType_t *pxStack; //任務堆疊起始地址
char pcTaskName[ configMAX_TASK_NAME_LEN ];//任務名字
#if ( portSTACK_GROWTH > 0 )
StackType_t *pxEndOfStack; //任務堆疊棧底
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting; //臨界區巢狀深度
#endif
#if ( configUSE_TRACE_FACILITY == 1 ) //trace 或到debug 的時候用到
UBaseType_t uxTCBNumber;
UBaseType_t uxTaskNumber;
#endif
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority; //任務基礎優先順序,優先順序反轉的時候用到
UBaseType_t uxMutexesHeld; //任務獲取到的互斥訊號量個數
#endif
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif
#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 ) //與本地儲存有關
void
*pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif
#if( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter; //用來記錄任務執行總時間
#endif
#if ( configUSE_NEWLIB_REENTRANT == 1 )
struct _reent xNewLib_reent; //定義一個newlib 結構體變數
#endif
#if( configUSE_TASK_NOTIFICATIONS == 1 ) //任務通知相關變數
volatile uint32_t ulNotifiedValue; //任務通知值
volatile uint8_t ucNotifyState; //任務通知狀態
#endif
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
//用來標記任務是動態建立的還是靜態建立的,如果是靜態建立的此變數就為pdTURE,
//如果是動態建立的就為pdFALSE
uint8_t ucStaticallyAllocated;
#endif
#if( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
} tskTCB;
//新版本的FreeRTOS 任務控制塊重新命名為TCB_t,但是本質上還是tskTCB,主要是為了相容
//舊版本的應用。
typedef tskTCB TCB_t;
可以看出來FreeRTOS 的任務控制塊中的成員變數相比UCOSIII 要少很多,而且大多數與裁剪有關,當不使用某些功能的時候與其相關的變數就不參與編譯,任務控制塊大小就會進一步的減小。

5.7 任務堆疊
FreeRTOS 之所以能正確的恢復一個任務的執行就是因為有任務堆疊在保駕護航,任務排程器在進行任務切換的時候會將當前任務的現場(CPU 暫存器值等)儲存在此任務的任務堆疊中,等到此任務下次執行的時候就會先用堆疊中儲存的值來恢復現場,恢復現場以後任務就會接著從上次中斷的地方開始執行。建立任務的時候需要給任務指定堆疊,如果使用的函式xTaskCreate()建立任務(動態方法)的話那麼任務堆疊就會由函式xTaskCreate()自動建立,後面分析xTaskCreate()的時候會講解。如果使用函式xTaskCreateStatic()建立任務(靜態方法)的話就需要程式設計師自行定義任務堆疊,然後堆疊首地址作為函式的引數puxStackBuffer 傳遞給函式,如下:
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint32_t ulStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
StackType_t * const puxStackBuffer, (1)
StaticTask_t * const pxTaskBuffer )
(1)、任務堆疊,需要使用者定義,然後將堆疊首地址傳遞給這個引數。堆疊大小:我們不管是使用函式xTaskCreate()還是xTaskCreateStatic()建立任務都需要指定任務堆疊大小。任務堆疊的資料型別為StackType_t,StackType_t 本質上是uint32_t,在portmacro.h 中有定義,如下:
#define portSTACK_TYPE uint32_t
#define portBASE_TYPE long
typedef portSTACK_TYPE StackType_t;
typedef long BaseType_t;
typedef unsigned long UBaseType_t;
可以看出StackType_t 型別的變數為4 個位元組,那麼任務的實際堆疊大小就應該是我們所定義的4 倍
——————————以上轉自原子哥開發手冊,學習記錄應用,謝謝!