> 文章列表 > 详解FreeRTOS中的软件定时器

详解FreeRTOS中的软件定时器

详解FreeRTOS中的软件定时器

软件定时器用于让某个任务定时执行,或者周期性执行。比如设定某个时间后执行某个函数,或者每隔一段时间执行某个函数。由软件定时器执行的函数称为软件定时器的回调函数

参考资料:

《Mastering the FreeRTOS™ Real Time Kernel》——Chapter 5 Software Timer Management

FreeRTOS全解析-6.软件定时器

目录

1.软件定时器的属性和状态

1.1软件定时器的周期

1.2软件定时器状态

1.3软件定时器的执行环境(上下文Context)

1.3.1RTOS守护任务(Daemon Task)(定时器服务Time Sevice)

1.3.2定时器命令队列

1.3.3守护任务调度

2.创建并启动软件定时器

3.定时器ID(Timer ID)

4.修改定时器的周期和重置软件定时器


1.软件定时器的属性和状态

在FreeRTOS中开启软件定时器功能:

1.构建FreeRTOS源文件FreeRTOS/ source /timers.c作为项目的一部分。

2. 在“FreeRTOSConfig.h”中将“configUSE_TIMERS”设置为1。

软件定时器回调函数

void ATimerCallback(TimerHandle_t xTimer)

软件定时器回调函数是在定时器服务中执行的,它们应该保持简短,并且不能进入阻塞态。定时器服务阻塞会影响内核,因此不能调用任何会导致阻塞的函数,比如vTaskDelay()。可以调用xQueueReceive()等函数,但前提是函数的xTicksToWait参数(指定函数的阻塞时间)设置为0。

1.1软件定时器的周期

一个软件定时器的“周期”是指软件定时器被启动和软件定时器的回调函数执行之间的时间。

单次定时器(一次性 one-shot)和周期性定时器(自动重载 Auto/-reload):

1.单次定时器只执行一次回调函数。可以手动重启,但不会自动重启。

2. 周期性定时器将在每次到期时重新启动自己,从而周期性地执行其回调函数。

1.2软件定时器状态

软件定时器可以处于以下两种状态之一:

1.休眠

休眠状态的软件定时器,是指一个软件定时器存在,且可以通过定时器句柄被引用,但是它并没有运行,所以它的回调函数不会执行。

2.运行

运行状态的软件定时器,根据设定的参数,到期运行一次或者周期性运行回调函数。

周期性定时器执行了回调函数后自动重新进入运行状态。

单次定时器执行过回调函数后就会进入休眠状态

1.3软件定时器的执行环境(上下文Context)

1.3.1RTOS守护任务(Daemon Task)(定时器服务Time Sevice)

所有软件定时器回调函数都在同一个RTOS守护(或'定时器服务')任务的上下文中执行。(在Linux上叫守护进程,FreeRTOS里称作任务)

守护任务是一个标准的FreeRTOS任务,在启动调度器时自动创建。它的优先级和堆栈大小分别由FreeRTOSConfig.h中的configTIMER_TASK_PRIORITY和configTIMER_TASK_STACK_DEPTH设置。

软件定时器回调函数不能调用会导致调用任务进入阻塞状态的FreeRTOS API函数,因为这样会导致守护任务进入阻塞态。

1.3.2定时器命令队列

一个任务调用软件定时器的API函数向守护任务发送命令,这个命令会被存在一个队列里,这个队列就叫定时器命令队列

命令示例“启动计时器”、“停止计时器”和“重置计时器”。

定时器命令队列是一个标准的FreeRTOS队列,在启动调度器时自动创建。定时器命令队列的长度由FreeRTOSConfig.h中的configTIMER_QUEUE_LENGTH设置。

1.3.3守护任务调度

守护任务像任何其他FreeRTOS任务一样调度。它只处理命令,或者当它是能够运行的最高优先级任务时,执行定时器回调函数。

如图,Task1运行在Task1中调用定时器API函数向守护任务发送启动定时器命令。因为守护任务优先级没有Task1高,所以守护任务不会立即处理命令,而是等到t4时,Task1进入阻塞态,守护任务才开始处理命令

