【freertos】007-系統節拍和系統延時管理實現細節

李柱明發表於2022-04-01

前言

本章節的時鐘系統節拍主要分析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()

時鐘節拍分析就按這個函式分析就好。

每當系統節拍定時器中斷時,南向介面層都會呼叫該函式來實現系統節拍需要處理的程式碼。主要是

  • 系統節拍數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

pxDelayedTaskListpxOverflowDelayedTaskList只是連結串列指標,分別指向當前正在使用的延時列表和溢位列表。

為什麼需要兩條延時列表?

為了解決系統節拍溢位問題。

如當系統節拍未溢位,pxDelayedTaskList指向xDelayedTaskList1pxOverflowDelayedTaskList指向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核心連結串列節點結構體:

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,實現任務自動進行切換。

變數xYieldPendingpdTRUE,會在下一次系統節拍中斷服務函式中,觸發一次任務切換。程式碼便是:

if( xYieldPending != pdFALSE )
{
    xSwitchRequired = pdTRUE;
}

但是實際實現啟用該功能是在在V9.0以及以上版本。

小結一下pxHigherPriorityTaskWokenxYieldPending

  • 在帶中斷保護的API中解鎖了更高優先順序的任務,需要在這些API內部標記一些變數來觸發任務切換。這些變數有pxHigherPriorityTaskWokenxYieldPending

  • pxHigherPriorityTaskWoken

    • 手動切換標記。
    • 區域性變數。
    • 如果帶中斷保護的API解鎖了更高優先順序的任務,會標記pxHigherPriorityTaskWokenpdTRUE,使用者根據這個變數呼叫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()函式內呼叫uxPendedTicksxTaskIncrementTick()實現逐個補回時鐘節拍處理。

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()獲取任務狀態是否處於阻塞態。有以下情況可以判斷任務處於阻塞態:

  1. 任務處於延時連結串列或者處於延時溢位連結串列。
  2. 任務處於掛起態,但是在等待某個事件,也屬於阻塞態。
  3. 處於掛起態,也沒有在等待事件,但是在等待任務通知,也屬於阻塞態。

這部分看下該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;
}

相關文章