FreeRTOS 學習三:任務管理
1. 簡介:
- 在 FreeRTOS 中沒有執行緒和程序的區別,只有一個被翻譯成任務的程式,相當於程序的概念,擁有獨立的棧空間。
- 對於實時性,可以分為 軟實時、硬實時:桌面電腦的輸入處理可以看做是軟實時,當鍵盤按下在某個時間內沒有做出相應,只是做出提示,提示超時,只會給人一種反應慢的印象,不能說不能用。硬實時則是像汽車上的安全氣囊,必須在特定時間內完成,一旦失敗,就算是不能使用的標誌。
- FreeRTOS 核心支援優先順序排程演算法,CPU 總是讓處於就緒態和優先順序高的任務先執行。
- FreeRTOS 核心同時支援輪詢排程演算法, CPU 對享有相同優先順序的任務,平分 CPU 時間。
- FreeRTOS 核心可以根據使用者需要設定成可剝奪型或不可剝奪型核心,可剝奪指的是高優先順序的任務能剝奪正在執行的低優先順序的任務,可以保證系統滿足實時性的要求。不可剝奪,遇到同時發生的任務會一直等待著先發生的任務完成,可以提高 CPU 的執行效率。
- FreeRTOS 對系統任務的數量沒有限制。
- 任務因為自己的優先順序而一直得不到執行的狀態叫做被餓死(starve)
2. 函式:
(1)變數型別定義:
/* Type definitions. */ #define portCHAR char #define portFLOAT float #define portDOUBLE double #define portLONG long #define portSHORT short #define portSTACK_TYPE uint32_t #define portBASE_TYPE long typedef portSTACK_TYPE StackType_t; typedef long BaseType_t; typedef unsigned long UBaseType_t;
(2)建立任務:
需要包含的標頭檔案:#include "task.h"
xTaskCreate函式原型:
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,是一個任務的函式指標
typedef void (*TaskFunction_t)( void * );
②pcName,任務的名字,有最大長度限制,包括 ‘\0’結束符,最大長度是 config_MAX_TASK_NAME_LEN,如果超過最大,會自動截斷
③usStackDepth,指定任務的棧的大小,單位是字(word)= 4 位元組,不是位元組(byte),使用者通過定義 configMINIMAL_STACK_SIZE 來決定空閒任務用的棧空間大小
④pvParameters,傳遞到建立任務的函式中的引數值
⑤uxPriority,任務的優先順序,取值[0,configMAX_PRIORITIES-1],沒有最大限制,configMAX_PRIORITIES 變數值時系統之前設定好的,如果優先順序取值大於區間,將會取區間裡的最大值
⑥pxCreatedTask,用於傳出任務的控制代碼,可以用來改變任務的優先順序,或者刪除任務,如果用不到,使用者可以設為 NULL
返回值:
pdTRUE,建立成功
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY,建立失敗,一般是由於記憶體堆空間不足所致
(3)作為任務的函式的原型:
void func(void * pvParameters);
【1】函式中沒有return
【2】一個任務函式可以用來建立若干個任務,建立的每個任務都有獨立的棧空間
【3】函式模板:
func(void * pvParameters)
{
...
for(;;)
{
}
vTaskDelete(NULL);/* 一般不會執行到這裡,傳入 NULL 表示刪除當前任務 */
}
(4)刪除任務:
需要包含的標頭檔案:task.h
vTaskDelete函式原型:
void vTaskDelete( TaskHandle_t xTaskToDelete )
引數:
xTaskToDelete,要刪除的任務,是NULL時,刪除自己
說明:
【1】需要配置 INCLUDE_vTaskDelete = 1,才能使用這個函式,從 RTOS 實時核心管理中移除任務,要刪除的任務將從就緒、封鎖、掛起事件列表中移除。
【2】空閒任務負責釋放核心分配的記憶體,任務自己佔用的記憶體需要應用程式自己顯示的釋放。
(5)任務延時:
需要包含標頭檔案:task.h
vTaskDelay函式原型:
void vTaskDelay( const TickType_t xTicksToDelay )
引數:
xTicksToDelay,延時多少個心跳週期,延遲的任務進入阻塞態,經過指定的心跳週期後,轉移到就緒態,將毫秒單位轉換成心跳週期單位使用常量 portTICK_RATE_MS ,比如要延時 250ms,則 xTicksToDelay = 250 / portTICK_RATE_MS
(6)週期性延時:
需要包含標頭檔案:task.h
vTaskDelayUntil函式原型:
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement )
引數:
pxPreviousWakeTime,用於儲存任務上一次離開阻塞態的時刻,這個時刻用於作為參考點計算任務下一次離開阻塞態的時刻,這個值自動更新,只需要付初始值即可,一般是 pxPreviousWakeTime = xTaskGetTickCount()
xTimeIncrement,迴圈週期時間,要延時 250ms,則 xTimeIncrement = 250 / portTICK_RATE_MS
說明:
【1】需要配置 INCLUDE_vTaskDelayUntil = 1,才能使用這個函式
【2】使用的時候,需要將這個函式放在迴圈中例如for(;;){vPrintString(pcTaskName);vTaskDelayUntil(...);}
【3】執行完任務後,任務本身進入阻塞態,等待時間的到達
【4】既然要作為週期任務,優先順序可能要設定成最高才能實現
(7)掛起任務:
需要包含標頭檔案:task.h
vTaskSuspend函式原型:
void vTaskSuspend( TaskHandle_t xTaskToSuspend )
引數:
xTaskToSuspend,要掛起的任務控制代碼,是NULL掛起呼叫此函式的任務
說明:
【1】需要配置 INCLUDE_vTaskSuspend = 1 ,才能使用這個函式
【2】掛起的任務不再佔用控制器的處理時間
(8)喚醒掛起的任務:
需要包含的標頭檔案:task.h
vTaskResume函式原型:
void vTaskResume( TaskHandle_t xTaskToResume )
引數:
xTaskToResume,要喚醒的任務控制代碼,喚醒(7)的任務
說明:
【1】需要配置 INCLUDE_vTaskSuspend = 1 才能使用此函式
(9)從中斷喚醒掛起的任務:
需要包含的標頭檔案:task.h
xTaskResumeFromISR函式原型:
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume )
引數:
xTaskToResume,要喚醒的任務控制代碼
返回值:
pdTRUE,成功
pdFALSE,失敗
說明:
【1】需要配置 INCLUDE_xTaskResumeFromISR = 1 和 INCLUDE_vTaskSuspend = 1 才能使用此函式
【2】當喚醒成功將引起上下文切換,失敗用於 ISR 確定是否上下文切換
(10)為任務分配標籤值:
需要包含的標頭檔案:task.h
vTaskSetApplicationTaskTag函式原型:
void vTaskSetApplicationTaskTag( TaskHandle_t xTask, TaskHookFunction_t pxHookFunction )
引數:
xTask,要分配標籤的任務,NULL是呼叫函式的任務
pxHookFunction,分配給任務的標籤值
說明:
【1】需要配置 configUSE_APPLICATION_TASK_TAG = 1 才能使用此函式
【2】分配的標籤只對應用程式有用,核心不使用
(11)啟動實時核心:
需要包含的標頭檔案:task.h
vTaskStartScheduler函式原型:
void vTaskStartScheduler( void )
說明:
【1】當此函式呼叫時,還有一個空閒任務自動被建立,這個函式呼叫成功後不會返回,直到執行任務呼叫 vTaskEndScheduler
【2】函式呼叫失敗的可能原因是可供給空閒任務的 RAM 不足
(12)停止實時核心執行:
需要包含標頭檔案:task.h
vTaskEndScheduler函式原型:
void vTaskEndScheduler( void )
說明:
【1】所有建立的任務將自動刪除,並且多工將停止
【2】此函式導致所有由核心分配的資源釋放,但是由應用程式分配的資源需要應用程式自己去釋放
(13)掛起所有活動的實時核心,同時允許中斷(包括核心滴答中斷)
vTaskSuspendAll函式原型:
void vTaskSuspendAll( void )
說明:
【1】任務呼叫此函式後,此任務將繼續執行,不會有任何被切換的危險,直到呼叫 xTaskResumeAll() 函式重啟核心
【2】當呼叫了此函式後,之後將不能在出現影響上下文切換的的函式(比如:vTaskDelayUntil(),xQueueSend()等)
3. 任務優先順序:
- 優先順序最大值由配置檔案 FreeRTOSConfig.h 中 configMAX_PRIORITIES 變數設定,這個值越大,核心花銷的記憶體空間就越大。
- 優先順序0最小
- 對優先順序操作的函式:
(1)任務優先順序的獲取:
需要包含的標頭檔案是:task.h
uxTaskPriorityGet函式原型:
UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask )
引數:
xTask,需要操作的任務,NULL表示自己
返回值:
任務的優先順序
說明:
【1】此函式的使用需要配置 INCLUDE_uxTaskPriorityGet = 1
(2)任務優先順序的重新設定:
需要包含標頭檔案:task.h
vTaskPrioritySet函式原型:
void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority )
引數:
xTask,任務
uxNewPriority,要設定的優先順序
說明:
【1】需要設定 INCLUDE_vTaskPrioritySet = 1
【2】如果更改的優先順序高於當前的任務的優先順序,上下文的切換髮生在此函式返回前
4. 任務調動說明:
要能夠選擇下一個執行的任務,排程器需要在每個時間片的結束時刻執行自己本身。一個稱為心跳(tick,有些地方被稱為時鐘滴答,本文中一律稱為時鐘心跳)中斷的週期性中斷用於此目的。時間片的長度通過心跳中斷的頻率進行設定,心跳中斷頻率由FreeRTOSConfig.h 中的編譯時配置常量 configTICK_RATE_HZ 進行配置。比如說,如果 configTICK_RATE_HZ 設為 1000(HZ),則時間片長度為 1ms。
心跳計數(tick count)值表示的是從排程器啟動開始,心跳中斷的總數,並假定心跳計數器不會溢位。使用者程式在指定延遲週期時不必考慮心跳計數溢位問題,因為時間連
貫性在核心中進行管理。
將排程器本身的執行時間在整個執行流程中體現出來的過程如下圖中紅色部分
任務的執行狀態被分為了 Running、Ready、Blocked、Suspended。
Running可以被翻譯成 執行狀態
這種狀態表示核心正在執行的狀態
Ready被翻譯成 準備好狀態
這種狀態表示任務準備好了,只要由核心呼叫,就能當下執行
Blocked被翻譯成 阻塞狀態
處於等待某個事件的任務叫做阻塞狀態,處於阻塞等待的任務可以被兩類事件喚醒:時間超時、其他任務的同步事件,處於此種狀態的任務不參與排程。
Suspended被翻譯成 暫停狀態
處於暫停狀態的任務不參與排程,一般應用程式用不到這個狀態。
任務所有狀態構成的狀態機如下:
5. 空閒任務與他的鉤子函式:
當呼叫 vTaskStartScheduler() 函式後,空閒任務就被自動的建立了,空閒任務是一個非常短小的迴圈,其任務優先順序是最小的 0,其他任務可以設定成和空閒任務相同的優先順序
通過空閒任務鉤子函式,可以直接在空閒任務中新增應用程式相關的功能,空閒任務鉤子函式會被空閒任務每迴圈一次就自動呼叫一次。空閒任務中可以做的事情可以但不完全是:可以測量設計的系統有多少富裕的處理時間、在系統空閒的時候讓系統自動進入省電模式。
空閒任務鉤子函式一定要注意的是,當應用程式用到vTaskDelete() 函式,一定要能儘快返回,因為在任務被刪除後,空閒任務負責回收資源。
空閒任務鉤子函式原型:
void vApplicationIdleHook(void)
說明:
【1】需要配置 configUSE_IDLE_HOOK = 1
【2】函式的名字不能變,一定要是上邊函式的名字
【3】當配置了變數/巨集,可以使用此函式後,在main.c中直接宣告,並實現其中的功能就可以使用了
6. 優先順序的例項圖表:
(1)TaskDelay 函式的圖表表示:
當我們同時建立了兩個相同的任務,然後任務中都有一段250ms的vTaskDelay延時,任務2優先順序2,任務1優先順序1,同時執行,則執行的圖表表示如下:
【1】表中我們可以看到,任務優先順序高的卻接近了空閒狀態,空閒狀態下是時間線。
【2】vTaskDelay延時就相當於放棄任務被排程的權利多長時間。
(2)執行中重新設定優先順序的程式和圖表表示:
行為說明:
- 任務1 建立在最高優先順序,以保證其可以最先執行。任務1 首先列印輸出兩個字串,然後將任務2 的優先順序提升到自己之上。
- 任務2 一旦擁有最高優先順序便啟動執行(進入執行態)。由於任何時候只可能有一個任務處於執行態,所以當任務2 執行時,任務1 處於就緒態。
- 任務2 列印輸出一個資訊,然後把自己的優先順序設回低於任務 1的初始值。
- 任務2 降低自己的優先順序意味著任務1 又成為具有最高優先順序的任務,所以任務1 重新進入執行態,任務2 被強制切入就緒態。
程式實現如下:
void vTask1( void *pvParameters )
{
unsigned portBASE_TYPE uxPriority;
uxPriority = uxTaskPriorityGet( NULL ); // 返回自己的優先順序
for( ;; )
{
vPrintString( "Task1 is running\r\n" );
vPrintString( "About to raise the Task2 priority\r\n" );
vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );
}
}
void vTask2( void *pvParameters )
{
unsigned portBASE_TYPE uxPriority;
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
vPrintString( "Task2 is running\r\n" );
vPrintString( "About to lower the Task2 priority\r\n" );
vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
}
}
xTaskHandle xTask2Handle;
int main( void )
{
xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, &xTask2Handle );
vTaskStartScheduler();
for( ;; );
}
圖表表示如下:
【1】從圖表中可以看出任務高的優先順序在高的位置,這種方式,我們理解的更容易
【2】對任務的優先順序的再次更改需要任務建立時的任務控制代碼(最後一個引數)
【3】到這裡我們可以看到,圖表好的形式如下:
(3)週期性延時的實現:
行為說明:
- 在優先順序1 上建立兩個任務,這兩個任務只是不停地列印輸出字串。
- 第三個任務建立在優先順序 2 上,高於另外兩個任務的優先順序。這個任務雖然也是列印輸出字串,但它是週期性的,呼叫了vTaskDelayUntil(),在每兩次列印之間讓自己處於阻塞態。
程式實現:
void vContinuousProcessingTask( void *pvParameters )
{
char *pcTaskName;
pcTaskName = ( char * ) pvParameters;
for( ;; )
{
vPrintString( pcTaskName );
}
}
void vPeriodicTask( void *pvParameters )
{
portTickType xLastWakeTime;
xLastWakeTime = xTaskGetTickCount();
for( ;; )
{
vPrintString( "Periodic task is running\r\n" );
vTaskDelayUntil( &xLastWakeTime, ( 10 / portTICK_RATE_MS ) );
}
}
圖表表示如下:
【1】兩個相同優先順序的任務在圖中也分了高低,是為了好區分
【2】此圖表表示的不是簡單的建立兩個相同的列印的函式,至少,保證了列印能完成才去排程,在圖中紅色段也至少含有幾個tick