FreeRTOS學習筆記(一)——基礎知識體系

一、 單任務系統(裸機)

主要是採用超級循環系統(前後臺系統),應用程序是一個無限的循環,循環中調用相應的函數完成相應的操作,這部分可以看做後臺行爲;中斷服務程序處理異步事件,這部分可以看做是前臺行爲。後臺也可以叫做任務級,前臺也叫作中斷級。

前後臺系統的編程思路有兩種:輪詢方式(實時性得不到保障,緊急與非緊急消息不能有效管理)、中斷方式(可以保證一定的實時性,緊急消息可以得到響應)。
裸機開發查詢方式流程圖
裸機開發中斷方式流程圖
採用中斷和查詢結合的方式可以解決大部分裸機應用,但隨着工程的複雜,裸機方式的缺點就暴露出來了:

  1. 必須在中斷(ISR)內處理時間關鍵運算:
  • ISR 函數變得非常複雜,並且需要很長執行時間。
  • ISR 嵌套可能產生不可預測的執行時間和堆棧需求。
  1. 超級循環和 ISR 之間的數據交換是通過全局共享變量進行的:
  • 應用程序的程序員必須確保數據一致性。
  1. 超級循環可以與系統計時器輕鬆同步,但:
  • 如果系統需要多種不同的週期時間,則會很難實現。
  • 超過超級循環週期的耗時函數需要做拆分。
  • 增加軟件開銷,應用程序難以理解。
  1. 超級循環使得應用程序變得非常複雜,因此難以擴展:
  • 一個簡單的更改就可能產生不可預測的副作用,對這種副作用進行分析非常耗時。
  • 超級循環概念的這些缺點可以通過使用實時操作系統 (RTOS) 來解決。

二、多任務系統(帶OS)

採用多任務系統可以以上的裸機開發遇到的4大缺點。
多任務系統流程圖
RTOS的實現重點就在這個OS任務調度器上,調度器的作用就是使用相關的調度算法來決定當前需要執行的任務。FreeRTOS就是一款支持多任務運行的實時操作系統,具有時間片、搶佔式和合作式三種調度方式。通過 FreeRTOS 實時操作系統可以將程序函數分成獨立的任務,併爲其提供合理的調度方式。

1. 任務堆棧

cubemx配置F407的堆棧配置文件
棧大小 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.執行狀態

  1. Running—運行態
    當任務處於實際運行狀態被稱之爲運行態,即 CPU 的使用權被這個任務佔用。
  2. Ready—就緒態
    處於就緒態的任務是指那些能夠運行(沒有被阻塞和掛起),但是當前沒有運行的任務,因爲同優先
    級或更高優先級的任務正在運行。
  3. Blocked—阻塞態
    由於等待信號量,消息隊列,事件標誌組等而處於的狀態被稱之爲阻塞態,另外任務調用延遲函數也
    會處於阻塞態。
  4. Suspended—掛起態
    類似阻塞態,通過調用函數 vTaskSuspend()對指定任務進行掛起,掛起後這個任務將不被執行,只
    有調用函數 xTaskResume()纔可以將這個任務從掛起態恢復。
    FreeRTOS任務狀態轉換圖

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 多任務啓動執行後,基本會按照如下的方式去執行:

  1. 首先執行的最高優先級的任務 Task1,Task1 會一直運行直到遇到系統阻塞式的 API 函數,比如延遲,事件標誌等待,信號量等待,Task1 任務會被掛起,也就是釋放 CPU 的執行權,讓低優先級的任務得到執行。
  2. FreeRTOS 操作系統繼續執行任務就緒列表中下一個最高優先級的任務 Task2,Task2 執行過程中有兩種情況:
  • Task1由於延遲時間到,接收到信號量消息等方面的原因,使得 Task1從掛起狀態恢復到就緒態,
    在搶佔式調度器的作用下,Task2 的執行會被 Task1 搶佔。
  • Task2 會一直運行直到遇到系統阻塞式的 API 函數,比如延遲,事件標誌等待,信號量等待,Task2任務會被掛起,繼而執行就緒列表中下一個最高優先級的任務。
  1. 如果用戶創建了多個任務並且採用搶佔式調度器的話,基本都是按照上面兩條來執行。 根據搶佔式調度器,當前的任務要麼被高優先級任務搶佔,要麼通過調用阻塞式 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 臨界區與開關中斷

代碼的臨界臨界區,一旦這部分代碼開始執行,則不允許任何中斷打斷。爲確保臨界區代碼
的執行不被中斷,在進入臨界區之前須關中斷,而臨界區代碼執行完畢後,要立即開中斷。

FreeRTOS 的源碼中有多處臨界段的地方, 臨界段雖然保護了關鍵代碼的執行不被打斷, 但也會影響系統的實時性。比如此時某個任務正在調用系統 API 函數,而且此時中斷正好關閉了,也就是進入到了臨界區中,這個時候如果有一個緊急的中斷事件被觸發,這個中斷就不能得到及時執行,必須等到中斷開啓纔可以得到執行, 如果關中斷時間超過了緊急中斷能夠容忍的限度, 危害是可想而知的。

FreeRTOS 源碼中就有多處臨界段的處理,跟 FreeRTOS 一樣,uCOS-II 和 uCOS-III 源碼中都是有臨界段的,而 RTX 的源碼中不存在臨界段。 另外,除了 FreeRTOS 操作系統源碼所帶的臨界段以外,用戶寫應用的時候也有臨界段的問題,比如以下兩種:

  • 讀取或者修改變量(特別是用於任務間通信的全局變量)的代碼,一般來說這是最常見的臨界代碼。
  • 調用公共函數的代碼,特別是不可重入的函數,如果多個任務都訪問這個函數,結果是可想而知的。
    總之,對於臨界段要做到執行時間越短越好,否則會影響系統的實時性。
  • 重入函數的實現特徵
    一般而言,重入函數具有如下特徵:
  1. 函數內部無整個軟件生命週期的變量(靜態變量)
  2. 未引用或者訪問整個軟件生命週期的變量(全局變量)

可重入函數示例:

/*A parameter is passed into the function. This will either be passed on the stack or in a CPU register. Either way is safe as each task maintains its own stack and its own set of register values.*/
long lAddOneHunderad ( long lVal1)
{
    /*This function scope variable will also be allocated to the stack or a register, depending on compiler and optimization level. Each task or interrupt that calls this function will have its own copy of lVar2.*/
    long lVal2;
    lVar2 = lVar1 + 100;

    /*Most likely the return value will be placed in a CPU register,although it too could be placed on the stack.*/
    return lVar2;
} 

不可重入函數示例

/*In this case lVar1 is a global variable so every task that calls the function will be accessing the same single copy of the variable.*/
long lVal1;
long lNonsenseFunction ( void )
{
    /*This variable is static so is not allocated on the stack.Each task that calls the function will be accessing the single copy of the variable.*/
    static long lState = 0; // 與函數內部無整個軟件生命週期的變量(靜態變量)相悖
    long lReturn;

    switch( lState )
    {
        case 0: 
            lReturn = lVar1 + 10;
            lState = 1;
            break;
        case 1:
            lReturn = lVar1 +20;
            1State = 0;
            break;
    }
}
  • 任務代碼臨界區處理
    FreeRTOS 任務代碼中臨界段的進入和退出主要是通過操作寄存器 basepri 實現的。進入臨界段前操作寄存器 basepri 關閉了所有小於等於宏定義configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY所定義的中斷優先級,這樣臨界段代碼就不會被中斷干擾到,而且實現任務切換功能的 PendSV 中斷和滴答定時器中斷是最低優先級中斷,所以此任務在執行臨界段代碼期間是不會被其它高優先級任務打斷的。退出臨界段時重新操作 basepri 寄存器,即打開被關閉的中斷(這裏我們不考慮不受 FreeRTOS 管理的更高優先級中斷)。
    FreeRTOS 進入和退出臨界段的函數如下:
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()

再進兩步跟蹤宏定義的實現如下:

void vPortEnterCritical( void )
{
	portDISABLE_INTERRUPTS();
	uxCriticalNesting++;
	/* This is not the interrupt safe version of the enter critical function so assert() if it is 
	being called from an interrupt context. Only API functions that end in "FromISR" can be used
	in an interrupt. Only assert if the critical nesting count is 1 to protect against
	recursive calls if the assert function also uses a critical section. */
	if( uxCriticalNesting == 1 )
	{
		configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
	}
}
/*-----------------------------------------------------------*/
void vPortExitCritical( void )
{
	configASSERT( uxCriticalNesting );
	uxCriticalNesting--;
	if( uxCriticalNesting == 0 )
	{
		portENABLE_INTERRUPTS();
	}
}

上面的這兩個函數都對變量 uxCriticalNesting 進行了操作。這個變量比較重要,用於臨界段的嵌套計數。初學的同學也許會問這裏直接的開關中斷不就可以了嗎,爲什麼還要做一個嵌套計數呢?主要是因爲直接的開關中斷方式不支持在開關中斷之間的代碼裏再次執行開關中斷的嵌套處理,假如當前我們的代碼是關閉中斷的,嵌套了一個含有開關中斷的臨界區代碼後,退出時中斷就成開的了,這樣就出問題了。 通過嵌套計數就有效地防止了用戶嵌套調用函數 taskENTER_CRITICAL 和 taskEXIT_CRITICAL 時出錯。

直接的開關中斷方式不支持嵌套調用例子說明
比如下面的例子:
void FunctionA()
{
	taskDISABLE_INTERRUPTS(); 關閉中斷
	FunctionB(); 調用函數 B
	FunctionC(); 調用函數 C
	taskENABLE_INTERRUPTS(); 打開中斷
}
void FunctionB()
{
	taskDISABLE_INTERRUPTS(); 關閉中斷
	//代碼
	taskENABLE_INTERRUPTS(); 打開中斷
}

工程中調用了 FunctionA 就會出現執行完 FunctionB 後中斷被打開的情況,此時 FunctionC 將不被保護了。
  • 中斷服務程序的臨界區處理
    與任務代碼裏臨界段的處理方式類似,中斷服務程序裏面臨界段的處理也有一對開關中斷函數。
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )

再進兩步跟蹤宏定義的實現如下:

static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
	__asm
	{
	/* Barrier instructions are not used as this function is only used to lower the BASEPRI value. */
	msr basepri, ulBASEPRI
	}
}
/*-----------------------------------------------------------*/
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
	uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
	__asm
	{
		/* Set BASEPRI to the max syscall priority to effect a critical section. */
		mrs ulReturn, basepri
		msr basepri, ulNewBASEPRI
		dsb
		isb
	}
	return ulReturn;
}
中斷服務程序裏面的臨界段代碼的開關中斷也是通過寄存器 basepri 實現的。這裏怎麼沒有中斷嵌套計數了呢?
是的,這裏換了另外一種實現方法,通過保存和恢復寄存器 basepri 的數值就可以實現嵌套使用。如果大家研究
過 uCOS-II 或者 III 的源碼,跟這裏的實現方式是一樣的,使用的時候一定要保證成對使用。

5.2 鎖

  1. 調度鎖
    調度鎖就是 RTOS 提供的調度器開關函數,如果某個任務調用了調度鎖開關函數,處於調度鎖開和調度鎖關之間的代碼在執行期間是不會被高優先級的任務搶佔的,即任務調度被禁止。這一點要跟臨界段的作用區分開,調度鎖只是禁止了任務調度,並沒有關閉任何中斷,中斷還是正常執行的。而臨界段進行了開關中斷操作。
    調度鎖相關函數;
void vTaskSuspendAll( void );
BaseType_t xTaskResumeAll(void)

函數 vTaskSuspendAll 用於實現 FreeRTOS 調度鎖關閉。
使用這個函數要注意以下問題:

  • 調度鎖函數只是禁止了任務調度,並沒有關閉任何中斷。
  • 調度鎖開啓函數 vTaskSuspendAll 和調度鎖關閉函數 xTaskResumeAll 一定要成對使用
  • 切不可在調度鎖開啓函數 vTaskSuspendAll 和調度鎖關閉函數 xTaskResumeAll 之間調用任何會引起任務切換的 API,比如 vTaskDelayUntil、 vTaskDelay、 xQueueSend 等。

函數 xTaskResumeAll用於實現 FreeRTOS 調度鎖開啓
調度鎖關閉後,如果需要任務切換,此函數返回 pdTRUE,否則返回 pdFALSE。
使用這個函數要注意以下問題:

  • 調度鎖函數只是禁止了任務調度,並沒有關閉任何中斷。
  • 調度鎖開啓函數 vTaskSuspendAll 和調度鎖關閉函數 xTaskResumeAll 一定要成對使用。
  • 切不可在調度鎖開啓函數 vTaskSuspendAll 和調度鎖關閉函數 xTaskResumeAll 之間調用任何會引起任務切換的 API,比如 vTaskDelayUntil、 vTaskDelay、 xQueueSend 等。
  1. 任務鎖
    簡單的說,爲了防止當前任務的執行被其它高優先級的任務打斷而提供的鎖機制就是任務鎖。
    FreeRTOS 也沒有專門的任務鎖函數,但是使用 FreeRTOS 現有的功能有兩種實現方法:
    (1) 通過給調度器加鎖實現
    利用 FreeRTOS 的調度鎖功能給調度器加鎖的話,將關閉任務切換功能,從而高優先級任務也就無法搶佔低優先級任務的執行,同時高優先級任務也是無法向低優先級任務切換的。 另外特別注意,調度鎖只是禁止了調度器工作,並沒有關閉任何中斷。
    (2) 通過關閉任務切換中斷 PendSV 和系統時鐘節拍中斷 Systick利用 FreeRTOS 的任務代碼臨界段處理函數就可以關閉 PendSV 中斷和 Systick 中斷。因爲進入臨界段前,操作寄存器 basepri 關閉了所有小於等於宏定義configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 所定義的中斷優先級(實現任務切換功能的 PendSV 中斷和滴答定時器中斷是最低優先級中斷,所以也是被關閉的),這樣低優先級任務在執行臨界段代碼期間是不會被高優先級任務打斷的,從而就實現了任務鎖的效果。
  2. 中斷鎖
    中斷鎖就是 RTOS 提供的開關中斷函數,FreeRTOS 沒有專門的中斷鎖函數,使用中斷服務程序臨界段處理函數就可以實現同樣效果。

5.1 FreeRTOS 系統時鐘節拍和時間管理

5.1.1 FreeRTOS 時鐘節拍

任何操作系統都需要提供一個時鐘節拍,以供系統處理諸如延時、 超時等與時間相關的事件。時鐘節拍是特定的週期性中斷,這個中斷可以看做是系統心跳。 中斷之間的時間間隔取決於不同的應用,一般是 1ms – 100ms。時鐘的節拍中斷使得內核可以將任務延遲若干個時鐘節拍,以及當任務等待事件發生時,提供等待超時等依據。時鐘節拍率越快,系統的額外開銷就越大。任何操作系統都需要提供一個時鐘節拍,以供系統處理諸如延時、 超時等與時間相關的事件。時鐘節拍是特定的週期性中斷,這個中斷可以看做是系統心跳。 中斷之間的時間間隔取決於不同的應用,一般是 1ms – 100ms。時鐘的節拍中斷使得內核可以將任務延遲若干個時鐘節拍,以及當任務等待事件發生時,提供等待超時等依據。時鐘節拍率越快,系統的額外開銷就越大。
對於 Cortex-M3 內核的 STM32F103 和 Cortex-M4 內核的 STM32F407 以及 F429,教程配套的例子都是用滴答定時器來實現系統時鐘節拍的。

  • 滴答定時器 Systick
    SysTick 定時器被捆綁在 NVIC 中,用於產生 SysTick 異常(異常號: 15), 滴答定時器是一個 24 位的遞減計數器,支持中斷。 使用比較簡單, 專門用於給操作系統提供時鐘節拍。

FreeRTOS 的系統時鐘節拍可以在配置文件 FreeRTOSConfig.h 裏面設置:

#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )

如上所示的宏定義配置表示系統時鐘節拍是 1KHz,即 1ms。

5.1.2 FreeRTOS 時間管理

時間管理功能是 FreeRTOS 操作系統裏面最基本的功能,同時也是必須要掌握好的。

  1. 時間延時
    FreeRTOS 中的時間延遲函數主要有以下兩個作用:
  • 爲週期性執行的任務提供延遲。
  • 對於搶佔式調度器,讓高優先級任務可以通過時間延遲函數釋放 CPU 使用權,從而讓低優先級任務可以得到執行。

通過如下的框圖來說明一下延遲函數對任務運行狀態的影響,有一個形象的認識。
延遲函數對任務運行狀態的影響

對任務 Task1 的運行狀態做說明,調度器支持時間片調度和搶佔式調度。
運行過程描述如下:

  • 起初任務 Task1 處於運行態,調用 vTaskDelay 函數後進入到阻塞狀態,也就是 blocked 狀態。
  • vTaskDelay 函數設置的延遲時間到,由於任務 Task1 不是當前就緒的最高優先級任務,所以不能進
    入到運行狀態,只能進入到就緒狀態,也就是 ready 狀態。
  • 一段時間後,調度器發現任務 Task1 是當前就緒的最高優先級任務,從而任務從就緒態切換到運行態。
  • 由於時間片調度,任務 Task1 由運行態切換到就緒態。
    FreeRTOS 時間相關的函數主要有以下 4 個:
void vTaskDelay(const TickType_t xTicksToDelay ); /* 延遲時間長度 ,用於任務的延遲。*/

void vTaskDelayUntil( TickType_t *pxPreviousWakeTime, /* 存儲任務上次處於非阻塞狀態時刻的變量地址 */
					const TickType_t xTimeIncrement ); /* 週期性延遲時間 */
					
volatile TickType_t xTaskGetTickCount( void );/* 用於獲取系統當前運行的時鐘節拍數。此函數用於在任務代碼裏面調用,如果在中斷服務程序裏面調用的話,需要使用函數xTaskGetTickCountFromISR,這兩個函數切不可混用。 */

volatile TickType_t xTaskGetTickCountFromISR( void );/* 用於獲取系統當前運行的時鐘節拍數。此函數用於在中斷服務程序裏面調用,如果在任務裏面調用的話,需要使用函數 xTaskGetTickCount,這兩個函數切不可混用。 */
  1. 函數 vTaskDelay 和 vTaskDelayUntil 的區別
    函數 vTaskDelayUntil 實現的是週期性延遲(絕對性延時),而函數 vTaskDelay 實現的是相對性延遲,反映到實際應用上有什麼區別呢?

舉例
運行條件:

  • 有一個 bsp_KeyScan 函數,這個函數處理時間大概耗時 2ms。
  • 有兩個任務,一個任務 Task1 是用的 vTaskDelay 延遲,延遲 10ms,另一個任務 Task2 是用的vTaskDelayUntil 延遲,延遲 10ms。
  • 不考慮任務被搶佔而造成的影響。
    實際運行過程效果:
  • Task1:
    bsp_KeyScan+ vTaskDelay (10) —> bsp_KeyScan + vTaskDelay (10)
    |----2ms + 10ms 爲一個週期------| |----2ms + 10ms 爲一個週期----|
    這個就是相對性的含義
  • Task2:
    bsp_KeyScan + vTaskDelayUntil ---------> bsp_KeyScan + vTaskDelayUntil
    |----10ms 爲一個週期(2ms 包含在 10ms 內)—| |----10ms 爲一個週期------|
    這就是週期性的含義

