從單片機到操作系統⑦——深入了解FreeRTOS的延時機制


沒研究過操作系統的源碼都不算學過操作系統

FreeRTOS 時間管理

時間管理包括兩個方面:系統節拍以及任務延時管理。

系統節拍:

在前面的文章也講得很多,想要系統正常運行,那么時鍾節拍是必不可少的,FreeRTOS的時鍾節拍通常由SysTick提供,它周期性的產生定時中斷,所謂的時鍾節拍管理的核心就是這個定時中斷的服務程序。FreeRTOS的時鍾節拍isr中核心的工作就是調用vTaskIncrementTick()函數。具體見上之前的文章。

延時管理

FreeRTOS提供了兩個系統延時函數:

  • 相對延時函數vTaskDelay() 
  • 絕對延時函數vTaskDelayUntil()

這些延時函數可不像我們以前用裸機寫代碼的延時函數操作系統不允許CPU在死等消耗着時間,因為這樣效率太低了。

同時,要告誡學操作系統的同學,千萬別用裸機的思想去學操作系統。

任務延時

任務可能需要延時,兩種情況,一種是任務被vTaskDelay或者vTaskDelayUntil延時,另外一種情況就是任務等待事件(比如等待某個信號量、或者某個消息隊列)時候指定了timeout(即最多等待timeout時間,如果等待的事件還沒發生,則不再繼續等待),在每個任務的循環中都必須要有阻塞的情況出現,否則比該任務優先級低的任務就永遠無法運行。

相對延時與絕對延時的區別

相對延時:vTaskDelay():

相對延時是指每次延時都是從任務執行函數vTaskDelay()開始,延時指定的時間結束

絕對延時:vTaskDelayUntil():

絕對延時是指調用vTaskDelayUntil()的任務每隔x時間運行一次。也就是任務周期運行。

相對延時:vTaskDelay()

相對延時vTaskDelay()是從調用vTaskDelay()這個函數的時候開始延時,但是任務執行的時候,可能發生了中斷,導致任務執行時間變長了,但是整個任務的延時時間還是1000個tick,這就不是周期性了,簡單看看下面代碼:

void vTaskA( void * pvParameters )  
 {  
	while(1) 
     {  
         //  ...
         //  這里為任務主體代碼
         //  ...
        
         /* 調用相對延時函數,阻塞1000個tick */
         vTaskDelay( 1000 );  
     }  
} 

可能說的不夠明確,可以看看圖解。

freertos-delay-1

當任務運行的時候,假設被某個高級任務或者是中斷打斷了,那么任務的執行時間就更長了,然而延時還是延時1000tick這樣子,整個系統的時間就混亂了。

如果還不夠明確,看看vTaskDelay()的源碼

void vTaskDelay( const TickType_t xTicksToDelay )
{
    BaseType_t xAlreadyYielded = pdFALSE;

    /* 延遲時間為零只會強制切換任務。 */
    if( xTicksToDelay > ( TickType_t ) 0U )		(1)
    {
        configASSERT( uxSchedulerSuspended == 0 );
        vTaskSuspendAll();						(2)
        {
            traceTASK_DELAY();
            /*將當前任務從就緒列表中移除,並根據當前系統節拍
            計數器值計算喚醒時間,然后將任務加入延時列表 */
            prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
        }
        xAlreadyYielded = xTaskResumeAll();
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    /* 強制執行一次上下文切換 */
    if( xAlreadyYielded == pdFALSE )
    {
        portYIELD_WITHIN_API();
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
}
  • (1):如果傳遞進來的延時時間是0,只能進行強制切換任務了,調用的是portYIELD_WITHIN_API(),它其實是一個宏,真正起作用的是portYIELD(),下面是它的源碼:
#define portYIELD()												\
{																\
	/* 設置PendSV以請求上下文切換。 */							\
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;				\
	__dsb( portSY_FULL_READ_WRITE );							\
	__isb( portSY_FULL_READ_WRITE );							\
}
  • (2):掛起當前任務

然后將當前任務從就緒列表刪除,然后加入到延時列表。是調用函數prvAddCurrentTaskToDelayedList()完成這一過程的。由於這個函數篇幅過長,就不講解了,有興趣可以看看,我就簡單說說過程。在FreeRTOS中有這么一個變量,是用來記錄systick的值的。

PRIVILEGED_DATA static volatile TickType_t xTickCount     = ( TickType_t ) 0U;

在每次tick中斷時xTickCount加一,它的值表示了系統節拍中斷的次數,那么啥時候喚醒被加入延時列表的任務呢?其實很簡單,FreeRTOS的做法將xTickCount(當前系統時間) + xTicksToDelay(要延時的時間)即可。當這個相對的延時時間到了之后就喚醒了,這個(xTickCount+ xTicksToDelay)時間會被記錄在該任務的任務控制塊中。

看到這肯定有人問,這個變量是TickType_t類型(32位)的,那肯定會溢出啊,沒錯,是變量都會有溢出的一天,可是FreeRTOS乃是世界第一的操作系統啊,FreeRTOS使用了兩個延時列表:

xDelayedTaskList1 和 xDelayedTaskList2

並使用兩個列表指針類型變量pxDelayedTaskListpxOverflowDelayedTaskList分別指向上面的延時列表1和延時列表2(在創建任務時將延時列表指針指向延時列表)如果內核判斷出xTickCount+xTicksToDelay溢出,就將當前任務掛接到列表指針 pxOverflowDelayedTaskList指向的列表中,否則就掛接到列表指針pxDelayedTaskList指向的列表中。當時間到了,就會將延時的任務從延時列表中刪除,加入就緒列表中,當然這時候就是由調度器覺得任務能不能運行了,如果任務的優先級大於當前運行的任務,那么調度器才會進行任務的調度。

絕對延時:vTaskDelayUntil()

vTaskDelayUntil()的參數指定了確切的滴答計數值

調用vTaskDelayUntil()是希望任務以固定頻率定期執行,而不受外部的影響,任務從上一次運行開始到下一次運行開始的時間間隔是絕對的,而不是相對的。假設主體任務被打斷0.3s,但是下次喚醒的時間是固定的,所以還是會周期運行。

freertos-delay-2

下面看看vTaskDelayUntil()的使用方法,注意了,這vTaskDelayUntil()的使用方法與vTaskDelay()不一樣:

void vTaskA( void * pvParameters )  
{  
    /* 用於保存上次時間。調用后系統自動更新 */
    static portTickType PreviousWakeTime;
    /* 設置延時時間,將時間轉為節拍數 */
    const portTickType TimeIncrement = pdMS_TO_TICKS(1000); 
    /* 獲取當前系統時間 */
    PreviousWakeTime = xTaskGetTickCount(); 
    while(1) 
     {  

         /* 調用絕對延時函數,任務時間間隔為1000個tick */
         vTaskDelayUntil( &PreviousWakeTime,TimeIncrement );  

         //  ...
         //  這里為任務主體代碼
         //  ...

     }  
} 

在使用的時候要將延時時間轉化為系統節拍,在任務主體之前要調用延時函數。

任務會先調用vTaskDelayUntil()使任務進入阻塞態,等到時間到了就從阻塞中解除,然后執行主體代碼,任務主體代碼執行完畢。會繼續調用vTaskDelayUntil()使任務進入阻塞態,然后就是循環這樣子執行。即使任務在執行過程中發生中斷,那么也不會影響這個任務的運行周期,僅僅是縮短了阻塞的時間而已。

下面來看看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)
    {
        /* 保存系統節拍中斷次數計數器 */
        const TickType_t xConstTickCount = xTickCount;

        /* 生成任務要喚醒的滴答時間。*/
        xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;

        /* pxPreviousWakeTime中保存的是上次喚醒時間,喚醒后需要一定時間執行任務主體代碼,
            如果上次喚醒時間大於當前時間,說明節拍計數器溢出了 具體見圖片 */
        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()需要塊時間,而不是喚醒時間,因此減去當前的滴答計數。 */
          prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
      }
      else
      {
          mtCOVERAGE_TEST_MARKER();
      }
  }
  xAlreadyYielded = xTaskResumeAll();

  /* 如果xTaskResumeAll尚未執行重新安排,我們可能會讓自己入睡。*/
  if( xAlreadyYielded == pdFALSE )
  {
    portYIELD_WITHIN_API();
  }
  else
  {
    mtCOVERAGE_TEST_MARKER();
  }
}

