下面看看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 * constpxPreviousWakeTime, 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:进入延时的时间点
第三种情况:常规无溢出的情况。
以时间为横轴,上一次唤醒的时间点小于下一次唤醒的时间点,这是很正常的情况。
第二种情况:唤醒时间计数器(xTimeToWake)溢出情况。
也就是代码中if( ( xTimeToWake <*pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
第一种情况:唤醒时间(xTimeToWake)与进入延时的时间点(xConstTickCount)都溢出情况。
也就是代码中if( ( xTimeToWake <*pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
从图中可以看出不管是溢出还是无溢出,都要求在下次唤醒任务之前,当前任务主体代码必须被执行完。也就是说任务执行的时间不允许大于延时的时间,总不能存在每10ms就要执行一次20ms时间的任务吧。计算的唤醒时间合法后,就将当前任务加入延时列表,同样延时列表也有两个。每次系统节拍中断,中断服务函数都会检查这两个延时列表,查看延时的任务是否到期,如果时间到期,则将任务从延时列表中删除,重新加入就绪列表。如果新加入就绪列表的任务优先级大于当前任务,则会触发一次上下文切换。
8
总结
如果任务调用相对延时,其运行周期完全是不可测的,如果任务的优先级不是最高的话,其误差更大,就好比一个必须要在5ms内相应的任务,假如使用了相对延时1ms,那么很有可能在该任务执行的时候被更高优先级的任务打断,从而错过5ms内的相应,但是调用绝对延时,则任务会周期性将该任务在阻塞列表中解除,但是,任务能不能运行,还得取决于任务的优先级,如果优先级最高的话,任务周期还是比较精确的(相对vTaskDelay来说),如果想要更加想精确周期性执行某个任务,可以使用系统节拍钩子函数vApplicationTickHook(),它在tick中断服务函数中被调用,因此这个函数中的代码必须简洁,并且不允许出现阻塞的情况。