从单片机到操作系统⑦——深入了解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开发”公众号

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容