6. 通信與同步機制

這裏經過自己的理解,現將通信與同步機制做一個比喻,方便初學者理解。

場景:A 和 B 相約做地鐵去公司,A/B互相告知自己去公司的經歷。

可能發生的情景:

  • 事件標誌組:A告訴B,我經過了1,2,3,4,5等等站;(這裏經過的站的數量就是時間標誌組的事件標誌的最大容量,不超過24個)
  • 二值信號量:A告訴B說自己到沒到公司(到或者沒到),沒有其他信息;(A/B只能告訴對方自己的一個二值狀態,如:到/沒到,沒有其他信息告訴)
  • 計數信號量:A告訴B說自己到沒到公司(到或者沒到),目前經過幾站,沒有其他信息;(A/B只能告訴對方自己的多個狀態,如:到/沒到/在路上/在哪站,沒有其他信息告訴)
  • 互斥信號量:(A與B之前都約C一起去公司,坐C的電動自行車去公司,C說明誰先到我這我帶誰去),A到C處時告訴B我已經到C處了,其他什麼話都沒有。然後C不忍心只有等送A到公司後再去接B;(這裏C每次只能載一個人的規則,防止了A/B發生人身衝突)
  • 消息郵箱:A告訴B,我經過了1,2,3,4,5等等站,每站的到站時間,我在每站做了什麼事等等信息,B收到後就說知道了;(這裏A只能說一次,一次說完,沒說完沒有機會接着上一次說,要麼就是等明天從來來過)
  • 消息隊列:A一直告訴B,我經過了哪站站,到站時間,我在這站做了什麼事等等信息,B收到後就說知道了,如果A頻繁的說,B聽的不耐煩了,B說你慢點說我前面的還沒來得及聽;(這裏A可以說多次,這個次數就是消息隊列的容量,當然,A既然可以告訴很多消息,A也可以選擇像事件標誌組,信號量那樣說部分信息)

6.1 事件標誌組

事件標誌組是實現多任務同步的有效機制之一。也許有不理解的初學者會問採用事件標誌組多麻煩,搞個全局變量不是更簡單?其實不然,在裸機編程時,使用全局變量的確比較方便,但是在加上 RTOS 後就是另一種情況了。 使用全局變量相比事件標誌組主要有如下三個問題:

  • 使用事件標誌組可以讓 RTOS 內核有效地管理任務,而全局變量是無法做到的,任務的超時等機制需要用戶自己去實現。
  • 使用了全局變量就要防止多任務的訪問衝突,而使用事件標誌組則處理好了這個問題,用戶無需擔心。
  • 使用事件標誌組可以有效地解決中斷服務程序和任務之間的同步問題。

任務間事件標誌組的實現是指各個任務之間使用事件標誌組實現任務的通信或者同步機制。
下面我們來說說 FreeRTOS 中事件標誌的實現,根據用戶在 FreeRTOSConfig.h 文件中的配置:

 #define configUSE_16_BIT_TICKS 1

配置宏定義 configUSE_16_BIT_TICKS 爲 1 時,每創建一個事件標誌組,用戶可以使用的事件標誌是8 個。

 #define configUSE_16_BIT_TICKS 0

配置宏定義 configUSE_16_BIT_TICKS 爲 0 時,每創建一個事件標誌組,用戶可以使用的事件標誌是24 個。
上面說的 8 個和 24 個事件標誌應該怎麼理解呢?其實就是定義了一個 16 位變量,僅使用了低 8bit或者定義了一個 32 位變量,僅使用了低 24bit。每一個 bit 用 0 和 1 兩種狀態來代表事件標誌。 反映到FreeRTOS 上就是將事件標誌存儲到了 EventBits_t 類型的變量中。

#if( configUSE_16_BIT_TICKS == 1 )
	typedef uint16_t TickType_t;
	#define portMAX_DELAY ( TickType_t ) 0xffff
#else
	typedef uint32_t TickType_t;
	#define portMAX_DELAY ( TickType_t ) 0xffffffffUL
	/* 32-bit tick type on a 32-bit architecture, so reads of the tick count do
	not need to be guarded with a critical section. */
	#define portTICK_TYPE_IS_ATOMIC 1
#endif

TickType_t 數據類型可以是 16 位數或者 32 位數,這樣就跟上面剛剛說的configUSE_16_BIT_TICKS 宏定義呼應上了。推薦配置宏定義 configUSE_16_BIT_TICKS爲 0,即用戶每創建一個事件標誌組,有 24 個標誌可以設置。
FreeRTOS 任務間事件標誌組的實現是指任務間使用事件標誌。
任務間事件標誌組的實現
FreeRTOS 中斷方式事件標誌組的實現是指中斷函數和 FreeRTOS 任務之間使用事件標誌。
中斷方式時間標誌組的實現
實際應用中,中斷方式的消息機制要注意以下四個問題:

  1. 中斷函數的執行時間越短越好,防止其它低於這個中斷優先級的異常不能得到及時響應。
  2. 實際應用中,建議不要在中斷中實現消息處理,用戶可以在中斷服務程序裏面發送消息通知任務,在任務中實現消息處理,這樣可以有效地保證中斷服務程序的實時響應。同時此任務也需要設置爲高優先級,以便退出中斷函數後任務可以得到及時執行。
  3. 中斷服務程序中一定要調用專用於中斷的事件標誌設置函數,即以 FromISR 結尾的函數。
  4. 在操作系統中實現中斷服務程序與裸機編程的區別。
    (1) 如果 FreeRTOS 工程的中斷函數中沒有調用 FreeRTOS 的事件標誌組 API 函數,與裸機編程是一樣的。
    (2) 如果 FreeRTOS 工程的中斷函數中調用了 FreeRTOS 的事件標誌組的 API 函數,退出的時候要檢測是否有高優先級任務就緒,如果有就緒的,需要在退出中斷後進行任務切換,這點跟裸機編程稍有區別;
    (3) 另外強烈推薦用戶將 Cortex-M3 內核的 STM32F103 和 Cortex-M4 內核STM32F407,F429的 NVIC 優先級分組設置爲 4,即:NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);這樣中斷優先級的管理將非常方便。
    (4)用戶要在 FreeRTOS 多任務開啓前就設置好優先級分組,一旦設置好切記不可再修改。

使用如下 11 個函數可以實現 FreeRTOS 的事件標誌組:

  • xEventGroupCreate()
  • xEventGroupCreateStatic()
  • vEventGroupDelete()
  • xEventGroupWaitBits()
  • xEventGroupSetBits()
  • xEventGroupSetBitsFromISR()
  • xEventGroupClearBits()
  • xEventGroupClearBitsFromISR()
  • xEventGroupGetBits()
  • xEventGroupGetBitsFromISR()
  • xEventGroupSync()

重點介紹這四個函數:

EventGroupHandle_t xEventGroupCreate( void );

返回值,如果創建成功,此函數返回事件標誌組的句柄,如果 FreeRTOSConfig.h 文件中定義的 heap空間不足會返回 NULL。

  • EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, /* 事件標誌組句柄 /
    const EventBits_t uxBitsToSet ); /
    事件標誌位設置 */
    函數 xEventGroupSetBits 用於設置指定的事件標誌位爲 1。
    第 1 個參數:是事件標誌組句柄。
    第 2 個參數:表示 24 個可設置的事件標誌位,EventBits_t 是定義的 32 位變量,低 24 位用於事件標誌設置。變量 uxBitsToSet 的低 24 位的某個位設置爲 1,那麼被設置的事件標誌組的相應位就設置爲 1。變量 uxBitsToSet 設置爲 0 的位對事件標誌相應位沒有影響。比如設置變量 uxBitsToSet = 0x0003 就表示將事件標誌的位 0 和位 1 設置爲 1,其餘位沒有變化。
    返回值:當前的事件標誌組數值。
    這個函數注意事項:
    (1)使用前一定要保證事件標誌組已經通過函數 xEventGroupCreate 創建了。
    (2) 此函數是用於任務代碼中調用的,故不可以在中斷服務程序中調用此函數,中斷服務程序中使用的是
    xEventGroupSetBitsFromISR
    (3) 用戶通過參數 uxBitsToSet 設置的標誌位並不一定會保留到此函數的返回值中,下面舉兩種情況:

(a) 調用此函數的過程中,其它高優先級的任務就緒了,並且也修改了事件標誌,此函數返回的事件標誌位會發生變化。
(b) 調用此函數的任務是一個低優先級任務,通過此函數設置了事件標誌後,讓一個等待此事件標誌的高優先級任務就緒了,會立即切換到高優先級任務去執行,相應的事件標誌位會被函數xEventGroupWaitBits 清除掉,等從高優先級任務返回到低優先級任務後,函數xEventGroupSetBits 的返回值已經被修改。

BaseType_t xEventGroupSetBitsFromISR(EventGroupHandle_t xEventGroup, /* 事件標誌組句柄 */
					const EventBits_t uxBitsToSet, /* 事件標誌位設置 */
					BaseType_t *pxHigherPriorityTaskWoken ); /* 高優先級任務是否被喚醒的狀態保存 */

