系列文章目錄
FreeRTOS源碼分析一:task創建(RISCV架構)
FreeRTOS源碼分析二:task啟動(RISCV架構)
FreeRTOS源碼分析三:列表數據結構
FreeRTOS源碼分析四:時鐘中斷處理響應流程
FreeRTOS源碼分析五:資源訪問控制(一)
文章目錄
- 系列文章目錄
- 前言
- 無符號溢出
- tick 溢出的幾種情況
- vTaskDelayUntil
- vTaskDelay
- prvAddCurrentTaskToDelayedList
- 附:一個數學證明
- 1) “溢出 ? wake < startTick ”
- 2) “不溢出 ? wake > startTick ”
- 總結
前言
// vTaskDelay - 簡單的相對延遲
void vTaskDelay( const TickType_t xTicksToDelay );// xTaskDelayUntil - 精確的絕對延遲
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,const TickType_t xTimeIncrement );
這兩個 API 都可以使當前任務進入延遲,并延遲一定的時間。本文從實現層面介紹他們的區別。
無符號溢出
在 C 語言中,無符號整數(unsigned int
、uint32_t
、TickType_t
等)是按模運算定義的。這一規則由 C 標準規定:
無符號整數運算的結果是對 2N2^N2N 取模的值,其中 NNN 是該類型的比特寬度。
例如:
uint8_t
范圍是 0 ~ 255- 任何運算結果超出這個范圍,就會自動對 256 取模
- 這不是溢出錯誤,而是自然回繞(wrap-around)
當結果超過最大值時,從 0 重新開始計數:
uint8_t a = 250;
uint8_t b = a + 10; // 250 + 10 = 260
// 按 256 取模:260 - 256 = 4
// b == 4
對于 32 位 tick 計數器:
uint32_t tick = 0xFFFFFFFE; // 最大值前兩步
tick++; // 0xFFFFFFFF
tick++; // 溢出后回到 0
- FreeRTOS 的
TickType_t
通常是 無符號 32 位(范圍 0 ~ 4,294,967,295) - 每次
tickCount++
,溢出時自動從 0 重新開始 - 不需要手動處理溢出運算,無符號加減法天生支持回繞
- 但比較邏輯需要自己設計,FreeRTOS 通過“雙鏈表 + 溢出判斷”來規避直接比較的問題
tick 溢出的幾種情況
#define mainQUEUE_SEND_FREQUENCY_MS pdMS_TO_TICKS( 1000 )/* Initialise xNextWakeTime - this only needs to be done once. */xNextWakeTime = xTaskGetTickCount();for( ; ; ){....../* Place this task in the blocked state until it is time to run again. */vTaskDelayUntil( &xNextWakeTime, mainQUEUE_SEND_FREQUENCY_MS );
}
這是 vTaskDelayUntil
的一般用法。我們一般要在某個位置獲取當前 tickCount 記為 startTick
,隨后調用 vTaskDelayUntil
延遲當前任務從 startTick
的一段時間。
我們以三個變量代替三個時間:startTick
為 xTaskGetTickCount 調用時返回的 tickCount。nowTick
為用戶調用 vTaskDelayUntil
時的 tickCount。而 tickDelay
為用戶指定的延遲 tick 數。
這里會存在兩個溢出的情況:
- 從
startTick
到nowTick
之間,系統 tick 已經溢出。這個表現為:nowTick < startTick
- 從
startTick
到startTick
+tickDelay
之間,系統 tick 會發生溢出。這個表現為:startTick + tickDelay < startTick
- 那么,為什么溢出之后會表現為
startTick + tickDelay < startTick
?附中有明確的數學證明。
這里我們另外定義一個變量:wake = startTick + tickDelay
,wake
可能溢出或沒有
那么,我們結合上面這兩種情況,在先判斷 從 startTick
到 nowTick
之間,系統 tick 溢出情況之后再判斷后面定時器的溢出情況,則有以下情況:
nowTick >= startTick
表明系統計數器未溢出,wake < startTick
(wake 本身溢出了) 或wake > nowTick >= startTick
(wake 未溢出) 這兩種情況都表明任務的待喚醒時間尚未抵達,任務需阻塞。若以上都不滿足(等價于wake >= startTick
且wake <= nowTick
),說明“計劃喚醒點已經過去”,不阻塞,立刻返回。nowTick < startTick
表明系統計數器溢出,只有當wake
也溢出且wake > nowTick
時才會阻塞:條件是(wake < startTick ) && (wake > nowTick)
。直觀解釋:大家都已經跨到新一圈了,而且wake
還在nowTick
之后,才需要等;否則就是“錯過了”或“在舊圈里”,不阻塞。
vTaskDelayUntil
這個時候來看 vTaskDelayUntil 的源碼就非常清晰了。
xTaskDelayUntil 用于延遲任務從 *pxPreviousWakeTime 到 *pxPreviousWakeTime + xTimeIncrement 這段時間
@returnpdTRUE
:本次調用確實延遲了任務(進入延遲列表)。pdFALSE
:任務未延遲(喚醒點已過,立即返回)。
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,const TickType_t xTimeIncrement )
{TickType_t xTimeToWake;BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;/* 參數有效性檢查 */configASSERT( pxPreviousWakeTime );configASSERT( ( xTimeIncrement > 0U ) );/* 掛起調度器,防止 tickCount 在計算期間發生變化 */vTaskSuspendAll();{/* 緩存當前系統 tick 計數(在本代碼塊中不會變化) */const TickType_t xConstTickCount = xTickCount;configASSERT( uxSchedulerSuspended == 1U );/* 計算下一次喚醒的時間點(無符號加法,可能會溢出) */xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;/* ---- 溢出情況分析 ----* 如果當前 tickCount < 上一次的喚醒時間,說明 tickCount 已經溢出過。* 在這種情況下,我們只有在 “下一次喚醒時間也發生溢出且它仍大于當前 tickCount” 時才需要延遲。* 這樣處理是因為這種情況等價于沒有溢出。*/if( xConstTickCount < *pxPreviousWakeTime ){if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) ){xShouldDelay = pdTRUE;}}else{/* ---- 未溢出情況 ----* 如果下一次喚醒時間溢出(xTimeToWake < prevWake),或* 下一次喚醒時間仍在未來(xTimeToWake > 當前 tickCount),則需要延遲。*/if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) ){xShouldDelay = pdTRUE;}}/* 更新 pxPreviousWakeTime,為下一次調用做準備 */*pxPreviousWakeTime = xTimeToWake;if( xShouldDelay != pdFALSE ){/* 將當前任務加入延遲列表。* 這里需要的是“等待的時間”而不是“目標喚醒時間”,* 所以要減去當前 tickCount 得到阻塞時長。*/prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );}}/* 恢復調度器,如果期間有更高優先級任務就緒,這里可能會發生任務切換 */xAlreadyYielded = xTaskResumeAll();/* 若 ResumeAll 未觸發切換(返回 pdFALSE),仍主動 yield 一次:* 1) 讓同優先級任務獲得公平的時間片/協作式讓出;* 2) 在禁搶占或端口差異下,統一通過顯式 yield 兌現切換。* 若此時系統中沒有更高/同優先級可運行任務,此調用幾乎為空操作。 */if( xAlreadyYielded == pdFALSE ){taskYIELD_WITHIN_API();}/* 返回本次調用是否真的延遲了任務 */return xShouldDelay;
}
- 函數用于周期性任務的延時,保證任務喚醒的時間間隔固定,不受執行時間波動影響。
- 與
vTaskDelay()
不同,它基于絕對喚醒時間(pxPreviousWakeTime
)而非相對延遲。 - 每次調用都會將
pxPreviousWakeTime
累加xTimeIncrement
,而不是更新為當前時間。這避免了周期漂移。 xTaskResumeAll()
會恢復調度器、處理掛起期間的就緒任務,并可能立即觸發切換。- 若未觸發切換(返回
pdFALSE
),仍調用taskYIELD_WITHIN_API()
:
vTaskDelay
vTaskDelay → 基于相對時間延遲任務,延遲是“從執行
vTaskDelay
開始算”,可能因執行時間累積產生周期漂移。
void vTaskDelay( const TickType_t xTicksToDelay )
{BaseType_t xAlreadyYielded = pdFALSE;/* 延時時間為 0 時,不阻塞任務,只是強制進行一次任務切換。 */if( xTicksToDelay > ( TickType_t ) 0U ){/* 掛起調度器,防止任務狀態修改過程被調度打斷。 */vTaskSuspendAll();{configASSERT( uxSchedulerSuspended == 1U );/* 當前任務不可能在事件列表中(它正運行著),因此直接將它* 從就緒列表移除,并加入延遲列表。 */prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );}/* 恢復調度器:* - 將掛起期間的 pending ready 任務并入就緒列表。* - 處理掛起期間累積的 tick(xPendedTicks)。* - 檢查是否需要立即任務切換,必要時發起切換。*/xAlreadyYielded = xTaskResumeAll();}/* 如果恢復調度器時沒有觸發切換(返回 pdFALSE),* 仍然調用一次 yield:* - 兌現同優先級任務的時間片輪轉。* - 處理 xYieldPending 標志(中斷中可能置位的“應切換”標志)。*/if( xAlreadyYielded == pdFALSE ){taskYIELD_WITHIN_API();}
}
prvAddCurrentTaskToDelayedList
prvAddCurrentTaskToDelayedList() 的主要功能是 把當前正在運行的任務移出就緒隊列,并加入到合適的延時/掛起隊列中,以實現延時阻塞功能(包括 Tick 溢出情況處理和無限期阻塞)。
static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait,const BaseType_t xCanBlockIndefinitely )
{TickType_t xTimeToWake;const TickType_t xConstTickCount = xTickCount; // 當前系統 Tick 值的快照List_t * const pxDelayedList = pxDelayedTaskList; // 當前延時任務列表List_t * const pxOverflowDelayedList = pxOverflowDelayedTaskList; // Tick 溢出延時列表#if ( INCLUDE_xTaskAbortDelay == 1 ){/* 進入延時隊列前,先清除任務的 ucDelayAborted 標志位* 用于檢測任務離開阻塞態時,是否是被中止延時(Abort Delay)喚醒的 */pxCurrentTCB->ucDelayAborted = ( uint8_t ) pdFALSE;}#endif/* 1. 從就緒隊列移除當前任務* 因為任務狀態鏈表項在就緒隊列和阻塞隊列中是同一個 list item,* 所以必須先從就緒隊列刪除才能放到阻塞隊列。 */if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ){/* 如果該優先級的就緒任務已經被清空,就更新優先級位圖,* 表示該優先級上已無就緒任務 */portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );}#if ( INCLUDE_vTaskSuspend == 1 ){/* 2. 判斷是否是無限期阻塞(portMAX_DELAY 且允許無限阻塞) */if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) ){/* 直接加入掛起任務列表(xSuspendedTaskList),* 保證它不會因為時間到而被喚醒,必須手動喚醒。 */listINSERT_END( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );}else{/* 3. 計算喚醒時間(可能會發生無符號溢出,但內核會處理) */xTimeToWake = xConstTickCount + xTicksToWait;/* 設置鏈表項的值為喚醒時間(用于延時隊列的排序) */listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );if( xTimeToWake < xConstTickCount ){/* 4. Tick 計數溢出* 如果喚醒時間比當前 Tick 值小,說明加法結果溢出,* 則把任務放入溢出延時隊列。 */vListInsert( pxOverflowDelayedList, &( pxCurrentTCB->xStateListItem ) );}else{/* 5. Tick 未溢出* 直接加入當前延時隊列(pxDelayedList),* 按喚醒時間升序插入。 */vListInsert( pxDelayedList, &( pxCurrentTCB->xStateListItem ) );/* 如果該任務的喚醒時間比當前系統記錄的最早喚醒時間還早,* 更新 xNextTaskUnblockTime(優化 Tick 處理,減少無意義掃描)。 */if( xTimeToWake < xNextTaskUnblockTime ){xNextTaskUnblockTime = xTimeToWake;}}}}
}
這里我們回顧:FreeRTOS源碼分析四:時鐘中斷處理響應流程 中提到,時鐘中斷會調用函數 xTaskIncrementTick
,它會對系統 tick 加1,當檢測到 tick 溢出時,會交換兩個延時隊列。如下所示:
BaseType_t xTaskIncrementTick( void )
{TCB_t * pxTCB; // 任務控制塊指針TickType_t xItemValue; // 延遲列表項的值(喚醒時間)BaseType_t xSwitchRequired = pdFALSE; // 是否需要任務切換標志/* 時鐘遞增應該在每個內核定時器事件上發生。* 如果調度器被掛起,則遞增待處理的時鐘計數。 */if( uxSchedulerSuspended == ( UBaseType_t ) 0U ){// === 調度器未被掛起的情況 ===/* 小優化:在此代碼塊中時鐘計數不會改變 */const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;/* 遞增RTOS時鐘,如果溢出到0則切換延遲和溢出延遲列表 */xTickCount = xConstTickCount;// 處理時鐘計數器溢出的情況(從最大值回繞到0)if( xConstTickCount == ( TickType_t ) 0U ){taskSWITCH_DELAYED_LISTS(); // 切換延遲任務列表}....................................
}
而宏 taskSWITCH_DELAYED_LISTS
則會切換兩個任務列表的角色:
#define taskSWITCH_DELAYED_LISTS() \do { \List_t * pxTemp; \\/* The delayed tasks list should be empty when the lists are switched. */ \configASSERT( ( listLIST_IS_EMPTY( pxDelayedTaskList ) ) ); \\pxTemp = pxDelayedTaskList; \pxDelayedTaskList = pxOverflowDelayedTaskList; \pxOverflowDelayedTaskList = pxTemp; \xNumOfOverflows = ( BaseType_t ) ( xNumOfOverflows + 1 ); \prvResetNextTaskUnblockTime(); \} while( 0 )
恰與這里兩個延時隊列想對應,當用戶延時會發生在 tick 溢出之后時,則加入溢出隊列。而當溢出發生,則溢出隊列就變為當前的延時隊列。
這樣上一輪“溢出延時鏈表”里的任務會自然地在新的 tick 空間里按照時間順序等待喚醒。xNextTaskUnblockTime
被重置,用于后續快速判斷“下一個到期點”。
附:一個數學證明
因為是無符號模 2N2^N2N 加法,而且 tickDelay > 0
(源碼里有 configASSERT( xTimeIncrement > 0U )
)。設:
startTick ∈ [0, 2^N-1]
tickDelay ∈ [1, 2^N-1]
- 真實和
S = startTick + tickDelay
- 存回寄存器/變量的結果
wake = S mod 2^N
結論: 溢出當且僅當 wake < startTick
。證明分兩步:
1) “溢出 ? wake < startTick ”
若溢出,則 S ≥ 2^N
,所以
wake=S?2N=startTick+tickDelay?2N=startTick?(2N?tickDelay).wake = S - 2^N = startTick + tickDelay - 2^N = startTick - (2^N - tickDelay). wake=S?2N=startTick+tickDelay?2N=startTick?(2N?tickDelay).
因為 tickDelay ≥ 1
,故 (2^N - tickDelay) ≤ 2^N - 1
且至少為 1,于是
wake=startTick?(2N?tickDelay)?≥1≤startTick?1<startTick.wake = startTick - \underbrace{(2^N - tickDelay)}_{\ge 1} \le startTick - 1 \;<\; startTick . wake=startTick?≥1(2N?tickDelay)??≤startTick?1<startTick.
所以一旦溢出,wake
一定小于 startTick
。
2) “不溢出 ? wake > startTick ”
若不溢出,則 S < 2^N
,wake = S = startTick + tickDelay
。又因 tickDelay ≥ 1
,
wake=startTick+tickDelay≥startTick+1>startTick.wake = startTick + tickDelay \ge startTick + 1 > startTick . wake=startTick+tickDelay≥startTick+1>startTick.
(只有當 tickDelay = 0
才可能 wake = startTick
,但這被斷言禁止了。)
因此,判斷溢出最簡單的辦法就是:做完 wake = startTick + tickDelay
的無符號加法后,看 wake < startTick
是否成立。成立就說明發生了進位丟棄。
總結
完結撒花!!!