假如守护任务优先级高的话,一旦发送命令,就切换到守护任务了,所以定时器也就立即启动了。

注意了,定时器的超时时间不是从守护任务接收到命令开始算的,而是从发送时间开始算的。

实际上发送的命令里包含了一个时间戳。时间戳记录了发送时间。例如,如果发送一个启动一个周期为10ms的定时器的命令,时间戳可以保证是发送后的10ms而不是守护任务处理命令后的10ms。

2.创建并启动软件定时器

xTimerCreate()用于创建一个软件计时器,并返回一个TimerHandle_t(软件定时器句柄)。软件定时器创建的时候是休眠状态,并没有立即启动。

软件计时器可以在调度程序运行之前创建,也可以在启动调度程序之后从任务中创建。

TimerHandle_t xTimerCreate( const char * const pcTimerName,                            TickType_t xTimerPeriodInTicks,                            UBaseType_t uxAutoReload,                            void * pvTimerID,                            TimerCallbackFunction_t pxCallbackFunction );
参数 作用
pcTimerName 软件定时器的名字,FreeRTOS不会用到,便于自己记忆就行
xTimerPeriodInTicks 以tick为单位指定的计时器周期。pdMS_TO_TICKS()宏可用于将以毫秒为单位指定的时间转换为以tick为单位指定的时间。
uxAutoReload 设置为pdTRUE创建周期(自动重载)计时器。设置为pdFALSE以创建单次(一次性)计时器。
pvTimerID 每个软件定时器都有一个ID值。ID是一个空指针,应用程序编写人员可以将其用于任何目的。当同一个回调函数被多个软件计时器使用时,ID特别有用,因为它可以用于提供计时器特定的存储。后面演示。
pxCallbackFunction 回调函数指针
返回值 如果返回NULL,则不能创建软件计时器,因为没有足够的堆内存。返回非NULL值表示软件计时器已经创建成功。返回值是已创建计时器的句柄。

xTimerStart()用于启动处于休眠状态的软件定时器,或重置(重新启动)处于运行状态的软件定时器。可以在启动调度器之前调用xTimerStart(),但是软件定时器在启动调度器之前不会实际启动。

BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );

这个函数的底层其实就是上期讲的队列发送FreeRTOS全解析-5.队列(Queue)

参数的意思也就显而易见了。

参数 作用
xTimer 软件定时器句柄。就是创建定时器的返回值。
xTicksToWait

指定如果队列已满,则调用任务应保持在Blocked状态等待的最大时间。

如果xTicksToWait为零且定时器命令队列已满,xTimerStart()将立即返回。

那么将xTicksToWait设置为portMAX_DELAY将导致调用任务无限期地保持在Blocked状态(没有超时),以等待timer命令队列中的可用空间。

和队列一样要使用portMAX_DELAY宏就要先FreeRTOSConfig.ht中的INCLUDE_vTaskSuspend设置为1

如果在启动调度器之前调用xTimerStart(),那么xTicksToWait的值将被忽略,xTimerStart()的行为就像xTicksToWait已被设置为零一样。

返回值

1.pdPASS命令成功发送。

2.pdFALSE队列已满无法写入。

xTimerStop()用于停止处于运行状态的软件定时器。停止软件计时器与将计时器转换为休眠状态相同。

例子如下:

程序创建了两个定时器,一个是单次的,一个是周期性的,回调函数里打印时间。

#define mainONE_SHOT_TIMER_PERIOD pdMS_TO_TICKS( 3333 )#define mainAUTO_RELOAD_TIMER_PERIOD pdMS_TO_TICKS( 500 )static void prvOneShotTimerCallback( TimerHandle_t xTimer ){  TickType_t xTimeNow;  xTimeNow = xTaskGetTickCount();  vPrintStringAndNumber( "One-shot timer callback executing", xTimeNow );  ulCallCount++;}static void prvAutoReloadTimerCallback( TimerHandle_t xTimer )  TickType_t xTimeNow;  xTimeNow = uxTaskGetTickCount();  vPrintStringAndNumber( "Auto-reload timer callback executing", xTimeNow );  ulCallCount++;}int main( void ){  TimerHandle_t xAutoReloadTimer, xOneShotTimer;  BaseType_t xTimer1Started, xTimer2Started;  xOneShotTimer = xTimerCreate("OneShot",mainONE_SHOT_TIMER_PERIOD,pdFALSE,0,prvOneShotTimerCallback );  xAutoReloadTimer = xTimerCreate("AutoReload",mainAUTO_RELOAD_TIMER_PERIOD,pdTRUE,0,prvAutoReloadTimerCallback );  if( ( xOneShotTimer != NULL ) && ( xAutoReloadTimer != NULL ) )  {    xTimer1Started = xTimerStart( xOneShotTimer, 0 );    xTimer2Started = xTimerStart( xAutoReloadTimer, 0 );      if( ( xTimer1Started == pdPASS ) && ( xTimer2Started == pdPASS ) )      {        vTaskStartScheduler();      }    }  for( ;; );}

效果:

3.定时器ID(Timer ID)

前文讲了每个软件定时器都有一个ID值。ID是一个空指针,应用程序编写人员可以将其用于任何目的。因为ID存储在void指针(void *)中,因此可以直接存储整数值,指向任何其他对象,或用作函数指针。

在 用函数xTimerCreate创建软件计时器时,会为ID分配一个初始值。在此之后,可以使用vTimerSetTimerlD() API函数更新ID,并使用pvTimerGetTimerID()来查询ID。

与其他软件定时器API函数不同,vTimerSetTimerlD()和pvTimerGetTimerlD()直接访问软件定时器——它们不向定时器命令队列发送命令。

void vTimerSetTimerID( const TimerHandle_t xTimer, void *pvNewID );void *pvTimerGetTimerID( TimerHandle_t xTimer );

例子:

static void prvTimerCallback( TimerHandle_t xTimer ){  TickType_t xTimeNow;  uint32_t ulExecutionCount;  ulExecutionCount = ( uint32_t ) pvTimerGetTimerID( xTimer );  ulExecutionCount++;  vTimerSetTimerID( xTimer, ( void * ) ulExecutionCount );  xTimeNow = xTaskGetTickCount();  if( xTimer == xOneShotTimer ) {    vPrintStringAndNumber( "One-shot timer callback executing", xTimeNow );  } else {    vPrintStringAndNumber( "Auto-reload timer callback executing", xTimeNow );    if( ulExecutionCount == 5 ) {      xTimerStop( xTimer, 0 );    }  }}

把定时器回调函数改成如上,把ID当做回调函数运行次数的计数,每次运行都取出ID并且加一,然后更新ID,当等于五时停止定时器,效果如下:

4.修改定时器的周期和重置软件定时器

软件定时器的周期可以使用xTimerChangePeriod()函数来改变。

如果使用xTimerChangePeriod()来更改已经在运行的计时器的周期,则计时器将使用新的周期值重新计算到期时间。重新计算的到期时间相对于调用xTimerChangePeriod()的时间,而不是相对于最初启动计时器的时间。

如果使用xTimerChangePeriod()来改变处于休眠状态(未运行的计时器)的周期,那么计时器将计算到期时间,并转换到运行状态(计时器将开始运行)。

BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,                              TickType_t xNewTimerPeriodInTicks,                              TickType_t xTicksToWait );

xTimerReset()用于重置定时器,重置软件定时器意味着重新启动定时器;计时器的到期时间被重新计算为相对于计时器重置的时间,而不是计时器最初启动的时间。

BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );

往期精彩:

嵌入式C语言几个重点(const、static、voliatile、位运算)

交叉编译环境、bootloader、kernel、根文件系统是什么?有什么联系?

嵌入式Linux驱动学习-7.什么是设备树?

从Linux内核中学习高级C语言宏技巧

嵌入式Linux驱动学习-5.驱动的分层分离思想

环保材料网