FreeRTOS高级篇9---FreeRTOS系统延时

FreeRTOS提供了两个系统延时函数:相对延时函数vTaskDelay()和绝对延时函数vTaskDelayUntil()。相对延时是指每次延时都是从任务执行函数vTaskDelay()开始,延时指定的时间结束;绝对延时是指每隔指定的时间,执行一次调用vTaskDelayUntil()函数的任务。换句话说:任务以固定的频率执行。
《FreeRTOS系列第11篇---FreeRTOS任务控制》一文中,已经介绍了这两个API函数的原型和用法,本文将分析这两个函数的实现原理。

1. 相对延时函数vTaskDelay()

考虑下面的任务,任务A在执行任务主体代码后,调用相对延时函数vTaskDelay()进入阻塞状态。系统中除了任务A外,还有其它任务,但是任务A的优先级最高。

void vTaskA( void * pvParameters )
 {
     /* 阻塞500ms. 注:宏pdMS_TO_TICKS用于将毫秒转成节拍数,FreeRTOS V8.1.0及
        以上版本才有这个宏,如果使用低版本,可以使用 500 / portTICK_RATE_MS */
     const portTickType xDelay = pdMS_TO_TICKS(500);  

     for( ;; )
     {
         //  ...
         //  这里为任务主体代码
         //  ...

         /* 调用系统延时函数,阻塞500ms */
         vTaskDelay( xDelay );
     }
}  

对于这样一个任务,执行过程如图1-1所示。当任务A获取CPU使用权后,先执行任务A的主体代码,之后调用系统延时函数vTaskDelay()进入阻塞状态。任务A进入阻塞后,其它任务得以执行。FreeRTOS内核会周期性的检查任务A的阻塞是否达到,如果阻塞时间达到,则将任务A设置为就绪状态。由于任务A的优先级最高,会抢占CPU,再次执行任务主体代码,不断循环。
从图1-1可以看出,任务A每次延时都是从调用延时函数vTaskDelay()开始算起的,延时是相对于这一时刻开始的,所以叫做相对延时函数。
从图1-1还可以看出,如果执行任务A的过程中发生中断,那么任务A执行的周期就会变长,所以使用相对延时函数vTaskDelay(),不能周期性的执行任务A。

 

图1-1:相对延时函数执行示意图

我们来看一下源码。

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

    /* 如果延时时间为0,则不会将当前任务加入延时列表 */
    if( xTicksToDelay > ( TickType_t ) 0U )
    {
        vTaskSuspendAll();
        {
            /* 将当前任务从就绪列表中移除,并根据当前系统节拍计数器值计算唤醒时间,然后将任务加入延时列表 */
            prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
        }
        xAlreadyYielded = xTaskResumeAll();
    }

    /* 强制执行一次上下文切换*/
    if( xAlreadyYielded == pdFALSE )
    {
        portYIELD_WITHIN_API();
    }
}

如果延时大于0,则会将当前任务从就绪列表删除,然后加入到延时列表。是调用函数prvAddCurrentTaskToDelayedList()完成这一过程的。我们在前面一系列博文中多次提到,tasks.c中定义了很多局部静态变量,其中有一个变量xTickCount定义如下所示:

static volatile TickType_t xTickCount = ( TickType_t ) 0U;

这个变量用来计数,记录系统节拍中断的次数,它在启动调度器时被清零,在每次系统节拍时钟发生中断后加1。相对延时函数会使用到这个变量,xTickCount表示了当前的系统节拍中断次数,这个值加上参数规定的延时时间(以系统节拍数表示)xTicksToDelay,就是下次唤醒任务的时间,xTickCount+ xTicksToDelay会被记录到任务TCB中,随着任务一起被挂接到延时列表。
        我们知道变量xTickCount是TickType_t类型的,它也会溢出。在32位架构中,当xTicksToDelay达到4294967295后再增加,就会溢出变成0。为了解决xTickCount溢出问题,FreeRTOS使用了两个延时列表:xDelayedTaskList1和xDelayedTaskList2,并使用两个列表指针类型变量pxDelayedTaskList和pxOverflowDelayedTaskList分别指向上面的延时列表1和延时列表2(在创建任务时将延时列表指针指向延时列表)。顺便说一下,上面的两个延时列表指针变量和两个延时列表变量都是在tasks.c中定义的静态局部变量。
        如果内核判断出xTickCount溢出,就将当前任务挂接到列表指针pxOverflowDelayedTaskList指向的列表中,否则就挂接到列表指针pxDelayedTaskList指向的列表中。
        每次系统节拍时钟中断,中断服务函数都会检查这两个延时列表,查看延时的任务是否到期,如果时间到期,则将任务从延时列表中删除,重新加入就绪列表。如果新加入就绪列表的任务优先级大于当前任务,则会触发一次上下文切换。

