需要獲取更好閱讀體驗的同學,請訪問我專門設立的站點查看,地址:http://rtos.100ask.net/
系列教程總目錄
本教程連載中,篇章會比較多,為方便同學們閱讀,點擊這里可以查看文章的 目錄列表,目錄列表頁面地址:https://blog.csdn.net/thisway_diy/article/details/121399484
概述
軟件定時器就是"鬧鍾",你可以設置鬧鍾,
-
在30分鍾后讓你起床工作
-
每隔1小時讓你例行檢查機器運行情況
軟件定時器也可以完成兩類事情:
- 在"未來"某個時間點,運行函數
- 周期性地運行函數
日常生活中我們可以定無數個"鬧鍾",這無數的"鬧鍾"要基於一個真實的鬧鍾。
在FreeRTOS里,我們也可以設置無數個"軟件定時器",它們都是基於系統滴答中斷(Tick Interrupt)。
本章涉及如下內容:
- 軟件定時器的特性
- Daemon Task
- 定時器命令隊列
- 一次性定時器、周期性定時器的差別
- 怎么操作定時器:創建、啟動、復位、修改周期
10.1 軟件定時器的特性
我們在手機上添加鬧鍾時,需要指定時間、指定類型(一次性的,還是周期性的)、指定做什么事;還有一些過時的、不再使用的鬧鍾。如下圖所示:
使用定時器跟使用手機鬧鍾是類似的:
- 指定時間:啟動定時器和運行回調函數,兩者的間隔被稱為定時器的周期(period)。
- 指定類型,定時器有兩種類型:
- 一次性(One-shot timers):
這類定時器啟動后,它的回調函數只會被調用一次;
可以手工再次啟動它,但是不會自動啟動它。 - 自動加載定時器(Auto-reload timers ):
這類定時器啟動后,時間到之后它會自動啟動它;
這使得回調函數被周期性地調用。
- 一次性(One-shot timers):
- 指定要做什么事,就是指定回調函數
實際的鬧鍾分為:有效、無效兩類。軟件定時器也是類似的,它由兩種狀態:
- 運行(Running、Active):運行態的定時器,當指定時間到達之后,它的回調函數會被調用
- 冬眠(Dormant):冬眠態的定時器還可以通過句柄來訪問它,但是它不再運行,它的回調函數不會被調用
定時器運行情況示例如下:
- Timer1:它是一次性的定時器,在t1啟動,周期是6個Tick。經過6個tick后,在t7執行回調函數。它的回調函數只會被執行一次,然后該定時器進入冬眠狀態。
- Timer2:它是自動加載的定時器,在t1啟動,周期是5個Tick。每經過5個tick它的回調函數都被執行,比如在t6、t11、t16都會執行。
10.2 軟件定時器的上下文
10.2.1 守護任務
要理解軟件定時器API函數的參數,特別是里面的xTicksToWait
,需要知道定時器執行的過程。
FreeRTOS中有一個Tick中斷,軟件定時器基於Tick來運行。在哪里執行定時器函數?第一印象就是在Tick中斷里執行:
- 在Tick中斷中判斷定時器是否超時
- 如果超時了,調用它的回調函數
FreeRTOS是RTOS,它不允許在內核、在中斷中執行不確定的代碼:如果定時器函數很耗時,會影響整個系統。
所以,FreeRTOS中,不在Tick中斷中執行定時器函數。
在哪里執行?在某個任務里執行,這個任務就是:RTOS Damemon Task,RTOS守護任務。以前被稱為"Timer server",但是這個任務要做並不僅僅是定時器相關,所以改名為:RTOS Damemon Task。
當FreeRTOS的配置項configUSE_TIMERS
被設置為1時,在啟動調度器時,會自動創建RTOS Damemon Task。
我們自己編寫的任務函數要使用定時器時,是通過"定時器命令隊列"(timer command queue)和守護任務交互,如下圖所示:
守護任務的優先級為:configTIMER_TASK_PRIORITY;定時器命令隊列的長度為configTIMER_QUEUE_LENGTH。
10.2.2 守護任務的調度
守護任務的調度,跟普通的任務並無差別。當守護任務是當前優先級最高的就緒態任務時,它就可以運行。它的工作有兩類:
- 處理命令:從命令隊列里取出命令、處理
- 執行定時器的回調函數
能否及時處理定時器的命令、能否及時執行定時器的回調函數,嚴重依賴於守護任務的優先級。下面使用2個例子來演示。
例子1:守護任務的優先性級較低
-
t1:Task1處於運行態,守護任務處於阻塞態。
守護任務在這兩種情況下會退出阻塞態切換為就緒態:命令隊列中有數據、某個定時器超時了。
至於守護任務能否馬上執行,取決於它的優先級。 -
t2:Task1調用
xTimerStart()
要注意的是,xTimerStart()
只是把"start timer"的命令發給"定時器命令隊列",使得守護任務退出阻塞態。
在本例中,Task1的優先級高於守護任務,所以守護任務無法搶占Task1。 -
t3:Task1執行完
xTimerStart()
但是定時器的啟動工作由守護任務來實現,所以xTimerStart()
返回並不表示定時器已經被啟動了。 -
t4:Task1由於某些原因進入阻塞態,現在輪到守護任務運行。
守護任務從隊列中取出"start timer"命令,啟動定時器。 -
t5:守護任務處理完隊列中所有的命令,再次進入阻塞態。Idel任務時優先級最高的就緒態任務,它執行。
-
注意:假設定時器在后續某個時刻tX超時了,超時時間是"tX-t2",而非"tX-t4",從
xTimerStart()
函數被調用時算起。
例子2:守護任務的優先性級較高
-
t1:Task1處於運行態,守護任務處於阻塞態。
守護任務在這兩種情況下會退出阻塞態切換為就緒態:命令隊列中有數據、某個定時器超時了。
至於守護任務能否馬上執行,取決於它的優先級。 -
t2:Task1調用
xTimerStart()
要注意的是,xTimerStart()
只是把"start timer"的命令發給"定時器命令隊列",使得守護任務退出阻塞態。
在本例中,守護任務的優先級高於Task1,所以守護任務搶占Task1,守護任務開始處理命令隊列。
Task1在執行xTimerStart()
的過程中被搶占,這時它無法完成此函數。 -
t3:守護任務處理完命令隊列中所有的命令,再次進入阻塞態。
此時Task1是優先級最高的就緒態任務,它開始執行。 -
t4:Task1之前被守護任務搶占,對
xTimerStart()
的調用尚未返回。現在開始繼續運行次函數、返回。 -
t5:Task1由於某些原因進入阻塞態,進入阻塞態。Idel任務時優先級最高的就緒態任務,它執行。
注意,定時器的超時時間是基於調用xTimerStart()
的時刻tX,而不是基於守護任務處理命令的時刻tY。假設超時時間是10個Tick,超時時間是"tX+10",而非"tY+10"。
10.2.3 回調函數
定時器的回調函數的原型如下:
void ATimerCallback( TimerHandle_t xTimer );
定時器的回調函數是在守護任務中被調用的,守護任務不是專為某個定時器服務的,它還要處理其他定時器。
所以,定時器的回調函數不要影響其他人:
-
回調函數要盡快實行,不能進入阻塞狀態
-
不要調用會導致阻塞的API函數,比如
vTaskDelay()
-
可以調用
xQueueReceive()
之類的函數,但是超時時間要設為0:即刻返回,不可阻塞
10.3 軟件定時器的函數
根據定時器的狀態轉換圖,就可以知道所涉及的函數:
10.3.1 創建
要使用定時器,需要先創建它,得到它的句柄。
有兩種方法創建定時器:動態分配內存、靜態分配內存。函數原型如下:
/* 使用動態分配內存的方法創建定時器
* pcTimerName:定時器名字, 用處不大, 盡在調試時用到
* xTimerPeriodInTicks: 周期, 以Tick為單位
* uxAutoReload: 類型, pdTRUE表示自動加載, pdFALSE表示一次性
* pvTimerID: 回調函數可以使用此參數, 比如分辨是哪個定時器
* pxCallbackFunction: 回調函數
* 返回值: 成功則返回TimerHandle_t, 否則返回NULL
*/
TimerHandle_t xTimerCreate( const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction );
/* 使用靜態分配內存的方法創建定時器
* pcTimerName:定時器名字, 用處不大, 盡在調試時用到
* xTimerPeriodInTicks: 周期, 以Tick為單位
* uxAutoReload: 類型, pdTRUE表示自動加載, pdFALSE表示一次性
* pvTimerID: 回調函數可以使用此參數, 比如分辨是哪個定時器
* pxCallbackFunction: 回調函數
* pxTimerBuffer: 傳入一個StaticTimer_t結構體, 將在上面構造定時器
* 返回值: 成功則返回TimerHandle_t, 否則返回NULL
*/
TimerHandle_t xTimerCreateStatic(const char * const pcTimerName,
TickType_t xTimerPeriodInTicks,
UBaseType_t uxAutoReload,
void * pvTimerID,
TimerCallbackFunction_t pxCallbackFunction,
StaticTimer_t *pxTimerBuffer );
回調函數的類型是:
void ATimerCallback( TimerHandle_t xTimer );
typedef void (* TimerCallbackFunction_t)( TimerHandle_t xTimer );
10.3.2 刪除
動態分配的定時器,不再需要時可以刪除掉以回收內存。刪除函數原型如下:
/* 刪除定時器
* xTimer: 要刪除哪個定時器
* xTicksToWait: 超時時間
* 返回值: pdFAIL表示"刪除命令"在xTicksToWait個Tick內無法寫入隊列
* pdPASS表示成功
*/
BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );
定時器的很多API函數,都是通過發送"命令"到命令隊列,由守護任務來實現。
如果隊列滿了,"命令"就無法即刻寫入隊列。我們可以指定一個超時時間xTicksToWait
,等待一會。
10.3.3 啟動/停止
啟動定時器就是設置它的狀態為運行態(Running、Active)。
停止定時器就是設置它的狀態為冬眠(Dormant),讓它不能運行。
涉及的函數原型如下:
/* 啟動定時器
* xTimer: 哪個定時器
* xTicksToWait: 超時時間
* 返回值: pdFAIL表示"啟動命令"在xTicksToWait個Tick內無法寫入隊列
* pdPASS表示成功
*/
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
/* 啟動定時器(ISR版本)
* xTimer: 哪個定時器
* pxHigherPriorityTaskWoken: 向隊列發出命令使得守護任務被喚醒,
* 如果守護任務的優先級比當前任務的高,
* 則"*pxHigherPriorityTaskWoken = pdTRUE",
* 表示需要進行任務調度
* 返回值: pdFAIL表示"啟動命令"無法寫入隊列
* pdPASS表示成功
*/
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken );
/* 停止定時器
* xTimer: 哪個定時器
* xTicksToWait: 超時時間
* 返回值: pdFAIL表示"停止命令"在xTicksToWait個Tick內無法寫入隊列
* pdPASS表示成功
*/
BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );
/* 停止定時器(ISR版本)
* xTimer: 哪個定時器
* pxHigherPriorityTaskWoken: 向隊列發出命令使得守護任務被喚醒,
* 如果守護任務的優先級比當前任務的高,
* 則"*pxHigherPriorityTaskWoken = pdTRUE",
* 表示需要進行任務調度
* 返回值: pdFAIL表示"停止命令"無法寫入隊列
* pdPASS表示成功
*/
BaseType_t xTimerStopFromISR( TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken );
注意,這些函數的xTicksToWait
表示的是,把命令寫入命令隊列的超時時間。命令隊列可能已經滿了,無法馬上把命令寫入隊列里,可以等待一會。
xTicksToWait
不是定時器本身的超時時間,不是定時器本身的"周期"。
創建定時器時,設置了它的周期(period)。xTimerStart()
函數是用來啟動定時器。假設調用xTimerStart()
的時刻是tX,定時器的周期是n,那么在tX+n
時刻定時器的回調函數被調用。
如果定時器已經被啟動,但是它的函數尚未被執行,再次執行xTimerStart()
函數相當於執行xTimerReset()
,重新設定它的啟動時間。
10.3.4 復位
從定時器的狀態轉換圖可以知道,使用xTimerReset()
函數可以讓定時器的狀態從冬眠態轉換為運行態,相當於使用xTimerStart()
函數。
如果定時器已經處於運行態,使用xTimerReset()
函數就相當於重新確定超時時間。假設調用xTimerReset()
的時刻是tX,定時器的周期是n,那么tX+n
就是重新確定的超時時間。
復位函數的原型如下:
/* 復位定時器
* xTimer: 哪個定時器
* xTicksToWait: 超時時間
* 返回值: pdFAIL表示"復位命令"在xTicksToWait個Tick內無法寫入隊列
* pdPASS表示成功
*/
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );
/* 復位定時器(ISR版本)
* xTimer: 哪個定時器
* pxHigherPriorityTaskWoken: 向隊列發出命令使得守護任務被喚醒,
* 如果守護任務的優先級比當前任務的高,
* 則"*pxHigherPriorityTaskWoken = pdTRUE",
* 表示需要進行任務調度
* 返回值: pdFAIL表示"停止命令"無法寫入隊列
* pdPASS表示成功
*/
BaseType_t xTimerResetFromISR( TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken );
10.3.5 修改周期
從定時器的狀態轉換圖可以知道,使用xTimerChangePeriod()
函數,處理能修改它的周期外,還可以讓定時器的狀態從冬眠態轉換為運行態。
修改定時器的周期時,會使用新的周期重新計算它的超時時間。假設調用xTimerChangePeriod()
函數的時間tX,新的周期是n,則tX+n
就是新的超時時間。
相關函數的原型如下:
/* 修改定時器的周期
* xTimer: 哪個定時器
* xNewPeriod: 新周期
* xTicksToWait: 超時時間, 命令寫入隊列的超時時間
* 返回值: pdFAIL表示"修改周期命令"在xTicksToWait個Tick內無法寫入隊列
* pdPASS表示成功
*/
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xTicksToWait );
/* 修改定時器的周期
* xTimer: 哪個定時器
* xNewPeriod: 新周期
* pxHigherPriorityTaskWoken: 向隊列發出命令使得守護任務被喚醒,
* 如果守護任務的優先級比當前任務的高,
* 則"*pxHigherPriorityTaskWoken = pdTRUE",
* 表示需要進行任務調度
* 返回值: pdFAIL表示"修改周期命令"在xTicksToWait個Tick內無法寫入隊列
* pdPASS表示成功
*/
BaseType_t xTimerChangePeriodFromISR( TimerHandle_t xTimer,
TickType_t xNewPeriod,
BaseType_t *pxHigherPriorityTaskWoken );
10.3.6 定時器ID
定時器的結構體如下,里面有一項pvTimerID
,它就是定時器ID:
怎么使用定時器ID,完全由程序來決定:
- 可以用來標記定時器,表示自己是什么定時器
- 可以用來保存參數,給回調函數使用
它的初始值在創建定時器時由xTimerCreate()
這類函數傳入,后續可以使用這些函數來操作:
- 更新ID:使用
vTimerSetTimerID()
函數 - 查詢ID:查詢
pvTimerGetTimerID()
函數
這兩個函數不涉及命令隊列,它們是直接操作定時器結構體。
函數原型如下:
/* 獲得定時器的ID
* xTimer: 哪個定時器
* 返回值: 定時器的ID
*/
void *pvTimerGetTimerID( TimerHandle_t xTimer );
/* 設置定時器的ID
* xTimer: 哪個定時器
* pvNewID: 新ID
* 返回值: 無
*/
void vTimerSetTimerID( TimerHandle_t xTimer, void *pvNewID );
10.4 示例24: 一般使用
本節程序為FreeRTOS_24_software_timer
。
要使用定時器,需要做些准備工作:
/* 1. 工程中 */
添加 timer.c
/* 2. 配置文件FreeRTOSConfig.h中 */
#define configUSE_TIMERS 1 /* 使能定時器 */
#define configTIMER_TASK_PRIORITY 31 /* 守護任務的優先級, 盡可能高一些 */
#define configTIMER_QUEUE_LENGTH 5 /* 命令隊列長度 */
#define configTIMER_TASK_STACK_DEPTH 32 /* 守護任務的棧大小 */
/* 3. 源碼中 */
#include "timers.h"
main函數中創建、啟動了2個定時器:一次性的、周期
static volatile uint8_t flagONEShotTimerRun = 0;
static volatile uint8_t flagAutoLoadTimerRun = 0;
static void vONEShotTimerFunc( TimerHandle_t xTimer );
static void vAutoLoadTimerFunc( TimerHandle_t xTimer );
/*-----------------------------------------------------------*/
#define mainONE_SHOT_TIMER_PERIOD pdMS_TO_TICKS( 10 )
#define mainAUTO_RELOAD_TIMER_PERIOD pdMS_TO_TICKS( 20 )
int main( void )
{
TimerHandle_t xOneShotTimer;
TimerHandle_t xAutoReloadTimer;
prvSetupHardware();
xOneShotTimer = xTimerCreate(
"OneShot", /* 名字, 不重要 */
mainONE_SHOT_TIMER_PERIOD, /* 周期 */
pdFALSE, /* 一次性 */
0, /* ID */
vONEShotTimerFunc /* 回調函數 */
);
xAutoReloadTimer = xTimerCreate(
"AutoReload", /* 名字, 不重要 */
mainAUTO_RELOAD_TIMER_PERIOD, /* 周期 */
pdTRUE, /* 自動加載 */
0, /* ID */
vAutoLoadTimerFunc /* 回調函數 */
);
if (xOneShotTimer && xAutoReloadTimer)
{
/* 啟動定時器 */
xTimerStart(xOneShotTimer, 0);
xTimerStart(xAutoReloadTimer, 0);
/* 啟動調度器 */
vTaskStartScheduler();
}
/* 如果程序運行到了這里就表示出錯了, 一般是內存不足 */
return 0;
}
這兩個定時器的回調函數比較簡單:
static void vONEShotTimerFunc( TimerHandle_t xTimer )
{
static int cnt = 0;
flagONEShotTimerRun = !flagONEShotTimerRun;
printf("run vONEShotTimerFunc %d\r\n", cnt++);
}
static void vAutoLoadTimerFunc( TimerHandle_t xTimer )
{
static int cnt = 0;
flagAutoLoadTimerRun = !flagAutoLoadTimerRun;
printf("run vAutoLoadTimerFunc %d\r\n", cnt++);
}
邏輯分析儀如下圖所示:
運行結果如下圖所示:
10.5 示例25: 消除抖動
本節程序為FreeRTOS_25_software_timer_readkey
。
在嵌入式開發中,我們使用機械開關時經常碰到抖動問題:引腳電平在短時間內反復變化。
怎么讀到確定的按鍵狀態?
- 連續讀很多次,知道數值穩定:浪費CPU資源
- 使用定時器:要結合中斷來使用
對於第2種方法,處理方法如下圖所示,按下按鍵后:
- 在t1產生中斷,這時不馬上確定按鍵,而是復位定時器,假設周期時20ms,超時時間為"t1+20ms"
- 由於抖動,在t2再次產生中斷,再次復位定時器,超時時間變為"t2+20ms"
- 由於抖動,在t3再次產生中斷,再次復位定時器,超時時間變為"t3+20ms"
- 在"t3+20ms"處,按鍵已經穩定,讀取按鍵值
main函數中創建了一個一次性的定時器,從來處理抖動;創建了一個任務,用來模擬產生抖動。代碼如下:
/*-----------------------------------------------------------*/
static void vKeyFilteringTimerFunc( TimerHandle_t xTimer );
void vEmulateKeyTask( void *pvParameters );
static TimerHandle_t xKeyFilteringTimer;
/*-----------------------------------------------------------*/
#define KEY_FILTERING_PERIOD pdMS_TO_TICKS( 20 )
int main( void )
{
prvSetupHardware();
xKeyFilteringTimer = xTimerCreate(
"KeyFiltering", /* 名字, 不重要 */
KEY_FILTERING_PERIOD, /* 周期 */
pdFALSE, /* 一次性 */
0, /* ID */
vKeyFilteringTimerFunc /* 回調函數 */
);
/* 在這個任務中多次調用xTimerReset來模擬按鍵抖動 */
xTaskCreate( vEmulateKeyTask, "EmulateKey", 1000, NULL, 1, NULL );
/* 啟動調度器 */
vTaskStartScheduler();
/* 如果程序運行到了這里就表示出錯了, 一般是內存不足 */
return 0;
}
模擬產生按鍵:每個循環里調用3次xTimerReset,代碼如下:
void vEmulateKeyTask( void *pvParameters )
{
int cnt = 0;
const TickType_t xDelayTicks = pdMS_TO_TICKS( 200UL );
for( ;; )
{
/* 模擬按鍵抖動, 多次調用xTimerReset */
xTimerReset(xKeyFilteringTimer, 0); cnt++;
xTimerReset(xKeyFilteringTimer, 0); cnt++;
xTimerReset(xKeyFilteringTimer, 0); cnt++;
printf("Key jitters %d\r\n", cnt);
vTaskDelay(xDelayTicks);
}
}
定時器回調函數代碼如下:
static void vKeyFilteringTimerFunc( TimerHandle_t xTimer )
{
static int cnt = 0;
printf("vKeyFilteringTimerFunc %d\r\n", cnt++);
}
在人戶函數中多次調用xTimerReset,只觸發1次定時器回調函數,運行結果如下圖所示: