韋東山freeRTOS系列教程之【第十章】軟件定時器(software timer)


需要獲取更好閱讀體驗的同學,請訪問我專門設立的站點查看,地址: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 ):
      這類定時器啟動后,時間到之后它會自動啟動它;
      這使得回調函數被周期性地調用。
  • 指定要做什么事,就是指定回調函數

實際的鬧鍾分為:有效、無效兩類。軟件定時器也是類似的,它由兩種狀態:

  • 運行(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()函數被調用時算起。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-OrEU5EKm-1638166526314)(pic/chap10/04_demon_task_priority_lower.png)]

例子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++);
}

邏輯分析儀如下圖所示:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-uPUUQMgp-1638166526319)(pic/chap10/07_timer_wave.png)]

運行結果如下圖所示:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-yuBnzEUP-1638166526319)(pic/chap10/08_timer_result1.png)]

10.5 示例25: 消除抖動

本節程序為FreeRTOS_25_software_timer_readkey

在嵌入式開發中,我們使用機械開關時經常碰到抖動問題:引腳電平在短時間內反復變化。

怎么讀到確定的按鍵狀態?

  • 連續讀很多次,知道數值穩定:浪費CPU資源
  • 使用定時器:要結合中斷來使用

對於第2種方法,處理方法如下圖所示,按下按鍵后:

  • 在t1產生中斷,這時不馬上確定按鍵,而是復位定時器,假設周期時20ms,超時時間為"t1+20ms"
  • 由於抖動,在t2再次產生中斷,再次復位定時器,超時時間變為"t2+20ms"
  • 由於抖動,在t3再次產生中斷,再次復位定時器,超時時間變為"t3+20ms"
  • 在"t3+20ms"處,按鍵已經穩定,讀取按鍵值

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-udX6o94W-1638166526320)(pic/chap10/09_filting_key.png)]

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次定時器回調函數,運行結果如下圖所示:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-wD6LDcbI-1638166526320)(pic/chap10/11_timer_result2.png)]


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM