目錄
- 1.特性
- 2.運行環境
- 2.1 守護任務
- 2.2 回調函數
- 2.3 內部源碼
- 3.和Linux對比
- 4.ID
- 5.數據傳輸
- 6.操作函數
- 6.1 創建
- 6.2 刪除
- 6.3 啟動
- 6.4 停止
- 6.5 復位(重置)
- 6.6 修改周期
- 6.7 注意事項
- 7.示例:一般使用
- 8.示例:定時器防抖
1.特性
定時器周期(Period):
- 定時器啟動后,會在指定的時間間隔到達時觸發回調函數。
- 這個時間間隔稱為定時器的周期,即從定時器啟動到回調函數執行的時間長度。
軟件定時器有兩種狀態:
-
運行(Running 或 Active)
-
- 當定時器處于運行狀態時,它的計時器正在倒計時,到期后會自動執行回調函數。
- 對于自動加載定時器,每次回調執行完后,它會自動重啟,繼續保持運行狀態。
-
冬眠(Dormant)
-
- 冬眠態的定時器雖然仍可以通過句柄訪問,但它不會觸發回調函數。
- 一次性定時器在回調執行后會進入冬眠狀態,直到手動再次啟動它。
定時器類型:
定時器主要有兩種類型,每種類型的觸發方式不同:
-
一次性定時器(One-shot timers):
-
- 啟動后,經過一個周期后僅觸發一次回調函數。
- 如果需要再次觸發,則必須手動重新啟動定時器。
- 這種類型適合只需要執行一次任務的場景,如延遲操作、超時處理等。
-
自動加載定時器(Auto-reload timers):
-
- 啟動后,在每個周期結束時自動重啟,因此回調函數會被周期性地調用。
- 不需要手動重新啟動,適用于周期性任務,如定時采樣、周期性狀態更新等。
回調函數:
- 定時器啟動時,必須指定一個回調函數,這個函數會在定時器到期時被調用。
- 回調函數中可以執行需要定時執行的操作,就像手機鬧鐘響后執行特定任務一樣。
手冊中的例子:
2.運行環境
2.1 守護任務
直覺上可能認為定時器在 Tick 中斷中判斷是否超時,并直接調用回調函數。但這樣做會帶來兩個問題:
- 中斷上下文限制:在 Tick 中斷中執行回調可能導致長時間阻塞,影響系統響應,甚至引起系統延遲。
- 不可預知的代碼執行:內核中斷上下文中執行的代碼必須非常短小,不允許調用阻塞API(如 vTaskDelay()),否則會破壞實時性。
為了避免這些問題,FreeRTOS 將定時器的回調函數放在一個專門的任務中執行,這個任務就是RTOS 守護任務(Timer Daemon Task)。
- 它不是在 Tick 中斷里運行,而是在任務上下文中運行,因此可以執行較復雜的回調函數。
- 通過這種設計,定時器回調函數不影響系統中斷響應,同時能調用大部分 API(但仍應避免阻塞操作)。
當配置項 configUSE_TIMERS
設置為 1 時,啟動調度器時系統會自動創建守護任務。
- 處理定時器命令:守護任務從“定時器命令隊列”中取出命令(例如啟動、停止定時器命令),并執行相應操作。
- 執行定時器回調函數:當定時器超時時,守護任務調用對應的回調函數。
守護任務的優先級由 configTIMER_TASK_PRIORITY
定義;定時器命令隊列的長度由 configTIMER_QUEUE_LENGTH
定義。
守護任務的調度與其他任務相同——只有當它是就緒態中優先級最高的任務時才會運行。
因此定時器的回調函數能否被及時處理取決于守護任務什么時候能夠被執行,優先級最高能夠搶占的話,就能較快處理回調函數。
守護任務優先級較低:
- t1:Task1(優先級較高)正在運行,而守護任務處于阻塞狀態(等待命令或定時器超時)。
- t2:Task1調用
xTimerStart()
,這個調用僅僅把一個“啟動定時器”的命令發送到定時器命令隊列。由于 Task1優先級較高,守護任務(優先級低)不能馬上搶占 CPU。 - t3:Task1執行完
xTimerStart()
后繼續運行,此時定時器命令仍滯留在命令隊列中。 - t4:當 Task1因某些原因進入阻塞態后,守護任務獲得 CPU,開始從命令隊列中取出命令并啟動定時器。
- t5:守護任務處理完所有命令后再次進入阻塞態,輪到其他任務或 Idle 任務執行。
**守護任務優先級較高:
**
- t1:Task1 正在運行,守護任務處于阻塞狀態。
- t2:Task1 調用
xTimerStart()
,將命令發送到定時器命令隊列。由于守護任務優先級較高,它立即搶占 CPU,從命令隊列中取出命令開始啟動定時器。 - t3:在 Task1 調用
xTimerStart()
的過程中被守護任務搶占,Task1暫時暫停。 - t4:守護任務處理完命令后,Task1繼續執行
xTimerStart()
的剩余部分,并返回。 - t5:此后,定時器超時時間是從 Task1 調用
xTimerStart()
時開始計算的,不受守護任務處理命令延遲的影響。
2.2 回調函數
關于定時器的回調函數也有一些要求:
- 回調函數必須快速完成,不應包含長時間運行或阻塞的操作。
- 避免調用可能阻塞的 API(例如 vTaskDelay());如果必須調用諸如 xQueueReceive() 之類的 API,超時時間應設置為 0,即刻返回。
- 回調函數不應影響守護任務的整體響應,確保其他定時器命令也能及時處理。
2.3 內部源碼
來看看守護在內部是如何被創建的,找到timers.c:
BaseType_t xTimerCreateTimerTask( void )
{BaseType_t xReturn = pdFAIL; /* 初始化返回值,默認返回失敗 *//* 此函數在調度器啟動時被調用,前提是 configUSE_TIMERS 被設置為 1。* 先檢查定時器服務任務所依賴的基礎設施(比如定時器命令隊列和定時器列表)* 是否已經創建或初始化。若已經創建,則無需重復初始化。 */prvCheckForValidListAndQueue();/* 如果定時器命令隊列已經被創建,則可以創建定時器任務 */if( xTimerQueue != NULL ){#if ( configSUPPORT_STATIC_ALLOCATION == 1 ){StaticTask_t * pxTimerTaskTCBBuffer = NULL; /* 用于存儲任務控制塊 (TCB) 的靜態內存 */StackType_t * pxTimerTaskStackBuffer = NULL; /* 用于存儲任務棧的靜態內存 */uint32_t ulTimerTaskStackSize; /* 定時器任務的棧大小 *//* 調用應用程序提供的回調函數獲取定時器任務的靜態內存。* 該函數會把任務的 TCB 和棧內存地址,以及棧大小返回給應用程序。 */vApplicationGetTimerTaskMemory( &pxTimerTaskTCBBuffer, &pxTimerTaskStackBuffer, &ulTimerTaskStackSize );/* 使用靜態內存創建定時器任務:* prvTimerTask 為任務函數,也就是定時器守護任務的函數* configTIMER_SERVICE_TASK_NAME 為任務名稱,* ulTimerTaskStackSize 為任務棧深度,* NULL 為任務參數,* (configTIMER_TASK_PRIORITY | portPRIVILEGE_BIT) 為任務優先級(可能加上特權位),* pxTimerTaskStackBuffer 和 pxTimerTaskTCBBuffer 為預先分配的靜態內存。 */xTimerTaskHandle = xTaskCreateStatic( prvTimerTask,configTIMER_SERVICE_TASK_NAME,ulTimerTaskStackSize,NULL,( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,pxTimerTaskStackBuffer,pxTimerTaskTCBBuffer );/* 如果任務句柄不為空,說明定時器任務創建成功 */if( xTimerTaskHandle != NULL ){xReturn = pdPASS;}}#else /* 如果不支持靜態內存分配,則使用動態分配 */{/* 使用 xTaskCreate 動態創建定時器任務:* prvTimerTask 為任務函數,* configTIMER_SERVICE_TASK_NAME 為任務名稱,* configTIMER_TASK_STACK_DEPTH 為任務棧深度,* NULL 為任務參數,* (configTIMER_TASK_PRIORITY | portPRIVILEGE_BIT) 為任務優先級,* &xTimerTaskHandle 存儲創建后返回的任務句柄。 */xReturn = xTaskCreate( prvTimerTask,configTIMER_SERVICE_TASK_NAME,configTIMER_TASK_STACK_DEPTH,NULL,( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,&xTimerTaskHandle );}#endif /* configSUPPORT_STATIC_ALLOCATION */}else{/* 如果定時器命令隊列沒有創建成功,則觸發覆蓋測試標記(用于測試覆蓋率) */mtCOVERAGE_TEST_MARKER();}/* 斷言 xReturn 必須為 pdPASS,否則程序運行時將報錯 */configASSERT( xReturn );/* 返回任務創建結果:pdPASS 表示成功,pdFAIL 表示失敗 */return xReturn;
}
這段代碼就是實現了創建定時器服務任務(Timer Service Task,也就是定時器守護任務)的功能,其中關鍵的任務函數就是:prvTimerTask,繼續查找一下這個函數,發現是并沒有這個函數的,但是找到了:
static portTASK_FUNCTION( prvTimerTask, pvParameters )
prvTimerTask
作為了一個參數,又查找了一下portTASK_FUNCTION
,發現在portmacro.h
中定義了一個這樣的宏:
#define portTASK_FUNCTION( vFunction, pvParameters ) void vFunction( void * pvParameters )# 宏名稱:portTASK_FUNCTION
# vFunction:任務函數的名稱。
# pvParameters:任務函數的參數名稱(通常是一個 void* 指針)。
其實就是freertos用于規范任務函數的聲明格式,確保代碼的可移植性和一致性,這樣看來static portTASK_FUNCTION( prvTimerTask, pvParameters )
實際上就是等同于:void prvTimerTask(void * pvParameters )
,也就說static portTASK_FUNCTION( prvTimerTask, pvParameters )
定義的就是守護任務的內容,來看看其定義;
static portTASK_FUNCTION( prvTimerTask, pvParameters )
{TickType_t xNextExpireTime; /* 變量:存儲下一個將到期的定時器的絕對到期時間 */BaseType_t xListWasEmpty; /* 變量:標記定時器列表是否為空。若為空,可能無需立即喚醒任務 *//* 這里將 pvParameters 轉換為 void,以避免編譯器關于未使用參數的警告 */( void ) pvParameters;#if ( configUSE_DAEMON_TASK_STARTUP_HOOK == 1 ){extern void vApplicationDaemonTaskStartupHook( void );/* 如果配置了守護任務啟動鉤子(Daemon Task Startup Hook),則在定時器任務開始運行時,* 允許應用程序在任務上下文中執行一些初始化代碼。這對于需要在調度器啟動后初始化的* 應用代碼非常有用。 */vApplicationDaemonTaskStartupHook();}#endif /* configUSE_DAEMON_TASK_STARTUP_HOOK *//* 無限循環,定時器任務一直運行 */for( ; ; ){/* 第一步:查詢當前定時器列表是否存在定時器,若存在,則獲得下一個即將到期的定時器的到期時間。* 函數 prvGetNextExpireTime() 會檢查定時器列表,并返回一個 Tick 值,該值表示下一個定時器到期的時刻。* 同時,它會通過 xListWasEmpty 參數告知調用者:如果定時器列表為空,則無定時器等待處理。 */xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );/* 第二步:調用 prvProcessTimerOrBlockTask() 函數* 作用:判斷是否有定時器已經超時,或者需要阻塞等待定時器超時或有命令到達。* 參數 xNextExpireTime 表示下一個定時器到期的時刻,* xListWasEmpty 表示定時器列表是否為空。** 具體:* - 如果有定時器到期,則該函數會調用對應的回調函數處理定時器超時事件。* - 如果沒有定時器到期,則定時器任務會進入阻塞狀態,等待到達指定時間或等待命令隊列中有新命令。*/prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );/* 第三步:處理定時器命令隊列中的所有命令。* 定時器命令隊列可能包含啟動、停止、重載定時器等命令,* 它們都是由其他任務通過定時器 API 發來的。* 函數 prvProcessReceivedCommands() 會遍歷并執行這些命令,確保定時器狀態與應用請求保持同步。*/prvProcessReceivedCommands();}
}
這玩意就是FreeRTOS 定時器守護任務的核心函數。無非三大功能,一直循環工作:
- 檢查當前定時器列表,找出下一個即將到期的定時器。
- 根據是否有定時器到期,決定立即處理定時器回調,或者阻塞等待。
- 處理定時器命令隊列中發來的命令(如啟動、停止定時器等)。
3.和Linux對比
在 Linux 中,定時器通常在內核中以軟中斷(softirq)或專門的定時器線程的形式運行。不同點:
-
執行上下文:
-
- Linux 定時器:通常在內核上下文中運行,可能由軟中斷調度;定時器回調可能會影響整個系統的中斷延遲。
- FreeRTOS 定時器:回調函數在 RTOS 守護任務中運行,不在中斷上下文中執行,這樣可以避免長時間中斷阻塞,提高系統實時性。
-
調度策略:
-
- Linux:定時器任務優先級由內核調度策略決定,有時會有較高的調度權重,且可以與用戶態任務共享 CPU。
- FreeRTOS:定時器守護任務的優先級由配置參數
configTIMER_TASK_PRIORITY
定義,開發者需合理設置以確保定時器命令及時處理,同時不干擾其他關鍵任務。
-
資源占用:
-
- Linux 定時器:通常支持高精度計時和復雜的定時器功能,但實現較為復雜;使用內核對象管理。
- FreeRTOS 定時器:設計更簡單、輕量,適用于嵌入式系統。利用定時器命令隊列和守護任務解耦定時器回調執行,降低了中斷負擔。
4.ID
在生產過程中,肯定會不止只有一個定時器,會創建多個定時器,那么這些定時器是如何去標記好區分的?看下一定時器的結構體就行了,其結構成員pvTimerID:
/* 定時器控制塊結構體(原名稱tmrTimerControl用于兼容內核感知調試工具) */
typedef struct tmrTimerControl
{/* 定時器名稱(字符串指針) * [功能] 用于調試時標識定時器,內核本身不使用此字段。* [注意] 允許使用未限定的char類型(根據Lint規則僅允許字符串和單字符使用) * [示例] 可設置為"LED_Blink_Timer"等有意義的名稱 */const char * pcTimerName;/* 定時器鏈表項(數據結構)* [功能] 內核事件管理的標準鏈表項,用于將定時器插入定時器隊列* [關聯] 與調度器中的xTimerList鏈表配合使用* [操作] 通過vListInsert()等鏈表API管理 */ListItem_t xTimerListItem;/* 定時器周期(以Tick為單位)* [功能] 定義定時器的觸發間隔時間* [單位] 1 Tick = 1 / configTICK_RATE_HZ 秒* [注意] 對于一次性定時器表示首次觸發間隔,對于自動重載定時器表示周期間隔* [示例] 若設為100,表示100個Tick后觸發 */TickType_t xTimerPeriodInTicks;/* 定時器標識符(通用指針)* [功能] 唯一標識定時器實例,用于同一回調函數處理多個定時器的場景* [用法] 在回調函數中通過pvTimerID區分不同定時器* [注意] 建議使用結構體指針或哈希值等具有唯一性的標識 */void * pvTimerID;/* 定時器回調函數指針* [功能] 定時器到期時執行的函數* [類型] 函數原型:void CallbackFunc(TimerHandle_t xTimer)* [注意] 回調函數應避免執行阻塞操作 */TimerCallbackFunction_t pxCallbackFunction;#if ( configUSE_TRACE_FACILITY == 1 )/* 跟蹤工具分配的定時器編號* [功能] 用于FreeRTOS+Trace等調試工具追蹤定時器行為* [生成] 由內核在創建定時器時自動分配* [查看] 可通過uxTimerGetTimerNumber() API獲取 */UBaseType_t uxTimerNumber;#endif/* 定時器狀態標志位(8位無符號整型)* [位域定義]:* bit0 - 定時器活動狀態(1:運行中,0:休眠)* bit1 - 內存分配方式(1:靜態分配,0:動態分配)* bit2~7 - 保留位* [操作] 通過宏pdTRUE/pdFALSE設置狀態* [注意] 直接修改此字段可能導致狀態不一致,建議使用API管理 */uint8_t ucStatus;
} xTIMER;
更新ID:使用 vTimerSetTimerID()
函數
查詢ID:查詢 pvTimerGetTimerID()
函數
5.數據傳輸
在 2.1 中說過:當配置項 configUSE_TIMERS
設置為 1 時,啟動調度器時系統會自動創建守護任務。來看看如果定義了,會設置什么內容,在FreeRTOS.h中:
#if configUSE_TIMERS == 1/* * 定時器服務任務優先級檢查 (configTIMER_TASK_PRIORITY)* 定義定時器守護任務(Timer Service Task)的調度優先級* 0~(configMAX_PRIORITIES-1),通常建議設置為中等優先級* 未定義時會導致定時器命令無法被及時處理* #define configTIMER_TASK_PRIORITY 3*/#ifndef configTIMER_TASK_PRIORITY#error 啟用定時器功能(configUSE_TIMERS=1)時,必須定義configTIMER_TASK_PRIORITY #endif /* configTIMER_TASK_PRIORITY *//* * 定時器命令隊列長度檢查 (configTIMER_QUEUE_LENGTH)* 定義定時器命令隊列的最大容量,影響同時處理的定時器操作數量* ≥5,高負載場景建議10以上* 隊列過小可能導致xTimerStart等操作失敗(返回pdFAIL)* 需求隊列長度 = 并發定時器操作峰值數 + 安全余量*/#ifndef configTIMER_QUEUE_LENGTH#error 啟用定時器功能(configUSE_TIMERS=1)時,必須定義configTIMER_QUEUE_LENGTH#endif /* configTIMER_QUEUE_LENGTH *//* * 定時器任務棧深度檢查 (configTIMER_TASK_STACK_DEPTH)* 定義定時器守護任務的棧空間大小(以字為單位)* 根據架構不同,建議≥100(如STM32可設為128)* 棧溢出會導致系統崩潰,建議通過uxTaskGetStackHighWaterMark()監控* 需考慮回調函數的最大棧消耗 + 系統安全余量*/#ifndef configTIMER_TASK_STACK_DEPTH#error 啟用定時器功能(configUSE_TIMERS=1)時,必須定義configTIMER_TASK_STACK_DEPTH#endif /* configTIMER_TASK_STACK_DEPTH */#endif /* configUSE_TIMERS */
可以看出還必須定義另外三個宏,否則使用定時器時是會出錯的,其中優先級、棧深度的宏定義是可以理解,畢竟定時器的守護任務是需要指定棧深度以及優先級的,但是為什么還要還要定義隊列的最大容量呢?
-
FreeRTOS 的軟件定時器功能通過 守護任務(Timer Service Task) 實現,而該守護任務與其他任務/中斷之間的通信完全依賴 定時器命令隊列。隊列的作用可歸納為:
-
- 命令中轉站:所有定時器操作(如啟動、停止、重置)均通過發送命令到隊列,由守護任務異步處理。
- 線程安全:避免多任務同時操作定時器數據結構導致的競態條件。
- 優先級解耦:允許低優先級任務發送命令,由高優先級的守護任務及時處理。
-
簡單點就是,用戶程序這邊調用了定時器的相關操作函數,比如啟動/刪除/停止定時器等,同時通過隊列來傳輸到內核的
6.操作函數
- 創建與刪除定時器:
使用 xTimerCreate / xTimerCreateStatic 創建定時器;使用 xTimerDelete 刪除定時器(僅適用于動態創建)。 - 啟動/停止/復位定時器:
使用 xTimerStart / xTimerStartFromISR 啟動定時器;使用 xTimerStop / xTimerStopFromISR 停止定時器;使用 xTimerReset / xTimerResetFromISR 復位定時器,使定時器重新開始計時。 - 修改定時器周期:
使用 xTimerChangePeriod / xTimerChangePeriodFromISR 動態修改定時器的周期,新周期的到期時間從調用時刻開始計算。
6.1 創建
動態創建定時器:
TimerHandle_t xTimerCreate( const char * const pcTimerName,const TickType_t xTimerPeriodInTicks,const UBaseType_t uxAutoReload,void * const pvTimerID,TimerCallbackFunction_t pxCallbackFunction );
-
pcTimerName:定時器名稱,僅用于調試時識別,不影響功能。
-
xTimerPeriodInTicks:定時器周期,單位是 Tick。定時器啟動后,在經過該 Tick 數后會觸發回調函數。
-
uxAutoReload:定時器類型。
-
- pdTRUE 表示自動加載定時器(周期性定時器),在每個周期結束后自動重啟。
- pdFALSE 表示一次性定時器,觸發回調函數后進入冬眠狀態,需要手動重新啟動。
-
pvTimerID:定時器 ID,供回調函數使用,以便識別定時器來源或關聯其他數據。
-
pxCallbackFunction:回調函數,當定時器超時時調用。回調函數的原型為:
typedef void (* TimerCallbackFunction_t)( TimerHandle_t xTimer );
-
返回值:
-
- 成功則返回定時器句柄(TimerHandle_t);如果內存分配失敗則返回 NULL。
靜態創建定時器:
TimerHandle_t xTimerCreateStatic( const char * const pcTimerName,const TickType_t xTimerPeriodInTicks,const UBaseType_t uxAutoReload,void * const pvTimerID,TimerCallbackFunction_t pxCallbackFunction,StaticTimer_t *pxTimerBuffer );
-
除了與動態創建相同的參數外,多了:
-
- pxTimerBuffer:指向一個 StaticTimer_t 類型的結構體內存,該內存由用戶提前分配,用于保存定時器數據結構。
-
返回值:成功返回定時器句柄,失敗返回 NULL。
6.2 刪除
對于動態創建的定時器,當不再需要時可以調用刪除函數以回收內存。
BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );
- xTimer:要刪除的定時器句柄。
- xTicksToWait:寫入刪除命令到定時器命令隊列的超時時間(Tick 數)。如果隊列滿,調用者可以等待一段時間(如使用 pdMS_TO_TICKS() 轉換后的 Tick 數);若在規定時間內命令無法寫入,則返回失敗。
返回值:
- pdPASS 表示命令成功寫入隊列,定時器刪除命令已發出;
- pdFAIL 表示在 xTicksToWait 內無法寫入刪除命令到隊列。
6.3 啟動
啟動定時器就是使其狀態變為“運行態
任務中啟動定時器:
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
-
xTimer:要啟動的定時器句柄。
-
xTicksToWait:寫入“啟動定時器”命令到命令隊列的超時時間。注意,此參數并非定時器的周期,而是命令寫入隊列時等待的時間。
-
返回值:
-
- pdPASS 表示啟動命令成功寫入命令隊列;
- pdFAIL 表示在 xTicksToWait Tick 內無法寫入啟動命令。
ISR 中啟動定時器:
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken );
-
xTimer:定時器句柄。
-
pxHigherPriorityTaskWoken:指向變量的指針,如果該操作使得定時器守護任務(Timer Daemon Task)從阻塞中喚醒,并且其優先級高于當前任務,則該變量被置為 pdTRUE,指示中斷退出后進行任務切換。
-
返回值:
-
- pdPASS 表示成功寫入啟動命令;
- pdFAIL 表示寫入命令失敗。
6.4 停止
停止則使其進入“冬眠態”。
任務中停止定時器:
BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );
-
xTimer:定時器句柄。
-
xTicksToWait:寫入“停止定時器”命令的超時時間。
-
返回值:
-
- pdPASS 表示成功寫入停止命令;
- pdFAIL 表示在規定時間內無法寫入命令。
ISR 中停止定時器:
BaseType_t xTimerStopFromISR( TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken );
- 與啟動定時器的 ISR 版本類似,多了 pxHigherPriorityTaskWoken 參數。
6.5 復位(重置)
復位定時器會將其超時時間重新設定,使其從當前時刻開始計算周期。復位可以看作是重新啟動定時器。
任務中復位定時器:
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );
-
xTimer:要復位的定時器句柄。
-
xTicksToWait:寫入復位命令到命令隊列的超時時間。
-
返回值:
-
- pdPASS 表示復位命令成功寫入隊列;
- pdFAIL 表示在 xTicksToWait 內寫入命令失敗。
ISR 中復位定時器:
BaseType_t xTimerResetFromISR( TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken );
- 同上,多了 pxHigherPriorityTaskWoken 參數,用于中斷上下文中判斷是否需要上下文切換。
6.6 修改周期
除了啟動和復位定時器,FreeRTOS 還提供修改定時器周期的接口,這樣可以動態地改變定時器的觸發間隔。
任務中修改定時器周期:
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer, TickType_t xNewPeriod, TickType_t xTicksToWait );
-
xTimer:要修改周期的定時器句柄。
-
xNewPeriod:新的周期,以 Tick 為單位。修改后,下一個回調的觸發時間為當前時間加上這個新周期。
-
xTicksToWait:寫入“修改周期”命令到定時器命令隊列的超時時間。
如果命令無法在規定時間內寫入隊列,則返回 pdFAIL。 -
返回值:
-
- pdPASS 表示命令成功寫入隊列,周期修改命令生效;
- pdFAIL 表示寫入命令失敗。
ISR 中修改定時器周期:
BaseType_t xTimerChangePeriodFromISR( TimerHandle_t xTimer, TickType_t xNewPeriod, BaseType_t *pxHigherPriorityTaskWoken );
-
同任務版本,但無法阻塞,因此立即嘗試寫入命令。
-
pxHigherPriorityTaskWoken:用于檢測是否需要在中斷退出時進行任務調度。
-
返回值:
-
- pdPASS 表示成功;
- pdFAIL 表示命令寫入失敗。
6.7 注意事項
- xTicksToWait 參數:
在所有涉及命令寫入定時器命令隊列的函數中,xTicksToWait 表示調用者等待將命令寫入隊列的時間,而不是定時器的超時時間或周期。這是因為定時器的所有操作(啟動、停止、復位、修改周期)都是通過向定時器命令隊列發送命令實現的。 - 命令隊列滿時,命令寫入會失敗(或阻塞等待,取決于 xTicksToWait 的設置)
- 這些命令最終由定時器守護任務處理,守護任務的優先級(configTIMER_TASK_PRIORITY)和命令隊列長度(configTIMER_QUEUE_LENGTH)決定了定時器命令的響應速度,從而影響定時器回調函數的調用時刻。
7.示例:一般使用
/* 定義全局變量 */
static TimerHandle_t xMyTimerHandle; // 定時器句柄,用于引用創建的定時器
static int flagTimer = 0; // 用于演示定時器回調執行時改變的標志變量/*-----------------------------------------------------------* Task1Function:* 這是一個示例任務,該任務在開始運行時啟動定時器,* 然后進入無限循環,不斷打印消息。* 定時器啟動后,會按照設定的周期自動觸發回調函數。*-----------------------------------------------------------*/
void Task1Function(void * pvParameters)
{volatile int i = 0;/* 啟動定時器 xMyTimerHandle,xTicksToWait 參數為 0 表示如果定時器命令隊列滿了,則不等待,立即返回 */xTimerStart(xMyTimerHandle, 0);/* 進入任務主循環 */while (1){/* 打印任務執行信息 */printf("Task1Function ...\r\n");/* 此處可添加延時函數,例如 vTaskDelay(),以免任務占用過多CPU資源但本例為了簡單演示,不添加延時 */}
}/*-----------------------------------------------------------* Task2Function:* 此任務示例目前沒有做任何事情,可用作后續擴展。*-----------------------------------------------------------*/
void Task2Function(void * pvParameters)
{volatile int i = 0;while (1){/* 空循環,暫時未實現功能 */}
}/*-----------------------------------------------------------* MyTimerCallbackFunction:* 定時器回調函數,當定時器超時時,該函數會被 RTOS 定時器守護任務調用。* 此處演示每次定時器超時時,將全局標志 flagTimer 取反,并打印調用次數。*-----------------------------------------------------------*/
void MyTimerCallbackFunction( TimerHandle_t xTimer )
{static int cnt = 0; // 靜態變量,用于記錄回調函數被調用的次數flagTimer = !flagTimer; // 取反 flagTimer 的值,演示狀態變化printf("MyTimerCallbackFunction: cnt = %d\r\n", cnt++);
}/*-----------------------------------------------------------* main 函數:程序入口* 主要步驟:* 1. 硬件初始化;* 2. 創建定時器(這里使用動態分配內存的方法);* 3. 創建任務;* 4. 啟動調度器。*-----------------------------------------------------------*/
int main( void )
{TaskHandle_t xHandleTask1; // 用于存儲 Task1 的任務句柄#ifdef DEBUGdebug(); // 如果定義了 DEBUG,調用調試函數
#endif/* 初始化硬件,平臺相關的初始化函數 */prvSetupHardware();/* 輸出啟動信息 */printf("Hello, world!\r\n");/* 創建定時器參數說明:- "mytimer":定時器的名稱(用于調試);- 100:定時器周期,單位為 Tick;- pdTRUE:自動加載定時器(自動重載),即定時器回調會周期性調用;- NULL:定時器ID,這里不使用,可以傳遞額外信息供回調函數識別定時器;- MyTimerCallbackFunction:定時器超時后的回調函數 */xMyTimerHandle = xTimerCreate("mytimer", 100, pdTRUE, NULL, MyTimerCallbackFunction);/* 檢查定時器創建是否成功 */if (xMyTimerHandle == NULL){printf("Error: Cannot create timer\r\n");/* 這里可以加入錯誤處理代碼 */}/* 創建 Task1 任務,任務函數為 Task1Function參數說明:- "Task1":任務名稱;- 100:任務棧大小(單位通常為字節或堆棧深度,依平臺而定);- NULL:傳遞給任務的參數;- 1:任務優先級;- &xHandleTask1:存儲任務句柄的變量地址 */xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);/* 如果需要,還可以創建 Task2 任務(此處被注釋掉,可根據需要啟用)xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL); *//* 啟動任務調度器。啟動調度器后,所有創建的任務開始并發執行,同時定時器服務任務也會被自動創建(如果配置了定時器功能)。 */vTaskStartScheduler();/* 如果程序運行到這里,通常表示調度器啟動失敗,可能是內存不足導致無法創建空閑任務。 */return 0;
}
還是需要在FreeRTOSConfig.h
中去定義一些宏:
#define configUSE_TIMERS 1
#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES-2)
#define configTIMER_QUEUE_LENGTH 10
#define configTIMER_TASK_STACK_DEPTH 100
高/低電平都是100ms,configTICK_RATE_HZ
設定是1000,也就是一個tick是1ms,定時器周期設定的是100個,也就是100ms,守護任務會搶占然后執行定時器的回調函數
8.示例:定時器防抖
在實際的按鍵操作中,可能會有機械抖動:
按下或松開一個按鍵,它的 GPIO 電平會反復變化,最后才穩定。一般是幾十毫秒才會穩定。
如果不處理抖動的話,用戶只操作一次按鍵,中斷程序可能會上報多個數據。怎么處理?
- 按鍵中斷程序中,可以循環判斷幾十亳秒,發現電平穩定之后再上報
- 使用定時器
顯然第 1 種方法太耗時,違背“中斷要盡快處理”的原則,你的系統會很卡。怎么使用定時器?看下圖:
核心在于:在 GPIO 中斷中并不立刻記錄按鍵值,而是修改定時器超時時間,10ms 后再處理。
- 如果 10ms 內又發生了 GPIO 中斷,那就認為是抖動,這時再次修改超時時間為 10ms(也就是重置定時器)。
- 只有 10ms 之內再無 GPIO 中斷發生,那么定時器的回調函數才會被調用。在定時器函數中打印按鍵值。
下面代碼實現類似,只不過設置的超時時間是2s
/* 定時器句柄和標志變量定義 */
static TimerHandle_t xMyTimerHandle; // FreeRTOS軟件定時器句柄
static int flagTimer = 0; // 定時器回調函數觸發的狀態標志/* 任務1函數 */
void Task1Function(void * param)
{volatile int i = 0; // volatile防止編譯器優化// 注意:此處定時器啟動被注釋,實際使用需取消注釋//xTimerStart(xMyTimerHandle, 0); // 啟動定時器(0表示不阻塞)while (1){// 典型問題:任務中未添加阻塞函數(如vTaskDelay),// 將導致該任務持續占用CPU,建議添加延時釋放CPU//printf("Task1Function ...\r\n");}
}/* 任務2函數(框架,暫無實際功能) */
void Task2Function(void * param)
{volatile int i = 0;while (1){}
}/* 定時器回調函數 */
void MyTimerCallbackFunction( TimerHandle_t xTimer )
{static int cnt = 0; // 靜態變量保持計數狀態flagTimer = !flagTimer; // 翻轉狀態標志/* * 注意:在回調中調用printf等耗時函數可能影響實時性,* 建議僅在調試時使用,實際應用替換為更高效的操作*/printf("Get GPIO Key cnt = %d\r\n", cnt++);
}/*-----------------------------------------------------------*//* 按鍵GPIO初始化 */
void KeyInit(void)
{GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能GPIOA時鐘GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // 配置PA0引腳GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉輸入模式(檢測低電平)GPIO_Init(GPIOA, &GPIO_InitStructure); // 應用配置
}/* 按鍵中斷初始化 */
void KeyIntInit(void)
{EXTI_InitTypeDef EXTI_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 使能復用功能時鐘/* 映射GPIOA0到EXTI0中斷線 */GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);/* 配置EXTI0中斷線 */EXTI_InitStructure.EXTI_Line = EXTI_Line0; // 中斷線0EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 中斷模式EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling; // 雙邊沿觸發EXTI_InitStructure.EXTI_LineCmd = ENABLE; // 使能中斷線EXTI_Init(&EXTI_InitStructure); // 應用配置/* 配置NVIC中斷控制器 */NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQChannel; // 外部中斷0通道NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 搶占優先級0NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子優先級0NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能中斷通道NVIC_Init(&NVIC_InitStructure); // 應用配置
}/* EXTI0中斷服務函數 */
void EXTI0_IRQHandler(void)
{static int cnt = 0; // 中斷計數if(EXTI_GetITStatus(EXTI_Line0) != RESET) // 確認是EXTI0中斷{printf("EXTI0_IRQHandler cnt = %d\r\n", cnt++); // 調試輸出/* * 關鍵操作:重置定時器實現消抖* 問題:此處應使用xTimerResetFromISR()而非xTimerReset()* 原因:在中斷中調用非ISR結尾的API可能導致上下文錯誤* 修正建議:* BaseType_t xHigherPriorityTaskWoken = pdFALSE;* xTimerResetFromISR(xMyTimerHandle, &xHigherPriorityTaskWoken);* portYIELD_FROM_ISR(xHigherPriorityTaskWoken);*/xTimerReset(xMyTimerHandle, 0); // 重置定時器(2000 ticks后觸發回調)EXTI_ClearITPendingBit(EXTI_Line0); // 清除中斷標志}
}/* 主函數 */
int main( void )
{TaskHandle_t xHandleTask1; // 任務1句柄(暫未使用)#ifdef DEBUGdebug(); // 調試初始化(如果定義了DEBUG)
#endifprvSetupHardware(); // 硬件初始化(假設包含時鐘配置等)printf("Hello, world!\r\n"); // 啟動信息輸出KeyInit(); // 初始化按鍵GPIOKeyIntInit(); // 配置按鍵中斷/* * 創建軟件定時器:* 參數1:定時器名稱(調試用)* 參數2:周期2000 ticks(單位取決于configTICK_RATE_HZ)* 參數3:自動重載模式(pdFALSE表示單次定時器)* 參數4:定時器ID(NULL表示不設置)* 參數5:回調函數指針*/xMyTimerHandle = xTimerCreate("mytimer", 2000, pdFALSE, NULL, MyTimerCallbackFunction);// 創建任務1(優先級1,堆棧深度100字)xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);//xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL); // 任務2暫未啟用vTaskStartScheduler(); // 啟動FreeRTOS調度器/* * 正常情況下不會執行到這里,* 除非內存不足導致空閑任務創建失敗*/return 0;
}