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

来源:https://www.cnblogs.com/iot-dev/archive/2019/10/15/11681031.html
-Advertisement-
Play Games

沒研究過操作系統的源碼都不算學過操作系統 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開發”公眾號!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本篇主要寫一些 腳本編輯工具 的使用。 概述 是一個功能強大的編輯工具,逐行讀取輸入文本,並根據指定的匹配模式進行查找,對符合條件的內容進行格式化輸出或者過濾處理。 傾向於將一行分成多個欄位然後再進行處理,且預設情況下欄位的分隔符為 或者 鍵。 執行結果可以通過 的功能將欄位數據列印顯示。 可以使用 ...
  • 1.先apt-get update一下當前預設的源,更新完成後先把vim命令安裝一下,再修改源倉庫為阿裡雲,否則無法直接編輯文件 2.先添加阿裡雲的源,編輯文件/etc/apt/sources.list,編輯完再次更新一下 deb http://mirrors.aliyun.com/ubuntu/ ...
  • 作為一個技術純小白,在Linux伺服器初始化MySQL資料庫的時候遇到了一點小問題: ​ 1、不會使用MySQL圖形工具,幾乎沒玩過 ​ 2、客戶的VPN沒有開放3306埠,沒法用navicat等工具連接資料庫 ​ 3、懶的再打開圖形工具,畢竟命令行接近萬能了…… 所以: 方法一、在初始化腳本文件 ...
  • 對稱加密演算法 非對稱加密演算法 單向散列(hash演算法) CA和證書 證書獲取 安全協議 OpenSSL openssl命令 創建CA和申請證書 ...
  • awk介紹 awk的工作原理 awk基本格式 awk之print格式 awk變數 awk之printf awk操作符 awk PATTERN awk action awk控制語句 awk控制語句if-else awk控制語句 while迴圈 awk控制語句do-while迴圈 awk控制語句 for ...
  • 準備 在移植之前,我們首先要獲取到FreeRTOS的官方的源碼包。這裡我們提供兩個下載鏈接: 一個是官網:http://www.freertos.org/ 另外一個是代碼托管網站:https://sourceforge.net/projects/freertos/files/FreeRTOS/ 這裡 ...
  • 寫在前面:傑傑這個月很忙~所以並沒有時間更新,現在健身房閉館裝修,晚上有空就更新一下!其實在公眾號沒更新的這段日子,每天都有兄弟在來關註我的公眾號,這讓我受寵若驚,在這裡謝謝大家的支持啦!!謝謝^ 在這裡我們就跟著火哥的書來學習一下FreeRTOS的消息隊列,這本書我覺得寫得很好,基本都講解到了,關 ...
  • 寫在前面 主要是為剛接觸 FreeRTOS 的用戶指出那些新手通常容易遇到的問題。這裡把最主要的篇幅放在棧溢出以及棧溢出j檢測上,因為棧相關的問題是初學者遇到最多的問題。 printf stdarg.c 當調用 C 標準庫 的函數時,棧空間使用量可能會急劇上升,特別是 IO 與字元串處理函數,比如 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...