與相對延時函數vTaskDelay不同,本函數增加了一個參數pxPreviousWakeTime用於指向一個變量,變量保存上次任務解除阻塞的時間,此后函數vTaskDelayUntil()在內部自動更新這個變量。由於變量xTickCount可能會溢出,所以程序必須檢測各種溢出情況,並且要保證延時周期不得小於任務主體代碼執行時間。

就會有以下3種情況,才能將任務加入延時鏈表中。

請記住這幾個單詞的含義:

  • xTimeIncrement:任務周期時間
  • pxPreviousWakeTime:上一次喚醒的時間點
  • xTimeToWake:下一次喚醒的系統時間點
  • xConstTickCount:進入延時的時間點
  1. 第三種情況:常規無溢出的情況。

以時間為橫軸,上一次喚醒的時間點小於下一次喚醒的時間點,這是很正常的情況。

freertos-delay-3

  1. 第二種情況:喚醒時間計數器(xTimeToWake)溢出情況。

也就是代碼中if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )

freertos-delay-4

  1. 第一種情況:喚醒時間(xTimeToWake)與進入延時的時間點(xConstTickCount)都溢出情況。

也就是代碼中if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )

freertos-delay-5

從圖中可以看出不管是溢出還是無溢出,都要求在下次喚醒任務之前,當前任務主體代碼必須被執行完。也就是說任務執行的時間不允許大於延時的時間,總不能存在每10ms就要執行一次20ms時間的任務吧。計算的喚醒時間合法后,就將當前任務加入延時列表,同樣延時列表也有兩個。每次系統節拍中斷,中斷服務函數都會檢查這兩個延時列表,查看延時的任務是否到期,如果時間到期,則將任務從延時列表中刪除,重新加入就緒列表。如果新加入就緒列表的任務優先級大於當前任務,則會觸發一次上下文切換。

總結

如果任務調用相對延時,其運行周期完全是不可測的,如果任務的優先級不是最高的話,其誤差更大,就好比一個必須要在5ms內相應的任務,假如使用了相對延時1ms,那么很有可能在該任務執行的時候被更高優先級的任務打斷,從而錯過5ms內的相應,但是調用絕對延時,則任務會周期性將該任務在阻塞列表中解除,但是,任務能不能運行,還得取決於任務的優先級,如果優先級最高的話,任務周期還是比較精確的(相對vTaskDelay來說),如果想要更加想精確周期性執行某個任務,可以使用系統節拍鈎子函數vApplicationTickHook(),它在tick中斷服務函數中被調用,因此這個函數中的代碼必須簡潔,並且不允許出現阻塞的情況。

關注我

歡迎關注我公眾號

更多資料歡迎關注“物聯網IoT開發”公眾號!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM