软件定时器用于让某个任务定时执行,或者周期性执行。比如设定某个时间后执行某个函数,或者每隔一段时间执行某个函数。由软件定时器执行的函数称为软件定时器的回调函数。
参考资料:
《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.修改定时器的周期和重置软件定时器
在FreeRTOS中开启软件定时器功能:
1.构建FreeRTOS源文件FreeRTOS/ source /timers.c作为项目的一部分。
2. 在“FreeRTOSConfig.h”中将“configUSE_TIMERS”设置为1。
软件定时器回调函数
void ATimerCallback(TimerHandle_t xTimer)
软件定时器回调函数是在定时器服务中执行的,它们应该保持简短,并且不能进入阻塞态。定时器服务阻塞会影响内核,因此不能调用任何会导致阻塞的函数,比如vTaskDelay()。可以调用xQueueReceive()等函数,但前提是函数的xTicksToWait参数(指定函数的阻塞时间)设置为0。
一个软件定时器的“周期”是指软件定时器被启动和软件定时器的回调函数执行之间的时间。
单次定时器(一次性 one-shot)和周期性定时器(自动重载 Auto/-reload):
1.单次定时器只执行一次回调函数。可以手动重启,但不会自动重启。
2. 周期性定时器将在每次到期时重新启动自己,从而周期性地执行其回调函数。
软件定时器可以处于以下两种状态之一:
1.休眠
休眠状态的软件定时器,是指一个软件定时器存在,且可以通过定时器句柄被引用,但是它并没有运行,所以它的回调函数不会执行。
2.运行
运行状态的软件定时器,根据设定的参数,到期运行一次或者周期性运行回调函数。
周期性定时器执行了回调函数后自动重新进入运行状态。
单次定时器执行过回调函数后就会进入休眠状态
所有软件定时器回调函数都在同一个RTOS守护(或'定时器服务')任务的上下文中执行。(在Linux上叫守护进程,FreeRTOS里称作任务)
守护任务是一个标准的FreeRTOS任务,在启动调度器时自动创建。它的优先级和堆栈大小分别由FreeRTOSConfig.h中的configTIMER_TASK_PRIORITY和configTIMER_TASK_STACK_DEPTH设置。
软件定时器回调函数不能调用会导致调用任务进入阻塞状态的FreeRTOS API函数,因为这样会导致守护任务进入阻塞态。
一个任务调用软件定时器的API函数向守护任务发送命令,这个命令会被存在一个队列里,这个队列就叫定时器命令队列。
命令示例“启动计时器”、“停止计时器”和“重置计时器”。
定时器命令队列是一个标准的FreeRTOS队列,在启动调度器时自动创建。定时器命令队列的长度由FreeRTOSConfig.h中的configTIMER_QUEUE_LENGTH设置。
守护任务像任何其他FreeRTOS任务一样调度。它只处理命令,或者当它是能够运行的最高优先级任务时,执行定时器回调函数。
如图,Task1运行在Task1中调用定时器API函数向守护任务发送启动定时器命令。因为守护任务优先级没有Task1高,所以守护任务不会立即处理命令,而是等到t4时,Task1进入阻塞态,守护任务才开始处理命令
假如守护任务优先级高的话,一旦发送命令,就切换到守护任务了,所以定时器也就立即启动了。
注意了,定时器的超时时间不是从守护任务接收到命令开始算的,而是从发送时间开始算的。
实际上发送的命令里包含了一个时间戳。时间戳记录了发送时间。例如,如果发送一个启动一个周期为10ms的定时器的命令,时间戳可以保证是发送后的10ms而不是守护任务处理命令后的10ms。
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( ;; );
}
效果:
前文讲了每个软件定时器都有一个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,当等于五时停止定时器,效果如下:
软件定时器的周期可以使用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.驱动的分层分离思想