本文主要學習FreeRTOS任務管理的相關知識,包括FreeRTOS創建/刪除任務、任務狀態、任務優先順序、延時函數、空閑任務和任務調度方法等知識 ...
1、準備材料
STM32CubeMX軟體(Version 6.10.0)
Keil µVision5 IDE(MDK-Arm)
一個滑動變阻器
邏輯分析儀nanoDLA
2、學習目標
本文主要學習FreeRTOS任務管理的相關知識,包括FreeRTOS創建/刪除任務、任務狀態、任務優先順序、延時函數、空閑任務和任務調度方法等知識
3、前提知識
3.1、任務函數長什麼樣?
FreeRTOS中任務是一個永遠不會退出的 C 函數,因此通常是作為無限迴圈實現,其不允許以任何方式從實現函數中返回,如果一個任務不再需要,可以顯示的將其刪除,其典型的任務函數結構如下所示
/**
* @brief 任務函數
* @retval None
*/
void ATaskFunction(void *pvParameters)
{
/*初始化或定義任務需要使用的變數*/
int iVariable = 0;
for(;;)
{
/*完成任務的功能代碼*/
}
/*跳出迴圈的任務需要被刪除*/
vTaskDelete(NULL);
}
3.2、創建一個任務
FreeRTOS提供了三個函數來創建任務(其中名為 xTaskCreateRestricted() 的函數僅供高級用戶使用,並且僅與 FreeRTOS MPU 埠相關,故此處不涉及該函數),具體的函數聲明如下所示
/**
* @brief 動態分配記憶體創建任務函數
* @param pvTaskCode:任務函數
* @param pcName:任務名稱,單純用於輔助調試
* @param usStackDepth:任務棧深度,單位為字(word)
* @param pvParameters:任務參數
* @param uxPriority:任務優先順序
* @param pxCreatedTask:任務句柄,可通過該句柄進行刪除/掛起任務等操作
* @retval pdTRUE:創建成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:記憶體不足創建失敗
*/
BaseType_t xTaskCreate(TaskFunction_t pvTaskCode,
const char * const pcName,
unsigned short usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask);
/**
* @brief 靜態分配記憶體創建任務函數
* @param pvTaskCode:任務函數
* @param pcName:任務名稱
* @param usStackDepth:任務棧深度,單位為字(word)
* @param pvParameters:任務參數
* @param uxPriority:任務優先順序
* @param puxStackBuffer:任務棧空間數組
* @param pxTaskBuffer:任務控制塊存儲空間
* @retval 創建成功的任務句柄
*/
TaskHandle_t xTaskCreateStatic(TaskFunction_t pvTaskCode,
const char * const pcName,
uint32_t ulStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
StackType_t * const puxStackBuffer,
StaticTask_t * const pxTaskBuffer);
上述兩個任務創建函數有如下幾點不同,之後如無特殊需要將一律使用動態分配記憶體的方式創建任務或其他實例
- xTaskCreateStatic 創建任務時需要用戶指定任務棧空間數組和任務控制塊的存儲空間,而 xTaskCreate 創建任務其存儲空間被動態分配,無需用戶指定
- xTaskCreateStatic 創建任務函數的返回值為成功創建的任務句柄,而 xTaskCreate 成功創建任務的句柄需要以參數形式提前定義並指定,同時其函數返回值僅表示任務創建成功/失敗
3.3、任務都有哪些狀態?
在FreeRTOS應用中往往會存在多個任務,但是對於單核的STM32等單片機而言,同一時刻只會有一個任務運行,因此對於一個任務來說要麼其處於運行狀態,要麼處於非運行狀態,而對於任務的非運行狀態又細分為以下三種狀態(尚不考慮被刪除的任務)
① 阻塞狀態:一個任務正在等待某個事件發生,調用可以進入阻塞狀態的API函數可以使任務進入阻塞狀態,等待的事件通常為以下兩種事件
-
時間相關事件:如 vTaskDelay() 或 vTaskDelayUntil(),處於運行狀態的任務調用這兩個延時函數就會進入阻塞狀態,等待延時時間結束後會進入就緒狀態,待任務調度後又會進入運行狀態
-
同步相關事件:例如嘗試進行讀取空隊列、嘗試寫入滿隊列、嘗試獲取尚未被釋放的二值信號量等等操作都會使任務進入阻塞狀態,這些同步事件會在後面的章節詳細講解
② 掛起狀態:一個任務暫時脫離調度器的調度,掛起狀態的任務對調度器來說不可見
- 讓一個任務進入掛起狀態的唯一方法是調用 vTaskSuspend() API函數
- 將一個任務從掛起狀態喚醒的唯一方法是調用 vTaskResume() API函數(在中斷中應調用掛起喚醒的中斷安全版本vTaskResumeFromISR() API函數)
/**
* @brief 掛起某個任務
* @param pxTaskToSuspend:被掛起的任務的句柄,通過傳入NULL來掛起自身
* @retval None
*/
void vTaskSuspend(TaskHandle_t pxTaskToSuspend);
/**
* @brief 將某個任務從掛起狀態恢復
* @param pxTaskToResume:正在恢復的任務的句柄
* @retval None
*/
void vTaskResume(TaskHandle_t pxTaskToResume);
/**
* @brief vTaskResume的中斷安全版本
* @param pxTaskToResume:正在恢復的任務的句柄
* @retval 返回退出中斷之前是否需要進行上下文切換(pdTRUE/pdFALSE)
*/
BaseType_t xTaskResumeFromISR(TaskHandle_t pxTaskToResume);
③ 就緒狀態:一個任務處於未運行狀態但是既沒有阻塞也沒有掛起,處於就緒狀態的任務當前尚未運行,但隨時可以進入運行狀態
下圖為一個任務在四種不同狀態(阻塞狀態、掛起狀態、就緒狀態和運行狀態)下完整的狀態轉移機製圖 (註釋1)
在程式中可以使用 eTaskGetState() API 函數利用任務的句柄查詢任務當前處於什麼狀態,任務的狀態由枚舉類型 eTaskState 表示,具體如下所示
/**
* @brief 查詢一個任務當前處於什麼狀態
* @param pxTask:要查詢任務狀態的任務句柄,NULL查詢自己
* @retval 任務狀態的枚舉類型
*/
eTaskState eTaskGetState(TaskHandle_t pxTask);
/*任務狀態枚舉類型返回值*/
typedef enum
{
eRunning = 0, /* 任務正在查詢自身的狀態,因此肯定是運行狀態 */
eReady, /* 就緒狀態 */
eBlocked, /* 阻塞狀態 */
eSuspended, /* 掛起狀態 */
eDeleted, /* 正在查詢的任務已被刪除,但其 TCB 尚未釋放 */
eInvalid /* 無效狀態 */
} eTaskState;
3.4、任務優先順序
FreeRTOS每個任務都擁有一個自己的優先順序,該優先順序可以在創建任務時以參數的形式傳入,也可以在需要修改時通過 vTaskPrioritySet() API函數動態設置優先順序
任務優先順序的設置範圍為1~(configMAX_PRIORITIES-1),任務設置的優先順序數字越大優先順序越高,設置優先順序時可以直接使用數字進行設置,也可以使用內核定義好的枚舉類型設置,另外可以使用 uxTaskPriorityGet() API函數獲取任務的優先順序,如下所示列出了部分優先順序枚舉類型定義
/*cmsis_os2.c中的定義*/
typedef enum {
osPriorityNone = 0, ///< No priority (not initialized).
osPriorityIdle = 1, ///< Reserved for Idle thread.
osPriorityLow = 8, ///< Priority: low
osPriorityNormal = 24, ///< Priority: normal
osPriorityAboveNormal = 32, ///< Priority: above normal
osPriorityHigh = 40, ///< Priority: high
osPriorityRealtime = 48, ///< Priority: realtime
osPriorityISR = 56, ///< Reserved for ISR deferred thread.
} osPriority_t;
任務的優先順序主要決定了在任務調度時,多個任務同時處於就緒態時應該讓哪個任務先執行,FreeRTOS調度器則保證了任何時刻總是在所有可運行的任務中選擇具有最高優先順序的任務,並將其進入運行態,如下所述為上述提到的兩個設置和獲取任務優先順序函數的具體聲明
/**
* @brief 設置任務優先順序
* @param pxTask:要修改優先順序的任務句柄,通過NULL改變任務自身優先順序
* @param uxNewPriority:要修改的任務優先順序
* @retval None
*/
void vTaskPrioritySet(TaskHandle_t pxTask, UBaseType_t uxNewPriority);
/**
* @brief 獲取任務優先順序
* @param pxTask:要獲取任務優先順序的句柄,通過NULL獲取任務自身優先順序
* @retval 任務優先順序
*/
UBaseType_t uxTaskPriorityGet(TaskHandle_t pxTask);
3.5、延時函數
學習STM32時經常會使用到HAL庫的延時函數HAL_Delay(),FreeRTOS也同樣提供了vTaskDelay() 和 vTaskDelayUntil() 兩個 API延時函數,如下所述
/**
* @brief 延時函數
* @param xTicksToDelay:延遲多少個心跳周期
* @retval None
*/
void vTaskDelay(TickType_t xTicksToDelay);
/**
* @brief 延時函數,用於實現一個任務固定執行周期
* @param pxPreviousWakeTime:保存任務上一次離開阻塞態的時刻
* @param xTimeIncrement:指定任務執行多少心跳周期
* @retval None
*/
void vTaskDelayUntil(TickType_t *pxPreviousWakeTime, TickType_t xTimeIncrement);
上述兩個延時函數與 HAL_Delay() 作用都是延時,但是FreeRTOS延時函數 API 可以讓任務進入阻塞狀態,而 HAL_Delay() 不具有該功能,因此如果一個任務需要使用延時,一般應該使用 FreeRTOS 的 API 函數讓任務進入阻塞狀態等待延時結束,處於阻塞狀態的任務便可以讓出內核處理其他任務
對於 vTaskDelayUntil() API函數的 pxPreviousWakeTime 參數一般通過 xTaskGetTickCount() API函數獲取,該函數作用為獲取滴答信號當前計數值,具體如下所述
/**
* @brief 獲取滴答信號當前計數值
* @retval 滴答信號當前計數值
*/
TickType_t xTaskGetTickCount(void);
/**
* @brief 獲取滴答信號當前計數值的中斷安全版本
*/
TickType_t xTaskGetTickCountFromISR(void);
/**
* @brief 周期任務函數結構
* @retval None
*/
void APeriodTaskFunction(void *pvParameters)
{
/*獲取任務創建後的滴答信號計數值*/
TickType_t pxPreviousWakeTime = xTaskGetTickCount();
for(;;)
{
/*完成任務的功能代碼*/
/*任務周期500ms*/
vTaskDelayUntil(&pxPreviousWakeTime, pdMS_TO_TICKS(500));
}
/*跳出迴圈的任務需要被刪除*/
vTaskDelete(NULL);
}
當一個任務因為延時函數或者其他同步事件進入阻塞狀態後,可以通過 xTaskAbortDelay() API 函數終止任務的阻塞狀態,即使事件任務等待尚未發生,或者任務進入時指定的超時時間阻塞狀態尚未過去,都會使其進入就緒狀態,具體函數描述如下所述
/**
* @brief 終止任務延時,退出阻塞狀態
* @param xTask:操作的任務句柄
* @retval pdPASS:任務成功從阻塞狀態中刪除,pdFALSE:任務不屬於阻塞狀態導致刪除失敗
*/
BaseType_t xTaskAbortDelay(TaskHandle_t xTask);
3.6、為什麼會有空閑任務?
3.6.1、概述
FreeRTOS 調度器決定在任何時刻處理器必須保持有一個任務運行,當用戶創建的所有任務都處於阻塞狀態不能運行時,空閑任務就會被運行
空閑任務是一個優先順序為0(最低優先順序)的非常短小的迴圈,其優先順序為 0 保證了不會影響到具有更高優先順序的任務進入運行態,一旦有更高優先順序的任務進入就緒態,空閑任務就會立刻切出運行態
空閑任務何時被創建?當調用 vTaskStartScheduler() 啟動調度器時就會自動創建一個空閑任務,如下圖所示,另外空閑任務還負責將分配給已刪除任務的記憶體釋放掉
3.6.2、空閑任務鉤子函數
空閑任務有一個鉤子函數,可以通過配置 configUSE_IDLE_HOOK 參數為 Enable 啟動空閑任務的鉤子函數,如果是使用STM32CubeMX軟體生成的工程則會自動生成空閑任務鉤子函數,當調度器調度內核進入空閑任務時就會調用鉤子函數
通常空閑任務鉤子函數主要被用於下方函數體內部註釋列舉的幾種情況,如下所述為空閑任務鉤子函數典型的任務函數結構
/**
* @brief 空閑任務鉤子函數
* @retval NULL
*/
void vApplicationIdleHook(void)
{
/*
1.執行低優先順序,或後臺需要不停處理的功能代碼
2.測試系統處理裕量(內核執行空閑任務時間越長表示內核越空閑)
3.將處理器配置到低功耗模式(Tickless模式)
*/
}
除了空閑任務鉤子函數外,FreeRTOS提供了一系列鉤子函數供用戶選擇使用,具體讀者可查看FreeRTOS教程1 基礎知識文章“4.1.3、外設參數配置”小節參數列表中的“Hook function related definitions”,使用之前只需在STM32CubeMX中啟用相關參數,然後在生成的代碼中找到鉤子函數使用即可
3.7、刪除任務
一個任務不再需要時,需要顯示調用 vTaskDelete() API函數將任務刪除,該函數需要傳入要刪除任務的句柄這個參數(傳入NULL時表示刪除自己),函數聲明如下所述
/**
* @brief 任務刪除函數
* @param pxTaskToDelete:要刪除的任務句柄,NULL表示刪除自己
* @retval None
*/
void vTaskDelete(TaskHandle_t pxTaskToDelete);
3.8、任務調度方法
調度器保證了總是在所有可運行的任務中選擇具有最高優先順序的任務,並將其進入運行態,根據 configUSE_PREEMPTION (使用搶占調度器) 和 configUSE_TIME_SLICING (使用時間片輪詢) 兩個參數的不同,FreeRTOS涉及三種不同的調度方法
- 時間片輪詢的搶占式調度方法(configUSE_PREEMPTION=1,configUSE_TIME_SLICING=1)
- 不用時間片輪詢的搶占式調度方法(configUSE_PREEMPTION=1,configUSE_TIME_SLICING=0)
- 協作式調度方法(configUSE_PREEMPTION=0)
本文只介紹搶占式調度方法(後續所有文章全部採用時間片輪詢的搶占式調度方法),不涉及協作式的調度方法
什麼是時間片?
FreeRTOS基礎時鐘的一個定時周期稱為一個時間片,所以其長度由 configTICK_RATE_HZ 參數決定,預設情況下為1000HZ(也即1ms)
對於時間片輪詢的搶占式調度方法,其在任務調度過程中一般滿足以下兩點要求
- 高優先順序的任務可以搶占低優先順序的任務
- 同等優先順序的任務根據時間片輪流執行
對於不用時間片輪詢的搶占式調度方法,其在任務調度過程中一般滿足以下兩點要求
- 高優先順序的任務同樣可以搶占低優先順序的任務
- 同等優先順序的任務不會按照時間片輪流執行,可能出現任務間占用處理器時間相差很大的情況
任務調度主要是由任務調度器 scheduler 負責,其由 FreeRTOS 內核管理,用戶一般無需控制任務調度器,但是 FreeRTOS 也給用戶提供了啟動、停止、掛起和恢復三個常見的控制 scheduler 的 API 函數,具體如下所述
/**
* @brief 啟動調度器
* @retval None
*/
void vTaskStartScheduler(void);
/**
* @brief 停止調度器
* @retval None
*/
void vTaskEndScheduler(void);
/**
* @brief 掛起調度器
* @retval None
*/
void vTaskSuspendAll(void);
/**
* @brief 恢復調度器
* @retval 返回是否會導致發生掛起的上下文切換(pdTRUE/pdFALSE)
*/
BaseType_t xTaskResumeAll(void);
除了任務被時間片輪詢切換或者高優先順序搶占發生切換兩種常見的調度方式外,還有其他的調度方式,比如任務自願讓出處理器給其他任務使用等函數,這些函數將在後續 “中斷管理” 章節中被詳細介紹,這裡簡單瞭解即可,如下所述
/**
* @brief 讓位於另一項同等優先順序的任務
* @retval None
*/
void taskYIELD(void);
/**
* @brief ISR 退出時是否執行上下文切換(彙編)
* @param xHigherPriorityTaskWoken:pdFASLE不請求上下文切換,反之請求上下文切換
* @retval None
*/
portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);
/**
* @brief ISR 退出時是否執行上下文切換(C語言)
* @param xHigherPriorityTaskWoken:pdFASLE不請求上下文切換,反之請求上下文切換
* @retval None
*/
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
3.9、工具函數
任務相關的實用工具函數較多,官方網站上一共列出了23個 API 函數,這裡筆者僅簡單介紹一些可能常用的 API 函數,如果讀者有其他希望瞭解的函數,可以自行前往 FreeRTOS/API 引用/任務實用程式 中瞭解
另外讀者應註意,如果要使用下方某些函數則可能需要在CubeMX的FREERTOS/Include parameters參數配置頁面中勾選啟用對應的API函數,具體可查看FreeRTOS教程1 基礎知識文章"4.1.3、外設參數配置"小節下方的參數表格
3.9.1、獲取任務信息
/**
* @brief 獲取一個任務的信息,需啟用參數configUSE_TRACE_FACILITY(預設啟用)
* @param xTask:需要查詢的任務句柄,NULL查詢自己
* @param pxTaskStatus:用於存儲任務狀態信息的TaskStatus_t結構體指針
* @param xGetFreeStackSpace:是否返回棧空間高水位值
* @param eState:指定查詢信息時任務的狀態,設置為eInvalid將自動獲取任務狀態
* @retval None
*/
void vTaskGetInfo(TaskHandle_t xTask,
TaskStatus_t *pxTaskStatus,
BaseType_t xGetFreeStackSpace,
eTaskState eState);
/**
* @brief 獲取當前任務句柄
* @retval 返回當前任務句柄
*/
TaskHandle_t xTaskGetCurrentTaskHandle(void);
/**
* @brief 獲取任務句柄(運行時間較長,不宜大量使用)
* @param pcNameToQuery:要獲取任務句柄的任務名稱字元串
* @retval 返回指定查詢任務的句柄
*/
TaskHandle_t xTaskGetHandle(const char *pcNameToQuery);
/**
* @brief 獲取空閑任務句柄
* @註意:需要設置 INCLUDE_xTaskGetIdleTaskHandle 為1,在CubeMX中不可調,需自行定義
* @retval 返回空閑任務句柄
*/
TaskHandle_t xTaskGetIdleTaskHandle(void);
/**
* @brief 獲取一個任務的高水位值(任務棧空間最少可用剩餘空間大小,單位為字(word))
* @param xTask:要獲取高水位值任務的句柄,NULL查詢自己
* @retval
*/
UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask);
/**
* @brief 獲取一個任務的任務名稱字元串
* @param xTaskToQuery:要獲取名稱字元串的任務的句柄,NULL查詢自己
* @retval 返回一個任務的任務名稱字元串
*/
char* pcTaskGetName(TaskHandle_t xTaskToQuery);
3.9.2、獲取內核信息
/**
* @brief 獲取系統內所有任務狀態,為每個任務返回一個TaskStatus_t結構體數組
* @param pxTaskStatusArray:數組的指針,數組每個成員都是TaskStatus_t類型,用於存儲獲取到的信息
* @param uxArraySize:設置數組pxTaskStatusArray的成員個數
* @param pulTotalRunTime:返回FreeRTOS運行後總的運行時間,NULL表示不返回該數據
* @retval 返回實際獲取的任務信息條數
*/
UBaseType_t uxTaskGetSystemState(TaskStatus_t * const pxTaskStatusArray,
const UBaseType_t uxArraySize,
unsigned long * const pulTotalRunTime);
/**
* @brief 返回調度器狀態
* @retval 0:被掛起,1:未啟動,2:正在運行
*/
BaseType_t xTaskGetSchedulerState(void);
/**
* @brief 獲取內核當前管理的任務總數
* @retval 返回內核當前管理的任務總數
*/
UBaseType_t uxTaskGetNumberOfTasks(void);
/**
* @brief 獲取內核中所有任務的字元串列表信息
* @param pcWriteBuffer:字元數組指針,用於存儲獲取的字元串信息
* @retval None
*/
void vTaskList(char *pcWriteBuffer);
3.9.3、其他函數
/**
* @brief 獲取一個任務的標簽值
* @param xTask:要獲取任務標簽值的任務句柄,NULL表示獲取自己的標簽值
* @retval 返回任務的標簽值
*/
TaskHookFunction_t xTaskGetApplicationTaskTag(TaskHandle_t xTask);
/**
* @brief 獲取一個任務的標簽值的中斷安全版本函數
*/
TaskHookFunction_t xTaskGetApplicationTaskTagFromISR(TaskHandle_t xTask);
/**
* @brief 設置一個任務的標簽值,標簽值保存在任務控制塊中
* @param xTask:要設置標簽值的任務的句柄,NULL表示設置自己
* @param pxTagValue:要設置的標簽值
* @retval None
*/
void vTaskSetApplicationTaskTag(TaskHandle_t xTask,
TaskHookFunction_t pxTagValue);
4、實驗一:嘗試任務基本操作
4.1、實驗目的
- 創建一個任務 TASK_GREEN_LED ,每 100ms 改變一次 GREEN_LED 的狀態
- 使用靜態記憶體分配創建一個任務 TASK_RED_LED ,每 500ms 改變一次 RED_LED 的狀態
- 創建一個任務 TASK_KEY_SCAN ,用於實現按鍵掃描功能,當開發板上的 KEY2 按鍵按下時刪除任務 TASK_GREEN_LED ,當開發板上的 KEY1 按鍵按下時掛起任務 TASK_RED_LED ,當開發板上的 KEY0 按鍵按下時恢復任務 TASK_RED_LED
4.2、CubeMX相關配置
首先讀者應按照FreeRTOS教程1 基礎知識章節配置一個可以正常編譯通過的 FreeRTOS 空工程,然後在此空工程的基礎上增加本實驗所提出的要求
本實驗需要初始化開發板上 GREEN_LED 和 RED_LED 兩個 LED 燈作為顯示,具體配置步驟請閱讀“STM32CubeMX教程2 GPIO輸出 - 點亮LED燈”,註意雖開發板不同但配置原理一致,如下圖所示
本實驗需要初始化開發板上 KEY2、KEY1 和 KEY0 用戶按鍵做普通輸入,具體配置步驟請閱讀“STM32CubeMX教程3 GPIO輸入 - 按鍵響應”,註意雖開發板不同但配置原理一致,如下圖所示
本實驗需要初始化 USART1 作為輸出信息渠道,具體配置步驟請閱讀“STM32CubeMX教程9 USART/UART 非同步通信”,如下圖所示
單擊 Middleware and Software Packs/FREERTOS ,在 Configuration 中單擊 Tasks and Queues 選項卡,首先雙擊預設任務修改其參數,然後單擊 Add 按鈕按要求增加另外兩個任務,由於按鍵掃描任務比閃爍 LED 燈任務重要,因此將其優先順序配置為稍高,配置好的界面如下圖所示
假設之前配置空工程時已經配置好了 Clock Configuration 和 Project Manager 兩個頁面,接下來直接單擊 GENERATE CODE 按鈕生成工程代碼即可
4.3、添加其他必要代碼
按照 “STM32CubeMX教程9 USART/UART 非同步通信” 實驗 “6、串口printf重定向” 小節增加串口 printf 重定向代碼,具體不再贅述
打開 freertos.c 文件夾,按要求增加三個任務的實現代碼,其中阻塞延時函數 osDelay() 為 vTaskDelay() 函數的包裝版本,具體源代碼如下所述
/*GREEN_LED閃爍任務函數*/
void TASK_GREEN_LED(void *argument)
{
/* USER CODE BEGIN TASK_GREEN_LED */
/* Infinite loop */
for(;;)
{
//每隔100ms閃爍一次GREEN_LED
HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin);
printf("TASK_GREEN_LED, GREEN LED BLINK!\r\n");
osDelay(pdMS_TO_TICKS(100));
}
/* USER CODE END TASK_GREEN_LED */
}
/*RED_LED閃爍任務函數*/
void TASK_RED_LED(void *argument)
{
/* USER CODE BEGIN TASK_RED_LED */
/* Infinite loop */
for(;;)
{
//每隔500ms閃爍一次RED_LED
HAL_GPIO_TogglePin(RED_LED_GPIO_Port, RED_LED_Pin);
printf("TASK_RED_LED, RED LED BLINK!\r\n");
osDelay(pdMS_TO_TICKS(500));
}
/* USER CODE END TASK_RED_LED */
}
/*KEY_SCAN按鍵掃描任務函數*/
void TASK_KEY_SCAN(void *argument)
{
/* USER CODE BEGIN TASK_KEY_SCAN */
uint8_t key_value = 0;
/* Infinite loop */
for(;;)
{
key_value = 0;
//按鍵KEY2按下
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
key_value = 3;
//按鍵KEY1按下
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
key_value = 2;
//按鍵KEY0按下
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
key_value = 1;
if(key_value != 0)
{
if(key_value == 3)
{
printf("\r\n\r\nKEY2 PRESSED, Delete TASK_GREEN_LED!\r\n\r\n");
//此處可使用vTaskDelete(task_GREEN_LEDHandle),但要註意不能重覆刪除句柄
osThreadTerminate(task_GREEN_LEDHandle);
}
else if(key_value == 2)
{
printf("\r\n\r\nKEY1 PRESSED, Suspend TASK_RED_LED!\r\n\r\n");
vTaskSuspend(task_RED_LEDHandle);
}
else if(key_value == 1)
{
printf("\r\n\r\nKEY0 PRESSED, Resume TASK_RED_LED!\r\n\r\n");
vTaskResume(task_RED_LEDHandle);
}
//有按鍵按下就進行按鍵消抖
osDelay(300);
}
else
osDelay(10);
}
/* USER CODE END TASK_KEY_SCAN */
}
當實現三個任務的函數體之後就不需要其他任何操作了,因為任務的創建、調用等工作的程式代碼 STM32CubeMX 軟體已經自動生成了,這裡為方便初學者理解做一下簡單介紹,之後便不再重覆介紹
打開工程項目中 main.c 文件,我們可以發現在主函數 main() 中調用了 MX_FREERTOS_Init() 函數,該函數中已經自動創建了我們在 STM32CubeMX 軟體中創建的三個任務,其中 osThreadNew() 函數為 xTaskCreate() / xTaskCreateStatic() 的包裝函數,如下圖所示
4.4、燒錄驗證
燒錄程式,打開串口助手,可以發現串口上源源不斷地輸出 TASK_GREEN_LED 和 TASK_RED_LED 運行的提示,每輸出5次 TASK_GREEN_LED 然後就會輸出1次 TASK_RED_LED,同時開發板上的紅色和綠色LED燈也不停閃爍
當按下開發板上的 KEY2 按鍵,串口提示刪除 TASK_GREEN_LED ,之後會發現只有 TASK_RED_LED 運行的串口輸出;當按下開發板上的 KEY1 按鍵,串口提示掛起 TASK_RED_LED,之後 TASK_RED_LED 會停止執行;最後按下開發板上的 KEY0 按鍵,串口提示恢復 TASK_RED_LED,TASK_RED_LED 恢復運行
上述整個過程串口輸出信息如下圖所示
如果不操作按鍵,其任務流程應該如下所述
- 在 t1 時刻,調度器剛剛開始運行,其瀏覽任務列表發現有兩個進入就緒態的任務,即剛剛創建好的任務 TASK_GREEN_LED 和 TASK_RED_LED,由於兩個任務優先順序均相同,但是 TASK_GREEN_LED 先建立,因此調度器決定先執行該任務,TASK_GREEN_LED 調用了延時函數 osDelay() 讓任務進入阻塞狀態,然後調度器發現還有就緒的任務,於是切換到任務 TASK_RED_LED ,同理執行到延時函數讓任務進入了阻塞狀態
- 在 t2 時刻,調度器發現任務列表裡已經沒有就緒的任務(兩個任務都進入了阻塞狀態),於是選擇執行空閑任務
- 在 t3 時刻,任務 TASK_GREEN_LED 延時結束,從阻塞狀態進入就緒狀態,由於任務 TASK_GREEN_LED 優先順序高於空閑任務,因此該任務搶占空閑任務進入運行狀態,執行完函數體再次遇到延時函數 osDelay() 讓任務進入阻塞狀態,然後不斷重覆步驟3的過程
- 在 t7 時刻,任務 TASK_GREEN_LED 和 TASK_RED_LED 同時延時結束,從阻塞狀態進入就緒狀態,然後調度器重覆步驟1的過程
上述任務流程圖具體如下圖所示
4.5、探討延時函數特性
如果將任務 TASK_GREEN_LED 和 TASK_RED_LED 函數體內的延時函數 osDelay() 更改為 HAL 庫的延時函數 HAL_Delay() 函數 ,根據“3.5、延時函數”小節內容可知,HAL_Delay() 函數不會使任務進入阻塞狀態
值得註意的是這兩個任務目前優先順序相同,均為 osPriorityNormal ,因此根據 “3.8、任務調度方法” 小節內容可知,採用時間片輪詢的搶占式調度方式對於同等優先順序的任務採用時間片輪詢執行,所以如果不操作按鍵,只修改延時函數後的任務流程應該如下圖所述
從圖上可以看出,由於任務不會進入阻塞狀態,因此兩個同等優先順序的任務會按照時間片輪流執行,而空閑函數則不會得到執行
4.6、任務被餓死了
接著上面所述,假設將任務 TASK_RED_LED 的優先順序修改為 osPriorityBelowNormal,該優先順序低於任務 TASK_GREEN_LED 的優先順序,然後保持延時函數為 HAL_Delay() 函數不變,並且不操作按鍵,其任務流程應該如下所述
從圖上可以看出,由於任務不會進入阻塞狀態,因此高優先順序的任務會一直得到執行,從而將低優先順序的任務餓死了,所以在實際使用中,任務應該使用能夠進入阻塞狀態的延時函數
4.7、使用 vTaskDelayUntil()
根據 ”4.4、燒錄驗證“ 小節任務流程圖可知,對任務延時並不能達到讓任務以固定周期執行,如果讀者希望能夠讓一個任務嚴格按照固定周期執行,可以使用 vTaskDelayUntil() 函數實現,修改任務函數如下所示
/*GREEN_LED閃爍任務函數*/
void TASK_GREEN_LED(void *argument)
{
/* USER CODE BEGIN TASK_GREEN_LED */
TickType_t previousWakeTime = xTaskGetTickCount();
/* Infinite loop */
for(;;)
{
//進入臨界段
taskENTER_CRITICAL();
//每隔100ms閃爍一次GREEN_LED
HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin);
printf("TASK_GREEN_LED, GREEN LED BLINK!\r\n");
//退出臨界段
taskEXIT_CRITICAL();
//也可使用osDelayUntil(pdMS_TO_TICKS(100));
vTaskDelayUntil(&previousWakeTime, pdMS_TO_TICKS(100));
}
/* USER CODE END TASK_GREEN_LED */
}
/*RED_LED閃爍任務函數*/
void TASK_RED_LED(void *argument)
{
/* USER CODE BEGIN TASK_RED_LED */
TickType_t previousWakeTime = xTaskGetTickCount();
/* Infinite loop */
for(;;)
{
//進入臨界段
taskENTER_CRITICAL();
//每隔500ms閃爍一次RED_LED
HAL_GPIO_TogglePin(RED_LED_GPIO_Port, RED_LED_Pin);
printf("TASK_RED_LED, RED LED BLINK!\r\n");
//退出臨界段
taskEXIT_CRITICAL();
//也可使用osDelayUntil(pdMS_TO_TICKS(500));
vTaskDelayUntil(&previousWakeTime, pdMS_TO_TICKS(100));
}
/* USER CODE END TASK_RED_LED */
}
由於 TASK_GREEN_LED 100ms 執行一次,TASK_RED_LED 500ms 執行一次,所以存在同時執行的情況,可能會導致串口輸出數據出錯,因此這裡使用了臨界段保護串口輸出程式,臨界段相關知識將在後續FreeRTOS教程3 中斷管理文章中介紹到
使用邏輯分析儀採集紅色和綠色兩個 LED 燈引腳電平變化,可以發現其執行周期與設置一致,誤差可以接受,具體如下圖所示
與單純使用延時函數的程式做對比,可以發現只使用延時函數的任務執行周期誤差較大,無法做到固定周期運行,具體如下圖所示
5、實驗二:獲取任務信息
5.1、實驗目的
- 創建任務 TASK_ADC,該任務通過 ADC1 的 IN5 通道周期採集電位器的電壓值,並通過串口輸出採集到的 ADC 值;
- 創建任務 TASK_KEY_SCAN ,當按鍵 KEY2 按下時根據任務句柄獲取單個任務的信息並通過串口輸出到串口助手上;當按鍵 KEY1 按下時獲取每個任務的高水位值並通過串口輸出到串口助手上;當按鍵 KEY0 按下時獲取系統任務列表並通過串口輸出到串口助手上;
5.2、CubeMX相關配置
同樣讀者應按照FreeRTOS教程1 基礎知識章節配置一個可以正常編譯通過的FreeRTOS空工程,然後在此空工程的基礎上增加本實驗所提出的要求
本實驗需要初始化開發板上 KEY2、KEY1 和 KEY0 用戶按鍵做普通輸入,具體配置步驟請閱讀“STM32CubeMX教程3 GPIO輸入 - 按鍵響應”,註意雖開發板不同但配置原理一致,如下圖所示
本實驗需要初始化 USART1 作為輸出信息渠道,具體配置步驟請閱讀“STM32CubeMX教程9 USART/UART 非同步通信”,如下圖所示
單擊 Analog 中的 ADC1 ,勾選 IN5 ,在下方的參數配置中僅將 IN5 的採樣時間修改為 15Cycles 即可,對 ADC 單通道採集感興趣的讀者可以閱讀“STM32CubeMX教程13 ADC - 單通道轉換”實驗,如下圖所示
單擊 Middleware and Software Packs/FREERTOS ,在 Configuration 中單擊 Tasks and Queues 選項卡,首先雙擊預設任務修改其參數,然後單擊 Add 按鈕按要求增加另外一個任務,配置好的界面如下圖所示
由於需要使用到一些獲取信息的函數,有些預設情況下並不能使用,需要用戶配置參數將其加入到編譯中,因此需要做以下兩個操作
- 在 Config parameters 中啟用 USE_TRACE_FACILITY 參數和 USE_STATS_FORMATTING_FUNCTIONS 參數,目的是為了使用 vTaskList() API 函數
- 在生成的工程代碼中找到 FreeRTOSConfig.h 文件,在用戶代碼區域添加下述代碼,目的是為了使用獲取空閑任務句柄 xTaskGetIdleTaskHandle() API 函數
#define INCLUDE_xTaskGetIdleTaskHandle 1
配置 Clock Configuration 和 Project Manager 兩個頁面,接下來直接單擊 GENERATE CODE 按鈕生成工程代碼即可
5.3、添加其他必要代碼
首先添加串口 printf 重定向函數,不再贅述,然後打開 freertos.c 文件,添加需要使用到的 ADC 的頭文件,如下所述
/*添加頭文件*/
#include "adc.h"
最後根據實驗目的編寫程式完成 TASK_ADC 和 TASK_KEY_SCAN 兩個任務,具體如下所示
/*ADC周期採集任務*/
void TASK_ADC(void *argument)
{
/* USER CODE BEGIN TASK_ADC */
TickType_t previousWakeTime = xTaskGetTickCount();
/* Infinite loop */
for(;;)
{
//開始臨界代碼段,不允許任務調度
taskENTER_CRITICAL();
HAL_ADC_Start(&hadc1);
if(HAL_ADC_PollForConversion(&hadc1,200)==HAL_OK)
{
uint32_t val=HAL_ADC_GetValue(&hadc1);
uint32_t Volt=(3300*val)>>12;
printf("val:%d, Volt:%d\r\n",val,Volt);
}
//結束臨界代碼段,重新允許任務調度
taskEXIT_CRITICAL();
//500ms周期
vTaskDelayUntil(&previousWakeTime, pdMS_TO_TICKS(500));
}
/* USER CODE END TASK_ADC */
}
/*按鍵掃描KEY_SCAN任務*/
void TASK_KEY_SCAN(void *argument)
{
/* USER CODE BEGIN TASK_KEY_SCAN */
uint8_t key_value = 0;
TaskHandle_t taskHandle = task_ADCHandle;
/* Infinite loop */
for(;;)
{
key_value = 0;
//按鍵KEY2按下
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
key_value = 3;
//按鍵KEY1按下
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
key_value = 2;
//按鍵KEY0按下
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
key_value = 1;
//如果有按鍵按下
if(key_value != 0)
{
if(key_value == 3)
{
taskHandle = task_ADCHandle;
TaskStatus_t taskInfo;
//是否獲取高水位值
BaseType_t getFreeStackSpace = pdTRUE;
//當前的狀態,設置為eInvalid將自動獲取任務狀態
eTaskState taskState = eInvalid;
//獲取任務信息
vTaskGetInfo(taskHandle, &taskInfo, getFreeStackSpace, taskState);
//開始臨界代碼段,不允許任務調度
taskENTER_CRITICAL();
printf("\r\n--- KEY2 PRESSED ---\r\n");
printf("Task_Info: Show task info,Get by vTaskGetInfo();\r\n");
printf("Task Name = %s\r\n", (uint8_t *)taskInfo.pcTaskName);
printf("Task Number = %d\r\n", (uint16_t)taskInfo.xTaskNumber);
printf("Task State = %d\r\n", taskInfo.eCurrentState);
printf("Task Priority = %d\r\n", (uint8_t)taskInfo.uxCurrentPriority);
printf("High Water Mark = %d\r\n\r\n", taskInfo.usStackHighWaterMark);
//結束臨界代碼段,重新允許任務調度
taskEXIT_CRITICAL();
}
else if(key_value == 2)
{
//開始臨界代碼段,不允許任務調度
taskENTER_CRITICAL();
printf("\r\n--- KEY1 PRESSED ---\r\n");
//獲取空閑任務句柄
taskHandle = xTaskGetIdleTaskHandle();
//獲取任務高水位值
UBaseType_t hwm = uxTaskGetStackHighWaterMark(taskHandle);
printf("Idle Task'Stack High Water Mark = %d\r\n", (uint16_t)hwm);
//Task_ADC的任務句柄
taskHandle=task_ADCHandle;
hwm = uxTaskGetStackHighWaterMark(taskHandle);
printf("Task_ADC'Stack High Water Mark = %d\r\n", (uint16_t)hwm);
//Task_KEY_SCAN的任務句柄
taskHandle=task_KEY_SCANHandle;
hwm = uxTaskGetStackHighWaterMark(taskHandle);
printf("Task_KEY_SCAN'Stack High Water Mark = %d\r\n", (uint16_t)hwm);
//獲取系統任務個數
UBaseType_t taskNum=uxTaskGetNumberOfTasks();
printf("There are now %d tasks in total!\r\n\r\n", (uint16_t)taskNum);
//結束臨界代碼段,重新允許任務調度
taskEXIT_CRITICAL();
}
else if(key_value == 1)
{
//開始臨界代碼段,不允許任務調度
taskENTER_CRITICAL();
printf("\r\n--- KEY0 PRESSED ---\r\n");
char infoBuffer[300];
//獲取任務列表
vTaskList(infoBuffer);
printf("%s\r\n\r\n",infoBuffer);
//結束臨界代碼段,重新允許任務調度
taskEXIT_CRITICAL();
}
//按鍵消抖
osDelay(300);
}
else
osDelay(10);
}
/* USER CODE END TASK_KEY_SCAN */
}
5.4、燒錄驗證
燒錄程式,打開串口助手,可以發現串口上源源不斷地輸出 TASK_ADC 採集到的 ADC 值,首先從一端緩慢旋轉滑動變阻器直到另一端,可以發現採集到的 ADC 值從 0 逐漸變為最大值 4095 ,表示 ADC 採集任務正常運行
按下 KEY2 按鍵,串口會輸出任務 TASK_ADC 的相關信息,包括任務名稱、任務數量、任務狀態、任務優先順序和任務棧高水位值等信息
按下 KEY1 按鍵,串口會輸出空閑任務、 ADC 採集任務和按鍵掃描任務三個任務的高水位值,同時會輸出系統中一共存在的任務數量
為什麼有4個任務?
按下 KEY0 按鍵,串口會以列表形式輸出系統中的所有任務,可以看到第4個任務是名為 Tmr Svc 的定時器守護任務,vTaskList() API 函數會將每個任務以 “Task_Name \tX\t25\t128\t2\r\n" 形式寫入緩存數組中,從左往右依次表示任務名稱、任務狀態(X:運行,R:就緒,B:阻塞)、任務優先順序、棧空間高水位置和任務編號
上述整個過程串口輸出信息如下圖所示
6、註釋詳解
註釋1:圖片來源 Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf
參考資料
Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf