01
FreeRTOS 時(shí)間管理
時(shí)間管理包括兩個(gè)方面:系統(tǒng)節(jié)拍以及任務(wù)延時(shí)管理。
2
系統(tǒng)節(jié)拍:
在前面的文章也講得很多,想要系統(tǒng)正常運(yùn)行,那么時(shí)鐘節(jié)拍是必不可少的,FreeRTOS的時(shí)鐘節(jié)拍通常由SysTick提供,它周期性的產(chǎn)生定時(shí)中斷,所謂的時(shí)鐘節(jié)拍管理的核心就是這個(gè)定時(shí)中斷的服務(wù)程序。FreeRTOS的時(shí)鐘節(jié)拍isr中核心的工作就是調(diào)用 vTaskIncrementTick() 函數(shù)。具體見(jiàn)上之前的文章。
3
今天主要講解延時(shí)的實(shí)現(xiàn)
FreeRTOS提供了兩個(gè)系統(tǒng)延時(shí)函數(shù):
**相對(duì)延時(shí)函數(shù)vTaskDelay() **
絕對(duì)延時(shí)函數(shù)vTaskDelayUntil()。
這些延時(shí)函數(shù)可不像我們以前用裸機(jī)寫(xiě)代碼的延時(shí)函數(shù)操作系統(tǒng)不允許CPU在死等消耗著時(shí)間,因?yàn)檫@樣效率太低了。
同時(shí),要告誡學(xué)操作系統(tǒng)的同學(xué),千萬(wàn)別用裸機(jī)的思想去學(xué)操作系統(tǒng)。
4
任務(wù)延時(shí)
任務(wù)可能需要延時(shí),兩種情況,一種是任務(wù)被vTaskDelay或者vTaskDelayUntil延時(shí),另外一種情況就是任務(wù)等待事件(比如等待某個(gè)信號(hào)量、或者某個(gè)消息隊(duì)列)時(shí)候指定了 timeout (即最多等待timeout時(shí)間,如果等待的事件還沒(méi)發(fā)生,則不再繼續(xù)等待),在每個(gè)任務(wù)的循環(huán)中都必須要有阻塞的情況出現(xiàn),否則比該任務(wù)優(yōu)先級(jí)低的任務(wù)就永遠(yuǎn)無(wú)法運(yùn)行。
5
相對(duì)延時(shí)與絕對(duì)延時(shí)的區(qū)別
相對(duì)延時(shí):vTaskDelay():
相對(duì)延時(shí)是指每次延時(shí)都是從任務(wù)執(zhí)行函數(shù) vTaskDelay() 開(kāi)始,延時(shí)指定的時(shí)間結(jié)束
絕對(duì)延時(shí):vTaskDelayUntil():
絕對(duì)延時(shí)是指調(diào)用 vTaskDelayUntil() 的任務(wù)每隔x時(shí)間運(yùn)行一次。也就是任務(wù)周期運(yùn)行。
6
相對(duì)延時(shí):vTaskDelay()
相對(duì)延時(shí) vTaskDelay() 是從調(diào)用 vTaskDelay() 這個(gè)函數(shù)的時(shí)候開(kāi)始延時(shí),但是任務(wù)執(zhí)行的時(shí)候,可能發(fā)生了中斷,導(dǎo)致任務(wù)執(zhí)行時(shí)間變長(zhǎng)了,但是整個(gè)任務(wù)的延時(shí)時(shí)間還是1000個(gè) tick ,這就不是周期性了,簡(jiǎn)單看看下面代碼:
void vTaskA( void * pvParameters )
{
while(1)
{
// ...
// 這里為任務(wù)主體代碼
// ...
/* 調(diào)用相對(duì)延時(shí)函數(shù),阻塞1000個(gè)tick */
vTaskDelay( 1000 );
}
}
可能說(shuō)的不夠明確,可以看看圖解。
當(dāng)任務(wù)運(yùn)行的時(shí)候,假設(shè)被某個(gè)高級(jí)任務(wù)或者是中斷打斷了,那么任務(wù)的執(zhí)行時(shí)間就更長(zhǎng)了,然而延時(shí)還是延時(shí)1000個(gè)tick這樣子,整個(gè)系統(tǒng)的時(shí)間就混亂了。
如果還不夠明確,看看 vTaskDelay() 的源碼
void vTaskDelay( const TickType_t xTicksToDelay )
{
BaseType_t xAlreadyYielded = pdFALSE;
/* 延遲時(shí)間為零只會(huì)強(qiáng)制切換任務(wù)。 */
if( xTicksToDelay > ( TickType_t ) 0U ) (1)
{
configASSERT( uxSchedulerSuspended == 0 );
vTaskSuspendAll(); (2)
{
traceTASK_DELAY();
/*將當(dāng)前任務(wù)從就緒列表中移除,并根據(jù)當(dāng)前系統(tǒng)節(jié)拍
計(jì)數(shù)器值計(jì)算喚醒時(shí)間,然后將任務(wù)加入延時(shí)列表 */
prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
}
xAlreadyYielded = xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 強(qiáng)制執(zhí)行一次上下文切換 */
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
(1):如果傳遞進(jìn)來(lái)的延時(shí)時(shí)間是0,只能進(jìn)行強(qiáng)制切換任務(wù)了,調(diào)用的是 portYIELD_WITHIN_API() ,它其實(shí)是一個(gè)宏,真正起作用的是 portYIELD() ,下面是它的源碼:
#define portYIELD() \\
{ \\
/* 設(shè)置PendSV以請(qǐng)求上下文切換。 */ \\
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \\
__dsb( portSY_FULL_READ_WRITE ); \\
__isb( portSY_FULL_READ_WRITE ); \\
}
(2):掛起當(dāng)前任務(wù)
然后將當(dāng)前任務(wù)從就緒列表刪除,然后加入到延時(shí)列表。是調(diào)用函數(shù) prvAddCurrentTaskToDelayedList() 完成這一過(guò)程的。由于這個(gè)函數(shù)篇幅過(guò)長(zhǎng),就不講解了,有興趣可以看看,我就簡(jiǎn)單說(shuō)說(shuō)過(guò)程。在FreeRTOS中有這么一個(gè)變量,是用來(lái)記錄systick的值的。
PRIVILEGED_DATA static volatile TickType_t xTickCount = ( TickType_t ) 0U;
在每次tick中斷時(shí)xTickCount加一,它的值表示了系統(tǒng)節(jié)拍中斷的次數(shù),那么啥時(shí)候喚醒被加入延時(shí)列表的任務(wù)呢?其實(shí)很簡(jiǎn)單,FreeRTOS的做法將 xTickCount (當(dāng)前系統(tǒng)時(shí)間)+ xTicksToDelay (要延時(shí)的時(shí)間)即可。當(dāng)這個(gè)相對(duì)的延時(shí)時(shí)間到了之后就喚醒了,這個(gè) (xTickCount+ xTicksToDelay) 時(shí)間會(huì)被記錄在該任務(wù)的任務(wù)控制塊中。
看到這肯定有人問(wèn),這個(gè)變量是TickType_t類型(32位)的,那肯定會(huì)溢出啊,沒(méi)錯(cuò),是變量都會(huì)有溢出的一天,可是FreeRTOS乃是世界第一的操作系統(tǒng)啊,FreeRTOS使用了兩個(gè)延時(shí)列表:
** xDelayedTaskList1和xDelayedTaskList2,**
并使用兩個(gè)列表指針類型變量pxDelayedTaskList和pxOverflowDelayedTaskList分別指向上面的延時(shí)列表1和延時(shí)列表2(在創(chuàng)建任務(wù)時(shí)將延時(shí)列表指針指向延時(shí)列表)如果內(nèi)核判斷出xTickCount+xTicksToDelay溢出,就將當(dāng)前任務(wù)掛接到列表指針 pxOverflowDelayedTaskList指向的列表中,否則就掛接到列表指針pxDelayedTaskList指向的列表中。當(dāng)時(shí)間到了,就會(huì)將延時(shí)的任務(wù)從延時(shí)列表中刪除,加入就緒列表中,當(dāng)然這時(shí)候就是由調(diào)度器覺(jué)得任務(wù)能不能運(yùn)行了,如果任務(wù)的優(yōu)先級(jí)大于當(dāng)前運(yùn)行的任務(wù),那么調(diào)度器才會(huì)進(jìn)行任務(wù)的調(diào)度。
7
絕對(duì)延時(shí):vTaskDelayUntil()
vTaskDelayUntil() 的參數(shù)指定了確切的滴答計(jì)數(shù)值
調(diào)用 vTaskDelayUntil() 是希望任務(wù)以固定頻率定期執(zhí)行,而不受外部的影響,任務(wù)從上一次運(yùn)行開(kāi)始到下一次運(yùn)行開(kāi)始的時(shí)間間隔是絕對(duì)的,而不是相對(duì)的。
下面看看 vTaskDelayUntil() 的使用方法,注意了,這 vTaskDelayUntil() 的使用方法與 vTaskDelay() 不一樣:
void vTaskA( void * pvParameters )
{
/* 用于保存上次時(shí)間。調(diào)用后系統(tǒng)自動(dòng)更新 */
static portTickType PreviousWakeTime;
/* 設(shè)置延時(shí)時(shí)間,將時(shí)間轉(zhuǎn)為節(jié)拍數(shù) */
const portTickType TimeIncrement = pdMS_TO_TICKS(1000);
/* 獲取當(dāng)前系統(tǒng)時(shí)間 */
PreviousWakeTime = xTaskGetTickCount();
while(1)
{
/* 調(diào)用絕對(duì)延時(shí)函數(shù),任務(wù)時(shí)間間隔為1000個(gè)tick */
vTaskDelayUntil( &PreviousWakeTime,TimeIncrement );
// ...
// 這里為任務(wù)主體代碼
// ...
}
}
在使用的時(shí)候要將延時(shí)時(shí)間轉(zhuǎn)化為系統(tǒng)節(jié)拍,在任務(wù)主體之前要調(diào)用延時(shí)函數(shù)。
任務(wù)會(huì)先調(diào)用 vTaskDelayUntil() 使任務(wù)進(jìn)入阻塞態(tài),等到時(shí)間到了就從阻塞中解除,然后執(zhí)行主體代碼,任務(wù)主體代碼執(zhí)行完畢。會(huì)繼續(xù)調(diào)用 vTaskDelayUntil() 使任務(wù)進(jìn)入阻塞態(tài),然后就是循環(huán)這樣子執(zhí)行。即使任務(wù)在執(zhí)行過(guò)程中發(fā)生中斷,那么也不會(huì)影響這個(gè)任務(wù)的運(yùn)行周期,僅僅是縮短了阻塞的時(shí)間而已。
下面來(lái)看看vTaskDelayUntil()的源碼:
void vTaskDelayUntil( 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(); (1)
{
/* 保存系統(tǒng)節(jié)拍中斷次數(shù)計(jì)數(shù)器 */
const TickType_t xConstTickCount = xTickCount;
/* 生成任務(wù)要喚醒的滴答時(shí)間。*/
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
/* pxPreviousWakeTime中保存的是上次喚醒時(shí)間,喚醒后需要一定時(shí)間執(zhí)行任務(wù)主體代碼,
如果上次喚醒時(shí)間大于當(dāng)前時(shí)間,說(shuō)明節(jié)拍計(jì)數(shù)器溢出了 具體見(jiàn)圖片 */
if( xConstTickCount < *pxPreviousWakeTime )
{
/ *由于此功能,滴答計(jì)數(shù)已溢出
持續(xù)呼喚。 在這種情況下,我們唯一的時(shí)間
實(shí)際延遲是如果喚醒時(shí)間也溢出,
喚醒時(shí)間大于滴答時(shí)間。 當(dāng)這個(gè)
就是這樣,好像兩個(gè)時(shí)間都沒(méi)有溢出。*/
if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
/ *滴答時(shí)間沒(méi)有溢出。 在這種情況下,如果喚醒時(shí)間溢出,
或滴答時(shí)間小于喚醒時(shí)間,我們將延遲。*/
if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* 更新喚醒時(shí)間,為下一次調(diào)用本函數(shù)做準(zhǔn)備. */
*pxPreviousWakeTime = xTimeToWake;
if( xShouldDelay != pdFALSE )
{
traceTASK_DELAY_UNTIL( xTimeToWake );
/* prvAddCurrentTaskToDelayedList()需要塊時(shí)間,而不是喚醒時(shí)間,因此減去當(dāng)前的滴答計(jì)數(shù)。 */
prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
xAlreadyYielded = xTaskResumeAll();
/* 如果xTaskResumeAll尚未執(zhí)行重新安排,我們可能會(huì)讓自己入睡。*/
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
與相對(duì)延時(shí)函數(shù)vTaskDelay不同,本函數(shù)增加了一個(gè)參數(shù)pxPreviousWakeTime用于指向一個(gè)變量,變量保存上次任務(wù)解除阻塞的時(shí)間,此后函數(shù) vTaskDelayUntil() 在內(nèi)部自動(dòng)更新這個(gè)變量。由于變量xTickCount可能會(huì)溢出,所以程序必須檢測(cè)各種溢出情況,并且要保證延時(shí)周期不得小于任務(wù)主體代碼執(zhí)行時(shí)間。
就會(huì)有以下3種情況,才能將任務(wù)加入延時(shí)鏈表中。
請(qǐng)記住這幾個(gè)單詞的含義:
** xTimeIncrement:任務(wù)周期時(shí)間
pxPreviousWakeTime:上一次喚醒的時(shí)間點(diǎn)
xTimeToWake:下一次喚醒的系統(tǒng)時(shí)間點(diǎn)
xConstTickCount:進(jìn)入延時(shí)的時(shí)間點(diǎn)**
第三種情況:常規(guī)無(wú)溢出的情況。
以時(shí)間為橫軸,上一次喚醒的時(shí)間點(diǎn)小于下一次喚醒的時(shí)間點(diǎn),這是很正常的情況。
第二種情況:?jiǎn)拘褧r(shí)間計(jì)數(shù)器 (xTimeToWake) 溢出情況。
也就是代碼中*if( ( xTimeToWake < pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
第一種情況:?jiǎn)拘褧r(shí)間 (xTimeToWake) 與進(jìn)入延時(shí)的時(shí)間點(diǎn) (xConstTickCount) 都溢出情況。
也就是代碼中*if( ( xTimeToWake < pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
從圖中可以看出不管是溢出還是無(wú)溢出,都要求在下次喚醒任務(wù)之前,當(dāng)前任務(wù)主體代碼必須被執(zhí)行完。也就是說(shuō)任務(wù)執(zhí)行的時(shí)間不允許大于延時(shí)的時(shí)間,總不能存在每10ms就要執(zhí)行一次20ms時(shí)間的任務(wù)吧。計(jì)算的喚醒時(shí)間合法后,就將當(dāng)前任務(wù)加入延時(shí)列表,同樣延時(shí)列表也有兩個(gè)。每次系統(tǒng)節(jié)拍中斷,中斷服務(wù)函數(shù)都會(huì)檢查這兩個(gè)延時(shí)列表,查看延時(shí)的任務(wù)是否到期,如果時(shí)間到期,則將任務(wù)從延時(shí)列表中刪除,重新加入就緒列表。如果新加入就緒列表的任務(wù)優(yōu)先級(jí)大于當(dāng)前任務(wù),則會(huì)觸發(fā)一次上下文切換。
8
總結(jié)
如果任務(wù)調(diào)用相對(duì)延時(shí),其運(yùn)行周期完全是不可測(cè)的,如果任務(wù)的優(yōu)先級(jí)不是最高的話,其誤差更大,就好比一個(gè)必須要在5ms內(nèi)相應(yīng)的任務(wù),假如使用了相對(duì)延時(shí) 1ms ,那么很有可能在該任務(wù)執(zhí)行的時(shí)候被更高優(yōu)先級(jí)的任務(wù)打斷,從而錯(cuò)過(guò)5ms內(nèi)的相應(yīng),但是調(diào)用絕對(duì)延時(shí),則任務(wù)會(huì)周期性將該任務(wù)在阻塞列表中解除,但是,任務(wù)能不能運(yùn)行,還得取決于任務(wù)的優(yōu)先級(jí),如果優(yōu)先級(jí)最高的話,任務(wù)周期還是比較精確的(相對(duì)vTaskDelay來(lái)說(shuō)),如果想要更加想精確周期性執(zhí)行某個(gè)任務(wù),可以使用系統(tǒng)節(jié)拍鉤子函數(shù) vApplicationTickHook() ,它在tick中斷服務(wù)函數(shù)中被調(diào)用,因此這個(gè)函數(shù)中的代碼必須簡(jiǎn)潔,并且不允許出現(xiàn)阻塞的情況。
本文是杰杰原創(chuàng),轉(zhuǎn)載請(qǐng)說(shuō)明出處。
-
FreeRTOS
+關(guān)注
關(guān)注
12文章
484瀏覽量
62136 -
定時(shí)中斷
+關(guān)注
關(guān)注
0文章
19瀏覽量
8553 -
Systick
+關(guān)注
關(guān)注
0文章
62瀏覽量
13070
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論