2. 绝对延时函数vTaskDelayUntil()

考虑下面的任务,任务B首先调用绝对延时函数vTaskDelayUntil ()进入阻塞状态,阻塞时间到达后,执行任务主体代码。系统中除了任务B外,还有其它任务,但是任务B的优先级最高。

void vTaskB( void * pvParameters )
{
    static portTickType xLastWakeTime;
    const portTickType xFrequency = pdMS_TO_TICKS(500);  

    // 使用当前时间初始化变量xLastWakeTime ,注意这和vTaskDelay()函数不同
    xLastWakeTime = xTaskGetTickCount();  

    for( ;; )
    {
        /* 调用系统延时函数,周期性阻塞500ms */
        vTaskDelayUntil( &xLastWakeTime,xFrequency );  

         //  ...
         //  这里为任务主体代码,周期性执行.注意这和vTaskDelay()函数也不同
         //  ...

    }
}  

对于这样一个任务,执行过程如图2-1所示。当任务B获取CPU使用权后,先调用系统延时函数vTaskDelayUntil()使任务进入阻塞状态。任务B进入阻塞后,其它任务得以执行。FreeRTOS内核会周期性的检查任务A的阻塞是否达到,如果阻塞时间达到,则将任务A设置为就绪状态。由于任务B的优先级最高,会抢占CPU,接下来执行任务主体代码。任务主体代码执行完毕后,会继续调用系统延时函数vTaskDelayUntil()使任务进入阻塞状态,周而复始。
从图2-1可以看出,从调用函数vTaskDelayUntil()开始,每隔固定+
-周期,任务B的主体代码就会被执行一次,即使任务B在执行过程中发生中断,也不会影响这个周期性,只是会缩短其它任务的执行时间!所以这个函数被称为绝对延时函数,它可以用于周期性的执行任务A的主体代码。

图2-1:绝对延时函数执行示意图

函数vTaskDelayUntil()是如何做到周期性的呢,我们来看一下源码。

void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
{
TickType_t xTimeToWake;
BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;

    vTaskSuspendAll();
    {
        /* 保存系统节拍中断次数计数器 */
        const TickType_t xConstTickCount = xTickCount;

        /* 计算任务下次唤醒时间(以系统节拍中断次数表示)   */
        xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;

        /* *pxPreviousWakeTime中保存的是上次唤醒时间,唤醒后需要一定时间执行任务主体代码,如果上次唤醒时间大于当前时间,说明节拍计数器溢出了 */
        if( xConstTickCount < *pxPreviousWakeTime )
        {
            /*只有当周期性延时时间大于任务主体代码执行时间,才会将任务挂接到延时列表.*/
            if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
            {
                xShouldDelay = pdTRUE;
            }
        }
        else
        {
            /* 也都是保证周期性延时时间大于任务主体代码执行时间 */
            if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
            {
                xShouldDelay = pdTRUE;
            }
        }

        /* 更新唤醒时间,为下一次调用本函数做准备. */
        *pxPreviousWakeTime = xTimeToWake;

        if( xShouldDelay != pdFALSE )
        {
            /* 将本任务加入延时列表,注意阻塞时间并不是以当前时间为参考,因此减去了当前系统节拍中断计数器值*/
            prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
        }
    }
    xAlreadyYielded = xTaskResumeAll();

    /* 强制执行一次上下文切换 */
    if( xAlreadyYielded == pdFALSE )
    {
        portYIELD_WITHIN_API();
    }
}

与相对延时函数vTaskDelay不同,本函数增加了一个参数pxPreviousWakeTime用于指向一个变量,变量保存上次任务解除阻塞的时间。这个变量在任务开始时必须被设置成当前系统节拍中断次数(见上文的任务B举例),此后函数vTaskDelayUntil()在内部自动更新这个变量。
由于变量xTickCount可能会溢出,所以程序必须检测各种溢出情况,并且要保证延时周期不得小于任务主体代码执行时间。这很好理解,不可能出现每5毫秒执行一个需要20毫秒才能执行完的任务。
如果我们以横坐标表示变量xTickCount的范围,则横坐标左端为0,右端为变量xTickCount所能表示的最大值。在如图2-2所示的三种情况下,才可以将任务加入延时列表。图2-2中,*pxPreviousWakeTime和xTimeToWake之间表示任务周期性延时时间,*pxPreviousWakeTime和xConstTickCount之间表示任务B主体代码执行时间。
图2-2中第一种情况处理系统节拍中断计数器(xConstTickCount)和唤醒时间计数器(xTimeToWake)溢出情况;第二种情况处理唤醒时间计数器(xTimeToWake)溢出情况;第三种情况处理常规无溢出的情况。从图中可以看出,不管是溢出还是无溢出,都要求在下次唤醒任务之前,当前任务主体代码必须被执行完。表现在图2-2中,就是变量xTimeToWake总是大于变量xConstTickCount(每溢出一次的话相当于加上一次最大值Max)。

 

图2-2:将任务加入延时列表的三种情况

计算的唤醒时间合法后,就将当前任务加入延时列表,同样延时列表也有两个。每次系统节拍中断,中断服务函数都会检查这两个延时列表,查看延时的任务是否到期,如果时间到期,则将任务从延时列表中删除,重新加入就绪列表。如果新加入就绪列表的任务优先级大于当前任务,则会触发一次上下文切换。

3.小结

上面的例子中,调用系统延时的任务都是最高优先级,这是为了便于分析而特意为之的,实际上的任务可不一定能设置为最高优先级。对于相对延时,如果任务不是最高优先级,则任务执行周期更不可测,这个问题不大,我们本来也不会使用它作为精确延时;对于绝对延时函数,如果任务不是最高优先级,则仍然能周期性的将任务解除阻塞,但是解除阻塞的任务不一定能获得CPU权限,因此任务主体代码也不会总是精确周期性执行。
如果要想精确周期性执行某个任务,可以使用系统节拍钩子函数vApplicationTickHook(),它在系统节拍中断服务函数中被调用,因此这个函数中的代码必须简洁。

时间: 2024-10-19 12:10:59

FreeRTOS高级篇9---FreeRTOS系统延时的相关文章

FreeRTOS高级篇8---FreeRTOS任务通知分析

在FreeRTOS版本V8.2.0中推出了全新的功能:任务通知.在大多数情况下,任务通知可以替代二进制信号量.计数信号量.事件组,可以替代数长度为1的队列(可以保存一个32位整数或指针值),并且任务通知速度更快.使用的RAM更少!我在< FreeRTOS系列第14篇---FreeRTOS任务通知>一文中介绍了任务通知如何使用以及局限性,今天我们将分析任务通知的实现源码,看一下任务通知是如何做到效率与RAM消耗双赢的.        在<FreeRTOS高级篇6---FreeRTOS信号量

FreeRTOS高级篇5---FreeRTOS队列分析

FreeRTOS提供了多种任务间通讯方式,包括: 任务通知(版本V8.2以及以上版本) 队列 二进制信号量 计数信号量 互斥量 递归互斥量 其中,二进制信号量.计数信号量.互斥量和递归互斥量都是使用队列来实现的,因此掌握队列的运行机制,是很有必要的.      队列是FreeRTOS主要的任务间通讯方式.可以在任务与任务间.中断和任务间传送信息.发送到队列的消息是通过拷贝实现的,这意味着队列存储的数据是原数据,而不是原数据的引用.先看一下队列的数据结构: typedef struct Queue

FreeRTOS高级篇2---FreeRTOS任务创建分析

在FreeRTOS基础系列<FreeRTOS系列第10篇---FreeRTOS任务创建和删除>中介绍了任务创建API函数xTaskCreate(),我们这里先回顾一下这个函数的声明: BaseType_t xTaskCreate( TaskFunction_tp vTaskCode, const char * constpcName, unsigned short usStackDepth, void *pvParameters, UBaseType_t uxPriority, TaskHan

FreeRTOS高级篇3---启动调度器

使用FreeRTOS,一个最基本的程序架构如下所示: int main(void) { 必要的初始化工作; 创建任务1; 创建任务2; ... vTaskStartScheduler(); /*启动调度器*/ while(1); } 任务创建完成后,静态变量指针pxCurrentTCB(见<FreeRTOS高级篇2---FreeRTOS任务创建分析>第7节内容)指向优先级最高的就绪任务.但此时任务并不能运行,因为接下来还有关键的一步:启动FreeRTOS调度器. 调度器是FreeRTOS操作系

FreeRTOS高级篇6---FreeRTOS信号量分析

FreeRTOS的信号量包括二进制信号量.计数信号量.互斥信号量(以后简称互斥量)和递归互斥信号量(以后简称递归互斥量).关于它们的区别可以参考< FreeRTOS系列第19篇---FreeRTOS信号量>一文. 信号量API函数实际上都是宏,它使用现有的队列机制.这些宏定义在semphr.h文件中.如果使用信号量或者互斥量,需要包含semphr.h头文件. 二进制信号量.计数信号量和互斥量信号量的创建API函数是独立的,但是获取和释放API函数都是相同的:递归互斥信号量的创建.获取和释放AP

FreeRTOS高级篇4---FreeRTOS任务切换分析

FreeRTOS任务相关的代码大约占总代码的一半左右,这些代码都在为一件事情而努力,即找到优先级最高的就绪任务,并使之获得CPU运行权.任务切换是这一过程的直接实施者,为了更快的找到优先级最高的就绪任务,任务切换的代码通常都是精心设计的,甚至会用到汇编指令或者与硬件相关的特性,比如Cortex-M3的CLZ指令.因此任务切换的大部分代码是由硬件移植层提供的,不同的平台,实现发方法也可能不同,这篇文章以Cortex-M3为例,讲述FreeRTOS任务切换的过程. FreeRTOS有两种方法触发任务

Dubbo高级篇教程--支付系统开发 之 简易版支付系统部署(单节点)

一.前期准备 1.MySQL数据库的安装:MySQL-5.6.22,自行安装 2.Dubbo视频教程--基础篇--第03节--ZooKeeper注册中心安装 3.Dubbo视频教程--基础篇--第06节--Dubbo管理控制台的安装 4.Dubbo视频教程--基础篇--第10节--Dubbo监控中心的介绍与简易监控中心的安装 5.持续集成管理平台(SVN.Nexus.Maven.Hudson)的安装: Dubbo视频教程--基础篇--第11节至18节 6.Dubbo视频教程--高级篇--第21节

FreeRTOS高级篇7---FreeRTOS内存管理分析

内存管理对应用程序和操作系统来说都非常重要.现在很多的程序漏洞和运行崩溃都和内存分配使用错误有关.        FreeRTOS操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些内存管理函数是如何实现的.这样做大有好处,可以增加系统的灵活性:不同的应用场合可以使用不同的内存分配实现,选择对自己更有利的内存管理策略.比如对于安全型的嵌入式系统,通常不允许动态内存分配,那么可以采用非常简单的内存管理策略,一经申请的内存,甚至不允许被释放.在满足设计要求的前提

FreeRTOS高级篇11---空闲任务分析

当RTOS调度器开始工作后,为了保证至少有一个任务在运行,空闲任务被自动创建,占用最低优先级(0优先级). xReturn = xTaskCreate( prvIdleTask, "IDLE",configMINIMAL_STACK_SIZE, (void * ) NULL, (tskIDLE_PRIORITY | portPRIVILEGE_BIT ), &xIdleTaskHandle); 空闲任务是FreeRTOS不可缺少的任务,因为FreeRTOS设计要求必须至少有一个