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

沒研究過操做系統的源碼都不算學過操做系統函數

FreeRTOS 時間管理

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

系統節拍:

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

延時管理

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

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

這些延時函數可不像咱們之前用裸機寫代碼的延時函數操做系統不容許CPU在死等消耗着時間,由於這樣效率過低了。指針

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

任務延時

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

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

相對延時:vTaskDelay():blog

相對延時是指每次延時都是從任務執行函數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開發」公衆號

相關文章
相關標籤/搜索