乾貨|FreeRTOS 學習筆記——FreeRTOS的軟體結構

我是從 FreeRTOS 官方的文檔《Mastering the FreeRTOS Real Time Kernel》開始學習它的,代碼和參考手冊都用的 9.0.0 版本。我還沒有用過其它的 RTOS, 所以也無意評價它的優缺點。當然,它無疑是一個優秀而且很流行的嵌入式 RTOS. 要上手也很快,本篇我就記錄一下如何將 FreeRTOS 的代碼加到已有的工程裡面,作為一個備忘參考(網上也能隨便搜到很多關於怎麼使用 FreeRTOS, 怎麼創建任務等等的文章。在我的學習筆記系列裡面這部分內容倒不是首要的,因為我想分享的是從我對 FreeRTOS 代碼的分析和實踐瞭解到它是怎麼工作的,帶來了什麼好處)。

文件組成

乾貨|FreeRTOS 學習筆記——FreeRTOS的軟件結構

從 freertos.org 網站上可以找到下載源代碼包的鏈接。以我用的 v9.0.0 代碼為參考,它的根目錄下有6個C源文件:

croutine.c

event_groups.c

list.c

queue.c

tasks.c

timers.c

這些文件是 FreeRTOS 的核心代碼,有的還是可選的。然後是兩個子目錄:includeportable. include 目錄下的頭文件包含了系統核心用到的宏定義,以及編程用到的 API 數據結構、函數原型等。在 portable 目錄下的文件提供一些會被 FreeRTOS 核心代碼調用的函數,這些函數的實現與運行環境有關係,或者是存在多種實現方式。比如,因為 FreeRTOS 支持多種硬件平臺,與平臺實現密切方式相關的代碼(例如彙編語言編寫的函數)就放到編譯器、CPU類型對應的子目錄下。portable 目錄下的文件不是系統核心,除了 FreeRTOS 代碼包裡提供的這些文件,用戶還可以根據自己的環境編寫函數。

乾貨|FreeRTOS 學習筆記——FreeRTOS的軟件結構

生成目標模塊的文件裡面,按照能運行起來的最小需求,tasks.clist.c 必不可少(因為 FreeRTOS 的內部數據結構用了鏈表,所以需要 list.c)。此外還必須有一個硬件實現相關的模塊,例如我用於 STM32 就選用了 /portable/GCC/ARM_CM3/port.c

在自己寫的程序源文件裡面,至少需要包含兩個頭文件:FreeRTOS.htask.h . 如果用了其它可選的功能 API,就要將相應的頭文件包含進來,具體看手冊說明。如果編譯時出現警告或錯誤說什麼什麼沒有定義,就先檢查一下是否缺了頭文件。

配置 (可選項)

FreeRTOS 的靈活性很大,許多功能不需要則可裁減掉。為了減少對內存的浪費,某些數據結構長度、數據位寬也是可以指定的。因此 FreeRTOS 要求用戶編寫一個 FreeRTOSConfig.h 頭文件,定義若干參數,對系統的功能和特性進行配置。

初次使用 FreeRTOS, 也不必自己書寫這個配置文件,只須從代碼包的 Demo 目錄下找一個和自己的環境相近的工程,將其中的 FreeRTOSConfig.h 拷貝出來,根據需要稍微調整一下就可以了。如果用的是 STM32, 還可以從 ST 官方的(比如 CubeF4 裡面) FreeRTOS 例子工程裡面借用一個。

有幾個與系統資源關係密切的宏定義要留意一下:

#define configCPU_CLOCK_HZ ( SystemCoreClock )

#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )

#define configMAX_PRIORITIES ( 5 )

#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 130 )

#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 75 * 1024 ) )

configCPU_CLOCK_HZ 是 CPU 的時鐘頻率,FreeRTOS 需要知道它才能正確配置硬件定時器。configTICK_RATE_HZ 是指定 FreeRTOS 用的時鐘中斷頻率,也就是決定所用時間間隔的單位。configMAX_PRIORITIES 指定存在多少種任務優先級,在夠用的前提下應儘量少。

configMINIMAL_STACK_SIZE 是給任務分配堆棧的最小值,FreeRTOS 會按這個值給 Idle 任務分配堆棧。configTOTAL_HEAP_SIZE 是決定分配多少內存給 FreeRTOS 自己管理,在動態創建任務和其它一些數據結構的時候使用,見下一節說明。

務必注意決定任務調度特點的兩個宏定義:

configUSE_PREEMPITON 指定是否使用搶佔式任務調度——即是否運行中的任務不主動交出控制權也允許被調度。

configUSE_TIME_SLICING 指定是否針對同一優先級下的任務使用時間片調度(在搶佔式調度前提下)。

有些可選的功能需要通過宏指定開啟,例如 configUSE_QUEUE_SETS, configUSE_TIMERS, configUSE_COUNTING_SEMAPHORES, configUSE_MUTEXES,

configUSE_TASK_NOTIFICATIONS 等。沒有指定則 FreeRTOS 會使用默認值,需要留意手冊中的說明。

也可以直接察看 FreeRTOS.h 中的條件編譯語句對未定義 configXXX_XXXX 宏的處理,大多數是默認定義為0, 也有少數定義為1的。另有幾不提供默認值的宏若沒有在 FreeRTOSConfig.h 中定義,就直接報錯了。

內存管理

前面我提到過,FreeRTOS 的每個任務都需要分配內存作為堆棧和TCB數據結構。有兩種內存分配方式——靜態分配,在編譯時決定;和動態分配,在運行時決定。通常我們會使用動態分配內存,這時候要讓 FreeRTOS 核心能夠分次申請一定大小的內存,類似於提供C語言的 malloc() free() 機制。

那麼為什麼 FreeRTOS 不直接使用C標準庫裡面的 malloc() 和 free() 函數呢?文檔裡面提到幾方面的考慮:(1)多任務調用, C庫函數是否可重入的問題。(2)實現的複雜度和代碼效率。(3)增加程序鏈接、調試的複雜度。

FreeRTOS 使用的內存分配和釋放函數是 pvPortMalloc()vPortFree(). 在 /portable/MemMang 目錄下面有幾個版本的文件,各自實現了這兩個函數:

heap_1.c 只分配內存,不可釋放。用在只創建而不銷燬的場合,不存在內存碎片問題。

heap_2.c 可以釋放分配的內存,使用最適匹配原則。

heap_3.c 藉助標準庫的 malloc() 和 free() 函數管理內存,額外增加代碼來避免重入。

heap_4.c 是在 heap_2 基礎上的改進,可以將鄰接的空閒內存塊合併。

heap_5.c 可以管理地址不連續的幾塊SRAM區域。

對於 heap_1.c, heap_2.c, heap_4.c 三種實現都需要配置文件中定義好 configTOTAL_HEAP_SIZE 這個宏,即在編譯時分配一大塊固定位置的內存,給 FreeRTOS 作為堆(Heap)使用。所有任務申請的內存都出自這一塊預定的內存裡面。heap_3.c 是由 C 庫函數負責內存管理,就不需要這個宏定義了。而針對 heap_5.c 顯然這一描述不夠,需要提供每塊 SRAM 區域分別的地址和大小的信息,所以它要求用戶調用 vPortDefineHeapRegions() 函數來指定內存詳情。

乾貨|FreeRTOS 學習筆記——FreeRTOS的軟件結構

如果對這幾個還不滿意,當然也可以自己寫內存管理的函數了(這是 portable 的部分,結合具體需求定製是它的初衷)。

其實對於小的很確定的應用,比如就創建一兩個任務來跑,動態分配內存都不是必須的。只要在創建任務的時候麻煩一點,手動指定作為堆棧和TCB的內存地址,就一樣能工作。這便是 FreeRTOS 支持的靜態創建方式。從 9.0.0 版本開始,已經可以完全去掉堆內存管理的代碼。要在配置文件中定義 configSUPPORT_STATIC_ALLOCATION 宏值為1才能支持使用靜態分配的內存創建對象。同樣還有一個宏 configSUPPORT_DYNAMIC_ALLOCATION (默認值為1)是決定是否支持動態分配內存的。

用靜態分配內存創建對象,要調用函數名以 Static 結尾的 API. 例如 xQueueCreateStatic(), 其原型如下:

QueueHandle_t xQueueCreateStatic(

UBaseType_t uxQueueLength,

UBaseType_t uxItemSize,

uint8_t *pucQueueStorageBuffer,

StaticQueue_t *pxQueueBuffer

);

而使用動態分配內存的常規版本 API 函數是 xQueueCreate():

QueueHandle_t xQueueCreate(

UBaseType_t uxQueueLength,

UBaseType_t uxItemSize

);

可見靜態內存方式需要用戶以參數形式提供對象使用的內存地址,可能還不止一塊。使用動態內存就直接按需要從堆中分配了,程序寫起來簡單。

此外,若支持靜態內存的選項打開,FreeRTOS 還要求用戶提供一個 vApplicationGetIdleTaskMemory() 函數,用來給系統獲取 Idle 任務的堆棧內存地址、TCB內存地址和堆棧大小。因為系統會用 xTaskCreateStatic() 來創建 Idle 任務。同樣的原因,在使用 Timer 功能時,要提供

vApplicationGetTimerTaskMemory() 函數。

系統自帶任務

FreeRTOS 總有一個任務在運行狀態,若當前無事可做,那麼就讓一個代表“系統空閒”的任務運行——這就是 Idle 任務。當 FreeRTOS 調度器啟動時,會隱含地創建這個任務。Idle 任務具有最低的優先級,它必須要讓位於任何更重要的任務,除非它們都阻塞或掛起了。

Idle 任務的一個用途是管理硬件的低功耗模式,我將另開一篇來討論。

除了 Idle 任務,FreeRTOS 還帶有一個 Timer 任務,也就是 Daemon Task, 用來輔助完成一些功能,例如軟件定時器。這個任務是可選的,在配置選項 configUSE_TIMERS 為1時才打開。它的代碼在 timers.c 中。Timer 任務的作用是處理跟時間有關的事務,但這些事務又不能放到時鐘中斷 ISR 裡去處理(並非響應硬件請求,沒有那麼高實時性要求), 還要接受調度器管理。

例如,創建軟件定時器的時候指定一個回調函數:

TimerHandle_t xTimerCreate(const char * const pcTimerName,

const TickType_t xTimerPeriodInTicks,

const UBaseType_t uxAutoReload,

void * const pvTimerID,

TimerCallbackFunction_t pxCallbackFunction

);

在定的時間到以後,pxCallbackFunction() 將被 Timer 任務調用,即是在 Timer 任務的上下文中執行,而不是創建這個軟件定時器的任務上下文中執行。

Timer 任務另一功能是執行 xTimerPendFunctionCall(), xTimerPendFunctionCallFromISR() 指定的函數調用,如我在第(5)篇筆記中寫的。這些被指定的函數,連同軟件定時器的回調函數,是一個個順序執行的,都使用 Timer 任務的堆棧。

系統使用的中斷

為了實現與時間有關的功能,硬件定時器是必須要用到的。FreeRTOS 有一個函數

xTaskIncrementTick() 是給硬件定時器的 ISR 調用的。因為核心代碼與硬件平臺無關,無法寫成中斷形式,故採用這種方式。實際上也相當於系統使用了一個定時器中斷,不過 ISR 屬於 portable 層面,用戶可以自由設計,這個中斷也不一定 FreeRTOS 獨佔,例如可以再做一些硬件方面的操作,只要定期及時調用 xTaskIncrementTick() 就可以了。為了加深理解,可以看看不同硬件平臺的 port.c 中如何處理。

FreeRTOS 在 ARM Cortex-m0/m3/m4/m7 平臺的實現中,還使用了 PendSV 中斷和 SVC 中斷,這是軟件產生的中斷,其 ISR 是調度器的一部分。但是在其它硬件平臺的實現中,未必有類似的軟中斷可用。

初始化和任務創建

在一個用 FreeRTOS 的工程裡面,幾乎必然用到的是創建任務,哪怕只有一個任務。比如可以在 main() 裡面創建首先要運行的任務,使用 xTaskCreate() 這個 API 函數:

BaseType_t xTaskCreate(TaskFunction_t pxTaskCode,

const char * const pcName,

const uint16_t usStackDepth,

void * const pvParameters,

UBaseType_t uxPriority,

TaskHandle_t * const pxCreatedTask);

第一個參數 pxTaskCode 是任務的主函數(入口地址),然後 pcName 是代表任務名稱的一個字符串。usStackDepth 很重要,是決定給任務分配多少內存作為堆棧。pvParameters 是向任務函數傳遞參數用的,uxPriority 是初始的任務優先級,pxCreatedTask 用來保存任務 handle, 也就是TCB的地址。當創建成功,這個函數返回 pdTRUE, 然後任務處於 ready 狀態,隨時可以運行。

實際上要等到調度器起用以後,前面所創建的任務才能運行。當主程序完成了系統的初始化工作,比如配置片上設備,創建好必需的任務、各種通信對象,就可以調用 vTaskStartScheduler() 來起用調度器。一般來說這個函數並不返回,因此餘下運行的就是各個任務的函數了。vTaskStartScheduler() 還會自動創建 Idle 任務和 Timer 任務,然後選擇最高優先級的一個任務開始運行。

幾個簡單常用的 API

在一個新平臺上使用 FreeRTOS 功能的時候,可以用一些 API 實現簡單的多任務運行,下面列舉幾個,都不涉及任務間通信。

vTaskDelay() 最常用來產生延時。調用它的任務立即被阻塞,等到經過要求的若干 Timer Tick 過後再恢復到就緒狀態。注意,下一次 Tick 的時刻(也就是硬件定時器中斷髮生時刻)和調用 vTaskDelay() 的時刻有一段不確定的間隔,因此要求精確的延遲時要考慮這個函數是否滿足需求。

vTaskDelayUntil() 功能類似,只不過它是指定一個絕對時間,而 vTaskDelay() 是從調用時刻計的相對時間。

xTaskGetTickCount() 返回調度器已運行的 Tick 數,也就是總執行時間。

vTaskSuspend()vTaskResume(), 這兩個函數可以將某一任務掛起,以及使掛起的任務恢復。

vTaskSuspendAll()vTaskResumeAll(): 這兩個表面上看起來是上兩個 API 功能的擴充,實際上原理完全不同。vTaskSuspendAll() 將調度器禁止,當前任務繼續執行,中斷也是允許的。限制是此時不能調用其它的 API, 直到用 vTaskResumeAll() 恢復調度器之後。和關鍵區域(critical section)的用法不同,關鍵區域是硬件上屏蔽中斷保證不會發生任務切換,API 還是可以使用的。

vTaskList() 接收一個字符緩衝區,生成文本格式的所有任務的列表,包括狀態信息。

小結:FreeRTOS入門運用不難,只需要把幾個文件添加進現有的工程裡面,就能改成多任務的。必要的步驟是決定配置選項,編寫 FreeRTOSConfig.h 文件,以及決定內存管理方式。

乾貨 | FreeRTOS 學習筆記——實驗:串口後臺打印

乾貨 | FreeRTOS學習筆記——中斷與任務切換

乾貨 | FreeRTOS學習筆記 ——應用場景

乾貨 | FreeRTOS 學習筆記 ——堆棧(任務切換的關鍵)

乾貨 | FreeRTOS學習筆記——任務狀態及切換

乾貨 | FreeRTOS 學習筆記——任務間通信

以上圖文內容均轉載自訂閱號:電子工程世界(微信搜索 eeworldbbs 關注)

歡迎微博@EEWORLD

如果您也寫過此類原創乾貨歡迎將您的原創發至:[email protected],一經入選,我們將幫你登上頭條!

與更多行業內網友進行交流請登陸EEWorld論壇。


分享到:


相關文章: