FreeRTOS的学习系列文章目录
FreeRTOS的学习(一)——STM32上的移植问题
FreeRTOS的学习(二)——任务优先级问题
FreeRTOS的学习(三)——中断机制
FreeRTOS的学习(四)——列表
FreeRTOS的学习(五)——系统延时
FreeRTOS的学习(六)——系统时钟
FreeRTOS的学习(七)——1.队列概念
FreeRTOS的学习(七)——2.队列入队源码分析
FreeRTOS的学习(七)——3.队列出队源码分析
FreeRTOS的学习(八)——1.二值信号量
FreeRTOS的学习(八)——2.计数型信号量
FreeRTOS的学习(八)——3.优先级翻转问题
FreeRTOS的学习(八)——4.互斥信号量
FreeRTOS的学习(九)——软件定时器
FreeRTOS的学习(十)——事件标志组
FreeRTOS的学习(十一)——任务通知
前言
在前面的学习过程中大概了解了FreeRTOS的每个任务可以相互独立的编程,通过系统定时中断进行任务级别的切换。其中延时函数vTaskDelay()作为FreeRTOS的延时函数,其也具有任务切换的功能,即延时函数会将当前任务放入阻塞态等待延时时间结束,然后重新将该任务放入就绪态,按优先级运行。
freeRTOS的系统延时又分为两种模式:一种是相对模式,容易由于任务主体时间影响而导致任务执行的时间太长。另一种是绝对模式,绝对模式可以很好的解决相对模式的问题,因为其延时时间是一个周期值,即任务周期运行的时间,那么该任务真正进入阻塞态的时间会根据任务主体的运行时间以及设定的周期时间来实时变化。不过依旧不能排除高优先级的任务级中断和更高优先级的中断带来的影响。
1 系统延时的相对模式
FreeRTOS的相对延时函数为vTaskDelay()。我们直接来看代码:
#if ( INCLUDE_vTaskDelay == 1 )
//延时函数
void vTaskDelay( const TickType_t xTicksToDelay )
{
BaseType_t xAlreadyYielded = pdFALSE;
/* A delay time of zero just forces a reschedule. */
if( xTicksToDelay > ( TickType_t ) 0U )
{
configASSERT( uxSchedulerSuspended == 0 );
vTaskSuspendAll(); //任务全部挂起
{
traceTASK_DELAY(); //没有实现
prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
}
xAlreadyYielded = xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER(); //没有实现
}
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* INCLUDE_vTaskDelay */
当想要延时的时间,准确的说应该是节拍数,一个节拍代表滴答定时器的中断时间,一般设置为1~100ms,频率过高往往对于资源的占用就更大。首先需要将所有任务调度器挂起;然后把当前任务放到延时列表,默认形参是设定的延时节拍数和pdFLASE;最后恢复任务调度器,但是要注意的是恢复调度器的函数存在返回值,xAlreadyYielded。该局部变量主要用于判断是否还需要进行上下文切换,如果在xTaskResumeAll()函数中没有进行上下文切换,即没有发生中断事件等,则需要通过xAlreadyYielded来判断是否要进行上下文切换,因为延时函数是肯定要引起任务调度的。
接下来再看prvAddCurrentTaskToDelayedList函数,为了不影响阅读,我删除了没使用的部分函数:
static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait,
const BaseType_t xCanBlockIndefinitely )
{
TickType_t xTimeToWake;
const TickType_t xConstTickCount = xTickCount;
/* Remove the task from the ready list before adding it to the blocked list
* as the same list item is used for both lists. */
if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
/* The current task must be in a ready list, so there is no need to
* check, and the port reset macro can be called directly. */
portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
#if ( INCLUDE_vTaskSuspend == 1 )
{
if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )//满足条件的话直接挂起
{
/* Add the task to the suspended task list instead of a delayed task
* list to ensure it is not woken by a timing event. It will block
* indefinitely. */
listINSERT_END( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* Calculate the time at which the task should be woken if the event
* does not occur. This may overflow but this doesn't matter, the
* kernel will manage it correctly. */
xTimeToWake = xConstTickCount + xTicksToWait;
/* The list item will be inserted in wake time order. */
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
if( xTimeToWake < xConstTickCount )//延时结束的时间溢出,故比当前时间点小
{
/* Wake time has overflowed. Place this item in the overflow
* list. */
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* The wake time has not overflowed, so the current block list
* is used. */
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
/* If the task entering the blocked state was placed at the
* head of the list of blocked tasks then xNextTaskUnblockTime
* needs to be updated too. */
if( xTimeToWake < xNextTaskUnblockTime )
{
xNextTaskUnblockTime = xTimeToWake;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
}
#endif /* INCLUDE_vTaskSuspend */
该函数首先会获取当前的定时器的节拍数xTickCount,该全局变量初始化为0,最大值为0xFFFFFFFH(32位系统中),其会根据系统定时器不断+1计数。
在prvAddCurrentTaskToDelayedList函数中,首先需要在就绪列表中移除当前任务的控制块状态列表,否则会出现阻塞列表和就绪列表都有该任务的情况。在移除之后就要充值就绪列表的优先级,准备下次任务。
当我们使能挂起态时,会多进行一次判断,也就是:
if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )
即设定的延时时间xTicksToWait==设定的延时最大值(0xFFFFFFFFH),并且挂起的标志位(也就是prvAddCurrentTaskToDelayedList函数的第二个形参)为真时。直接挂起态的列表尾部插入该任务。这种情况一般比较极端,正常也不会使用。
另一种情况就是通过设定的延时时间xTicksToWait+进入该函数时的时间戳xConstTickCount的和,设定唤醒的时间戳xTimeToWake。
listSET_LIST_ITEM_VALUE函数就是将唤醒的时间戳作为列表的xItemValue(列表中用来排序的值,见讲列表的博客,可以用于依次唤醒)。
那么难免就会存在一个问题,如果计数的xTickCount溢出了怎么办?FreeRTOS根据溢出时可能出现的情况做了判断:
如果唤醒任务的时间戳算出来比挂起任务时的时间戳小,那么计数器肯定是溢出了。那么该任务的状态列表就会被插入到一个溢出时才用的列表pxOverflowDelayedTaskList,反之,则正常插入到pxDelayedTaskList中,这种情况下还对接下来要唤醒的任务作判断,如果当前任务比下一个要唤醒的任务早,就要更新下一个任务的唤醒时间戳。
这里要注意的是,一个延时任务列表和一个溢出延时任务列表是共用的。那么然后呢?如果延时任务列表也溢出了呢?这里要注意的是,当检测到延时任务插入到溢出列表时,系统会将两个列表的功能互换,也就达到了循环使用的目的,溢出列表的存在是为了保证当前任务的实时性,所以溢出操作没有在当前的过程中处理。另外类似的用法在FreeRTOS中还有很多,注入定时器列表pxCurrentTimerList和pxOverflowTimerList都是类似的使用。
if( xTimeToWake < xConstTickCount )
{
/* Wake time has overflowed. Place this item in the overflow
* list. */
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* The wake time has not overflowed, so the current block list
* is used. */
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
/* If the task entering the blocked state was placed at the
* head of the list of blocked tasks then xNextTaskUnblockTime
* needs to be updated too. */
if( xTimeToWake < xNextTaskUnblockTime )
{
xNextTaskUnblockTime = xTimeToWake;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
每次系统节拍时钟中断,中断服务函数都会检查这上述两个延时列表,查看延时的任务是否到期,如果时间到期,则将任务从延时列表中删除,重新加入就绪列表。如果新加入就绪列表的任务优先级大于当前任务,则会触发一次上下文切换。
2 系统延时的绝对模式
在讲绝对模式的函数前,提一下新版本FreeRTOS和之前版本的一个区别,具体是从哪个版本开始改的,我也不太清楚。
FreeRTOS在新版中使用的绝对模式的条件编译不再是INCLUDE_vTaskDelayUntil,而是INCLUDE_xTaskDelayUntil,这个条件编译的区别是由于相比于原来的绝对模式,函数加上了返回值。而为了版本向下兼容,在宏定义中做了如下操作:
#ifndef INCLUDE_xTaskDelayUntil
#ifdef INCLUDE_vTaskDelayUntil
#define INCLUDE_xTaskDelayUntil INCLUDE_vTaskDelayUntil
#endif
#endif
也就是即使没有定义INCLUDE_xTaskDelayUntil,只要定义了INCLUDE_vTaskDelayUntil,INCLUDE_xTaskDelayUntil就作相同的宏定义。
接下来继续分析绝对模式的函数,首先贴出完整的函数:
#if ( INCLUDE_xTaskDelayUntil == 1 )
BaseType_t xTaskDelayUntil( 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();
{
/* Minor optimisation. The tick count cannot change in this
* block. */
const TickType_t xConstTickCount = xTickCount;
/* Generate the tick time at which the task wants to wake. */
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
if( xConstTickCount < *pxPreviousWakeTime )
{
/* The tick count has overflowed since this function was
* lasted called. In this case the only time we should ever
* actually delay is if the wake time has also overflowed,
* and the wake time is greater than the tick time. When this
* is the case it is as if neither time had overflowed. */
if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
/* The tick time has not overflowed. In this case we will
* delay if either the wake time has overflowed, and/or the
* tick time is less than the wake time. */
if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* Update the wake time ready for the next call. */
*pxPreviousWakeTime = xTimeToWake;
if( xShouldDelay != pdFALSE )
{
traceTASK_DELAY_UNTIL( xTimeToWake );
/* prvAddCurrentTaskToDelayedList() needs the block time, not
* the time to wake, so subtract the current tick count. */
prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
xAlreadyYielded = xTaskResumeAll();
/* Force a reschedule if xTaskResumeAll has not already done so, we may
* have put ourselves to sleep. */
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
return xShouldDelay;
}
#endif /* INCLUDE_xTaskDelayUntil */
可以看到,xTaskDelayUntil函数需要两个形参:
1.pxPreviousWakeTime:上一次任务延时结束被唤醒的时间点,这个值在第一次调用绝对模式的系统延时时,需要获取系统节拍数值,之后就不需要了,所以一般在任务函数的while循环之前获取即可。
2.xTimeIncrement:任务需要延时的时间节拍数,这里和vTaskDelay的形参的意义并不一样,xTimeIncrement是任务运行的周期,包括任务主体程序,vTaskDelayUntil运行时间以及阻塞的时间(阻塞的时间这部分是多算了vTaskDelayUntil其中一部分程序运行的时间的),所以主体程序运行的时间必须小于该值,否则会出错。
绝对模式相比于相对模式会多一个时间戳,也就是任务的主体内容执行完之后的时间戳。
函数的运行过程分别为一下几步:
1.挂起任务调度器;
2.记录进入xTaskDelayUntil函数的时间戳,并保存在xConstTickCount中;
3.根据上一次唤醒任务的时间戳(*pxPreviousWakeTime)+任务绝对周期时间(xTimeIncrement)求得下一次任务唤醒的时间戳。在正常情况下,我们可以得到如下图所示的时间戳关系图。
当然也存在一些溢出的情况,绝对延时函数相比于相对延时函数就复杂了一些,因为其实质上多了一个时间戳,所以溢出的情况也多了一种。
为了方便查看,将相关的溢出处理函数在下面再贴一遍:
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();
}
}
4.首先讨论第一种情况,也就是xConstTickCount溢出,且此时xTimeToWake也必定溢出的情况,正常情况下,如果xTimeToWake溢出了,那么基本上他的值肯定是比pxPreviousWakeTime 小的,不然这个周期延时时间也有点离谱了。因此这种情况下,系统也没什么操作。反之,也就是( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount )时,xShouldDelay为pdTRUE,允许延时。此时的溢出情况如下图:
5.还有一种情况是只有xTimeToWake溢出,FreeRTOS将其加入到这个条件判断中( xTimeToWake < pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount )。前述的条件语句的前半部分就是溢出的情况,那么此时的溢出情况如下图所示:
6,更新上一次唤醒时间的值pxPreviousWakeTime = xTimeToWake,所以绝对延时函数在使用时只需要再一开始获得系统节拍数值即可,内部会对上一次唤醒时间自动更新。
7.判断是否不允许延时(xShouldDelay是否为pdFALSE),调用prvAddCurrentTaskToDelayedList()进行延时,延时的时间是设置任务的阻塞时间,即xTimeToWake 减去当前的时间 xConstTickCount。那么有的朋友可能要问了,xTimeToWake -xConstTickCount这个值能直接用吗,不是有种情况是xTimeToWake 溢出,那xTimeToWake 肯定<xTimeToWake 啊。没错,但是这种情况再相对延时函数中已经讨论了,具体的处理办法实在prvAddCurrentTaskToDelayedList内部就有的,所以不要被绕进去了哈哈哈。
8.调用函数xTaskResumeAll()恢复任务调度器,返回xShouldDelay。
总结
到这儿,系统延时基本就讲完了,于我而言,vTaskDelay就够了,不够某些情况周期也是有用的吧,不过还是要注意主体函数应该尽可能精简,另外就是及时是周期函数也不太准,不过也够用了把,真要准的频率不都用中断了么……