在FreeRTOS中可以使用的延时函数分为两类,第一类是操作系统提供的延时函数,另一类是自己编写的延时函数,也就是我们从裸机移植到操作系统中的延时函数。这两种函数各有优劣,下面就来详细介绍一下这些延时函数。
一、FreeRTOS中的延时函数
FreeRTOS中的延时函数有相对模式和绝对模式,vTaskDelay()是相对模式,vTaskDelayUntil()是绝对模式,这些函数都是在FreeRTOS中封装好的,只需开启其宏定义即可使用,我们只需了解其函数接口调用即可。
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();
/* A task that is removed from the event list while the
scheduler is suspended will not get placed in the ready
list or removed from the blocked list until the scheduler
is resumed.
This task cannot be in an event list as it is the currently
executing task. */
prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
}
xAlreadyYielded = xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 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();
}
}
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();
{
/* 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();
}
}
当执行vTaskDelay()和vTaskDelayUntil()时当前任务会进入阻塞态并进行任务切换,在A任务延时的时候会去执行B任务,这样的好处就是能充分利用CPU资源,提高利用率。
二、移植裸机中的延时函数
裸机中的延时函数有很多实现方法,可以通过定时器实现,比如systick滴答定时器或者芯片的timer定时器资源或RTC时钟等等,也可以重复执行同样的操作比如__NOP()来实现延时。一般微秒级以上的使用定时器延时,纳秒级使用__NOP()延时。
1、定时器延时(以systick为例)
笔者选择以systick为例是因为它比较特殊,是FreeRTOS的系统时钟节拍计数器,这意味着我们不能使用完延时后关闭systick定时器,也不能随意更改systick的计数频率。
所有的定时器延时都要考虑延时函数的可重入性问题,在裸机我们基本上不需要考虑这类问题,毕竟是单线程,不会在调用延时函数delay_ms的中途中又调用一次这个函数delay_ms。但是在操作系统中我们就得考虑这是否会造成冲突了,这个就叫函数的可重入性。
不符合可重入性的函数是什么样的呢,举个例子:
//不满足可重入性的延时函数
void delay_ms(uint32_t count)
{
u32 temp;
SysTick->LOAD=(u32)count*fac_ms; //时间加载(SysTick->LOAD为24bit)
SysTick->VAL =0x00; //清空计数器
SysTick->CTRL=0x05 ; //开始倒数
do
{
temp=SysTick->CTRL;
}while((temp&0x01)&&!(temp&(1<<16))); //等待时间到达,看CTRL的第16位(COUNTFLAG)是否为1,看STRL的第0位(ENABLE)是否为1
SysTick->CTRL=0x00; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
systick是一个24位的倒数定时器,这里通过改变systick的重装载值,计算好重装载值让计数器倒数到0时刚好完成延时所需的时间并触发CTRL的第16位置高。在循环里死等直到检测到CTRL的第16位为1时跳出循环结束延时,然后关闭systick。这个延时函数在裸机上当然没有问题,但是移植到FreeRTOS中就有两个问题:首先systick作为操作系统时钟节拍不能关闭,其次不满足函数可重入性,假设线程A进入delay_ms,计数器重装载值开始倒数计时,这时另一个优先级更高的线程B打断线程A也进入了delay_ms,计数器又重装载值开始倒数计时,是不是就不满足可重入性了?
这个可重入性问题也是可以解决的,只需要换一种方法来延时,这个方法叫时钟摘取法,我们不改变systick的配置,而是不断的读取CTRL和VAL的值,等到计数值满足延时时间要求跳出循环。
//满足可重入性的延时函数
void delay_ms(uint32_t count)
{
uint8_t i;
uint32_t told=SysTick->VAL;
uint32_t ticks;
uint32_t tnow;
uint32_t tcnt=0;
uint32_t reload=SysTick->LOAD;
ticks=(SystemCoreClock/1000)*count;//计算完成延时时间所需的systick计数值
told=SysTick->VAL;//开始延时前记录当前的计数器值
while(1)//循环等待延时结束
{
tnow=SysTick->VAL;
if(tnow!=told)
{
if(tnow<told)
tcnt+=told-tnow;//记录systick的计数值
else
tcnt+=reload+told-tnow;//如果systick重装载了,记录systick的计数值
told=tnow;
if(tcnt>=ticks)//systick的计数值达到所需延时时间
break;//结束延时
}
}
}
工作原理注释里应该写的比较清楚了,如果不了解systick寄存器可以查一下手册,补充一点代码里SystemCoreClock是systick的时钟源即系统时钟,如果是微秒级延时delay_us就是ticks=(SystemCoreClock/1000000)*count。
2、__NOP()延时
在高精度的比如纳秒级延时中,受限于MCU的主频,systick的延时理论值往往和实际值相差甚远,以GD32F305为例,最高120M的系统时钟,理论上计数一次1/120M=8.3ns,可以纳秒延时,但实际上延时1微秒就已经有0.2微秒的误差了,推测是延时函数进栈出栈以及一些运算操作造成了延时误差,想要实现精度更高的ns级延时就需要用__NOP()来实现。比如GD32F305这款芯片120M系统时钟,那么一个机器周期就是1/120M=8.3333ns,调用一条汇编指令__NOP()就多花8.3333ns的时间,笔者通过逻辑分析仪抓波形实测用这个__NOP()实现ns级延时是非常准的,很好用,但是据说存在编译器优化导致延时不准的问题,所以使用__NOP()时最好不要开或者使用等级低的编译器优化。
总结
FreeRTOS的延时函数vTaskDelay()和vTaskDelayUntil()可以通过任务调度在延时的进入阻塞态,执行其它任务,提高CPU利用率,但是任务调度需要花费时间,尤其是频繁的任务调度会浪费大量时间,微秒级或者精度更高的延时也会造成较大的延时误差。
FreeRTOS的延时函数CPU利用率高,但执行效率低,裸机的延时函数执行效率高,但CPU利用率低,因此在选择操作系统和裸机的延时函数时需要我们去权衡利弊。延时时间较长的情况下适用FreeRTOS的延时函数vTaskDelay()和vTaskDelayUntil(),延时时间短的情况下适用裸机的定时器延时函数delay_us()。