函數 xEventGroupSetBits 用於設置指定的事件標誌位爲 1。
第 1 個參數:是事件標誌組句柄。
第 2 個參數:表示 24 個可設置的事件標誌位,EventBits_t 是定義的 32 位變量,低 24 位用於事件標誌設置。變量 uxBitsToSet 的低 24 位的某個位設置爲 1,那麼被設置的事件標誌組的相應位就設置爲 1。 變量 uxBitsToSet 設置爲 0 的位對事件標誌相應位沒有影響。比如設置變量 uxBitsToSet = 0x0003 就表示將事件標誌的位 0 和位 1 設置爲 1,其餘位沒有變化。
第 3 個參數:用於保存是否有高優先級任務準備就緒。如果函數執行完畢後,此參數的數值是 pdTRUE,說明有高優先級任務要執行,否則沒有。
返回值:如果消息成功發送給 daemon 任務(就是 FreeRTOS 的定時器任務)返回 pdPASS,否則返回 pdFAIL,另外 daemon 任務中的消息隊列滿了也會返回 pdFAIL。
這個函數注意事項:
(1)使用前一定要保證事件標誌已經通過函數 xEventGroupCreate 創建了。同時要在 FreeRTOSConfig.h文件中使能如下三個宏定義:

#define INCLUDE_xEventGroupSetBitFromISR 1
#define configUSE_TIMERS 1
#define INCLUDE_xTimerPendFunctionCall 1

(2)函數 xEventGroupSetBitsFromISR 是用於中斷服務程序中調用的,故不可以在任務代碼中調用此函數,任務代碼中使用的是 xEventGroupSetBits。
(3) 函數 xEventGroupSetBitsFromISR 對事件標誌組的操作是不確定性操作,因爲不知道當前有多少個任務在等待此事件標誌。而 FreeRTOS 不允許在中斷服務程序和臨界段中執行不確定性操作。 爲了不在中斷服務程序中執行,就通過此函數給 FreeRTOS 的 daemon 任務(就是 FreeRTOS 的定時器任務)發送消息,在 daemon 任務中執行事件標誌的置位操作。 同時也爲了不在臨界段中執行此不確定操作,將臨界段改成由調度鎖來完成。這樣不確定性操作在中斷服務程序和臨界段中執行的問題就都得到解決了。
(4)由於函數 xEventGroupSetBitsFromISR 對事件標誌的置位操作是在 daemon 任務裏面執行的,如果想讓置位操作立即生效,即讓等此事件標誌的任務能夠得到及時執行,需要設置 daemon 任務的優先級高於使用此事件標誌組的所有其它任務。
(5) 通過下面的使用舉例重點一下函數 xEventGroupSetBitsFromISR 第三個參數的規範用法,初學者務必要注意。

static void TIM2_IRQHandler(void)
{
	BaseType_t xResult;
	BaseType_t xHigherPriorityTaskWoken = pdFALSE;
	/* 中斷消息處理, 此處省略 */
	……
	/* 向任務 vTaskMsgPro 發送事件標誌 */
	xResult = xEventGroupSetBitsFromISR(xCreatedEventGroup, /* 事件標誌組句柄 */
										BIT_0 , /* 設置 bit0 */
										&xHigherPriorityTaskWoken );
	/* 消息被成功發出 */
	if( xResult != pdFAIL )
	{
		/* 如果 xHigherPriorityTaskWoken = pdTRUE,那麼退出中斷後切到當前最高優先級任務執行 */
		portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
	}
}
EventBits_t xEventGroupWaitBits(const EventGroupHandle_t xEventGroup, /* 事件標誌組句柄 */
				const EventBits_t uxBitsToWaitFor, /* 等待被設置的事件標誌位 */
				const BaseType_t xClearOnExit, /* 選擇是否清零被置位的事件標誌位 */
				const BaseType_t xWaitForAllBits, /* 選擇是否等待所有標誌位都被設置 */
				TickType_t xTicksToWait ); /* 設置等待時間 */

函數 xEventGroupWaitBits 等待事件標誌被設置。
第 1 個參數:是事件標誌組句柄。
第 2 個參數:表示等待 24 個事件標誌位中的指定標誌,EventBits_t 是定義的 32 位變量,低 24 位用於事件標誌設置。比如設置變量 uxBitsToWaitFor = 0x0003 就表示等待事件標誌的位 0 和位 1 設置爲 1。 此參數切不可設置爲 0。
第 3 個參數:選擇是否清除已經被置位的事件標誌,如果這個參數設置爲 pdTRUE,且函數xEventGroupWaitBits 在參數 xTicksToWait 設置的溢出時間內返回,那麼相應被設置的事件標誌位會被清零。 如果這個參數設置爲 pdFALSE,對已經被設置的事件標誌位沒有影響。
第 4 個參數:選擇是否等待所有的標誌位都被設置,如果這個參數設置爲 pdTRUE,要等待第 2 個參數 uxBitsToWaitFor 所指定的標誌位全部被置 1,函數纔可以返回。當然,超出了在參數xTicksToWait 設置的溢出時間也是會返回的。如果這個參數設置爲 pdFALSE,第 2 個參數uxBitsToWaitFor 所指定的任何標誌位被置 1,函數都會返回,超出溢出時間也會返回。
第 5 個參數:設置等待時間,單位時鐘節拍週期。 如果設置爲 portMAX_DELAY,表示永久等待。
返回值:由於設置的時間超時或者指定的事件標誌位被置 1,導致函數退出時返回的事件標誌組數值。
這個函數注意事項:
(1)此函數切不可在中斷服務程序中調用。
(2)這裏再着重說明下這個函數的返回值,通過返回值用戶可以檢測是哪個事件標誌位被置 1 了。

(a)如果由於設置的等待時間超時,函數的返回值可會有部分事件標誌位被置 1。
(b)如果由於指定的事件標誌位被置1而返回,並且設置了這個函數的參數xClearOnExit爲pdTRUE,那麼此函數的返回值是清零前的事件標誌組數值。
另外,調用此函數的任務在離開阻塞狀態到退出函數 xEventGroupWaitBits 之間這段時間,如果一個高優先級的任務搶佔執行了,並且修改了事件標誌位,那麼此函數的返回值會跟當前的事件標誌組數值不同。

6.2 定時器組

FreeRTOS支持的定時器組,或者叫軟件定時器,又或者叫用戶定時器均可。軟件定時器的功能比較簡單,也容易掌握。被稱爲定時器組是因爲用戶可以創建多個定時器,創建的個數是可配置的。
FreeRTOS軟件定時器組的時基是基於系統時鐘節拍實現的,之所以叫軟件定時器是因爲它的實現不需要使用任何硬件定時器,而且可以創建很多個,綜合這些因素,這個功能就被稱之爲軟件定時器組。 既然是定時器,那麼它實現的功能與硬件定時器也是類似的。在硬件定時器中,我們是在定時器中斷中實現需要的功能,而使用軟件定時器時,我們是在創建軟件定時器時指定軟件定時器的回調函數,在回調函數中實現相應的功能。
FreeRTOS提供的軟件定時器支持單次模式和週期性模式,單次模式就是用戶創建了定時器並啓動了定時器後,定時時間到將不再重新執行,這就是單次模式軟件定時器的含義。週期模式就是此定時器會按照設置的時間週期重複去執行,這就是週期模式軟件定時器的含義。另外就是單次模式或者週期模式的定時時間到後會調用定時器的回調函數,用戶可以回調函數中加入需要執行的工程代碼。
定時器任務(Daemon 任務)
爲了更好的管理FreeRTOS的定時器組件,專門創建了一個定時器任務,或者稱之爲Daemon任務。關於這個任務,我們上章節在講解事件標誌組的時候有用到。
FreeRTOS定時器組的大部分API函數都是通過消息隊列給定時器任務發消息,在定時器任務裏面執行實際的操作。爲了更好的說明這個問題,我們將官方在線版手冊中的這個截圖貼出來進行說明:
在這裏插入圖片描述
左側圖是用戶應用程序,右側是定時器任務。在用戶應用程序裏面調用了定時器組API函數xTimerReset,這個函數會通過消息隊列給定時器任務發消息,在定時器任務裏面執行實際操作。消息隊列在此處的作用有一個專門的名字:Timer command queue,即專門發送定時器組命令的隊列。

使用軟件定時器組注意事項:
定時器回調函數是在定時器任務中執行的,實際應用中切不可在定時器回調函數中調用任何將定時 器任務掛起的函數,比如vTaskDelay(),
vTaskDelayUntil()以及非零延遲的消息隊列和信號量相關的函數。將定時器任務掛起,會導致定時器任務負責的相關功能都不能正確執行了。

使用如下 20 個函數可以實現 FreeRTOS 的事件標誌組:

  • xTimerCreate()
  • xTimerCreateStatic()
  • xTimerIsTimerActive()
  • xTimerStart()
  • xTimerStop()
  • xTimerChangePeriod()
  • xTimerDelete()
  • xTimerReset()
  • xTimerStartFromISR()
  • xTimerStopFromISR()
  • xTimerChangePeriodFromISR()
  • xTimerResetFromISR()
  • pvTimerGetTimerID()
  • vTimerSetTimerID()
  • xTimerGetTimerDaemonTaskHandle()
  • xTimerPendFunctionCall()
  • xTimerPendFunctionCallFromISR()
  • pcTimerGetName()
  • xTimerGetPeriod()
  • xTimerGetExpiryTime()

重點介紹這3個函數:

xTimerCreate() 
xTimerStart () 
pvTimerGetTimerID ()

函數 xTimerCreate

TimerHandle_t xTimerCreate
					( const char * const pcTimerName,               /* 定時器名字 */
	  				const TickType_t xTimerPeriod,                /* 定時器週期,單位系統時鐘節拍 
	  				const UBaseType_t uxAutoReload,     /* 選擇單次模式或者週期模式 */ 
	  				void * const pvTimerID,                       /* 定時器 ID */ 
					TimerCallbackFunction_t pxCallbackFunction ); /* 定時器回調函數 */

函數描述: 函數xTimerCreate用於創建軟件定時器。

  • 第1個參數是定時器名字,用於調試目的,方便識別不同的定時器。
  • 第2個參數是定時器週期,單位系統時鐘節拍。
  • 第3個參數是選擇週期模式還是單次模式,若參數爲pdTRUE,則表示選擇週期模式,若參數爲pdFALSE,則表示選擇單次模式。
  • 第4個參數是定時器ID,當創建不同的定時器,但使用相同的回調函數時,在回調函數中通過不同的ID號來區分不同的定時器。
  • 第5個參數是定時器回調函數。
  • 返回值,創建成功返回定時器的句柄,由於FreeRTOSCongfig.h文件中heap空間不足,或者定時器週期設置爲0,會返回NULL

函數 xTimerStart
函數原型:

BaseType_t xTimerStart( TimerHandle_t xTimer,    /* 定時器句柄 */
										TickType_t xBlockTime ); /* 成功啓動定時器前的最大等待時間設置,單位系統時鐘節拍 */

函數描述:函數xTimerStart用於啓動軟件定時器。

  • 第1個參數是定時器句柄。
  • 第2個參數是成功啓動定時器前的最大等待時間設置,單位系統時鐘節拍,定時器組的大部分API函數不是直接運行的,而是通過消息隊列給定時器任務發消息來實現的,此參數設置的等待時間就是當消息隊列已經滿的情況下,等待消息隊列有空間時的最大等待時間。
  • 返回值,返回pdFAIL表示此函數向消息隊列發送消息失敗,返回pdPASS表示此函數向消息隊列發送消息成功。定時器任務實際執行消息隊列發來的命令依賴於定時器任務的優先級,如果定時器任務是高優先級會及時得到執行,如果是低優先級,就要等待其餘高優先級任務釋放CPU權纔可以得到執行。

使用這個函數要注意以下問題:

  1. 使用前一定要保證定時器組已經通過函數xTimerCreate創建了。
  2. 在FreeRTOSConfig.h文件中使能宏定義: #define configUSE_TIMERS 1
  3. 對於已經被激活的定時器,即調用過函數xTimerStart進行啓動,再次調用此函數相當於調用了函數xTimerReset對定時器時間進行了復位。
  4. 如果在啓動FreeRTOS調度器前調用了此函數,定時器是不會立即執行的,需要等到啓動了FreeRTOS調度器纔會得到執行,即從此刻開始計時,達到xTimerCreate中設置的單次或者週期性延遲時間纔會執行相應的回調函數。

函數 pvTimerGetTimerID
函數原型:

void *pvTimerGetTimerID( TimerHandle_t xTimer ); /* 定時器句柄 */      

函數描述: 函數pvTimerGetTimerID用於返回使用函數xTimerCreate創建的軟件定時器ID。

  • 第1個參數是定時器句柄。
  • 返回值,返回定時器ID。

使用這個函數要注意以下問題:

  1. 使用前一定要保證定時器組已經通過函數xTimerCreate創建了。
  2. 在FreeRTOSConfig.h文件中使能宏定義: #define configUSE_TIMERS 1
  3. 創建不同的定時器時,可以對定時器使用相同的回調函數,在回調函數中通過此函數獲取是哪個定時器的時間到了,這個功能就是此函數的主要作用。

6.3 消息隊列

cubemx配置freertos的消息隊列

消息隊列的概念及其作用
消息隊列就是通過 RTOS 內核提供的服務,任務或中斷服務子程序可以將一個消息(注意,FreeRTOS
消息隊列傳遞的是實際數據,並不是數據地址,RTX,uCOS-II 和 uCOS-III 是傳遞的地址)放入到隊列。
同樣,一個或者多個任務可以通過 RTOS 內核服務從隊列中得到消息。通常,先進入消息隊列的消息先傳
給任務,也就是說,任務先得到的是最先進入到消息隊列的消息,即先進先出的原則(FIFO),FreeRTOS
的消息隊列支持 FIFO 和 LIFO 兩種數據存取方式。

需要說明的是,freertos消息隊列是通過副本機制傳遞的,而不是引用。

在這裏插入圖片描述
查看源碼可以看到底層實現xQueueSend函數的宏定義如下

#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )

xQueueGenericSend()函數內有這個函數

xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );

prvCopyDataToQueue()函數內可以看到

( void ) memcpy( ( void * ) pxQueue->pcWriteTo, pvItemToQueue, ( size_t ) pxQueue->uxItemSize ); /*lint !e961 !e418 MISRA exception as the casts are only redundant for some ports, plus previous logic ensures a null pointer can only be passed to memcpy() if the copy size is 0. */

freertos的消息隊列發送函數是通過使用memcpy複製的內容。
以簡單的數據元素爲例:

uint8_t ucCount = 0;

xQueueSend(xQueue1,(void *) &ucCount,(TickType_t)10);

這裏是發送隊列消息函數,下面看接收:

uint8_t ucQueueMsgValue;

xQueueReceive(xQueue1, /* 消息隊列句柄 */
(void *)&ucQueueMsgValue, /* 存儲接收到的數據到變量ucQueueMsgValue中 */
(TickType_t)xMaxBlockTime)

這裏是最簡單的uint_8類型元素,要想把發送函數的uint_8定義的數據,包括該數據在發送函數之前被更改後的值發送給接收函數,我們需要傳遞給發送函數send一個uint_8定義數據的地址,這樣可以通過地址傳遞到memcpy函數,實現複製,這也就是爲什麼上面說的freertos的消息隊列不是引用而是複製,要是引用的話,可以直接傳這個uint_8類型的數據,而我們此時在freertos操作系統上,是副本傳遞,通過memcpy,所以需要給uint_8類型數據的地址。
同樣的在本次實驗中採用了消息隊列傳結構體指針地址來實現大數據量的傳輸提高效率。

/* 發送函數 */
MSG * TXMSG;
TXMSG = &DATAMSG;//DATAMSG是一個結構實體而且是全局區定義

	/* 初始化結構體內容 */
	TXMSG->ucMessageID = 1;
	TXMSG->ucData[0] = 0;
/* 消息隊列傳遞結構體指針的地址 */
  if(xQueueSend(myQueue02Handle,(void*)&TXMSG,0) ==errQUEUE_FULL)
		{
			printf("myQueue02Handle errQUEUE_FULL\r\n");
		}
/* 接收函數 */
  MSG *RXMSG;
	/* 初始化結構體內容 */
	TXMSG->ucMessageID = 1;
	TXMSG->ucData[0] = 0;
/* 接收消息隊列結構體指針的地址 */
if(xQueueReceive( myQueue02Handle, (void*)&RXMSG, 10) == pdPASS)
{
  printf("\r\nRXMSG->ucMessageID = %d \r",RXMSG->ucMessageID);
  printf("RXMSG->ucData[0] = ");
  for(i=0;i<20;i++)
    printf(" %03d",RXMSG->ucData[i]);
  printf("\r\n");
  HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);
}

這裏的關鍵就在第二個參數ptMsg,它已經是指針了,爲什麼還要取地址,這樣不是一個二級指針了嗎,而它的參數是void *,給人的感覺應該就是傳一個地址,雖然二級指針也是地址,但是總覺得不應該設計成二級指針賦值給一個一級指針,哪怕你是void*。但是我們既然使用了freertos,就要遵循別人的設計,別人這樣做,肯定有自己的道理,我們做到熟練應用即可。試想,消息發送函數,要發送數據,要得到這個數據的地址以給memcpy,如果傳遞的數據本身就是地址(指針),那麼我們要把這個地址傳到接收函數去,就應該得到此時指針的地址纔行,也就是傳遞一個指針的值,注意不是指針指向的值。關鍵我們要通過memcpy函數,傳遞一個指針的值通過memcpy必然是需要二級指針的,這樣纔可以操作一級指針的值,這樣也就可以理解爲什麼ptMsg已經是指針了,卻還是要傳遞ptMsg的地址,因爲只有這樣,纔可以通過memcpy函數把ptMsg指針的值給到接收函數的指針,這樣在接收函數中操作這個結構體類型的指針,就可以得到發送端的數據。這樣做的好處是,避免了大數據的拷貝,只拷貝指針,提高了效率,但是使用指針,一定不要在棧空間開闢,這也是爲什麼我們定義g_tMsg結構體實體在全局區。但是freertos任務中一直有while(1),元素生命週期一直都在,此時還是可以使用局部變量做數據傳遞工具,但是這樣的編程模式應該摒棄,我們採用全局區開闢的空間。

1. 環境
  • packages版本(STM32F4 1.21)
  • cubemx版本(version4.27.0 && STM32Cube v1.0)
  • MDK版本(KEIL6 V5.23.0.0)

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

2. cubemx配置

主要內容:

  1. 配置時鐘
  2. 配置串口
  3. 使能freertos
  4. 勾選任務信息相關宏定義(3個,方便查看任務信息)
  5. 添加消息隊列

STM32F07開發板實驗,關鍵代碼實現

/* cubemx自動生成的創建2個消息隊列 */
/* 創建10個uint8_t型消息隊列 */
  osMessageQDef(myQueue01, 10, uint8_t);
  myQueue01Handle = osMessageCreate(osMessageQ(myQueue01), NULL);

/* 創建10個存儲指針變量的消息隊列 */
  osMessageQDef(myQueue02, 1, MSG*);
  myQueue02Handle = osMessageCreate(osMessageQ(myQueue02), NULL);

freertos.c源文件

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * File Name          : freertos.c
  * Description        : Code for freertos applications
  ******************************************************************************
  * This notice applies to any and all portions of this file
  * that are not between comment pairs USER CODE BEGIN and
  * USER CODE END. Other portions of this file, whether
  * inserted by the user or by software development tools
  * are owned by their respective copyright owners.
  *
  * Copyright (c) 2019 STMicroelectronics International N.V.
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted, provided that the following conditions are met:
  *
  * 1. Redistribution of source code must retain the above copyright notice,
  *    this list of conditions and the following disclaimer.
  * 2. Redistributions in binary form must reproduce the above copyright notice,
  *    this list of conditions and the following disclaimer in the documentation
  *    and/or other materials provided with the distribution.
  * 3. Neither the name of STMicroelectronics nor the names of other
  *    contributors to this software may be used to endorse or promote products
  *    derived from this software without specific written permission.
  * 4. This software, including modifications and/or derivative works of this
  *    software, must execute solely and exclusively on microcontroller or
  *    microprocessor devices manufactured by or for STMicroelectronics.
  * 5. Redistribution and use of this software other than as permitted under
  *    this license is void and will automatically terminate your rights under
  *    this license.
  *
  * THIS SOFTWARE IS PROVIDED BY STMICROELECTRONICS AND CONTRIBUTORS "AS IS"
  * AND ANY EXPRESS, IMPLIED OR STATUTORY WARRANTIES, INCLUDING, BUT NOT
  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
  * PARTICULAR PURPOSE AND NON-INFRINGEMENT OF THIRD PARTY INTELLECTUAL PROPERTY
  * RIGHTS ARE DISCLAIMED TO THE FULLEST EXTENT PERMITTED BY LAW. IN NO EVENT
  * SHALL STMICROELECTRONICS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
  * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
  * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  *
  ******************************************************************************
  */
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */     
#include "bsp_usart.h"
#include "bsp_gpio.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */
volatile unsigned long  ulHighFrequencyTimerTicks = 0ul;
typedef struct AMessage
{
    char ucMessageID;
    char ucData[ 20 ];
}MSG;

MSG DATAMSG;
/* USER CODE END Variables */
osThreadId PrintfTaskHandle;
osThreadId myTask02Handle;
osThreadId myTask03Handle;
osMessageQId myQueue01Handle;
osMessageQId myQueue02Handle;
osMessageQId myQueue03Handle;
osMessageQId myQueue04Handle;
osMessageQId myQueue05Handle;
osTimerId myTimer01Handle;
osTimerId myTimer02Handle;
osMutexId myMutex01Handle;
osSemaphoreId myBinarySem01Handle;

/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */
   void Show_SYS_INFO_Task(void);
/* USER CODE END FunctionPrototypes */

void StartPrintfTask(void const * argument);
void StartTask02(void const * argument);
void StartTask03(void const * argument);
void Callback01(void const * argument);
void Callback02(void const * argument);

void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */

/* Hook prototypes */
void configureTimerForRunTimeStats(void);
unsigned long getRunTimeCounterValue(void);
uint8_t buff[50] ={0x01,0x02,0x03} ;
/* USER CODE BEGIN 1 */
/* Functions needed when configGENERATE_RUN_TIME_STATS is on */
void configureTimerForRunTimeStats(void)
{
	ulHighFrequencyTimerTicks = 0ul;
}

unsigned long getRunTimeCounterValue(void)
{
	return ulHighFrequencyTimerTicks;
}

/* USER CODE END 1 */

/**
  * @brief  FreeRTOS initialization
  * @param  None
  * @retval None
  */
void MX_FREERTOS_Init(void) {
  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Create the mutex(es) */
  /* definition and creation of myMutex01 */
  osMutexDef(myMutex01);
  myMutex01Handle = osMutexCreate(osMutex(myMutex01));

  /* USER CODE BEGIN RTOS_MUTEX */
  /* add mutexes, ... */
  /* USER CODE END RTOS_MUTEX */

  /* Create the semaphores(s) */
  /* definition and creation of myBinarySem01 */
  osSemaphoreDef(myBinarySem01);
  myBinarySem01Handle = osSemaphoreCreate(osSemaphore(myBinarySem01), 1);

  /* USER CODE BEGIN RTOS_SEMAPHORES */
  /* add semaphores, ... */
  /* USER CODE END RTOS_SEMAPHORES */

  /* Create the timer(s) */
  /* definition and creation of myTimer01 */
  osTimerDef(myTimer01, Callback01);
  myTimer01Handle = osTimerCreate(osTimer(myTimer01), osTimerPeriodic, NULL);

  /* definition and creation of myTimer02 */
  osTimerDef(myTimer02, Callback02);
  myTimer02Handle = osTimerCreate(osTimer(myTimer02), osTimerPeriodic, NULL);

  /* USER CODE BEGIN RTOS_TIMERS */
  /* start timers, add new ones, ... */
  /* USER CODE END RTOS_TIMERS */

  /* Create the thread(s) */
  /* definition and creation of PrintfTask */
  osThreadDef(PrintfTask, StartPrintfTask, osPriorityNormal, 0, 512);
  PrintfTaskHandle = osThreadCreate(osThread(PrintfTask), NULL);

  /* definition and creation of myTask02 */
  osThreadDef(myTask02, StartTask02, osPriorityNormal, 0, 256);
  myTask02Handle = osThreadCreate(osThread(myTask02), NULL);

  /* definition and creation of myTask03 */
  osThreadDef(myTask03, StartTask03, osPriorityNormal, 0, 256);
  myTask03Handle = osThreadCreate(osThread(myTask03), NULL);

  /* USER CODE BEGIN RTOS_THREADS */
  /* add threads, ... */
  /* USER CODE END RTOS_THREADS */

  /* Create the queue(s) */
  /* definition and creation of myQueue01 */
/* what about the sizeof here??? cd native code */
  osMessageQDef(myQueue01, 10, uint8_t);
  myQueue01Handle = osMessageCreate(osMessageQ(myQueue01), NULL);
//	myQueue01Handle = xQueueCreate(10,4);
  /* definition and creation of myQueue02 */
/* what about the sizeof here??? cd native code */
  osMessageQDef(myQueue02, 1, MSG*);
  myQueue02Handle = osMessageCreate(osMessageQ(myQueue02), NULL);
////	myQueue02Handle = xQueueCreate(10,sizeof(struct AMessage*));
//	myQueue02Handle = xQueueCreate(10,sizeof(MSG *));

//  /* definition and creation of myQueue03 */
///* what about the sizeof here??? cd native code */
//  osMessageQDef(myQueue03, 32, uint8_t);
//  myQueue03Handle = osMessageCreate(osMessageQ(myQueue03), NULL);

//  /* definition and creation of myQueue04 */
///* what about the sizeof here??? cd native code */
//  osMessageQDef(myQueue04, 32, uint8_t);
//  myQueue04Handle = osMessageCreate(osMessageQ(myQueue04), NULL);

//  /* definition and creation of myQueue05 */
///* what about the sizeof here??? cd native code */
//  osMessageQDef(myQueue05, 32, uint8_t);
//  myQueue05Handle = osMessageCreate(osMessageQ(myQueue05), NULL);

  /* USER CODE BEGIN RTOS_QUEUES */
  /* add queues, ... */
  /* USER CODE END RTOS_QUEUES */
}

/* USER CODE BEGIN Header_StartPrintfTask */
/**
  * @brief  Function implementing the PrintfTask thread.
  * @param  argument: Not used
  * @retval None
  */
/* USER CODE END Header_StartPrintfTask */
void StartPrintfTask(void const * argument)
{

  /* USER CODE BEGIN StartPrintfTask */

	MSG * TXMSG;
	TXMSG = &DATAMSG;
	uint8_t j = 0;
	uint8_t i;

	TXMSG->ucMessageID = 1;
	TXMSG->ucData[0] = 0;
  /* Infinite loop */
  for(;;)
  {  	
	if(key0_value == KEY0_PRES)
	{
		osSemaphoreRelease(myBinarySem01Handle);
		key0_value = 0;
	}
	if(key1_value == KEY1_PRES)
	{
		TXMSG->ucMessageID++;
		for(i=0;i<20;i++,j++)
		{
			TXMSG->ucData[i]=j;
		}
		if(xQueueSend(myQueue02Handle,(void*)&TXMSG,0) ==errQUEUE_FULL)
		{
			printf("myQueue02Handle errQUEUE_FULL\r\n");
		}
		key1_value = 0;
	}
	if(key2_value == KEY2_PRES)
	{
		if(xQueueSend(myQueue01Handle,(void*)&key2_value,10) == errQUEUE_FULL)
		{
			printf("myQueue01Handle errQUEUE_FULL\r\n");
		}
		key2_value = 0;
	}
	if(keyup_value == WKUP_PRES)
	{
		Show_SYS_INFO_Task();
		keyup_value = 0;
	}
	j++;
	if(j%10 == 0)
	  HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);

    osDelay(10);
  }
  /* USER CODE END StartPrintfTask */
}

/* USER CODE BEGIN Header_StartTask02 */
/**
* @brief Function implementing the myTask02 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTask02 */
void StartTask02(void const * argument)
{
  /* USER CODE BEGIN StartTask02 */
  /* Infinite loop */
  for(;;)
  {	 
	  osSemaphoreWait(myBinarySem01Handle, osWaitForever);
	  HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);
	  printf("StartTask02 osSemaphoreWait(myBinarySem01Handle);\r\n");     
	  osDelay(10);
  }
  /* USER CODE END StartTask02 */
}

/* USER CODE BEGIN Header_StartTask03 */
/**
* @brief Function implementing the myTask03 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTask03 */
void StartTask03(void const * argument)
{
  /* USER CODE BEGIN StartTask03 */
	uint8_t pvBuffer ;
	MSG *RXMSG;
	uint16_t i = 0;
  /* Infinite loop */
  for(;;)
  {
	if(xQueueReceive( myQueue02Handle, (void*)&RXMSG, 10) == pdPASS)
	{
		printf("\r\nRXMSG->ucMessageID = %d \r",RXMSG->ucMessageID);
		printf("RXMSG->ucData[0] = ");
		for(i=0;i<20;i++)
			printf(" %03d",RXMSG->ucData[i]);
		printf("\r\n");
		HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);
	}
	if(xQueueReceive( myQueue01Handle, (void *) &pvBuffer, 10) == pdPASS)  
	{
		printf("myQueue01Handle %03d \r\n",pvBuffer);
		HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);
	}
	osDelay(10);
  }
  /* USER CODE END StartTask03 */
}

/* Callback01 function */
void Callback01(void const * argument)
{
  /* USER CODE BEGIN Callback01 */

  /* USER CODE END Callback01 */
}

/* Callback02 function */
void Callback02(void const * argument)
{
  /* USER CODE BEGIN Callback02 */

  /* USER CODE END Callback02 */
}

/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
void Show_SYS_INFO_Task(void)
{
	uint8_t pcWriteBuffer[200];
	printf("=================================================\r\n");
	printf("\r\ntask_name  \tstate\t prior\trtack\t Id\r\n");
	vTaskList((char *)&pcWriteBuffer);
	printf("%s\r\n", pcWriteBuffer);

	printf("\r\ntask_name     time_count(10us) usage_pec\r\n");
	vTaskGetRunTimeStats((char *)&pcWriteBuffer);
	printf("%s\r\n", pcWriteBuffer);

}
/* USER CODE END Application */

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

FreeRTOSConfig.h頭文件

/*
    FreeRTOS V9.0.0 - Copyright (C) 2016 Real Time Engineers Ltd.
    All rights reserved

    VISIT http://www.FreeRTOS.org TO ENSURE YOU ARE USING THE LATEST VERSION.

    This file is part of the FreeRTOS distribution.

    FreeRTOS is free software; you can redistribute it and/or modify it under
    the terms of the GNU General Public License (version 2) as published by the
    Free Software Foundation >>!AND MODIFIED BY!<< the FreeRTOS exception.

	***************************************************************************
    >>!   NOTE: The modification to the GPL is included to allow you to     !<<
    >>!   distribute a combined work that includes FreeRTOS without being   !<<
    >>!   obliged to provide the source code for proprietary components     !<<
    >>!   outside of the FreeRTOS kernel.                                   !<<
	***************************************************************************

    FreeRTOS is distributed in the hope that it will be useful, but WITHOUT ANY
    WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    FOR A PARTICULAR PURPOSE.  Full license text is available on the following
    link: http://www.freertos.org/a00114.html

    ***************************************************************************
     *                                                                       *
     *    FreeRTOS provides completely free yet professionally developed,    *
     *    robust, strictly quality controlled, supported, and cross          *
     *    platform software that is more than just the market leader, it     *
     *    is the industry's de facto standard.                               *
     *                                                                       *
     *    Help yourself get started quickly while simultaneously helping     *
     *    to support the FreeRTOS project by purchasing a FreeRTOS           *
     *    tutorial book, reference manual, or both:                          *
     *    http://www.FreeRTOS.org/Documentation                              *
     *                                                                       *
    ***************************************************************************

    http://www.FreeRTOS.org/FAQHelp.html - Having a problem?  Start by reading
	the FAQ page "My application does not run, what could be wrong?".  Have you
	defined configASSERT()?

	http://www.FreeRTOS.org/support - In return for receiving this top quality
	embedded software for free we request you assist our global community by
	participating in the support forum.

	http://www.FreeRTOS.org/training - Investing in training allows your team to
	be as productive as possible as early as possible.  Now you can receive
	FreeRTOS training directly from Richard Barry, CEO of Real Time Engineers
	Ltd, and the world's leading authority on the world's leading RTOS.

    http://www.FreeRTOS.org/plus - A selection of FreeRTOS ecosystem products,
    including FreeRTOS+Trace - an indispensable productivity tool, a DOS
    compatible FAT file system, and our tiny thread aware UDP/IP stack.

    http://www.FreeRTOS.org/labs - Where new FreeRTOS products go to incubate.
    Come and try FreeRTOS+TCP, our new open source TCP/IP stack for FreeRTOS.

    http://www.OpenRTOS.com - Real Time Engineers ltd. license FreeRTOS to High
    Integrity Systems ltd. to sell under the OpenRTOS brand.  Low cost OpenRTOS
    licenses offer ticketed support, indemnification and commercial middleware.

    http://www.SafeRTOS.com - High Integrity Systems also provide a safety
    engineered and independently SIL3 certified version for use in safety and
    mission critical applications that require provable dependability.

    1 tab == 4 spaces!
*/

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

/*-----------------------------------------------------------
 * Application specific definitions.
 *
 * These definitions should be adjusted for your particular hardware and
 * application requirements.
 *
 * THESE PARAMETERS ARE DESCRIBED WITHIN THE 'CONFIGURATION' SECTION OF THE
 * FreeRTOS API DOCUMENTATION AVAILABLE ON THE FreeRTOS.org WEB SITE.
 *
 * See http://www.freertos.org/a00110.html.
 *----------------------------------------------------------*/

/* USER CODE BEGIN Includes */   	      
/* Section where include file can be added */
/* USER CODE END Includes */

/* Ensure stdint is only used by the compiler, and not the assembler. */
#if defined(__ICCARM__) || defined(__CC_ARM) || defined(__GNUC__)
    #include <stdint.h>
    extern uint32_t SystemCoreClock;
/* USER CODE BEGIN 0 */   	      
    extern void configureTimerForRunTimeStats(void);
    extern unsigned long getRunTimeCounterValue(void);  
/* USER CODE END 0 */       
#endif

#define configUSE_PREEMPTION                     1
#define configSUPPORT_STATIC_ALLOCATION          0
#define configSUPPORT_DYNAMIC_ALLOCATION         1
#define configUSE_IDLE_HOOK                      0
#define configUSE_TICK_HOOK                      0
#define configCPU_CLOCK_HZ                       ( SystemCoreClock )
#define configTICK_RATE_HZ                       ((TickType_t)1000)
#define configMAX_PRIORITIES                     ( 7 )
#define configMINIMAL_STACK_SIZE                 ((uint16_t)64)
#define configTOTAL_HEAP_SIZE                    ((size_t)15360)
#define configMAX_TASK_NAME_LEN                  ( 16 )
#define configGENERATE_RUN_TIME_STATS            1
#define configUSE_TRACE_FACILITY                 1
#define configUSE_STATS_FORMATTING_FUNCTIONS     1
#define configUSE_16_BIT_TICKS                   0
#define configUSE_MUTEXES                        1
#define configQUEUE_REGISTRY_SIZE                8
#define configUSE_PORT_OPTIMISED_TASK_SELECTION  1

/* Co-routine definitions. */
#define configUSE_CO_ROUTINES                    0
#define configMAX_CO_ROUTINE_PRIORITIES          ( 2 )

/* Software timer definitions. */
#define configUSE_TIMERS                         1
#define configTIMER_TASK_PRIORITY                ( 2 )
#define configTIMER_QUEUE_LENGTH                 10
#define configTIMER_TASK_STACK_DEPTH             128

/* Set the following definitions to 1 to include the API function, or zero
to exclude the API function. */
#define INCLUDE_vTaskPrioritySet            1
#define INCLUDE_uxTaskPriorityGet           1
#define INCLUDE_vTaskDelete                 1
#define INCLUDE_vTaskCleanUpResources       0
#define INCLUDE_vTaskSuspend                1
#define INCLUDE_vTaskDelayUntil             0
#define INCLUDE_vTaskDelay                  1
#define INCLUDE_xTaskGetSchedulerState      1
#define INCLUDE_pcTaskGetTaskName           1
#define INCLUDE_xTaskGetCurrentTaskHandle   1
#define INCLUDE_eTaskGetState               1

/* Cortex-M specific definitions. */
#ifdef __NVIC_PRIO_BITS
 /* __BVIC_PRIO_BITS will be specified when CMSIS is being used. */
 #define configPRIO_BITS         __NVIC_PRIO_BITS
#else
 #define configPRIO_BITS         4
#endif

/* The lowest interrupt priority that can be used in a call to a "set priority"
function. */
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY   15

/* The highest interrupt priority that can be used by any interrupt service
routine that makes calls to interrupt safe FreeRTOS API functions.  DO NOT CALL
INTERRUPT SAFE FREERTOS API FUNCTIONS FROM ANY INTERRUPT THAT HAS A HIGHER
PRIORITY THAN THIS! (higher priorities are lower numeric values. */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5

/* Interrupt priorities used by the kernel port layer itself.  These are generic
to all Cortex-M ports, and do not rely on any particular library functions. */
#define configKERNEL_INTERRUPT_PRIORITY 		( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
/* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!!
See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 	( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

/* Normal assert() semantics without relying on the provision of an assert.h
header file. */
/* USER CODE BEGIN 1 */
#define configASSERT( x ) if ((x) == 0) {taskDISABLE_INTERRUPTS(); for( ;; );}
/* USER CODE END 1 */

/* Definitions that map the FreeRTOS port interrupt handlers to their CMSIS
standard names. */
#define vPortSVCHandler    SVC_Handler
#define xPortPendSVHandler PendSV_Handler

/* IMPORTANT: This define MUST be commented when used with STM32Cube firmware,
              to prevent overwriting SysTick_Handler defined within STM32Cube HAL */
/* #define xPortSysTickHandler SysTick_Handler */

/* USER CODE BEGIN 2 */    
/* Definitions needed when configGENERATE_RUN_TIME_STATS is on */
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS configureTimerForRunTimeStats
#define portGET_RUN_TIME_COUNTER_VALUE getRunTimeCounterValue    
/* USER CODE END 2 */

/* USER CODE BEGIN Defines */   	      
/* Section where parameter definitions can be added (for instance, to override default ones in FreeRTOS.h) */
/* USER CODE END Defines */

#endif /* FREERTOS_CONFIG_H */

FreeRTOS使用queue的注意事項

  1. FreeRTOS消息隊列最大傳遞數據爲一個32位值,且底層實現是memcpy,所以需要傳遞變量地址;如果只是簡單的變量傳輸可以直接傳遞變量地址,osMessageQDef(myQueue01, 32, uint8_t);中的最後一個參數type可以爲uint8_t或uint16_t或uint32_t等。
  2. 如果想要傳輸多字節數據,我們只要將指向數據的地址的指針傳遞過去即可(指針本身大小爲32位),如我們定義一個結構體指針TXMSG用來存放發送數據,數據完成打包後,使用在xQueueSend(myQueue02Handle,(void*)&TXMSG,0)將結構體指針的地址用消息隊列發送出去。在消息隊列接收端同樣定義一個相同類型的結構體指針RXMSG來接收數據xQueueReceive( myQueue02Handle, (void*)&RXMSG, 10),這樣即可以實現多字節大量數據的傳輸。
typedef struct AMessage
{
    char ucMessageID;
    char ucData[ 20 ];
}MSG;
  1. 多字節的大數據傳輸時,一定要在全局區定義並賦初始值以便程序運行時即可分配空間,如果在函數內部定義爲局部變量(要傳輸的結構體指針),在函數結束時釋放空間,則改地址可能被修改,在消息隊列接收時出現數據錯誤。

在定義接受和發送結構體指針時一定記得要賦初始值以便在在程序運行時即分配空間,否則易出現傳輸數據異常。
MSG DATAMSG; MSG * TXMSG = &DATAMSG;

源代碼1上傳github

源代碼2上傳github

源代碼3上傳github

串口打印效果:

在這裏插入圖片描述

6.4 計數信號量&二值信號量&互斥信號量

信號量(semaphores)是20世紀60年代中期Edgser Dijkstra發明的。使用信號量的最初目的是爲了給共享資源建立一個標誌,該標誌表示該共享資源被佔用情況。這樣,當一個任務在訪問共享資源之前,就可以先對這個標誌進行查詢,從而在瞭解資源被佔用的情況之後,再來決定自己的行爲。 實際的應用中,信號量的作用又該如何體現呢?比如有個30人的電腦機房,我們就可以創建信號量的初始化值是30,表示30個可用資源,不理解的初學者表示信號量還有初始值?是的,信號量說白了就是共享資源的數量。另外我們要求一個同學使用一臺電腦,這樣每有一個同學使用一臺電腦,那麼信號量的數值就減一,直到30臺電腦都被佔用,此時信號量的數值就是0。如果此時還有幾個同學沒有電腦可以使用,那麼這幾個同學就得等待,直到有同學離開。有一個同學離開,那麼信號量的數值就加1,有兩個就加2,依此類推。剛纔沒有電腦用的同學此時就有電腦可以用了,有幾個同學用,信號量就減幾,直到再次沒有電腦可以用,這麼一個過程就是使用信號量來管理共享資源的過程。

平時使用信號量主要實現以下兩個功能:

  • 兩個任務之間或者中斷函數跟任務之間的同步功能,這個和前面章節講解的事件標誌組是類似的。其實就是共享資源爲1的時候。
  • 多個共享資源的管理,就像上面舉的機房上機的例子。 針對這兩種功能,FreeRTOS分別提供了二值信號量和計數信號量,其中二值信號量可以理解成計數信號量的一種特殊形式,即初始化爲僅有一個資源可以使用,只不過FreeRTOS對這兩種都提供了API函數,而像RTX,uCOS-II和III是僅提供了一個信號量功能,設置不同的初始值就可以分別實現二值信號量和計數信號量。當然,FreeRTOS使用計數信號量也能夠實現同樣的效果。

三者的關係
二值信號量是計數信號量的特例,二值信號量與互斥信號量原理大同,不同點在互斥信號量可以避免多任務訪問共享變量時出現的優先級反轉。

優先級反轉
在這裏插入圖片描述
運行過程描述如下:

  • 任務Task1運行的過程需要調用函數printf,發現任務Task3正在調用,任務Task1會被掛起,等待Task3釋放函數printf。
  • 在調度器的作用下,任務Task3得到運行,Task3運行的過程中,由於任務Task2就緒,搶佔了Task3的運行。優先級翻轉問題就出在這裏了,從任務執行的現象上看,任務Task1需要等待Task2執行完畢纔有機會得到執行,這個與搶佔式調度正好反了,正常情況下應該是高優先級任務搶佔低優先級任務的執行,這裏成了高優先級任務Task1等待低優先級任務Task2完成。所以這種情況被稱之爲優先級翻轉問題
  • 任務Task2執行完畢後,任務Task3恢復執行,Task3釋放互斥資源後,任務Task1得到互斥資源,從而可以繼續執行。 上面就是一個產生優先級翻轉問題的現象。

防止優先級翻轉的過程

運行條件:

  • 創建2個任務Task1和Task2,優先級分別爲1和3,也就是任務Task2的優先級最高。
  • 任務Task1和Task2互斥訪問串口打印printf。
  • 使用FreeRTOS的互斥信號量實現串口打印printf的互斥訪問。

運行過程描述如下:

  • 低優先級任務Task1執行過程中先獲得互斥資源printf的執行。此時任務Task2搶佔了任務Task1的執行,任務Task1被掛起。任務Task2得到執行。
  • 任務Task2執行過程中也需要調用互斥資源,但是發現任務Task1正在訪問,此時任務Task1的優先級會被提升到與Task2同一個優先級,也就是優先級3,這個就是所謂的優先級繼承(Priority inheritance),這樣就有效地防止了優先級翻轉問題。任務Task2被掛起,任務Task1有新的優先級繼續執行。
  • 任務Task1執行完畢並釋放互斥資源後,優先級恢復到原來的水平。由於互斥資源可以使用,任務 Task2獲得互斥資源後開始執行。

6.5 任務計數信號量&任務二值信號量&任務事件標誌組&任務消息郵箱

FreeRTOS計數信號量的另一種實現方式----基於任務通知(Task Notifications)的計數信號量,這裏我們將這種方式實現的計數信號量稱之爲任務計數信號量。任務計數信號量效率更高,需要的RAM空間更小。當然,缺點也是有的,它沒有前面介紹的計數信號量實現的功能全面。

任務通知的介紹
FreeRTOS每個已經創建的任務都有一個任務控制塊(task control block),任務控制塊就是一個結構體變量,用於記錄任務的相關信息。結構體變量中有一個32位的變量成員ulNotifiedValue是專門用於任務通知的。 通過任務通知方式可以實現計數信號量,二值信號量,事件標誌組和消息郵箱(消息郵箱就是消息隊列長度爲1的情況)。使用方法與前面章節講解的事件標誌組和信號量基本相同,只是換了不同的函數來實現。
任務通知方式實現的計數信號量,二值信號量,事件標誌組和消息郵箱是通過修改變量ulNotifiedValue實現的:

  • 設置接收任務控制塊中的變量ulNotifiedValue可以實現消息郵箱。
  • 如果接收任務控制塊中的變量ulNotifiedValue還沒有被其接收到,也可以用新數據覆蓋原有數據 ,這就是覆蓋方式的消息郵箱。  設置接收任務控制塊中的變量ulNotifiedValue的bit0-bit31數值可以實現事件標誌組。
  • 設置接收任務控制塊中的變量ulNotifiedValue數值進行加一或者減一操作可以實現計數信號量和二值信號量。

採用這種方式有什麼優勢呢?根據官方的測試數據,喚醒由於信號量和事件標誌組而處於阻塞態的任務,速度提升了45%,而且這種方式需要的RAM空間更小。但這種方式實現的信號量和事件標誌組也有它的侷限性,主要表現在以下兩個方面:

  • 任務通知方式僅可以用在只有一個任務等待信號量,消息郵箱或者事件標誌組的情況,不過實際項目項目中這種情況也是最多的。
  • 使用任務通知方式實現的消息郵箱替代前面章節講解的消息隊列時,發送消息的任務不支持超時等待,即消息隊列中的數據已經滿了,可以等待消息隊列有空間可以存新的數據,而任務通知方式實現的消息郵箱不支持超時等待。

任務計數信號量/任務二值信號量/任務事件標誌組消息郵箱 的源碼追蹤

osThreadDef(LED_Task, LED_TaskStart, osPriorityBelowNormal, 0, 256);
LED_TaskHandle = osThreadCreate(osThread(LED_Task), NULL);
	xTaskCreate((TaskFunction_t)thread_def->pthread,(const portCHAR *)thread_def->name,thread_def->stacksize, argument, makeFreeRtosPriority(thread_def->tpriority),&handle) != pdPASS)
		TCB_t *pxNewTCB;//這個就是任務控制塊
			typedef tskTCB TCB_t;
				#if( configUSE_TASK_NOTIFICATIONS == 1 )
					volatile uint32_t ulNotifiedValue;
					volatile uint8_t ucNotifyState;
				#endif

7. 動態內存管理

8. 獨立看門狗檢測任務執行

9.低功耗功能

9.1 低功耗睡眠模式

9.1 低功耗停機模式

9.1 低功耗待機模式

10. 使用FreeRTOS總結

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章