首頁 > 軟體

FreeRTOS實時作業系統多工管理基礎知識

2022-04-06 16:00:11

RTOS 系統的核心就是任務管理,FreeRTOS 也不例外,而且大多數學習 RTOS 系統的工程師或者學生主要就是為了使用 RTOS 的多工處理功能,初步上手 RTOS 系統首先必須掌握的也是任務的建立、刪除、掛起和恢復等操作,由此可見任務管理的重要性。

什麼是多工系統?

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

前後臺系統的實時性差,前後臺系統各個任務(應用程式)都是排隊等著輪流執行,不管你這個程式現在有多緊急,沒輪到你就只能等著!相當於所有任務(應用程式)的優先順序都是一樣的。但是前後臺系統簡單啊,資源消耗也少啊!在稍微大一點的嵌入式應用中前後臺系統就明顯力不從心了,此時就需要多工系統出馬了。

多工系統會把一個大問題(應用)“分而治之”,把大問題劃分成很多個小問題,逐步的把小問題解決掉,大問題也就隨之解決了,這些小問題可以單獨的作為一個小任務來處理。這些小任務是並行處理的,注意,並不是說同一時刻一起執行很多個任務,而是由於每個任務執行的時間很短,導致看起來像是同一時刻執行了很多個任務一樣。多個任務帶來了一個新的問題,究竟哪個任務先執行,哪個任務後執行呢?完成這個功能的東西在 RTOS 系統中叫做任務排程器。不同的系統其任務排程器的實現方法也不同,比如 FreeRTOS 是一個搶佔式的實時多工系統,那麼其任務排程器也是搶佔式的。

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

FreeRTOS  任務與協程

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

1.任務(Task) 的特性

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

任務特性: 簡單。沒有使用限制。支援搶佔支援優先順序每個任務都擁有堆疊導致了 RAM 使用量增大。如果使用搶佔的話的必須仔細的考慮重入的問題。

2.協程(Co-routine)的特性

協程是為那些資源很少的 MCU 而做的,但是隨著 MCU 的飛速發展,效能越來越強大,現
在協程幾乎很少用到了!但是 FreeRTOS 目前還沒有把協程移除的計劃,但是 FreeRTOS 是絕對
不會再更新和維護協程了,因此協程大家瞭解一下就行了。在概念上協程和任務是相似的,但
是有如下根本上的不同:

  • 堆疊使用:所有的協程使用同一個堆疊(如果是任務的話每個任務都有自己的堆疊),這樣就比使用任務消耗更少的 RAM。
  • 排程器和優先順序:協程使用合作式的排程器,但是可以在使用搶佔式的排程器中使用協程。
  • 宏實現:協程是通過宏定義來實現的。
  • 使用限制:為了降低對 RAM 的消耗做了很多的限制。

任務狀態

FreeRTOS 中的任務永遠處於下面幾個狀態中的某一個:

執行態

當一個任務正在執行時,那麼就說這個任務處於執行態,處於執行態的任務就是當前正在使用處理器的任務。如果使用的是單核處理器的話那麼不管在任何時刻永遠都只有一個任務處於執行態。

就緒態

處於就緒態的任務是那些已經準備就緒(這些任務沒有被阻塞或者掛起),可以執行的任務,但是處於就緒態的任務還沒有執行,因為有一個同優先順序或者更高優先順序的任務正在執行!

阻塞態

如果一個任務當前正在等待某個外部事件的話就說它處於阻塞態,比如說如果某個任務呼叫了函數 vTaskDelay()的話就會進入阻塞態,直到延時週期完成。任務在等待佇列、號誌、事件組、通知或互斥號誌的時候也會進入阻塞態。任務進入阻塞態會有一個超時時間,當超過這個超時時間任務就會退出阻塞態,即使所等待的事件還沒有來臨!

掛起態

像阻塞態一樣,任務進入掛起態以後也不能被排程器呼叫進入執行態,但是進入掛起態的任務沒有超時時間。任務進入和退出掛起態通過呼叫函數 vTaskSuspend()和 xTaskResume()。

任務優先順序

每 個 任 務 都 可 以 分 配 一 個 從 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。此時處於就緒態的優先順序相同的任務就會使用時間片輪轉排程器獲取執行時間。

任務實現

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

void vATaskFunction(void *pvParameters) 
{
    for( ; ; ) 
    {
        //--任務應用程式--
        vTaskDelay(); 
        /*此處不一定要用延時函數,其他只要能讓 FreeRTOS 發生工作切換的 API 函數都可以,
        比如請求號誌、佇列等,甚至直接呼叫任務排程器。只不過最常用的就是 FreeRTOS 的延時函數。*/
    }
    /*不能從任務函數中返回或者退出,從任務函數中返回或退出的話就會呼叫
    configASSERT(),前提是你定義了 configASSERT()。如果一定要從任務函數中退出的話那一定
    要呼叫函數 vTaskDelete(NULL)來刪除此任務。*/
    //vTaskDelete(NULL);  (5)
}

任務控制塊

FreeRTOS 的每個任務都有一些屬性需要儲存,FreeRTOS 把這些屬性集合到一起用一個結構體來表示,這個結構體叫做任務控制塊:TCB_t,在使用函數 xTaskCreate()建立任務的時候就會自動的給每個任務分配一個任務控制塊。在老版本的 FreeRTOS 中任務控制塊叫做 tskTCB,新版本重新命名為 TCB_t,但是本質上還是 tskTCB,本教學後面提到任務控制塊的話均用 TCB_t表示,此結構體在檔案 tasks.c 中有定義。 FreeRTOS 的任務控制塊中的成員變數相比 UCOSIII 要少很多,而且大多數與裁剪有關,當不使用某些功能的時候與其相關的變數就不參與編譯,任務控制塊大小就會進一步的減小。

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;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 之所以能正確的恢復一個任務的執行就是因為有任務堆疊在保駕護航,任務排程器在進行工作切換的時候會將當前任務的現場(CPU 暫存器值等)儲存在此任務的任務堆疊中,等到此任務下次執行的時候就會先用堆疊中儲存的值來恢復現場,恢復現場以後任務就會接著從上次中斷的地方開始執行。

建立任務的時候需要給任務指定堆疊,如果使用的函數 xTaskCreate()建立任務(動態方法)的話那麼任務堆疊就會由函數 xTaskCreate()自動建立,後面分析 xTaskCreate()的時候會講解。如果使用函數 xTaskCreateStatic()建立任務(靜態方法)的話就需要程式設計師自行定義任務堆疊,然後堆疊首地址作為函數的引數 puxStackBuffer 傳遞給函數。

任務堆疊的資料型別為 StackType_t,StackType_t 本質上是 uint32_t,在 portmacro.h 中有定義。所以 StackType_t 型別的變數為 4 個位元組,那麼任務的實際堆疊大小就應該是我們所定義的 4 倍。

以上就是FreeRTOS實時作業系統多工管理基礎知識的詳細內容,更多關於FreeRTOS實時作業系統多工管理的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com