FreeRTOS學習筆記(一)——基礎知識體系
文章目錄
- 一、 單任務系統(裸機)
- 二、多工系統(帶OS)
一、 單任務系統(裸機)
主要是採用超級迴圈系統(前後臺系統),應用程式是一個無限的迴圈,迴圈中呼叫相應的函式完成相應的操作,這部分可以看做後臺行為;中斷服務程式處理非同步事件,這部分可以看做是前臺行為。後臺也可以叫做任務級,前臺也叫作中斷級。
前後臺系統的程式設計思路有兩種:輪詢方式(實時性得不到保障,緊急與非緊急訊息不能有效管理)、中斷方式(可以保證一定的實時性,緊急訊息可以得到響應)。
採用中斷和查詢結合的方式可以解決大部分裸機應用,但隨著工程的複雜,裸機方式的缺點就暴露出來了:
- 必須在中斷(ISR)內處理時間關鍵運算:
- ISR 函式變得非常複雜,並且需要很長執行時間。
- ISR 巢狀可能產生不可預測的執行時間和堆疊需求。
- 超級迴圈和 ISR 之間的資料交換是通過全域性共享變數進行的:
- 應用程式的程式設計師必須確保資料一致性。
- 超級迴圈可以與系統計時器輕鬆同步,但:
- 如果系統需要多種不同的週期時間,則會很難實現。
- 超過超級迴圈週期的耗時函式需要做拆分。
- 增加軟體開銷,應用程式難以理解。
- 超級迴圈使得應用程式變得非常複雜,因此難以擴充套件:
- 一個簡單的更改就可能產生不可預測的副作用,對這種副作用進行分析非常耗時。
- 超級迴圈概念的這些缺點可以通過使用實時作業系統 (RTOS) 來解決。
二、多工系統(帶OS)
採用多工系統可以以上的裸機開發遇到的4大缺點。
RTOS的實現重點就在這個OS任務排程器上,排程器的作用就是使用相關的排程演算法來決定當前需要執行的任務。FreeRTOS就是一款支援多工執行的實時作業系統,具有時間片、搶佔式和合作式三種排程方式。通過 FreeRTOS 實時作業系統可以將程式函式分成獨立的任務,併為其提供合理的排程方式。
1. 任務堆疊
棧大小 0x400 = 1024,單位位元組
在RTOS下,上面截圖裡設定的棧大小有了一個新名字叫做系統棧空間,而任務棧是不使用這裡的空間,哪裡使用這裡的棧空間呢,實際上是中斷函式和中斷巢狀。
- 由於 Cortex-M3 和 M4 核心具有雙堆疊指標,MSP 主堆疊指標和 PSP 程序堆疊指標,或者叫 PSP
任務堆疊指標也是可以的。在 FreeRTOS 作業系統中,主堆疊指標 MSP 是給系統棧空間使用的,進
程堆疊指標 PSP 是給任務棧使用的。也就是說,在 FreeRTOS 任務中,所有棧空間的使用都是通過
PSP 指標進行指向的。一旦進入了中斷函式以及可能發生的中斷巢狀都是用的 MSP 指標。這個知識
點要記住它,當前可以不知道這是為什麼,但是一定要記住。- 實際應用中系統棧空間分配多大,主要是看可能發生的中斷巢狀層數,下面我們就按照最壞執行情況
進行考慮,所有的暫存器都需要入棧,此時分為兩種情況:
* 64 位元組:
對於 Cortex-M3 核心和未使用 FPU(浮點運算單元)功能的 Cortex-M4 核心在發生中斷時需要將 16 個通用暫存器全部入棧,每個暫存器佔用 4 個位元組,也就是 16*4 = 64 位元組的空間。可能發生幾次中斷巢狀就是要 64 乘以幾即可。當然,這種是最壞執行情況,也就是所有的暫存器都入棧。(注:任務執行的過程中發生中斷的話,有 8 個暫存器是自動入棧的,這個棧是任務棧,進入中斷以後其餘暫存器入棧以及發生中斷巢狀都是用的系統棧)
* 200 位元組
對於具有 FPU(浮點運算單元)功能的 Cortex-M4 核心,如果在任務中進行了浮點運算,那麼在發生中斷的時候除了 16 個通用暫存器需要入棧,還有 34 個浮點暫存器也是要入棧的,也就是(16+34)*4 = 200 位元組的空間。當然,這種是最壞執行情況,也就是所有的暫存器都入棧。
1.1 任務棧大小確定
- 函式的棧大小計算起來是比較麻煩的,那麼有沒有簡單的辦法來計算呢?有的,一般 IDE 開發環境都有這樣的功能,比如 MDK 會生成一個 htm 檔案,通過這個檔案使用者可以知道每個被呼叫函式的最大棧需求以及各個函式之間的呼叫關係。但是 MDK 無法確定通過函式指標實現函式呼叫時的棧需求。另外,發生中斷或中斷巢狀時的現場保護需要的棧空間也不會統計。
- 一般來說,使用者可以事先給任務分配一個大的棧空間,然後通過列印任務棧的使用情況,執行一段時間就會有個大概的範圍了。這種方法比較簡單且實用些。
1.2 棧溢位檢測機制
棧生長方向從高地址向低地址生長(M4 和 M3 是這種方式)
- 上圖示識 3 的位置是區域性變數 int i 和 int array[10]佔用的棧空間,但申請了棧空間後已經越界了。這個就是所謂的棧溢位了。如果使用者在函式 test 中通過陣列 array 修改了這部分越界區的資料且這部分越界的棧空間暫時沒有用到或者資料不是很重要,情況還不算嚴重,但是如果儲存的是關鍵資料,會直接導致系統崩潰。
- 上圖示識 4 的位置是區域性變數申請了棧空間後,棧指標向下偏移(返回地址+變數 i+10 個數組元素)*4 =48 個位元組。
- 上圖示識 5 的位置可能是其它任務的棧空間,也可能是全域性變數或者其它用途的儲存區,如果 test函式在使用中還有用到棧的地方就會從這裡申請,這部分越界的空間暫時沒有用到或者資料不是很重要,情況還不算嚴重,但是如果儲存的是關鍵資料,會直接導致系統崩潰。
FreeRTOS 提供了兩種棧溢位檢測機制,這兩種檢測都是在任務切換時才會進行:
- 在任務切換時檢測任務棧指標是否過界了,如果過界了,在任務切換的時候會觸發棧溢位鉤子函式。
void vApplicationStackOverflowHook( TaskHandle_t xTask,signed char *pcTaskName );
使用者可以在鉤子函式裡面做一些處理。這種方法不能保證所有的棧溢位都能檢測到。比如任務在執行的過程中出現過棧溢位。任務切換前棧指標又恢復到了正常水平,這種情況在任務切換的時候是檢測不到的。又比如任務棧溢位後,把這部分棧區的資料修改了,這部分棧區的資料不重要或者暫時沒有用到還好,但如果是重要資料被修改將直接導致系統進入硬體異常,這種情況下,棧溢位檢測功能也是檢測不到的。
使用方法一需要使用者在 FreeRTOSConfig.h 檔案中配置如下巨集定義:
#define configCHECK_FOR_STACK_OVERFLOW 1
- 任務建立的時候將任務棧所有資料初始化為 0xa5,任務切換時進行任務棧檢測的時候會檢測末尾的 16 個位元組是否都是 0xa5,通過這種方式來檢測任務棧是否溢位了。相比方法一,這種方法的速度稍慢些,但是這樣就有效地避免了方法一里面的部分情況。不過依然不能保證所有的棧溢位都能檢測到,比如任務棧末尾的 16 個位元組沒有用到,即沒有被修改,但是任務棧已經溢位了,這種情況是檢測不到的。另外任務棧溢位後,任務棧末尾的 16 個位元組沒有修改,但是溢位部分的棧區資料被修改了,這部分棧區的資料不重要或者暫時沒有用到還好,但如果是重要資料被修改將直接導致系統進入硬體異常,這種情況下,棧溢位檢測功能也是檢測不到的。
使用方法二需要使用者在 FreeRTOSConfig.h 檔案中配置如下巨集定義:
#define configCHECK_FOR_STACK_OVERFLOW 2
除了 FreeRTOS 提供的這兩種棧溢位檢測機制,還有其它的棧溢位檢測機制,大家可以在 Mircrium 官方釋出的如下這個博文中學習:
https://www.micrium.com/detecting-stack-overflows-part-2-of-2/
鉤子函式:
鉤子函式的主要作用就是對原有函式的功能進行擴充套件,使用者可以根據自己的需要往裡面新增相關的測試程式碼,大家可以在 FreeRTOS 工程中檢索這個鉤子函式 vApplicationStackOverflowHook 所在的位置。
2. 任務狀態
FreeRTOS的任務狀態(4種)
1.執行態(Running) 2.就緒態(Ready) 3.阻塞態(Blocked) 4.掛起態(Suspended)
ucos的任務狀態(5種)
1.睡眠狀態 2.就緒狀態 3.等待狀態 4.中斷服務狀態 5.執行狀態
- Running—執行態
當任務處於實際執行狀態被稱之為執行態,即 CPU 的使用權被這個任務佔用。 - Ready—就緒態
處於就緒態的任務是指那些能夠執行(沒有被阻塞和掛起),但是當前沒有執行的任務,因為同優先
級或更高優先順序的任務正在執行。 - Blocked—阻塞態
由於等待訊號量,訊息佇列,事件標誌組等而處於的狀態被稱之為阻塞態,另外任務呼叫延遲函式也
會處於阻塞態。 - Suspended—掛起態
類似阻塞態,通過呼叫函式 vTaskSuspend()對指定任務進行掛起,掛起後這個任務將不被執行,只
有呼叫函式 xTaskResume()才可以將這個任務從掛起態恢復。
3. 任務優先順序
3.1任務優先順序說明
- FreeRTOS 中任務的最高優先順序是通過 FreeRTOSConfig.h 檔案中的 configMAX_PRIORITIES 進行配置的,使用者實際可以使用的優先順序範圍是 0 到 configMAX_PRIORITIES – 1。比如我們配置此巨集定義為 5,那麼使用者可以使用的優先順序號是 0,1,2,3,4,不包含 5,對於這一點,初學者要特別的注意。
- 使用者配置任務的優先順序數值越小,那麼此任務的優先順序越低,空閒任務的優先順序是 0。
- 建議使用者配置巨集定義 configMAX_PRIORITIES 的最大值不要超過 32,即使用者任務可以使用的優先順序範圍是0到31。
3.2 任務優先順序分配方案
- IRQ 任務:IRQ 任務是指通過中斷服務程式進行觸發的任務,此類任務應該設定為所有任務裡面優先順序最高的。
- 高優先順序後臺任務:比如按鍵檢測,觸控檢測,USB 訊息處理,串列埠訊息處理等,都可以歸為這一類任務。
- 低優先順序的時間片排程任務:比如 emWin 的介面顯示,LED 數碼管的顯示等不需要實時執行的都可以歸為這一類任務。 實際應用中使用者不必拘泥於將這些任務都設定為優先順序 1 的同優先順序任務,可以設定多個優先順序,只需注意這類任務不需要高實時性。
- 空閒任務:空閒任務是系統任務。
特別注意:IRQ 任務和高優先順序任務必須設定為阻塞式(呼叫訊息等待或者延遲等函式即可),只有這樣,高優先順序任務才會釋放 CPU 的使用權,,從而低優先順序任務才有機會得到執行。這裡的優先順序分配方案是我們推薦的一種方式,實際專案也可以不採用這種方法。 調試出適合專案需求的才是最好的。
3.3 任務優先順序與終端優先順序的區別
這兩個之間沒有任何關係,不管中斷的優先順序是多少,中斷的優先順序永遠高於任何任務的優先順序,即任務在執行的過程中,中斷來了就開始執行中斷服務程式。
另外對於 STM32F103,F407 和 F429 來說,中斷優先順序的數值越小,優先順序越高。 而 FreeRTOS的任務優先順序是,任務優先順序數值越小,任務優先順序越低。
4. 任務排程
FreeRTOS就是一款支援多工執行的實時作業系統,具有時間片、搶佔式和合作式三種排程方式。
- 合作式排程,主要用在資源有限的裝置上面,現在已經很少使用了。出於這個原因,後面的
FreeRTOS 版本中不會將合作式排程刪除掉,但也不會再進行升級了。 - 搶佔式排程,每個任務都有不同的優先順序,任務會一直執行直到被高優先順序任務搶佔或者遇到阻塞式的 API 函式,比如 vTaskDelay。
- 時間片排程,每個任務都有相同的優先順序,任務會執行固定的時間片個數或者遇到阻塞式的 API 函式,比如vTaskDelay,才會執行同優先順序任務之間的任務切換。
4.1 排程器
排程器就是使用相關的排程演算法來決定當前需要執行的任務。所有的排程器有一個共同的
特性:
- 排程器可以區分就緒態任務和掛起任務(由於延遲,訊號量等待,郵箱等待,事件組等待等原因而使
得任務被掛起)。 - 排程器可以選擇就緒態中的一個任務,然後啟用它(通過執行這個任務)。 當前正在執行的任務是運
行態的任務。 - 不同調度器之間最大的區別就是如何分配就緒態任務間的完成時間。
嵌入式實時作業系統的核心就是排程器和任務切換,排程器的核心就是排程演算法。任務切換的實現在不同的嵌入式實時作業系統中區別不大,基本相同的硬體核心架構,任務切換也是相似的。排程演算法就有些區別了。
4.1.1搶佔式排程器
使用了搶佔式排程,最高優先順序的任務一旦就緒,總能得到 CPU 的控制權。 比如,當一個執行著的任務被其它高優先順序的任務搶佔,當前任務的 CPU 使用權就被剝奪了,或者說被掛起了,那個高優先順序的任務立刻得到了 CPU 的控制權並執行。 又比如,如果中斷服務程式使一個高優先順序的任務進入就緒態,中斷完成時,被中斷的低優先順序任務被掛起,優先順序高的那個任務開始執行。使用搶佔式排程器,使得最高優先順序的任務什麼時候可以得到 CPU 的控制權並執行是可知的,同時使得任務級響應時間得以最優化。
總的來說,學習搶佔式排程要掌握的最關鍵一點是:每個任務都被分配了不同的優先順序,搶佔式排程器會獲得就緒列表中優先順序最高的任務,並執行這個任務。
在 FreeRTOS 的配置檔案 FreeRTOSConfig.h 中禁止使用時間片排程,那麼每個任務必須配
置不同的優先順序。當 FreeRTOS 多工啟動執行後,基本會按照如下的方式去執行:
- 首先執行的最高優先順序的任務 Task1,Task1 會一直執行直到遇到系統阻塞式的 API 函式,比如延遲,事件標誌等待,訊號量等待,Task1 任務會被掛起,也就是釋放 CPU 的執行權,讓低優先順序的任務得到執行。
- FreeRTOS 作業系統繼續執行任務就緒列表中下一個最高優先順序的任務 Task2,Task2 執行過程中有兩種情況:
- Task1由於延遲時間到,接收到訊號量訊息等方面的原因,使得 Task1從掛起狀態恢復到就緒態,
在搶佔式排程器的作用下,Task2 的執行會被 Task1 搶佔。- Task2 會一直執行直到遇到系統阻塞式的 API 函式,比如延遲,事件標誌等待,訊號量等待,Task2任務會被掛起,繼而執行就緒列表中下一個最高優先順序的任務。
- 如果使用者建立了多個任務並且採用搶佔式排程器的話,基本都是按照上面兩條來執行。 根據搶佔式排程器,當前的任務要麼被高優先順序任務搶佔,要麼通過呼叫阻塞式 API 來釋放 CPU 使用權讓低優先順序任務執行,沒有使用者任務執行時就執行空閒任務.
4.1.2 時間片排程器
在小型的嵌入式 RTOS 中,最常用的的時間片排程演算法就是 Round-robin 排程演算法。這種排程演算法可以用於搶佔式或者合作式的多工中。另外,時間片排程適合用於不要求任務實時響應的情況。
實現 Round-robin 排程演算法需要給同優先順序的任務分配一個專門的列表,用於記錄當前就緒的任務,併為每個任務分配一個時間片(也就是需要執行的時間長度,時間片用完了就進行任務切換)。
在 FreeRTOS 作業系統中只有同優先順序任務才會使用時間片排程,另外還需要使用者在FreeRTOSConfig.h 檔案中使能巨集定義:
#define configUSE_TIME_SLICING 1
預設情況下,此巨集定義已經在 FreeRTOS.h 檔案裡面使能了,使用者可以不用在FreeRTOSConfig.h 檔案中再單獨使能。
示例:
- 建立 4 個同優先順序任務 Task1,Task2,Task3 和 Task4。
- 先執行任務 Task1,執行夠 5 個系統時鐘節拍後,通過時間片排程切換到任務 Task2。
- 任務 Task2 執行夠 5 個系統時鐘節拍後,通過時間片排程切換到任務 Task3。
- 任務 Task3 在執行期間呼叫了阻塞式 API 函式,呼叫函式時,雖然 5 個系統時鐘節拍的時間片大小還沒有用完,此時依然會通過時間片排程切換到下一個任務 Task4。 (注意,沒有用完的時間片不會再使用,下次任務 Task3 得到執行還是按照 5 個系統時鐘節拍執行)
- 任務 Task4 執行夠 5 個系統時鐘節拍後,通過時間片排程切換到任務 Task1。
5. 臨界區、鎖與系統時間
5.1 臨界區與開關中斷
5.2 鎖
- 排程鎖
- 任務鎖
- 中斷鎖