【freertos】007-系統節拍和系統延時管理實現細節
- 2022 年 4 月 1 日
- 筆記
- /label/FreeRTOS, freeRTOS, 嵌入式, 教程集合
前言
本章節的時鐘系統節拍主要分析FreeRTOS內核相關及北向介面層,南向介面層不分析。
本章節的系統延時主要分析任務系統延時實現。
原文:李柱明部落格://www.cnblogs.com/lizhuming/p/16085130.html
筆記手碼。
相關程式碼倉庫:李柱明 gitee
7.1 系統節拍配置
FreeRTOS的系統時鐘節拍可以在配置文件FreeRTOSConfig.h裡面設置:#define configTICK_RATE_HZ( ( TickType_t ) 1000 )
7.2 系統時鐘節拍的原理
系統時鐘節拍不僅僅只記錄系統運行時長,還涉及到系統的時間管理,任務延時等等。
系統節拍數:
系統會通過南向介面層實現定時回調,維護一個全局變數xTickCount
。
每次定時回調會將變數xTickCount
加1。
這個變數xTickCount
就是系統時基節拍數。
獲取時鐘節拍數其實也就是返回該值。
注意:
系統節拍數不是每個tick都在實時累加的,在調度器掛起的情況下,觸發產生的tick會記錄下來,在恢復調度器後按掛起調度器產生的tick數逐個跑回xTaskIncrementTick()
,快進模擬。
7.3 系統節拍中的處理:xTaskIncrementTick()
時鐘節拍分析就按這個函數分析就好。
每當系統節拍定時器中斷時,南向介面層都會調用該函數來實現系統節拍需要處理的程式碼。主要是
- 系統節拍數
xTickCount
加1。 - 檢查本次節拍是否解除某些任務的阻塞。
- 標記是否需要觸發任務切換。
7.3.1 調度器正常
uxSchedulerSuspended
這個變數記錄調度器運行狀態:
pdFALSE
表示調度器正常,沒有被掛起。pdTRUE
表示調度器被掛起。
7.3.1.1 系統節拍數統計
調度器正常的情況下,xTickCount
加1。
7.3.1.2 延時列表
先看下面幾條鏈表的源碼。
/* Lists for ready and blocked tasks. --------------------
* xDelayedTaskList1 and xDelayedTaskList2 could be moved to function scope but
* doing so breaks some kernel aware debuggers and debuggers that rely on removing
* the static qualifier. */
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; /*< Prioritised ready tasks. */
PRIVILEGED_DATA static List_t xDelayedTaskList1; /*< Delayed tasks. */
PRIVILEGED_DATA static List_t xDelayedTaskList2; /*< Delayed tasks (two lists are used - one for delays that have overflowed the current tick count. */
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList; /*< Points to the delayed task list currently being used. */
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList; /*< Points to the delayed task list currently being used to hold tasks that have overflowed the current tick count. */
PRIVILEGED_DATA static List_t xPendingReadyList; /*< Tasks that have been readied while the scheduler was suspended. They will be moved to the ready list when the scheduler is resumed. */
需要注意的是,延時鏈表其實只有兩條:
xDelayedTaskList1
xDelayedTaskList2
而pxDelayedTaskList
和pxOverflowDelayedTaskList
只是鏈表指針,分別指向當前正在使用的延時列表和溢出列表。
為什麼需要兩條延時列表?
為了解決系統節拍溢出問題。
如當系統節拍未溢出,pxDelayedTaskList
指向xDelayedTaskList1
,pxOverflowDelayedTaskList
指向xDelayedTaskList2
時;
任務需要喚醒的時間在未溢出範圍內,記錄到pxDelayedTaskList
指向的xDelayedTaskList1
;
任務需要喚醒的時間在超出溢出範圍,記錄到pxOverflowDelayedTaskList
指向的xDelayedTaskList2
;
當系統節拍溢出時,會做如下處理:
pxDelayedTaskList
更新指向xDelayedTaskList2
。pxOverflowDelayedTaskList
更新指向xDelayedTaskList1
。
這樣就實現了pxDelayedTaskList
始終指向未溢出的任務延時列表。
7.3.1.3 系統節拍溢出處理
對於嵌入式系統而已,xTickCount
系統節拍佔位也就8、32、64或者更大,但是也有溢出的時候,所以需要做溢出處理。
xTickCount
系統節拍溢出處理是調用taskSWITCH_DELAYED_LISTS()實現
-
交換延時列表指針和溢出延時列表指針;
-
溢出次數記錄
xNumOfOverflows
; -
調用
prvResetNextTaskUnblockTime()
更新下一次解除阻塞的時間到xNextTaskUnblockTime
。-
如果延時列表為空,說明沒有任務因為延時阻塞。把下次需要喚醒的時間更新為最大值。說明未來不需要檢查延時列表。
-
如果延時列表不為空,說明有任務等待喚醒。從延時列表的第一個任務節點中把節點值取出來,該值就是延時列表中未來最近有任務需要喚醒的時間。
- freertos內核鏈表採用的是非通用雙向循環鏈表,節點結構體如下程式碼所示。其中
xItemValue
可由用戶自定義賦值,在freertos延時列表中,用於記錄當前任務需要喚醒的時間節拍值。 - 學習freertos內核鏈表的可以參考:李柱明-雙向非通用鏈表
- freertos內核鏈表採用的是非通用雙向循環鏈表,節點結構體如下程式碼所示。其中
-
freertos內核鏈表節點結構體:
struct xLIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
configLIST_VOLATILE TickType_t xItemValue; /*< The value being listed. In most cases this is used to sort the list in ascending order. */
struct xLIST_ITEM * configLIST_VOLATILE pxNext; /*< Pointer to the next ListItem_t in the list. */
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /*< Pointer to the previous ListItem_t in the list. */
void * pvOwner; /*< Pointer to the object (normally a TCB) that contains the list item. There is therefore a two way link between the object containing the list item and the list item itself. */
struct xLIST * configLIST_VOLATILE pxContainer; /*< Pointer to the list in which this list item is placed (if any). */
listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE /*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
};
typedef struct xLIST_ITEM ListItem_t; /* For some reason lint wants this as two separate definitions. */
xNextTaskUnblockTime
變數就是表示當前系統未來最近一次延時列表任務中有任務需要喚醒的時間。
利用這個變數就不需要在每次tick到了都檢查下延時列表是否需要解除阻塞,節省CPU開銷。
7.3.1.4 任務喚醒處理
系統節拍溢出處理完後,檢查是否需要喚醒任務。
如:
if( xConstTickCount >= xNextTaskUnblockTime )
{
/* 延時的任務到期,需要被喚醒 */
}
進入上面程式碼邏輯分支以後,循環以下內容:
如果延時列表為空,則把xNextTaskUnblockTime
更新到最大值。
如果延時列表不為空,則從延時列表中把任務句柄拿出來,分析:
-
如果該任務需要喚醒的時間比系統節拍時間早,則
- 把該任務從延時列表移除,重新插入到就緒列表;
- 如果是因為事件阻塞,還要把該任務從事件列表中刪除;
- 如果解除阻塞的任務優先順序比當前運行的任務優先順序高,就標記觸發任務調度
xSwitchRequired = pdTRUE;
-
如果該喚醒時間在未來,更新這個時間到
xNextTaskUnblockTime
,且退出遍歷延時列表。
7.3.1.5 時間片處理
處理完任務阻塞後,便開始處理時間片的問題。
freertos的時間片不是真正意義的時間片,不能隨意設置時間片多少個tick,只能默認一個tick。其實現就看這裡程式碼就知道了。偽時間片。
每次tick都會檢查是否有其他任務共享當前優先順序,有就標記需要任務切換。
/* 如果有其它任務與當前任務共享一個優先順序,則這些任務共享處理器(時間片) */
#if ( (configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
if(listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
7.3.1.6 tick鉤子
時間片處理完,可以執行tick鉤子函數了。
需要注意的是,tick鉤子函數vApplicationTickHook()
是在系統滴答中跑的,所以這個函數內容要短小,不能大量使用堆棧,且只能調用以」FromISR” 或 “FROM_ISR」結尾的API函數。
另外,在程式碼中也能看到,在uxPendedTicks
值為0才會執行tick鉤子,這是因為不論調度器是否掛起,都會執行vApplicationTickHook()
。
而在調度器掛起期間,tick鉤子也在執行,所以在補回時鐘節拍的處理就不在執行tick鉤子。
上述的uxPendedTicks
值,是記錄調度器掛起期間產生的tick數。
7.3.1.7 xYieldPending
該變數為了實現自動切換而萌生。
在函數xTaskIncrementTick()
內,xSwitchRequired
為返回值,為真,在外部調用會觸發任務切換。
但是函數中xYieldPending
變數也會觸發xSwitchRequired
為真。
我們需要了解xYieldPending
這個變數的含義。
帶中斷保護的API函數(後綴FromISR
),都會有一個參數pxHigherPriorityTaskWoken
。
如果這些API函數導致一個任務解鎖,且該任務的優先順序高於當前運行任務,這些API會標記*pxHigherPriorityTaskWoken = pdTRUE;
,然後再退出欄位前,老版本的FreeRTOS需要手動觸發一次任務調度。
如在中斷中跑:
BaseType_txHigherPriorityTaskWoken = pdFALSE;
/* 收到一幀數據,向命令行解釋器任務發送通知 */
vTaskNotifyGiveFromISR(xCmdAnalyzeHandle,&xHigherPriorityTaskWoken);
/* 是否需要強制上下文切換 */
portYIELD_FROM_ISR(xHigherPriorityTaskWoken );
從FreeRTOSV7.3.0起,pxHigherPriorityTaskWoken
成為一個可選參數,並可以設置為NULL。
轉而使用xYieldPending
來實現帶中斷保護的API函數解鎖一個更高優先順序任務後,標記該變數為pdTRUE
,實現任務自動進行切換。
變數xYieldPending
為pdTRUE
,會在下一次系統節拍中斷服務函數中,觸發一次任務切換。程式碼便是:
if( xYieldPending != pdFALSE )
{
xSwitchRequired = pdTRUE;
}
但是實際實現啟用該功能是在在V9.0以及以上版本。
小結一下pxHigherPriorityTaskWoken
和xYieldPending
:
-
在帶中斷保護的API中解鎖了更高優先順序的任務,需要在這些API內部標記一些變數來觸發任務切換。這些變數有
pxHigherPriorityTaskWoken
和xYieldPending
。 -
pxHigherPriorityTaskWoken
:- 手動切換標記。
- 局部變數。
- 如果帶中斷保護的API解鎖了更高優先順序的任務,會標記
pxHigherPriorityTaskWoken
為pdTRUE
,用戶根據這個變數調用portYIELD_FROM_ISR()
來實現手動切換任務。
-
xYieldPending
:- 自動切換標記。
- 全家變數。
- 如果標記為
pdTRUE
,在執行xTaskIncrementTick()
時鐘節拍處理時,調度器正常的情況下回觸發一次任務切換。
帶中斷保護API內部參考程式碼:
if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
{
/*如果解除阻塞的任務優先順序大於當前任務優先順序,則設置上下文切換標識,等退出函數後手動切換上下文,或者在系統節拍中斷服務程式中自動切換上下文*/
if(pxHigherPriorityTaskWoken != NULL )
{
*pxHigherPriorityTaskWoken= pdTRUE; /* 設置手動切換標誌*/
}
xYieldPending= pdTRUE; /* 設置自動切換標誌*/
}
7.3.2 調度器掛起
如果調度器掛起,正在執行的任務會一直繼續執行,內核不再調度,直到該任務調用xTaskResumeAll()
恢復調度器。
在調度器掛起期間不會進行任務切換,但是其中產生的系統節拍都會記錄在變數uxPendedTicks
中。
在恢復調度器後,會在xTaskResumeAll()
函數內調用uxPendedTicks
次xTaskIncrementTick()
實現逐個補回時鐘節拍處理。
7.4 系統節拍相關API
獲取系統節拍:xTaskGetTickCount
作用:用於普通任務中,用於獲取系統當前運行的時鐘節拍數。
原型:
volatile TickType_t xTaskGetTickCount( void );
參數:無。
返回:返回當前運行的時鐘節拍數。
7.4.1 獲取系統節拍中斷保護調用:xTaskGetTickCountFromISR()
作用:用於中斷中,用於獲取系統當前運行的時鐘節拍數。
原型:
volatile TickType_t xTaskGetTickCountFromISR( void );
7.4.2 系統節拍API 實戰
當前配置是configTICK_RATE_HZ
是1000,即是1ms觸發一次系統節拍。
7.5 系統延時API相關
系統提供兩個延時API:
- 相對延時函數
vTaskDelay()
; - 絕對延時函數
vTaskDelayUntil()
; - 終止延時函數
xTaskAbortDelay()
。
7.6 相對延時:vTaskDelay()
7.6.1 API使用
函數原型:
void vTaskDelay(const TickType_t xTicksToDelay );
函數說明:
vTaskDelay()
用於相對延時,是指每次延時都是從任務執行函數vTaskDelay()
開始,延時指定的時間結束。xTicksToDelay
參數用於設置延遲的時鐘節拍個數。- 延時的最大值宏在portmacro.h中有定義:
#define portMAX_DELAY (TickType_t )0xffffffffUL
圖中N就是參數xTicksToDelay
。
7.6.2 相對延時實現原理
原理:原理就是通過當前時間點和延時時長這兩個值算出未來需要喚醒的時間,記錄當前任務未來喚醒的時間點,然後把當前任務從就緒鏈表移到延時鏈表。
未來喚醒時間 = 當前時間 + 延時時間。
xTimeToWake = xConstTickCount + xTicksToWait;
7.6.3 實現細節
7.6.3.1 傳入參數為0
傳入參數為0時,不會把當前任務進行阻塞。
但是會觸發一次任務調度。
7.6.3.2 掛起調度器
進入延時函數,在掛起調度器前會檢查下當前當前是否已經掛起調度器了,如果硬體掛起調度器了還調用阻塞的相關API,系統會掛掉。
/* 如果調度器掛起了,那就沒得玩了!!! */
configASSERT( uxSchedulerSuspended == 0 );
如果當前調度器沒有被掛起,那可以進入延時處理,先掛起調度器,防止在遷移任務時被其它任務打斷。
/* 掛起調度器 */
vTaskSuspendAll();
7.6.3.3 計算出未來喚醒時間
計算出未來喚醒時間點,這個就是相對延時和絕對延時的主要區別。
相對延時,未來喚醒時間點xTimeToWake
是當前系統節拍加上xTicksToWait
需要延時的節拍數。
然後把這個值記錄到當前任務狀態節點裡面的節點值xItemValue
里,用於插入延時列表排序使用。
xTimeToWake = xConstTickCount + xTicksToWait;
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
7.6.3.4 遷移任務到延時鏈表
從就緒鏈表遷移到延時鏈表時,調用prvAddCurrentTaskToDelayedList()
實現。
如果啟用了終止延時功能,先pxCurrentTCB->ucDelayAborted
把這個標誌位複位,因為要出現進入延時了。
先把任務從就緒鏈表中移除。
移除後,如果當前任務同等優先順序沒有其它任務了,需要處理下就緒任務優先順序點陣圖:
- 如果開啟了優先順序優化功能:需要把這個優先順序在圖表
uxTopReadyPriority
中對應的位清除。 - 如果沒有開啟優先順序優化功能:我認為也應該更新
uxTopReadyPriority
這個值,讓系統知道當前就緒任務最高優先順序已經不是當前任務的優先順序值了。但是freertos並沒有這樣做。 - 優先順序優化功能可以查看我前面章節說的前導零指令。
/* 把當前任務先從就緒鏈表中移除。 */
if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
/* 如果開啟了優先順序優化功能:需要把這個優先順序在圖表`uxTopReadyPriority`中對應的位清除。
如果沒有開啟優先順序優化功能,這個宏為空的,不處理。 */
portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
}
如果計算出未來喚醒時間點溢出了,就把當前任務插入到溢出延時鏈表,到系統節拍溢出時就換使用該鏈表作為延時鏈表的。
如果未來喚醒時間點沒有溢出,就插入當前延時鏈表,等待喚醒。如果喚醒時間比當前所有延時任務需要喚醒的時間還要早,那就更新下系統當前未來最近需要喚醒的時間值。
if( xTimeToWake < xConstTickCount )
{
/* 喚醒時間點的系統節拍溢出,就插入到溢出延時列表中。 */
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* 喚醒時間的系統節拍沒有溢出,就插入當前延時鏈表。 */
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
/* 如果喚醒時間比當前所有延時任務需要喚醒的時間還要早,那就更新下系統當前未來最近需要喚醒的時間值。 */
if( xTimeToWake < xNextTaskUnblockTime )
{
xNextTaskUnblockTime = xTimeToWake;
}
}
7.6.3.5 強制任務調度
恢復調度器後,如果在恢復調度器時沒有觸發過任務調度,那必須進行一次觸發任務調度,要不然本任務會繼續往下跑,不符合設計邏輯。
/* 恢復調度器 */
xAlreadyYielded = xTaskResumeAll();
if( xAlreadyYielded == pdFALSE )
{
/* 強制調度 */
portYIELD_WITHIN_API();
}
7.7 絕對延時:vTaskDelayUntil()
7.7.1 API使用
函數原型:
BaseType_t vTaskDelayUntil( TickType_t *pxPreviousWakeTime, const TickType_t xTimeIncrement );
函數說明:
vTaskDelayUntil()
用於絕對延時,也叫周期性延時。想像下精度不高的定時器。pxPreviousWakeTime
參數是存儲任務上次處於非阻塞狀態時刻的變數地址。xTimeIncrement
參數用於設置周期性延時的時鐘節拍個數。- 返回:pdFALSE 說明延時失敗。
- 使用此函數需要在FreeRTOSConfig.h配置文件中開啟:
#defineINCLUDE_vTaskDelayUntil 1
- 需要保證周期性延時比任務主體運行時間長。
- 相對延時的意思是延時配置的N個節拍後恢復當前任務為就緒態。
- 絕對延時的意思是延時配置的N個節拍後該任務跑回到當前絕對延時函數。
圖中N就是參數xTimeIncrement
,其中黃色延時部分需要延時多少是vTaskDelayUntil()
實現的。
7.7.2 絕對延時實現原理
原理:實現周期延時的原理就是,通過上次喚醒的時間點、當前時間點和延時周期三個值算出剩下需要延時的時間,得出未來需要喚醒當前任務的時間,然後把當前任務從就緒鏈表遷移到延時鏈表。
未來喚醒時間 = 上次喚醒時間 + 周期。
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
7.7.3 實現細節
7.7.3.1 參數檢查
指針不能為空,周期值不能為0,調度器沒有被掛起。
configASSERT( pxPreviousWakeTime );
configASSERT( ( xTimeIncrement > 0U ) );
configASSERT( uxSchedulerSuspended == 0 );
7.7.3.2 掛起調度器
需要注意的是,在調用該函數時,調度器必須是正常的。
如果當前調度器沒有被掛起,那可以進入延時處理,先掛起調度器,防止在遷移任務時被其它任務打斷。
7.7.3.3 未來喚醒時間
能把任務從就緒鏈表遷移到延時鏈表就緒阻塞的主要條件是喚醒時間在未來。
先算出未來喚醒時間:
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
7.7.3.4 溢出處理
如果當前時間對比上次喚醒的時間已經溢出了,那只有未來喚醒的時間值比當前的時間值還大,才能就緒阻塞處理。
這種情況如下圖:
程式碼如下:
if( xConstTickCount < *pxPreviousWakeTime )
{
/* 只有當周期性延時時間大於任務主體程式碼執行時間,即是喚醒時間在未來,才會將任務掛接到延時鏈表 */
if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
}
如果當前時間對比上次喚醒時間沒有溢出過,需要考慮兩種情況:
- 未來時間喚醒時間已經溢出。
- 未來時間喚醒時間沒有溢出。
對於未來時間沒有溢出,就是下圖:
如果未來喚醒時間比上次喚醒的時間還小,便可說明喚醒時間在未來,這種判斷程式碼就是:
/* 當前時間沒有溢出的情況下,未來喚醒時間小於上次喚醒時間,可以說明未來喚醒時間在未來。 */
if( xConstTickCount >= *pxPreviousWakeTime && xTimeToWake < *pxPreviousWakeTime)
{
xShouldDelay = pdTRUE;
}
而對於未來時間也沒有溢出的情況如下圖:
對於這種情況,未來喚醒時間值比當前時間值大,當前時間值又比上次喚醒時間值大,也可以說明喚醒時間在未來。
/* 當前時間沒有溢出的情況下, 喚醒時間比當前時間還大,可以說明未來喚醒時間在未來。 */
if( xConstTickCount >= *pxPreviousWakeTime && xTimeToWake > xConstTickCount)
{
xShouldDelay = pdTRUE;
}
小結下,只需要證明到實際時空時間值是:上次喚醒 < 當前時間 < 未來喚醒
。即可說明當前任務主體運行時間比周期時間小,可以進行延時阻塞。
7.7.3.5 遷移到延時鏈表
參考相對延時的遷移到延時鏈表章節。
需要注意的是,傳入prvAddCurrentTaskToDelayedList()
的參數應該是相對延時值,而不是未來喚醒時間。
7.7.3.6 強制任務調度
恢復調度器後,如果在恢復調度器時沒有觸發過任務調度,那必須進行一次觸發任務調度,要不然本任務會繼續往下跑,不符合設計邏輯。
/* 恢復調度器 */
xAlreadyYielded = xTaskResumeAll();
if( xAlreadyYielded == pdFALSE )
{
/* 強制調度 */
portYIELD_WITHIN_API();
}
7.8 終止任務阻塞:xTaskAbortDelay()
使用該功能前需要在FreeRTOSConfig.h文件中配置宏INCLUDE_xTaskAbortDelay
為1來使用該功能。
7.8.1 API 使用
函數原型:
BaseType_t xTaskAbortDelay( TaskHandle_t xTask );
函數說明:
-
xTaskAbortDelay()
函數用於解除任務的阻塞狀態,將任務插入就緒鏈表中。 -
xTask
:任務句柄。 -
返回:
pdPASS
:任務解除阻塞成功。pdFAIL
或其它:沒有解除任務阻塞還在任務不在阻塞狀態。
7.8.2 實現細節
7.8.2.1 參數檢查
主要檢查任務句柄值是否有效。
/* 如果傳入的任務句柄是NULL,直接斷言 */
configASSERT( pxTCB );
7.8.2.2 掛起調度器
掛起調度器,防止任務被切走處理。
7.8.2.3 獲取任務狀態
通過API eTaskGetState()
獲取任務狀態是否處於阻塞態。有以下情況可以判斷任務處於阻塞態:
- 任務處於延時鏈表或者處於延時溢出鏈表。
- 任務處於掛起態,但是在等待某個事件,也屬於阻塞態。
- 處於掛起態,也沒有在等待事件,但是在等待任務通知,也屬於阻塞態。
這部分看下該API源碼即可。
如果不在阻塞態,可以xTaskAbortDelay()
函數直接返回pdFAIL
。
7.8.2.4 解除任務狀態並重新插入就緒鏈表
解除任務所有狀態,在阻塞態時,其實就是先把任務遷出對應的任務狀態鏈表。
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
然後加入臨界處理因為事件而阻塞的問題,進入臨界處理是因為部分中斷回調也會接觸到任務事件鏈表。
如果任務是因為事件而阻塞的,需要從事件鏈表中移除,解除阻塞,並且標記上強制解除阻塞標記。
/* 進入臨界 */
taskENTER_CRITICAL();
{
/* 因為事件而阻塞 */
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
/* 移除任務的事件 */
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
/* 強制解除阻塞標誌 */
pxTCB->ucDelayAborted = pdTRUE;
}
}
/* 退出臨界 */
taskEXIT_CRITICAL();
處理完事件鏈表後,可以將其重新插入到就緒鏈表。
/* 重新加入就行鏈表 */
prvAddTaskToReadyList( pxTCB );
7.8.2.5 恢復調度器
把阻塞的任務成功遷入到就緒鏈表後,如果開啟了搶佔式調度,如果解除阻塞的任務優先順序大於當前在跑的任務優先順序,需要任務切換。
通過xYieldPending = pdTRUE;
標記在恢復調度器時進行任務切換。這個是一個確保。
在恢復調度器API xTaskResumeAll()
裡面,前面章節有分析過這個API,有興趣的同學可以往前翻。
在這個API裡面,恢復調度器也會逐個恢復系統節拍,然後在最後檢查xYieldPending
變數是否需要觸發任務切換。
7.9 系統延時實戰
程式碼地址:李柱明 gitee
- 找到release分支中的 freertos_on_linux_task_delay 文件夾,拉下來,直接make。
創建三個任務說明相對延時、絕對延時和解除阻塞:
/** @brief lzmStaticTestTack
* @details
* @param
* @retval
* @author lizhuming
*/
static void lzmStaticTestTask(void* parameter)
{
int tick_cnt = 0;
/* task init */
printf("start lzmStaticTestTask\r\n");
for(;;)
{
vTaskDelay(500); /* 假設任務主體需要 500 個節拍運行 */
tick_cnt = xTaskGetTickCount();
printf("delay task tick_cnt befor sleep [1][%d]\r\n", tick_cnt); /* 阻塞前 */
vTaskDelay(1000);
tick_cnt = xTaskGetTickCount();
printf("delay task after wake up [1][%d]\r\n", tick_cnt); /* 喚醒後 */
}
}
/** @brief lzmTestTask
* @details
* @param
* @retval
* @author lizhuming
*/
static void lzmTestTask(void* parameter)
{
int tick_cnt = 0;
TickType_t pervious_wake_time = 0;
/* task init */
printf("start lzmTestTask\r\n");
tick_cnt = xTaskGetTickCount();
pervious_wake_time = tick_cnt;
for(;;)
{
tick_cnt = xTaskGetTickCount();
printf("delayunitil task tick_cnt [2][%d]\r\n", tick_cnt); /* 觀測下是否按1000個tick的周期跑 */
vTaskDelay(500); /* 假設任務主體需要 500 個節拍運行 */
xTaskAbortDelay(lzmAbortDelayTaskHandle); /* 解除其他任務阻塞 */
vTaskDelayUntil(&pervious_wake_time, 1000); /* 周期1000個tick */
}
}
/** @brief lzmAbortDelayTask
* @details
* @param
* @retval
* @author lizhuming
*/
static void lzmAbortDelayTask(void* parameter)
{
int tick_cnt = 0;
/* task init */
printf("start lzmAbortDelayTask\r\n");
tick_cnt = xTaskGetTickCount();
for(;;)
{
vTaskDelay(portMAX_DELAY); /* 永久阻塞 */
tick_cnt = xTaskGetTickCount();
printf("unblock tick_cnt [3][%d]\r\n", tick_cnt); /* 如果被解除阻塞一次,就列印一次 */
}
}
運行成功:
附件
系統節拍統計:xTaskIncrementTick()
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
/* 每當系統節拍定時器中斷髮生,移植層都會調用該函數.函數將系統節拍中斷計數器加1,然後檢查新的系統節拍中斷計數器值是否解除某個任務.*/
if(uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{ /* 調度器正常 */
const TickType_txConstTickCount = xTickCount + 1;
/* 系統節拍中斷計數器加1,如果計數器溢出(為0),交換延時列表指針和溢出延時列表指針 */
xTickCount = xConstTickCount;
if( xConstTickCount == ( TickType_t ) 0U )
{
taskSWITCH_DELAYED_LISTS();
}
/* 查看是否有延時任務到期.任務按照喚醒時間的先後順序存儲在隊列中,這意味著只要隊列中的最先喚醒任務沒有到期,其它任務一定沒有到期.*/
if( xConstTickCount >=xNextTaskUnblockTime )
{
for( ;; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList) != pdFALSE )
{
/* 如果延時列表為空,設置xNextTaskUnblockTime為最大值 */
xNextTaskUnblockTime = portMAX_DELAY;
break;
}
else
{
/* 如果延時列表不為空,獲取延時列表第一個列表項值,這個列表項值存儲任務喚醒時間.
喚醒時間到期,延時列表中的第一個列表項所屬的任務要被移除阻塞狀態 */
pxTCB = ( TCB_t * )listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xItemValue =listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
if( xConstTickCount < xItemValue )
{
/* 任務還未到解除阻塞時間?將當前任務喚醒時間設置為下次解除阻塞時間. */
xNextTaskUnblockTime = xItemValue;
break;
}
/* 從阻塞列表中刪除到期任務 */
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
/* 是因為等待事件而阻塞?是的話將到期任務從事件列表中刪除 */
if(listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
/* 將解除阻塞的任務放入就緒列表 */
prvAddTaskToReadyList( pxTCB );
#if ( configUSE_PREEMPTION == 1 )
{
/* 使能了搶佔式內核.如果解除阻塞的任務優先順序大於當前任務,觸發一次上下文切換標誌 */
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired= pdTRUE;
}
}
#endif /*configUSE_PREEMPTION */
}
}
}
/* 如果有其它任務與當前任務共享一個優先順序,則這些任務共享處理器(時間片) */
#if ( (configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
if(listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* ( (configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */
#if (configUSE_TICK_HOOK == 1 )
{
/* 調用時間片鉤子函數*/
if( uxPendedTicks == ( UBaseType_t ) 0U )
{
vApplicationTickHook();
}
}
#endif /*configUSE_TICK_HOOK */
#if (configUSE_PREEMPTION == 1 )
{ /* 如果在中斷中調用的API函數喚醒了更高優先順序的任務,並且API函數的參數pxHigherPriorityTaskWoken為NULL時,變數xYieldPending用於上下文切換標誌 */
if( xYieldPending!= pdFALSE )
{
xSwitchRequired = pdTRUE;
}
}
#endif /*configUSE_PREEMPTION */
}
else
{ /* 調度器掛起狀態,變數uxPendedTicks用於統計調度器掛起期間,系統節拍中斷次數.
當調用恢復調度器函數時,會執行uxPendedTicks次本函數(xTaskIncrementTick()):
恢復系統節拍中斷計數器,如果有任務阻塞到期,則刪除阻塞狀態 */
++uxPendedTicks;
/* 調用時間片鉤子函數*/
#if (configUSE_TICK_HOOK == 1 )
{
vApplicationTickHook();
}
#endif
}
return xSwitchRequired;
}
系統節拍溢出處理:taskSWITCH_DELAYED_LISTS()
/* pxDelayedTaskList和pxOverflowDelayedTaskList在tick計數溢出時切換 */
#define taskSWITCH_DELAYED_LISTS() \
{ \
List_t * pxTemp; \
\
/* 當列表被切換時,延遲的任務列表應該為空 */ \
configASSERT( ( listLIST_IS_EMPTY( pxDelayedTaskList ) ) ); \
\
pxTemp = pxDelayedTaskList; \
pxDelayedTaskList = pxOverflowDelayedTaskList; \
pxOverflowDelayedTaskList = pxTemp; \
xNumOfOverflows++; \
prvResetNextTaskUnblockTime(); \
}
static void prvResetNextTaskUnblockTime( void )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
/* 如果延時列表為空,說明沒有任務因為延時阻塞。把下次需要喚醒的時間更新為最大值。 */
xNextTaskUnblockTime = portMAX_DELAY;
}
else
{
/* 如果延時列表不為空,說明有任務等待喚醒。從延時列表中的第一個任務節點中把節點值取出來,該值就是延時列表中未來最近有任務需要喚醒的時間。 */
xNextTaskUnblockTime = listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxDelayedTaskList );
}
}
相對延時:vTaskDelay()
void vTaskDelay( const TickType_t xTicksToDelay )
{
BaseType_t xAlreadyYielded = pdFALSE;
/* 如果延時輸入的參數為0,那只是為了觸發一次調度。
如果輸入的參數不為0,才是為了延時。 */
if( xTicksToDelay > ( TickType_t ) 0U )
{
/* 如果調度器掛起了,那就沒得玩了!!! */
configASSERT( uxSchedulerSuspended == 0 );
/* 掛起調度器 */
vTaskSuspendAll();
{
traceTASK_DELAY();
/* 把當前任務從就緒鏈表中移到延時鏈表。 */
prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
}
/* 恢復調度器。 */
xAlreadyYielded = xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 如果在恢復調度器時,內部沒有觸發任務調度,那這裡需要強制觸發調度,要不然本任務就會繼續跑,不符合期待。 */
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
添加當前任務到延時列表:prvAddCurrentTaskToDelayedList()
static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely )
{
TickType_t xTimeToWake;
const TickType_t xConstTickCount = xTickCount;
#if ( INCLUDE_xTaskAbortDelay == 1 )
{
/* 先把解除延時阻塞的標誌位複位。 */
pxCurrentTCB->ucDelayAborted = pdFALSE;
}
#endif
/* 把當前任務先從就緒鏈表中移除。 */
if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
/* 如果當前任務同等優先順序沒有其它任務了,就需要把這個優先順序在圖表 uxTopReadyPriority 中對應的位清除 */
portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )
{
/* 如果延時為最大值,且允許無限期阻塞。那直接插入到掛起列表中。 */
listINSERT_END( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* 相對延時,算出未來需要喚醒的時間點。 */
xTimeToWake = xConstTickCount + xTicksToWait;
/* 把當前喚醒值配置到節點內部值裡面,插入鏈表時排序用。 */
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
if( xTimeToWake < xConstTickCount )
{
/* 喚醒時間點的系統節拍溢出,就插入到溢出延時列表中。 */
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* 喚醒時間的系統節拍沒有溢出,就插入當前延時鏈表。 */
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
/* 如果開啟了優先順序優化功能:需要把這個優先順序在圖表`uxTopReadyPriority`中對應的位清除。
如果沒有開啟優先順序優化功能,這個宏為空的,不處理。 */
if( xTimeToWake < xNextTaskUnblockTime )
{
xNextTaskUnblockTime = xTimeToWake;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
}
絕對延時:xTaskDelayUntil()
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
{
TickType_t xTimeToWake;
BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;
configASSERT( pxPreviousWakeTime );
configASSERT( ( xTimeIncrement > 0U ) );
configASSERT( uxSchedulerSuspended == 0 );
vTaskSuspendAll();
{
/* 獲取當前時鐘節拍值。 */
const TickType_t xConstTickCount = xTickCount;
/* 算出未來喚醒時間點 */
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
/* 如果當前時間對比上次喚醒的時間已經溢出過了 */
if( xConstTickCount < *pxPreviousWakeTime )
{
/* 只有當周期性延時時間大於任務主體程式碼執行時間,即是喚醒時間在未來,才會將任務掛接到延時鏈表 */
if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
/* 保證喚醒時間在未來即可將任務掛接到延時鏈表 */
if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* 更新上次喚醒時間值,用於下一個周期使用 */
*pxPreviousWakeTime = xTimeToWake;
if( xShouldDelay != pdFALSE )
{
traceTASK_DELAY_UNTIL( xTimeToWake );
/* 將當前任務從就緒鏈表遷移到延時鏈表 */
prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* 恢復調度器。 */
xAlreadyYielded = xTaskResumeAll();
/* 如果在恢復調度器時,內部沒有觸發任務調度,那這裡需要強制觸發調度,要不然本任務就會繼續跑,不符合期待。 */
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
return xShouldDelay;
}
解除任務阻塞:xTaskAbortDelay()
BaseType_t xTaskAbortDelay( TaskHandle_t xTask )
{
TCB_t * pxTCB = xTask;
BaseType_t xReturn;
/* 如果傳入的任務句柄是NULL,直接斷言 */
configASSERT( pxTCB );
/* 掛起調度器 */
vTaskSuspendAll();
{
/* 獲取任務狀態,如果當前為阻塞態,才能解除阻塞嘛 */
if( eTaskGetState( xTask ) == eBlocked )
{
xReturn = pdPASS;
/* 移除任務所有狀態,遷出對應的任務狀態鏈表 */
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
/* 進入臨界,處理因為事件而阻塞的問題。
進入臨界處理是因為部分中斷回調也會接觸到任務事件鏈表。
進入臨界算是給任務事件鏈表「上鎖」吧*/
taskENTER_CRITICAL();
{
/* 因為事件而阻塞 */
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
/* 移除任務的事件 */
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
/* 強制解除阻塞標誌 */
pxTCB->ucDelayAborted = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* 退出臨界 */
taskEXIT_CRITICAL();
/* 重新加入就行鏈表 */
prvAddTaskToReadyList( pxTCB );
#if ( configUSE_PREEMPTION == 1 )
{
/* 如果解除阻塞的任務優先順序大於當前在跑的任務優先順序,需要任務切換 */
if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
{
/* 標記在恢復調度器時進行任務切換 */
xYieldPending = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
}
else
{
xReturn = pdFAIL;
}
}
/* 恢復調度器 */
( void ) xTaskResumeAll();
return xReturn;
}