前言
本章節的時鐘系統節拍主要分析FreeRTOS核心相關及北向接口層,南向接口層不分析。
本章節的系統延時主要分析任務系統延時實作。
原文:李柱明部落格:https://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()
時鐘節拍分析就按這個函數分析就好。
每當系統節拍定時器中斷時,南向接口層都會調用該函數來實作系統節拍需要處理的代碼。
系統節拍
xTaskIncrementTick()
這個函數的主要内容:
- 排程器沒有被挂起:
- 計算系統節拍。
- 如果目前節拍已經大于或等于下一個需要解鎖任務的節拍,就檢索延時連結清單,解鎖任務。如果被解鎖的任務中有更高優先級的任務,需要觸發排程。
- 如果目前任務優先級對應的就緒連結清單有其他任務,且開啟了時間片排程,切換任務,觸發排程。
- 如果排程器被挂起,則隻記錄節拍遞增的次數,恢複排程器時按記錄補回來。
小筆記:上述就會存在一個問題,如果就緒連結清單中有比目前任務優先級更加高的任務,是有可能不會被監測到的,是以核心在解鎖一個比目前任務優先級更加高的任務,需要主動觸發一次任務排程。
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核心連結清單采用的是非通用雙向循環連結清單,節點結構體如下代碼所示。其中
可由使用者自定義指派,在freertos延時清單中,用于記錄目前任務需要喚醒的時間節拍值。xItemValue
- 學習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
- 如果沒有開啟優先級優化功能:我認為也應該更新
這個值,讓系統知道目前就緒任務最高優先級已經不是目前任務的優先級值了。但是freertos并沒有這樣做。uxTopReadyPriority
- 優先級優化功能可以檢視我前面章節說的前導零指令。
/* 把目前任務先從就緒連結清單中移除。 */
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;
}