文章目錄
- 系列教程總目錄
- 概述
- 3.1 基本概念
-
3.2 任務創建與刪除
- 3.2.1 什么是任務
- 3.2.2 創建任務
- 3.2.3 示例1: 創建任務
- 3.2.4 示例2: 使用任務參數
- 3.2.5 任務的刪除
- 3.2.6 示例3: 刪除任務
-
3.3 任務優先級和Tick
- 3.3.1 任務優先級
- 3.3.2 Tick
- 3.3.3 示例4: 優先級實驗
- 3.3.4 示例5: 修改優先級
-
3.4 任務狀態
- 3.4.1 阻塞狀態(Blocked)
- 3.4.2 暫停狀態(Suspended)
- 3.4.3 就緒狀態(Ready)
- 3.4.4 完整的狀態轉換圖
-
3.5 Delay函數
- 3.5.1 兩個Delay函數
- 3.5.2 示例6: Delay
-
3.6 空閑任務及其鉤子函數
- 3.6.1 介紹
- 3.6.2 使用鉤子函數的前提
-
3.7 調度算法
- 3.7.1 重要概念
- 3.7.2 配置調度算法
- 3.7.3 示例7: 調度
- 3.7.4 對比效果: 搶占與否
- 3.7.5 對比效果: 時間片輪轉與否
- 3.7.6 對比效果: 空閑任務讓步
?
需要獲取更好閱讀體驗的同學,請訪問我專門設立的站點查看,地址:http://rtos.100ask.net/
系列教程總目錄
本教程連載中,篇章會比較多,為方便同學們閱讀,點擊這里可以查看文章的 目錄列表,目錄列表頁面地址:https://blog.csdn.net/thisway_diy/article/details/121399484
概述
在本章中,會涉及如下內容:
- FreeRTOS如何給每個任務分配CPU時間
- 如何選擇某個任務來運行
- 任務優先級如何起作用
- 任務有哪些狀態
- 如何實現任務
- 如何使用任務參數
- 怎么修改任務優先級
- 怎么刪除任務
- 怎么實現周期性的任務
- 如何使用空閑任務
- ?
3.1 基本概念
對于整個單片機程序,我們稱之為application,應用程序。
使用FreeRTOS時,我們可以在application中創建多個任務(task),有些文檔把任務也稱為線程(thread)。
?以日常生活為例,比如這個母親要同時做兩件事:
- 喂飯:這是一個任務
- 回信息:這是另一個任務
這可以引入很多概念:
-
任務狀態(State):
- 當前正在喂飯,它是running狀態;另一個"回信息"的任務就是"not running"狀態
-
"not running"狀態還可以細分:
- ready:就緒,隨時可以運行
- blocked:阻塞,卡住了,母親在等待同事回信息
- suspended:掛起,同事廢話太多,不管他了
-
優先級(Priority)
- 我工作生活兼顧:喂飯、回信息優先級一樣,輪流做
- 我忙里偷閑:還有空閑任務,休息一下
- 廚房著火了,什么都別說了,先滅火:優先級更高
-
棧(Stack)
- 喂小孩時,我要記得上一口喂了米飯,這口要喂青菜了
- 回信息時,我要記得剛才聊的是啥
- 做不同的任務,這些細節不一樣
- 對于人來說,當然是記在腦子里
- 對于程序,是記在棧里
- 每個任務有自己的棧
-
事件驅動
- 孩子吃飯太慢:先休息一會,等他咽下去了、等他提醒我了,再喂下一口
-
協助式調度(Co-operative Scheduling)
-
你在給同事回信息
- 同事說:好了,你先去給小孩喂一口飯吧,你才能離開
- 同事不放你走,即使孩子哭了你也不能走
-
你好不容易可以給孩子喂飯了
- 孩子說:好了,媽媽你去處理一下工作吧,你才能離開
- 孩子不放你走,即使同事連發信息你也不能走
-
你在給同事回信息
這涉及很多概念,后續章節詳細分析。
3.2 任務創建與刪除
3.2.1 什么是任務
在FreeRTOS中,任務就是一個函數,原型如下:
void ATaskFunction( void *pvParameters );
要注意的是:
- 這個函數不能返回
- 同一個函數,可以用來創建多個任務;換句話說,多個任務可以運行同一個函數
-
函數內部,盡量使用局部變量:
- 每個任務都有自己的棧
-
每個任務運行這個函數時
- 任務A的局部變量放在任務A的棧里、任務B的局部變量放在任務B的棧里
- 不同任務的局部變量,有自己的副本
-
函數使用全局變量、靜態變量的話
- 只有一個副本:多個任務使用的是同一個副本
- 要防止沖突(后續會講)
下面是一個示例:
void ATaskFunction( void *pvParameters )
{
/* 對于不同的任務,局部變量放在任務的棧里,有各自的副本 */
int32_t lVariableExample = 0;
/* 任務函數通常實現為一個無限循環 */
for( ;; )
{
/* 任務的代碼 */
}
/* 如果程序從循環中退出,一定要使用vTaskDelete刪除自己
* NULL表示刪除的是自己
*/
vTaskDelete( NULL );
/* 程序不會執行到這里, 如果執行到這里就出錯了 */
}
3.2.2 創建任務
創建任務時使用的函數如下:
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函數指針, 任務函數
const char * const pcName, // 任務的名字
const configSTACK_DEPTH_TYPE usStackDepth, // 棧大小,單位為word,10表示40字節
void * const pvParameters, // 調用任務函數時傳入的參數
UBaseType_t uxPriority, // 優先級
TaskHandle_t * const pxCreatedTask ); // 任務句柄, 以后使用它來操作這個任務
參數說明:
參數 | 描述 |
---|---|
pvTaskCode |
函數指針,可以簡單地認為任務就是一個C函數。 它稍微特殊一點:永遠不退出,或者退出時要調用"vTaskDelete(NULL)" |
pcName |
任務的名字,FreeRTOS內部不使用它,僅僅起調試作用。 長度為:configMAX_TASK_NAME_LEN |
usStackDepth |
每個任務都有自己的棧,這里指定棧大小。 單位是word,比如傳入100,表示棧大小為100 word,也就是400字節。 最大值為uint16_t的最大值。 怎么確定棧的大小,并不容易,很多時候是估計。 精確的辦法是看反匯編碼。 |
pvParameters | 調用pvTaskCode函數指針時用到:pvTaskCode(pvParameters) |
uxPriority |
優先級范圍:0~(configMAX_PRIORITIES – 1) 數值越小優先級越低, 如果傳入過大的值,xTaskCreate會把它調整為(configMAX_PRIORITIES – 1) |
pxCreatedTask |
用來保存xTaskCreate的輸出結果:task handle。 以后如果想操作這個任務,比如修改它的優先級,就需要這個handle。 如果不想使用該handle,可以傳入NULL。 |
返回值 |
成功:pdPASS; 失敗:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失敗原因只有內存不足) 注意:文檔里都說失敗時返回值是pdFAIL,這不對。 pdFAIL是0,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY是-1。 |
3.2.3 示例1: 創建任務
代碼為:FreeRTOS_01_create_task
使用2個函數分別創建2個任務。
任務1的代碼:
void vTask1( void *pvParameters )
{
const char *pcTaskName = "T1 run\r\n";
volatile uint32_t ul; /* volatile用來避免被優化掉 */
/* 任務函數的主體一般都是無限循環 */
for( ;; )
{
/* 打印任務1的信息 */
printf( pcTaskName );
/* 延遲一會(比較簡單粗暴) */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
}
}
}
任務2的代碼:
void vTask2( void *pvParameters )
{
const char *pcTaskName = "T2 run\r\n";
volatile uint32_t ul; /* volatile用來避免被優化掉 */
/* 任務函數的主體一般都是無限循環 */
for( ;; )
{
/* 打印任務1的信息 */
printf( pcTaskName );
/* 延遲一會(比較簡單粗暴) */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
}
}
}
main函數:
int main( void )
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);
/* 啟動調度器 */
vTaskStartScheduler();
/* 如果程序運行到了這里就表示出錯了, 一般是內存不足 */
return 0;
}
運行結果如下:
?注意:
- task 2先運行!
- 要分析xTaskCreate的代碼才能知道原因:更高優先級的、或者后面創建的任務先運行。
任務運行圖:
- 在t1:Task2進入運行態,一直運行直到t2
- 在t2:Task1進入運行態,一直運行直到t3;在t3,Task2重新進入運行態
3.2.4 示例2: 使用任務參數
代碼為:FreeRTOS_02_create_task_use_params
我們說過,多個任務可以使用同一個函數,怎么體現它們的差別?
- 棧不同
- 創建任務時可以傳入不同的參數
我們創建2個任務,使用同一個函數,代碼如下:
void vTaskFunction( void *pvParameters )
{
const char *pcTaskText = pvParameters;
volatile uint32_t ul; /* volatile用來避免被優化掉 */
/* 任務函數的主體一般都是無限循環 */
for( ;; )
{
/* 打印任務的信息 */
printf(pcTaskText);
/* 延遲一會(比較簡單粗暴) */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
}
}
}
上述代碼中的pcTaskText
來自參數pvParameters
,pvParameters
來自哪里?創建任務時傳入的。
代碼如下:
- 使用xTaskCreate創建2個任務時,第4個參數就是pvParameters
- 不同的任務,pvParameters不一樣
static const char *pcTextForTask1 = "T1 run\r\n";
static const char *pcTextForTask2 = "T2 run\r\n";
int main( void )
{
prvSetupHardware();
xTaskCreate(vTaskFunction, "Task 1", 1000, (void *)pcTextForTask1, 1, NULL);
xTaskCreate(vTaskFunction, "Task 2", 1000, (void *)pcTextForTask2, 1, NULL);
/* 啟動調度器 */
vTaskStartScheduler();
/* 如果程序運行到了這里就表示出錯了, 一般是內存不足 */
return 0;
}
3.2.5 任務的刪除
刪除任務時使用的函數如下:
void vTaskDelete( TaskHandle_t xTaskToDelete );
參數說明:
參數 | 描述 |
---|---|
pvTaskCode |
任務句柄,使用xTaskCreate創建任務時可以得到一個句柄。 也可傳入NULL,這表示刪除自己。 |
怎么刪除任務?舉個不好的例子:
-
自殺:
vTaskDelete(NULL)
-
被殺:別的任務執行
vTaskDelete(pvTaskCode)
,pvTaskCode是自己的句柄 -
殺人:執行
vTaskDelete(pvTaskCode)
,pvTaskCode是別的任務的句柄
3.2.6 示例3: 刪除任務
代碼為:FreeRTOS_03_delete_task
本節代碼會涉及優先級的知識,可以只看vTaskDelete的用法,忽略優先級的講解。
我們要做這些事情:
- 創建任務1:任務1的大循環里,創建任務2,然后休眠一段時間
- 任務2:打印一句話,然后就刪除自己
任務1的代碼如下:
void vTask1( void *pvParameters )
{
const TickType_t xDelay100ms = pdMS_TO_TICKS( 100UL );
BaseType_t ret;
/* 任務函數的主體一般都是無限循環 */
for( ;; )
{
/* 打印任務的信息 */
printf("Task1 is running\r\n");
ret = xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
if (ret != pdPASS)
printf("Create Task2 Failed\r\n");
// 如果不休眠的話, Idle任務無法得到執行
// Idel任務會清理任務2使用的內存
// 如果不休眠則Idle任務無法執行, 最后內存耗盡
vTaskDelay( xDelay100ms );
}
任務2的代碼如下:
void vTask2( void *pvParameters )
{
/* 打印任務的信息 */
printf("Task2 is running and about to delete itself\r\n");
// 可以直接傳入參數NULL, 這里只是為了演示函數用法
vTaskDelete(xTask2Handle);
}
main函數代碼如下:
int main( void )
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
/* 啟動調度器 */
vTaskStartScheduler();
/* 如果程序運行到了這里就表示出錯了, 一般是內存不足 */
return 0;
}
運行結果如下:
任務運行圖:
- main函數中創建任務1,優先級為1。任務1運行時,它創建任務2,任務2的優先級是2。
- 任務2的優先級最高,它馬上執行。
- 任務2打印一句話后,就刪除了自己。
-
任務2被刪除后,任務1的優先級最高,輪到任務1繼續運行,它調用
vTaskDelay()
進入Block狀態 - 任務1 Block期間,輪到Idle任務執行:它釋放任務2的內存(TCB、棧)
- 時間到后,任務1變為最高優先級的任務繼續執行。
- 如此循環。
在任務1的函數中,如果不調用vTaskDelay,則Idle任務用于沒有機會執行,它就無法釋放創建任務2是分配的內存。
而任務1在不斷地創建任務,不斷地消耗內存,最終內存耗盡再也無法創建新的任務。
現象如下:
任務1的代碼中,需要注意的是:xTaskCreate的返回值。
- 很多手冊里說它失敗時返回值是pdFAIL,這個宏是0
- 其實失敗時返回值是errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY,這個宏是-1
- 為了避免混淆,我們使用返回值跟pdPASS來比較,這個宏是1
3.3 任務優先級和Tick
3.3.1 任務優先級
在上個示例中我們體驗過優先級的使用:高優先級的任務先運行。
優先級的取值范圍是:0~(configMAX_PRIORITIES – 1),數值越大優先級越高。
FreeRTOS的調度器可以使用2種方法來快速找出優先級最高的、可以運行的任務。使用不同的方法時,configMAX_PRIORITIES 的取值有所不同。
-
通用方法
使用C函數實現,對所有的架構都是同樣的代碼。對configMAX_PRIORITIES的取值沒有限制。但是configMAX_PRIORITIES的取值還是盡量小,因為取值越大越浪費內存,也浪費時間。
configUSE_PORT_OPTIMISED_TASK_SELECTION被定義為0、或者未定義時,使用此方法。 -
架構相關的優化的方法
架構相關的匯編指令,可以從一個32位的數里快速地找出為1的最高位。使用這些指令,可以快速找出優先級最高的、可以運行的任務。
使用這種方法時,configMAX_PRIORITIES的取值不能超過32。
configUSE_PORT_OPTIMISED_TASK_SELECTION被定義為1時,使用此方法。
在學習調度方法之前,你只要初略地知道:
- FreeRTOS會確保最高優先級的、可運行的任務,馬上就能執行
- 對于相同優先級的、可運行的任務,輪流執行
這無需記憶,就像我們舉的例子:
- 廚房著火了,當然優先滅火
- 喂飯、回復信息同樣重要,輪流做
3.3.2 Tick
對于同優先級的任務,它們“輪流”執行。怎么輪流?你執行一會,我執行一會。
"一會"怎么定義?
人有心跳,心跳間隔基本恒定。
FreeRTOS中也有心跳,它使用定時器產生固定間隔的中斷。這叫Tick、滴答,比如每10ms發生一次時鐘中斷。
如下圖:
- 假設t1、t2、t3發生時鐘中斷
- 兩次中斷之間的時間被稱為時間片(time slice、tick period)
- 時間片的長度由configTICK_RATE_HZ 決定,假設configTICK_RATE_HZ為100,那么時間片長度就是10ms
相同優先級的任務怎么切換呢?請看下圖:
- 任務2從t1執行到t2
-
在t2發生tick中斷,進入tick中斷處理函數:
- 選擇下一個要運行的任務
- 執行完中斷處理函數后,切換到新的任務:任務1
- 任務1從t2執行到t3
- 從下圖中可以看出,任務運行的時間并不是嚴格從t1,t2,t3哪里開始
有了Tick的概念后,我們就可以使用Tick來衡量時間了,比如:
vTaskDelay(2); // 等待2個Tick,假設configTICK_RATE_HZ=100, Tick周期時10ms, 等待20ms
// 還可以使用pdMS_TO_TICKS宏把ms轉換為tick
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms
注意,基于Tick實現的延時并不精確,比如vTaskDelay(2)
的本意是延遲2個Tick周期,有可能經過1個Tick多一點就返回了。
如下圖:
使用vTaskDelay函數時,建議以ms為單位,使用pdMS_TO_TICKS把時間轉換為Tick。
這樣的代碼就與configTICK_RATE_HZ無關,即使配置項configTICK_RATE_HZ改變了,我們也不用去修改代碼。
3.3.3 示例4: 優先級實驗
代碼為:FreeRTOS_04_task_priority
本程序會創建3個任務:
- 任務1、任務2:優先級相同,都是1
- 任務3:優先級最高,是2
任務1、2代碼如下:
void vTask1( void *pvParameters )
{
/* 任務函數的主體一般都是無限循環 */
for( ;; )
{
/* 打印任務的信息 */
printf("T1\r\n");
}
}
void vTask2( void *pvParameters )
{
/* 任務函數的主體一般都是無限循環 */
for( ;; )
{
/* 打印任務的信息 */
printf("T2\r\n");
}
}
任務3代碼如下:
void vTask3( void *pvParameters )
{
const TickType_t xDelay3000ms = pdMS_TO_TICKS( 3000UL );
/* 任務函數的主體一般都是無限循環 */
for( ;; )
{
/* 打印任務的信息 */
printf("T3\r\n");
// 如果不休眠的話, 其他任務無法得到執行
vTaskDelay( xDelay3000ms );
}
}
main函數代碼如下:
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);
xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL);
/* 啟動調度器 */
vTaskStartScheduler();
/* 如果程序運行到了這里就表示出錯了, 一般是內存不足 */
return 0;
}
運行情況如下圖所示:
- 任務3優先執行,直到它調用vTaskDelay主動放棄運行
- 任務1、任務2:輪流執行
調度情況如下圖所示:
3.3.4 示例5: 修改優先級
本節代碼為:FreeRTOS_05_change_priority
。
使用uxTaskPriorityGet來獲得任務的優先級:
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );
使用參數xTask來指定任務,設置為NULL表示獲取自己的優先級。
使用vTaskPrioritySet 來設置任務的優先級:
void vTaskPrioritySet( TaskHandle_t xTask,
UBaseType_t uxNewPriority );
使用參數xTask來指定任務,設置為NULL表示設置自己的優先級;
參數uxNewPriority表示新的優先級,取值范圍是0~(configMAX_PRIORITIES – 1)。
main函數的代碼如下,它創建了2個任務:任務1的優先級更高,它先執行:
int main( void )
{
prvSetupHardware();
/* Task1的優先級更高, Task1先執行 */
xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, &xTask2Handle );
/* 啟動調度器 */
vTaskStartScheduler();
/* 如果程序運行到了這里就表示出錯了, 一般是內存不足 */
return 0;
}
任務1的代碼如下:
void vTask1( void *pvParameters )
{
UBaseType_t uxPriority;
/* Task1,Task2都不會進入阻塞或者暫停狀態
* 根據優先級決定誰能運行
*/
/* 得到Task1自己的優先級 */
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
printf( "Task 1 is running\r\n" );
printf("About to raise the Task 2 priority\r\n" );
/* 提升Task2的優先級高于Task1
* Task2會即刻執行
*/
vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );
/* 如果Task1能運行到這里,表示它的優先級比Task2高
* 那就表示Task2肯定把自己的優先級降低了
*/
}
}
任務2的代碼如下:
void vTask2( void *pvParameters )
{
UBaseType_t uxPriority;
/* Task1,Task2都不會進入阻塞或者暫停狀態
* 根據優先級決定誰能運行
*/
/* 得到Task2自己的優先級 */
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
/* 能運行到這里表示Task2的優先級高于Task1
* Task1提高了Task2的優先級
*/
printf( "Task 2 is running\r\n" );
printf( "About to lower the Task 2 priority\r\n" );
/* 降低Task2自己的優先級,讓它小于Task1
* Task1得以運行
*/
vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
}
}
調度情況如下圖所示:
- 1:一開始Task1優先級最高,它先執行。它提升了Task2的優先級。
- 2:Task2的優先級最高,它執行。它把自己的優先級降低了。
- 3:Task1的優先級最高,再次執行。它提升了Task2的優先級。
- 如此循環。
- 注意:Task1的優先級一直是2,Task2的優先級是3或1,都大于0。所以Idel任務沒有機會執行。
3.4 任務狀態
以前我們很簡單地把任務的狀態分為2中:運行(Runing)、非運行(Not Running)。
對于非運行的狀態,還可以繼續細分,比如前面的FreeRTOS_04_task_priority
中:
- Task3執行vTaskDelay后:處于非運行狀態,要過3秒種才能再次運行
- Task3運行期間,Task1、Task2也處于非運行狀態,但是它們隨時可以運行
-
這兩種"非運行"狀態就不一樣,可以細分為:
- 阻塞狀態(Blocked)
- 暫停狀態(Suspended)
- 就緒狀態(Ready)
3.4.1 阻塞狀態(Blocked)
在日常生活的例子中,母親在電腦前跟同事溝通時,如果同事一直沒回復,那么母親的工作就被卡住了、被堵住了、處于阻塞狀態(Blocked)。重點在于:母親在等待。
在FreeRTOS_04_task_priority
實驗中,如果把任務3中的vTaskDelay調用注釋掉,那么任務1、任務2根本沒有執行的機會,任務1、任務2被"餓死"了(starve)。
在實際產品中,我們不會讓一個任務一直運行,而是使用"事件驅動"的方法讓它運行:
- 任務要等待某個事件,事件發生后它才能運行
- 在等待事件過程中,它不消耗CPU資源
- 在等待事件的過程中,這個任務就處于阻塞狀態(Blocked)
在阻塞狀態的任務,它可以等待兩種類型的事件:
-
時間相關的事件
- 可以等待一段時間:我等2分鐘
- 也可以一直等待,直到某個絕對時間:我等到下午3點
-
同步事件:這事件由別的任務,或者是中斷程序產生
- 例子1:任務A等待任務B給它發送數據
- 例子2:任務A等待用戶按下按鍵
-
同步事件的來源有很多(這些概念在后面會細講):
- 隊列(queue)
- 二進制信號量(binary semaphores)
- 計數信號量(counting semaphores)
- 互斥量(mutexes)
- 遞歸互斥量、遞歸鎖(recursive mutexes)
- 事件組(event groups)
- 任務通知(task notifications)
在等待一個同步事件時,可以加上超時時間。比如等待隊里數據,超時時間設為10ms:
- 10ms之內有數據到來:成功返回
- 10ms到了,還是沒有數據:超時返回
3.4.2 暫停狀態(Suspended)
在日常生活的例子中,母親正在電腦前跟同事溝通,母親可以暫停:
- 好煩啊,我暫停一會
- 領導說:你暫停一下
FreeRTOS中的任務也可以進入暫停狀態,唯一的方法是通過vTaskSuspend函數。函數原型如下:
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
參數xTaskToSuspend表示要暫停的任務,如果為NULL,表示暫停自己。
要退出暫停狀態,只能由別人來操作:
- 別的任務調用:vTaskResume
- 中斷程序調用:xTaskResumeFromISR
實際開發中,暫停狀態用得不多。
3.4.3 就緒狀態(Ready)
這個任務完全準備好了,隨時可以運行:只是還輪不到它。這時,它就處于就緒態(Ready)。
3.4.4 完整的狀態轉換圖
3.5 Delay函數
3.5.1 兩個Delay函數
有兩個Delay函數:
- vTaskDelay:至少等待指定個數的Tick Interrupt才能變為就緒狀態
- vTaskDelayUntil:等待到指定的絕對時刻,才能變為就緒態。
這2個函數原型如下:
void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: 等待多少給Tick */
/* pxPreviousWakeTime: 上一次被喚醒的時間
* xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement)
* 單位都是Tick Count
*/
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement );
下面畫圖說明:
- 使用vTaskDelay(n)時,進入、退出vTaskDelay的時間間隔至少是n個Tick中斷
-
使用xTaskDelayUntil(&Pre, n)時,前后兩次退出xTaskDelayUntil的時間至少是n個Tick中斷
- 退出xTaskDelayUntil時任務就進入的就緒狀態,一般都能得到執行機會
- 所以可以使用xTaskDelayUntil來讓任務周期性地運行
3.5.2 示例6: Delay
本節代碼為:FreeRTOS_06_taskdelay
。
本程序會創建2個任務:
-
Task1:
- 高優先級
-
設置變量flag為1,然后調用
vTaskDelay(xDelay50ms);
或vTaskDelayUntil(&xLastWakeTime, xDelay50ms);
-
Task2:
- 低優先級
- 設置變量flag為0
main函數代碼如下:
int main( void )
{
prvSetupHardware();
/* Task1的優先級更高, Task1先執行 */
xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
/* 啟動調度器 */
vTaskStartScheduler();
/* 如果程序運行到了這里就表示出錯了, 一般是內存不足 */
return 0;
}
Task1的代碼中使用條件開關來選擇Delay函數,把#if 1
改為#if 0
就可以使用vTaskDelayUntil
,代碼如下:
void vTask1( void *pvParameters )
{
const TickType_t xDelay50ms = pdMS_TO_TICKS( 50UL );
TickType_t xLastWakeTime;
int i;
/* 獲得當前的Tick Count */
xLastWakeTime = xTaskGetTickCount();
for( ;; )
{
flag = 1;
/* 故意加入多個循環,讓程序運行時間長一點 */
for (i = 0; i <5; i++)
printf( "Task 1 is running\r\n" );
#if 1
vTaskDelay(xDelay50ms);
#else
vTaskDelayUntil(&xLastWakeTime, xDelay50ms);
#endif
}
}
Task2的代碼如下:
void vTask2( void *pvParameters )
{
for( ;; )
{
flag = 0;
printf( "Task 2 is running\r\n" );
}
}
使用Keil的邏輯分析觀察flag變量的bit波形,如下:
- flag為1時表示Task1在運行,flag為0時表示Task2在運行,也就是Task1處于阻塞狀態
- vTaskDelay:指定的是阻塞的時間
- vTaskDelayUntil:指定的是任務執行的間隔、周期
3.6 空閑任務及其鉤子函數
3.6.1 介紹
在FreeRTOS_03_delete_task
的實驗里,我們體驗過空閑任務(Idle任務)的作用:釋放被刪除的任務的內存。
除了上述目的之外,為什么必須要有空閑任務?一個良好的程序,它的任務都是事件驅動的:平時大部分時間處于阻塞狀態。有可能我們自己創建的所有任務都無法執行,但是調度器必須能找到一個可以運行的任務:所以,我們要提供空閑任務。在使用vTaskStartScheduler()
函數來創建、啟動調度器時,這個函數內部會創建空閑任務:
- 空閑任務優先級為0:它不能阻礙用戶任務運行
- 空閑任務要么處于就緒態,要么處于運行態,永遠不會阻塞
空閑任務的優先級為0,這意味著一旦某個用戶的任務變為就緒態,那么空閑任務馬上被切換出去,讓這個用戶任務運行。在這種情況下,我們說用戶任務"搶占"(pre-empt)了空閑任務,這是由調度器實現的。
要注意的是:如果使用vTaskDelete()
來刪除任務,那么你就要確保空閑任務有機會執行,否則就無法釋放被刪除任務的內存。
我們可以添加一個空閑任務的鉤子函數(Idle Task Hook Functions),空閑任務的循環每執行一次,就會調用一次鉤子函數。鉤子函數的作用有這些:
- 執行一些低優先級的、后臺的、需要連續執行的函數
- 測量系統的空閑時間:空閑任務能被執行就意味著所有的高優先級任務都停止了,所以測量空閑任務占據的時間,就可以算出處理器占用率。
- 讓系統進入省電模式:空閑任務能被執行就意味著沒有重要的事情要做,當然可以進入省電模式了。
空閑任務的鉤子函數的限制:
- 不能導致空閑任務進入阻塞狀態、暫停狀態
-
如果你會使用
vTaskDelete()
來刪除任務,那么鉤子函數要非常高效地執行。如果空閑任務移植卡在鉤子函數里的話,它就無法釋放內存。
3.6.2 使用鉤子函數的前提
在FreeRTOS\Source\tasks.c
中,可以看到如下代碼,所以前提就是:
- 把這個宏定義為1:configUSE_IDLE_HOOK
-
實現
vApplicationIdleHook
函數
3.7 調度算法
3.7.1 重要概念
這些知識在前面都提到過了,這里總結一下。
正在運行的任務,被稱為"正在使用處理器",它處于運行狀態。在單處理系統中,任何時間里只能有一個任務處于運行狀態。
非運行狀態的任務,它處于這3中狀態之一:阻塞(Blocked)、暫停(Suspended)、就緒(Ready)。就緒態的任務,可以被調度器挑選出來切換為運行狀態,調度器永遠都是挑選最高優先級的就緒態任務并讓它進入運行狀態。
阻塞狀態的任務,它在等待"事件",當事件發生時任務就會進入就緒狀態。事件分為兩類:時間相關的事件、同步事件。所謂時間相關的事件,就是設置超時時間:在指定時間內阻塞,時間到了就進入就緒狀態。使用時間相關的事件,可以實現周期性的功能、可以實現超時功能。同步事件就是:某個任務在等待某些信息,別的任務或者中斷服務程序會給它發送信息。怎么"發送信息"?方法很多,有:任務通知(task notification)、隊列(queue)、事件組(event group)、信號量(semaphoe)、互斥量(mutex)等。這些方法用來發送同步信息,比如表示某個外設得到了數據。
3.7.2 配置調度算法
所謂調度算法,就是怎么確定哪個就緒態的任務可以切換為運行狀態。
通過配置文件FreeRTOSConfig.h的兩個配置項來配置調度算法:configUSE_PREEMPTION、configUSE_TIME_SLICING。
還有第三個配置項:configUSE_TICKLESS_IDLE,它是一個高級選項,用于關閉Tick中斷來實現省電,后續單獨講解。現在我們假設configUSE_TICKLESS_IDLE被設為0,先不使用這個功能。
調度算法的行為主要體現在兩方面:高優先級的任務先運行、同優先級的就緒態任務如何被選中。調度算法要確保同優先級的就緒態任務,能"輪流"運行,策略是"輪轉調度"(Round Robin Scheduling)。輪轉調度并不保證任務的運行時間是公平分配的,我們還可以細化時間的分配方法。
從3個角度統一理解多種調度算法:
可否搶占?高優先級的任務能否優先執行(配置項: configUSE_PREEMPTION)
- 可以:被稱作"可搶占調度"(Pre-emptive),高優先級的就緒任務馬上執行,下面再細化。
-
不可以:不能搶就只能協商了,被稱作"合作調度模式"(Co-operative Scheduling)
- 當前任務執行時,更高優先級的任務就緒了也不能馬上運行,只能等待當前任務主動讓出CPU資源。
- 其他同優先級的任務也只能等待:更高優先級的任務都不能搶占,平級的更應該老實點
可搶占的前提下,同優先級的任務是否輪流執行(配置項:configUSE_TIME_SLICING)
- 輪流執行:被稱為"時間片輪轉"(Time Slicing),同優先級的任務輪流執行,你執行一個時間片、我再執行一個時間片
- 不輪流執行:英文為"without Time Slicing",當前任務會一直執行,直到主動放棄、或者被高優先級任務搶占
在"可搶占"+"時間片輪轉"的前提下,進一步細化:空閑任務是否讓步于用戶任務(配置項:configIDLE_SHOULD_YIELD)
- 空閑任務低人一等,每執行一次循環,就看看是否主動讓位給用戶任務
- 空閑任務跟用戶任務一樣,大家輪流執行,沒有誰更特殊
列表如下:
配置項 | A | B | C | D | E |
---|---|---|---|---|---|
configUSE_PREEMPTION | 1 | 1 | 1 | 1 | 0 |
configUSE_TIME_SLICING | 1 | 1 | 0 | 0 | x |
configIDLE_SHOULD_YIELD | 1 | 0 | 1 | 0 | x |
說明 | 常用 | 很少用 | 很少用 | 很少用 | 幾乎不用 |
注:
- A:可搶占+時間片輪轉+空閑任務讓步
- B:可搶占+時間片輪轉+空閑任務不讓步
- C:可搶占+非時間片輪轉+空閑任務讓步
- D:可搶占+非時間片輪轉+空閑任務不讓步
- E:合作調度
3.7.3 示例7: 調度
本節代碼為:FreeRTOS_07_scheduler
。后續的實驗都是基于這個程序,通過修改配置項來觀察效果。
代碼里創建了3個任務:Task1、Task2的優先級都是0,跟空閑任務一樣,Task3優先級最高為2。程序里定義了4個全局變量,當某個的任務執行時,對應的變量就被設為1,可以通過Keil的邏輯分析儀查看任務切換情況:
static volatile int flagIdleTaskrun = 0; // 空閑任務運行時flagIdleTaskrun=1
static volatile int flagTask1run = 0; // 任務1運行時flagTask1run=1
static volatile int flagTask2run = 0; // 任務2運行時flagTask2run=1
static volatile int flagTask3run = 0; // 任務3運行時flagTask3run=1
main函數代碼如下:
int main( void )
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 0, NULL);
xTaskCreate(vTask2, "Task 2", 1000, NULL, 0, NULL);
xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL);
/* 啟動調度器 */
vTaskStartScheduler();
/* 如果程序運行到了這里就表示出錯了, 一般是內存不足 */
return 0;
}
任務1、任務2代碼如下,它們是"連續任務"(continuous task):
void vTask1( void *pvParameters )
{
/* 任務函數的主體一般都是無限循環 */
for( ;; )
{
flagIdleTaskrun = 0;
flagTask1run = 1;
flagTask2run = 0;
flagTask3run = 0;
/* 打印任務的信息 */
printf("T1\r\n");
}
}
void vTask2( void *pvParameters )
{
/* 任務函數的主體一般都是無限循環 */
for( ;; )
{
flagIdleTaskrun = 0;
flagTask1run = 0;
flagTask2run = 1;
flagTask3run = 0;
/* 打印任務的信息 */
printf("T2\r\n");
}
}
任務3代碼如下,它會調用vTaskDelay
,這樣別的任務才可以運行:
void vTask3( void *pvParameters )
{
const TickType_t xDelay5ms = pdMS_TO_TICKS( 5UL );
/* 任務函數的主體一般都是無限循環 */
for( ;; )
{
flagIdleTaskrun = 0;
flagTask1run = 0;
flagTask2run = 0;
flagTask3run = 1;
/* 打印任務的信息 */
printf("T3\r\n");
// 如果不休眠的話, 其他任務無法得到執行
vTaskDelay( xDelay5ms );
}
}
提供了一個空閑任務的鉤子函數:
void vApplicationIdleHook(void)
{
flagIdleTaskrun = 1;
flagTask1run = 0;
flagTask2run = 0;
flagTask3run = 0;
/* 故意加入打印讓flagIdleTaskrun變為1的時間維持長一點 */
printf("Id\r\n");
}
3.7.4 對比效果: 搶占與否
在FreeRTOSConfig.h
中,定義這樣的宏,對比邏輯分析儀的效果:
// 實驗1:搶占
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 1
// 實驗2:不搶占
#define configUSE_PREEMPTION 0
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 1
從下面的對比圖可以知道:
- 搶占時:高優先級任務就緒時,就可以馬上執行
-
不搶占時:優先級失去意義了,既然不能搶占就只能協商了,圖中任務1一直在運行(一點都沒有協商精神),其他任務都無法執行。即使任務3的
vTaskDelay
已經超時、即使它的優先級更高,都沒辦法執行。
3.7.5 對比效果: 時間片輪轉與否
在FreeRTOSConfig.h
中,定義這樣的宏,對比邏輯分析儀的效果:
// 實驗1:時間片輪轉
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 1
// 實驗2:時間片不輪轉
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 0
#define configIDLE_SHOULD_YIELD 1
從下面的對比圖可以知道:
- 時間片輪轉:在Tick中斷中會引起任務切換
- 時間片不輪轉:高優先級任務就緒時會引起任務切換,高優先級任務不再運行時也會引起任務切換。可以看到任務3就緒后可以馬上執行,它運行完畢后導致任務切換。其他時間沒有任務切換,可以看到任務1、任務2都運行了很長時間。
3.7.6 對比效果: 空閑任務讓步
在FreeRTOSConfig.h
中,定義這樣的宏,對比邏輯分析儀的效果:
// 實驗1:空閑任務讓步
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 1
// 實驗2:空閑任務不讓步
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 0
從下面的對比圖可以知道:
- 讓步時:在空閑任務的每個循環中,會主動讓出處理器,從圖中可以看到flagIdelTaskrun的波形很小
- 不讓步時:空閑任務跟任務1、任務2同等待遇,它們的波形寬度是差不多的
審核編輯:符乾江
評論
查看更多