本文主要學習 FreeRTOS 軟體定時器的相關知識,包括軟體定時器回調函數、屬性、狀態、運行原理和常見 API 函數等知識 ...
1、準備材料
STM32CubeMX軟體(Version 6.10.0)
Keil µVision5 IDE(MDK-Arm)
2、學習目標
本文主要學習 FreeRTOS 軟體定時器的相關知識,包括軟體定時器回調函數、屬性、狀態、運行原理和常見 API 函數等知識
3、前提知識
3.1、軟體定時器回調函數
軟體定時器的回調函數是一個返回值為 void 類型,並且只有軟體定時器句柄一個參數的 C 語言函數,其函數的具體原型如下所述
/**
* @brief 軟體定時器回調函數
* @param xTimer:軟體定時器句柄
* @retval None
*/
void ATimerCallback(TimerHandle_t xTimer)
{
/* do something */
}
軟體定時器回調函數會在定時器設定的時間到期時在 RTOS 守護進程任務中被執行,軟體定時器回調函數從頭到尾執行,並以正常方式退出
需要讀者註意的是軟體定時器的回調函數應儘可能簡短,並且在該函數體內不能調用任何會使任務進入阻塞狀態的 API 函數,但是如果設置調用函數的 xTicksToWait 參數為 0 ,則可以調用如 xQueueReceive() 等 API 函數
3.2、軟體定時器屬性和狀態
3.2.1、周期
這個屬性比較好理解,軟體定時器的周期指的是 從軟體定時器啟動到軟體定時器回調函數執行之間的時間,該屬性是定時器不可或缺的重要屬性
3.2.2、分類
軟體定時器根據行為的不同分為了 單次定時器(One-shot timers) 和 周期定時器(Auto-reload timers) 兩種類型,如下圖展示了兩種不同類型軟體定時器的行為差異 (註釋1)
3.2.3、狀態
根據定時器是否正在運行可以將定時器分為 運行狀態(Running) 和 休眠狀態(Dormant) 兩種不同狀態,如下圖所示展示了單次定時器和周期定時器在兩種不同狀態之間的轉換過程
從圖上可以看出以下幾點內容
- 不管是單次定時器還是周期定時器,在定時器創建成功之後都處於休眠狀態,一旦調用啟動、複位或改變定時器周期的 API 函數就會使定時器從休眠狀態轉移到運行狀態;
- 單次定時器定時時間到期之後執行一次回調函數就會自動轉換為休眠狀態,而周期定時器會一直處於運行狀態;
- 當對處於運行狀態的定時器調用停止 API 函數時,不管是哪種定時器都會轉變為休眠狀態
定時器的狀態可以通過 xTimerlsTimerActive() API 函數查詢,該函數具體聲明如下所述
/**
* @brief 查詢軟體定時器是否處於運行或休眠狀態
* @param xTimer:要查詢的定時器句柄
* @retval 如果定時器處於休眠狀態則返回pdFALSE,如果定時器處於運行狀態則返回pdTRUE
*/
BaseType_t xTimerIsTimerActive(TimerHandle_t xTimer);
3.3、軟體定時器運行原理
3.3.1、RTOS 守護進程任務
首先讀者應該知道的一點是所有軟體定時器的回調函數都在同一個 RTOS 守護進程任務的上下文中執行,這個 RTOS 守護進程任務和空閑任務一樣,在調度器啟動的時候會被自動創建, RTOS 守護進程任務的優先順序和堆棧大小分別由 configTIMER_TASK_PRIORITY 和 configTIMER_TASK_STACK_DEPTH 兩個參數設置(可在 STM32CubeMX 軟體中配置)
”3.1、軟體定時器回調函數“ 小節提到在回調函數中不能使用會使任務進入阻塞狀態的 API 函數,這是因為調用會使任務進入阻塞狀態的 API 函數會使 RTOS 守護進程任務進入阻塞狀態,這是不被允許的
3.3.2、定時器命令隊列
上面提到的軟體定時器的啟動、複位、改變定時器周期和停止等操作的 API 函數只是將控制定時器的命令從調用任務發送到稱為 “定時器命令隊列” 的隊列上,然後由 RTOS 守護進程任務從定時器命令隊列中取出命令對定時器實際操作
定時器命令隊列是 FreeRTOS 里的一個標準隊列,其也是在調度程式啟動時被自動創建的,定時器命令隊列的長度可以由 configTIMER_QUEUE LENGTH 參數設置
如下圖所示為軟體定時器 API 函數使用定時器命令隊列與 RTOS 守護程式任務進行通信的示意圖
3.3.3、守護進程任務調度
守護進程任務是一個 FreeRTOS 任務,所以其任務調度會遵循和其他任務一樣的調度規則,當守護進程任務是能夠運行的最高優先順序任務時,它將會處理定時器隊列中的命令或執行定時器的回調函數
守護進程任務的優先順序在 STM32CubeMX 中預設為 2 ,當守護進程任務的優先順序低於調用 xTimerStart() 等 API 函數的任務的優先順序時,其會在任務結束之後輪到守護進程任務執行時對 “開始定時器” 命令進行處理,具體如下圖所示
當守護進程任務的優先順序高於調用 xTimerStart() 等 API 函數的任務的優先順序時,一旦任務調用 xTimerStart() 等 API 函數將命令寫入定時器命令隊列,守護進程任務便可以搶占該任務,立即處理寫入定時器命令隊列的命令,處理完畢之後進入阻塞狀態,處理器返回原任務繼續執行,具體如下圖所示
3.4、創建、啟動軟體定時器
同樣,根據 FreeRTOS API 的慣例,創建軟體定時器仍然提供了動態記憶體創建和靜態記憶體創建兩個不同的 API 函數,軟體定時器可以在調度程式運行之前創建,也可以在調度程式啟動後從任務創建,如下所示為兩個 API 函數聲明
/**
* @brief 動態分配記憶體創建軟體定時器
* @param pcTimerName:定時器的描述性名稱,輔助調試用
* @param xTimerPeriod:定時器的周期,參考 “3.2.1、周期” 小節
* @param uxAutoReload:pdTRUE表示周期軟體定時器,pdFASLE表示單次軟體定時器
* @param pvTimerID:定時器ID
* @param pxCallbackFunction:定時器回調函數指針,參考 “3.1、軟體定時器回調函數” 小節
* @retval 創建成功則返回創建的定時器的句柄,失敗則返回NULL
*/
TimerHandle_t xTimerCreate(const char * const pcTimerName,
const TickType_t xTimerPeriod,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction);
/**
* @brief 動態分配記憶體創建軟體定時器
* @param pcTimerName:定時器的描述性名稱,輔助調試用
* @param xTimerPeriod:定時器的周期,參考 “3.2.1、周期” 小節
* @param uxAutoReload:pdTRUE表示周期軟體定時器,pdFASLE表示單次軟體定時器
* @param pvTimerID:定時器ID
* @param pxCallbackFunction:定時器回調函數指針,參考 “3.1、軟體定時器回調函數” 小節
* @param pxTimerBuffer:指向StaticTimer_t類型的變數,然後用該變數保存定時器的狀態
* @retval 創建成功則返回創建的定時器的句柄,失敗則返回NULL
*/
TimerHandle_t xTimerCreateStatic(const char * const pcTimerName,
const TickType_t xTimerPeriod,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction
StaticTimer_t *pxTimerBuffer);
創建完的軟體定時器處於休眠狀態,需要調用啟動定時器或其他 API 函數才會進入運行狀態,xTimerStart() 可以在調度程式啟動之前調用,但是完成此操作後,軟體定時器直到調度程式啟動的時間才會真正啟動,啟動定時器的 API 函數如下所述
/**
* @brief 啟動定時器
* @param xTimer:要操作的定時器句柄
* @param xBlockTime:參考 “3.4.1、xTicksToWait 參數” 小節
* @retval 參考 “3.4.2、函數返回值” 小節
*/
BaseType_t xTimerStart(TimerHandle_t xTimer,
TickType_t xTicksToWait);
/**
* @brief 啟動定時器的中斷安全版本
* @param xTimer:要操作的定時器句柄
* @param pxHigherPriorityTaskWoken:用於通知應用程式編寫者是否應該執行上下文切換
* @retval 參考 “3.4.2、函數返回值” 小節
*/
BaseType_t xTimerStartFromISR(TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken);
3.4.1、xTicksToWait 參數
xTimerStart() 使用定時器命令隊列向守護進程任務發送 “啟動定時器” 命令, xTicksToWait 指定調用任務應保持在阻塞狀態以等待定時器命令隊列上的空間變得可用的最長時間(如果隊列已滿),該參數需要註意以下幾點
-
如果 xTicksToWait 為零且定時器命令隊列已滿,xTimerStart() 將立即返回,該參數以滴答定時器時間刻度為單位,可以使用巨集 pdMS_TO_TICKS() 將以毫秒為單位的時間轉換為以刻度為單位的時間,例如 pdMS_TO_TICKS(50) 表示阻塞 50ms
-
如果在 FreeRTOSConfig.h 中將 INCLUDE_vTaskSuspend 設置為 1,則將 xTicksToWait 設置為 portMAX_DELAY 將導致調用任務無限期地保持在阻塞狀態(沒有超時),以等待定時器命令隊列中的空間變得可用
-
如果在啟動調度程式之前調用 xTimerStart(),則 xTicksToWait 的值將被忽略,並且 xTimerStart() 的行為就像 xTicksToWait 已設置為零一樣
3.4.2、xTimerStart() 函數返回值
有兩種可能的返回值,分別為 pdPASS 和 pdFALSE ,具體如下所述
① 僅當 “啟動定時器” 命令成功發送到定時器命令隊列時,才會返回 pdPASS
- 如果守護程式任務的優先順序高於調用 xTimerStart() 的任務的優先順序,則調度程式將確保在 xTimerStart() 返回之前處理啟動命令。這是因為一旦定時器命令隊列中有數據,守護任務就會搶占調用 xTimerStart() 的任務,從而總是保證將命令成功發送到定時器命令隊列
- 如果指定了阻塞時間(xTicksToWait 不為零),則調用任務可能會被置於阻塞狀態,以等待定時器命令隊列中的空間在函數返回之前變得可用,只要在阻塞時間到期之前命令已成功寫入定時器命令隊列,就可以返回 pdPASS
② 如果由於隊列已滿或超過阻塞時間等原因無法將 “啟動定時器” 命令寫入定時器命令隊列,則將返回 pdFALSE
- 如果指定了阻塞時間(xTicksToWait 不為零),則調用任務將被置於阻塞狀態以等待守護進程任務在定時器命令隊列中騰出空間,但是指定的阻塞時間在等待定時器命令隊列中騰出空間之前已過期,所以返回 pdFALSE
3.6、軟體定時器 ID
每個軟體定時器都有一個 ID ,它是一個標簽值,應用程式編寫者可以將其用於任何目的, ID 被存儲在空指針中,因此可以直接存儲整數值,指向任何其他對象,或用作函數指針
創建軟體定時器時會為 ID 分配一個初始值,之後可以使用 vTimerSetTimerID() API 函數更新 ID,並且可以使用 pvTimerGetTimerID() API 函數查詢 ID ,這兩個 API 函數具體如下所示
/**
* @brief 設置定時器ID值
* @param xTimer:要操作的定時器句柄
* @param pvNewID:想要設置軟體定時器的新ID值
* @retval None
*/
void vTimerSetTimerID(TimerHandle_t xTimer, void *pvNewID);
/**
* @brief 獲取定時器ID值
* @param xTimer:要操作的定時器句柄
* @retval 正在查詢的軟體定時器ID
*/
void *pvTimerGetTimerID(TimerHandle_t xTimer);
註意:與其他軟體定時器 API 函數不同,vTimerSetTimerID() 和 pvTimerGetTimerID() 直接訪問軟體定時器,它們不向定時器命令隊列發送命令
如果創建了多個軟體定時器,並且所有軟體定時器均使用了同一個回調函數,則可以給軟體定時器設置不同的 ID 值,然後在回調函數中通過 ID 值判斷軟體定時器觸發的來源
3.7、改變軟體定時器周期
創建軟體定時器時就會為定時器周期設置初始值,後續也可以使用 xTimerChangePeriod() 函數動態更改軟體定時器的周期,該函數具體聲明如下所示
/**
* @brief 改變軟體定時器的周期
* @param xTimer:要操作的定時器句柄
* @param xNewPeriod:軟體定時器的新周期,以刻度為單位指定
* @param xBlockTime:參考 “3.4.1、xTicksToWait 參數” 小節
* @retval 參考 “3.4.2、xTimerStart() 函數返回值” 小節
*/
BaseType_t xTimerChangePeriod(TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xBlockTime);
/**
* @brief 改變軟體定時器周期的中斷安全版本
* @param xTimer:要操作的定時器句柄
* @param xNewPeriod:軟體定時器的新周期,以刻度為單位指定
* @param pxHigherPriorityTaskWoken:用於通知應用程式編寫者是否應該執行上下文切換
* @retval 參考 “3.4.2、xTimerStart() 函數返回值” 小節
*/
BaseType_t xTimerChangePeriodFromISR(TimerHandle_t xTimer,
TickType_t xNewPeriod,
BaseType_t *pxHigherPriorityTaskWoken);
如果 xTimerChangePeriod() 用於更改已運行的定時器的周期,則定時器將使用新的周期值重新計算其到期時間,重新計算的到期時間是相對於調用 xTimerChangePeriod() 的時間,而不是相對於定時器最初啟動的時間
如果使用 xTimerChangePeriod() 更改處於休眠狀態(未運行的定時器)的定時器的周期,則定時器將計算到期時間,並轉換到運行狀態(定時器將開始運行)
另外如果希望查詢一個定時器的定時周期,可以通過 xTimerGetPeriod() API 函數查詢,具體函數聲明如下所述
/**
* @brief 查詢一個軟體定時器的周期
* @param xTimer:要查詢的定時器句柄
* @retval 返回一個軟體定時器的周期
*/
TickType_t xTimerGetPeriod(TimerHandle_t xTimer);
3.8、重置軟體定時器
重置軟體定時器是指重新啟動定時器,定時器的到期時間將根據重置定時器的時間重新計算,而不是相對於定時器最初啟動的時間,如下圖對此進行了演示,其中顯示了一個定時器,該定時器啟動的周期為 6,然後重置兩次,最後到期並執行其回調函數
FreeRTOS中使用 xTimerReset() API 函數重置軟體定時器,除此之外還可用於啟動處於休眠狀態的定時,該函數具體聲明如下所述
/**
* @brief 重置軟體定時器
* @param xTimer:要操作的定時器句柄
* @param xBlockTime:參考 “3.4.1、xTicksToWait 參數” 小節
* @retval 參考 “3.4.2、xTimerStart() 函數返回值” 小節
*/
BaseType_t xTimerReset(TimerHandle_t xTimer,
TickType_t xBlockTime);
/**
* @brief 重置軟體定時器的中斷安全版本
* @param xTimer:要操作的定時器句柄
* @param pxHigherPriorityTaskWoken:用於通知應用程式編寫者是否應該執行上下文切換
* @retval 參考 “3.4.2、xTimerStart() 函數返回值” 小節
*/
BaseType_t xTimerResetFromISR(TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken);
3.8、停止、刪除軟體定時器
/**
* @brief 停止軟體定時器
* @param xTimer:要操作的定時器句柄
* @param xBlockTime:參考 “3.4.1、xTicksToWait 參數” 小節
* @retval 參考 “3.4.2、xTimerStart() 函數返回值” 小節
*/
BaseType_t xTimerStop(TimerHandle_t xTimer,
TickType_t xBlockTime);
/**
* @brief 刪除軟體定時器
* @param xTimer:要操作的定時器句柄
* @param xBlockTime:參考 “3.4.1、xTicksToWait 參數” 小節
* @retval 參考 “3.4.2、xTimerStart() 函數返回值” 小節
*/
BaseType_t xTimerDelete(TimerHandle_t xTimer,
TickType_t xBlockTime);
/**
* @brief 停止軟體定時器的中斷安全版本
* @param xTimer:要操作的定時器句柄
* @param pxHigherPriorityTaskWoken:用於通知應用程式編寫者是否應該執行上下文切換
* @retval 參考 “3.4.2、xTimerStart() 函數返回值” 小節
*/
BaseType_t xTimerStopFromISR(TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken);
3.9、其他 API 函數
/**
* @brief 將軟體定時器的“模式”更新為 自動重新載入定時器 或 一次性定時器
* @param xTimer:要操作的定時器句柄
* @param uxAutoReload:設置為pdTRUE則將定時器設置為周期軟體定時器,設置為pdFASLE則將定時器設置為單次軟體定時器
* @retval None
*/
void vTimerSetReloadMode(TimerHandle_t xTimer,
const UBaseType_t uxAutoReload);
/**
* @brief 查詢軟體定時器是 單次定時器 還是 周期定時器
* @param xTimer:要查詢的定時器句柄
* @retval 如果為周期軟體定時器則返回pdTRUE,否則返回pdFALSE
*/
BaseType_t xTimerGetReloadMode(TimerHandle_t xTimer);
/**
* @brief 查詢軟體定時器到期的時間
* @param xTimer:要查詢的定時器句柄
* @retval 如果要查詢的定時器處於活動狀態則返回定時器下一次到期的時間,否則未定義返回值
*/
TickType_t xTimerGetExpiryTime(TimerHandle_t xTimer);
4、實驗一:軟體定時器的應用
4.1、實驗目標
- 創建一個周期軟體定時器 TimerPeriodic 和一個單次軟體定時器 TimerOnce
- 創建一個按鍵掃描任務 Task_KeyScan,根據不同按鍵實現不同響應
- 當按鍵 WK_UP 按下時,設置周期定時器以 500ms 周期執行;當按鍵 KEY2 按下時,設置單次定時器以 1s 周期執行一次;當按鍵 KEY1 按下時,對周期定時器進行複位操作;當按鍵 KEY0 按下時,停止 TimerPeriodic 周期定時器
4.2、CubeMX相關配置
首先讀者應按照 "FreeRTOS教程1 基礎知識" 章節配置一個可以正常編譯通過的 FreeRTOS 空工程,然後在此空工程的基礎上增加本實驗所提出的要求
本實驗需要初始化 USART1 作為輸出信息渠道,具體配置步驟請閱讀“STM32CubeMX教程9 USART/UART 非同步通信”,如下圖所示
本實驗需要初始化開發板上 WK_UP、KEY2、KEY1 和 KEY0 用戶按鍵做普通輸入,具體配置步驟請閱讀“STM32CubeMX教程3 GPIO輸入 - 按鍵響應”,註意雖開發板不同但配置原理一致,如下圖所示
單擊 Middleware and Software Packs/FREERTOS ,在 Configuration 中單擊 Tasks and Queues 選項卡,雙擊預設任務修改其參數,如下所示
單擊 Timers and Semaphores ,在 Timers 中創建周期、單次兩個軟體定時器,如下所示
配置 Clock Configuration 和 Project Manager 兩個頁面,接下來直接單擊 GENERATE CODE 按鈕生成工程代碼即可
4.3、添加其他必要代碼
按照 “STM32CubeMX教程9 USART/UART 非同步通信” 實驗 “6、串口printf重定向” 小節增加串口 printf 重定向代碼,具體不再贅述
首先應該在 freertos.c 中添加軟體定時器的頭文件和使用到的 printf 的頭文件,如下所述
#include "timers.h"
#include "stdio.h"
然後實現按鍵掃描任務函數體,當按鍵 WK_UP 按下時啟動周期軟體定時器,當按鍵 KEY2 按下時啟動單次軟體定時器,當按鍵 KEY1 按下時對周期軟體定時器進行複位操作,當按鍵 KEY0 按下時停止周期定時器,具體如下所述
void AppTask_KeyScan(void *argument)
{
/* USER CODE BEGIN AppTask_KeyScan */
uint8_t key_value = 0;
/* Infinite loop */
for(;;)
{
key_value = 0;
//按鍵WK_UP按下
if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin) == GPIO_PIN_SET)
key_value = 4;
//按鍵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 == 4)
{
if(xTimerChangePeriod(TimerPeriodicHandle, 500, pdMS_TO_TICKS(500)) == pdTRUE)
{
printf("\r\nWK_UP PRESSED, TimerPeriodic Start!\r\n\r\n");
}
}
if(key_value == 3)
{
if(xTimerChangePeriod(TimerOnceHandle, 1000, pdMS_TO_TICKS(500)) == pdTRUE)
{
printf("\r\nKEY2 PRESSED, TimerOnce Start!\r\n\r\n");
}
}
else if(key_value == 2)
{
if(xTimerReset(TimerPeriodicHandle, pdMS_TO_TICKS(500)) == pdTRUE)
{
printf("\r\nKEY1 PRESSED, TimerPeriodic Reset!\r\n\r\n");
}
}
else if(key_value == 1)
{
if(xTimerStop(TimerPeriodicHandle, pdMS_TO_TICKS(500)) == pdTRUE)
{
printf("\r\nKEY0 PRESSED, TimerPeriod Stop!\r\n\r\n");
}
}
//有按鍵按下就進行按鍵消抖
osDelay(300);
}
else
osDelay(10);
}
/* USER CODE END AppTask_KeyScan */
}
最後實現單次/周期軟體定時器的兩個回調函數即可,回調函數內不做任何具體操作,僅通過串口輸出提示信息,如下所述
/* appTimerPeriodic function */
void appTimerPeriodic(void *argument)
{
/* USER CODE BEGIN appTimerPeriodic */
printf("Into appTimerPeriodic Function\r\n");
/* USER CODE END appTimerPeriodic */
}
/* appTimerOnce function */
void appTimerOnce(void *argument)
{
/* USER CODE BEGIN appTimerOnce */
printf("Into appTimerOnce Function\r\n");
/* USER CODE END appTimerOnce */
}
4.4、燒錄驗證
燒錄程式,打開串口助手後無任何信息輸出,當按下開發板上的 WK_UP 按鍵之後,會啟動以 500ms 為周期的周期軟體定時器,此時周期軟體定時器的回調函數會周期得到執行;當按下開發板上的 KEY2 按鍵之後,會啟動 1s 為周期的單次軟體定時器,此時單次軟體定時器的回調函數會得到執行,並且只執行了一次就停止了執行;當按下開發板上的 KEY1 按鍵時,會複位周期定時器;當按下開發板上的 KEY0 按鍵時,會停止周期定時器,整個過程串口的輸出信息如下圖所示
5、註釋詳解
註釋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