主頁 > 作業系統 > 從單片機到作業系統⑦——深入了解FreeRTOS的延時機制

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

2020-09-14 09:39:21 作業系統

沒研究過作業系統的原始碼都不算學過作業系統

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開發”公眾號!

轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/33406.html

標籤:嵌入式

上一篇:從0開始學FreeRTOS-(任務調度)-4

下一篇:FreeRTOS優化與錯誤排查方法

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • CA和證書

    1、在 CentOS7 中使用 gpg 創建 RSA 非對稱密鑰對 gpg --gen-key #Centos上生成公鑰/密鑰對(存放在家目錄.gnupg/) 2、將 CentOS7 匯出的公鑰,拷貝到 CentOS8 中,在 CentOS8 中使用 CentOS7 的公鑰加密一個檔案 gpg -a ......

    uj5u.com 2020-09-10 00:09:53 more
  • Kubernetes K8S之資源控制器Job和CronJob詳解

    Kubernetes的資源控制器Job和CronJob詳解與示例 ......

    uj5u.com 2020-09-10 00:10:45 more
  • VMware下安裝CentOS

    VMware下安裝CentOS 一、軟硬體準備 1 Centos鏡像準備 1.1 CentOS鏡像下載地址 下載地址 1.2 CentOS鏡像下載程序 點擊下載地址進入如下圖的網站,選擇需要下載的版本,這里選擇的是Centos8,點擊如圖所示。 決定選擇Centos8后,選擇想要的鏡像源進行下載,此 ......

    uj5u.com 2020-09-10 00:12:10 more
  • 如何使用Grep命令查找多個字串

    如何使用Grep 命令查找多個字串 大家好,我是良許! 今天向大家介紹一個非常有用的技巧,那就是使用 grep 命令查找多個字串。 簡單介紹一下,grep 命令可以理解為是一個功能強大的命令列工具,可以用它在一個或多個輸入檔案中搜索與正則運算式相匹配的文本,然后再將每個匹配的文本用標準輸出的格式 ......

    uj5u.com 2020-09-10 00:12:28 more
  • git配置http代理

    git配置http代理 經常遇到克隆 github 慢的問題,這里記錄一下幾種配置 git 代理的方法,解決 clone github 過慢。 目錄 git配置代理 git單獨配置github代理 git配置全域代理 配置終端環境變數 git配置代理 主要使用 git config 命令 git單獨 ......

    uj5u.com 2020-09-10 00:12:33 more
  • Linux npm install 裝包時提示Error EACCES permission denied解

    npm install 裝包時提示Error EACCES permission denied解決辦法 ......

    uj5u.com 2020-09-10 00:12:53 more
  • Centos 7下安裝nginx,使用yum install nginx,提示沒有可用的軟體包

    Centos 7下安裝nginx,使用yum install nginx,提示沒有可用的軟體包。 18 (flaskApi) [root@67 flaskDemo]# yum -y install nginx 19 已加載插件:fastestmirror, langpacks 20 Loading ......

    uj5u.com 2020-09-10 00:13:13 more
  • Linux查看服務器暴力破解ssh IP

    在公網的服務器上經常遇到別人爆破你服務器的22埠,用來挖礦或者干其他嘿嘿嘿的事情~ 這種情況下正確的做法是: 修改默認ssh的22埠 使用設定密鑰登錄或者白名單ip登錄 建議服務器密碼為復雜密碼 創建普通用戶登錄服務器(root權限過大) 建立堡壘機,實作統一管理服務器 統計爆破IP [root ......

    uj5u.com 2020-09-10 00:13:17 more
  • CentOS 7系統常見快捷鍵操作方式

    Linux系統中一些常見的快捷方式,可有效提高操作效率,在某些時刻也能避免操作失誤帶來的問題。 ......

    uj5u.com 2020-09-10 00:13:31 more
  • CentOS 7作業系統目錄結構介紹

    作業系統存在著大量的資料檔案資訊,相應檔案資訊會存在于系統相應目錄中,為了更好的管理資料資訊,會將系統進行一些目錄規劃,不同目錄存放不同的資源。 ......

    uj5u.com 2020-09-10 00:13:35 more
最新发布
  • vim的常用命令

    Vim的6種基本模式 1. 普通模式在普通模式中,用的編輯器命令,比如移動游標,洗掉文本等等。這也是Vim啟動后的默認模式。這正好和許多新用戶期待的操作方式相反(大多數編輯器默認模式為插入模式)。 2. 插入模式在這個模式中,大多數按鍵都會向文本緩沖中插入文本。大多數新用戶希望文本編輯器編輯程序中一 ......

    uj5u.com 2023-04-20 08:43:21 more
  • vim的常用命令

    Vim的6種基本模式 1. 普通模式在普通模式中,用的編輯器命令,比如移動游標,洗掉文本等等。這也是Vim啟動后的默認模式。這正好和許多新用戶期待的操作方式相反(大多數編輯器默認模式為插入模式)。 2. 插入模式在這個模式中,大多數按鍵都會向文本緩沖中插入文本。大多數新用戶希望文本編輯器編輯程序中一 ......

    uj5u.com 2023-04-20 08:42:36 more
  • docker學習

    ###Docker概述 真實專案部署環境可能非常復雜,傳統發布專案一個只需要一個jar包,運行環境需要單獨部署。而通過Docker可將jar包和相關環境(如jdk,redis,Hadoop...)等打包到docker鏡像里,將鏡像發布到Docker倉庫,部署時下載發布的鏡像,直接運行發布的鏡像即可。 ......

    uj5u.com 2023-04-19 09:26:53 more
  • 設定Windows主機的瀏覽器為wls2的默認瀏覽器

    這里以Chrome為例。 1. 準備作業 wsl是可以使用Windows主機上安裝的exe程式,出于安全考慮,默認情況下改功能是無法使用。要使用的話,終端需要以管理員權限啟動。 我這里以Windows Terminal為例,介紹如何默認使用管理員權限打開終端,具體操作如下圖所示: 2. 操作 wsl ......

    uj5u.com 2023-04-19 09:25:49 more
  • docker學習

    ###Docker概述 真實專案部署環境可能非常復雜,傳統發布專案一個只需要一個jar包,運行環境需要單獨部署。而通過Docker可將jar包和相關環境(如jdk,redis,Hadoop...)等打包到docker鏡像里,將鏡像發布到Docker倉庫,部署時下載發布的鏡像,直接運行發布的鏡像即可。 ......

    uj5u.com 2023-04-19 09:19:04 more
  • Linux學習筆記

    IP地址和主機名 IP地址 ifconfig可以用來查詢本機的IP地址,如果不能使用,可以通過install net-tools安裝。 Centos系統下ens33表示主網卡;inet后表示IP地址;lo表示本地回環網卡; 127.0.0.1表示代指本機;0.0.0.0可以用于代指本機,同時在放行設 ......

    uj5u.com 2023-04-18 06:52:01 more
  • 解決linux系統的kdump服務無法啟動的問題

    問題:專案麒麟系統服務器的kdump服務無法啟動,沒有相關日志無法定位問題。 1、查看服務狀態是關閉的,重啟系統也無法啟動 systemctl status kdump 2、修改grub引數,修改“crashkernel”為“512M(有的機器數值太大太小都會導致報錯,建議從128M開始試,或者加個 ......

    uj5u.com 2023-04-12 09:59:50 more
  • 解決linux系統的kdump服務無法啟動的問題

    問題:專案麒麟系統服務器的kdump服務無法啟動,沒有相關日志無法定位問題。 1、查看服務狀態是關閉的,重啟系統也無法啟動 systemctl status kdump 2、修改grub引數,修改“crashkernel”為“512M(有的機器數值太大太小都會導致報錯,建議從128M開始試,或者加個 ......

    uj5u.com 2023-04-12 09:59:01 more
  • 你是不是暴露了?

    作者:袁首京 原創文章,轉載時請保留此宣告,并給出原文連接。 如果您是計算機相關從業人員,那么應該經歷不止一次網路安全專項檢查了,你肯定是收到過資訊系統技術檢測報告,要求你加強風險監測,確保你提供的系統服務堅實可靠了。 沒檢測到問題還好,檢測到問題的話,有些處理起來還是挺麻煩的,尤其是線上正在運行的 ......

    uj5u.com 2023-04-05 16:52:56 more
  • 細節拉滿,80 張圖帶你一步一步推演 slab 記憶體池的設計與實作

    1. 前文回顧 在之前的幾篇記憶體管理系列文章中,筆者帶大家從宏觀角度完整地梳理了一遍 Linux 記憶體分配的整個鏈路,本文的主題依然是記憶體分配,這一次我們會從微觀的角度來探秘一下 Linux 內核中用于零散小記憶體塊分配的記憶體池 —— slab 分配器。 在本小節中,筆者還是按照以往的風格先帶大家簡單 ......

    uj5u.com 2023-04-05 16:44:11 more