FreeRTOS学习笔记九【软件定时器】
软降定时器用于在将来某个时间点或者以固定频率执行指定的函数。它执行的函数称为回调函数。软件定时器由内核实现并受其控制,它不需要硬件支持,与硬件定时器或计数器无关。在FreeRTOS中,除非软件定时器的回调函数真正执行,否则软件定时器不会使用任何CPU时间。软件定时器的功能是可选的,需要在项目中使用它时:
- 将源文件中的FreeRTOS/Source/timers.c添加到项目中。
- 将FreeRTOSConfig.h中的configUSE_TIMERS设置为1。
目的
- 软件定时器的特征与任务的特征比较。
- RTOS守护程序任务。
- 定时器命令队列。
- 一次性软件定时器和固定频率软件定时器的区别。
- 如何创建、启动、重置定时器和更改软件计时器的周期。
软件定时器的属性与状态
定时周期
软件定时的定时周期就是软件定时器启动到它执行回调函数之间的时间。
一次性定时器和固定频率定时器
- 一次性定时器
一次性定时器一旦启动,将只会执行依次回调函数,它不会自动重启,但是可以手动重启。 - 固定的频率定时器(自动重载定时器)
固定频率定时器在每次到期后自动重启,从而使回调函数以固定频率执行。
下图展示了两种定时的差异。
图中定时器1使一次性定时器,它的定时周期是6个滴答周期,从t1启动,知道t7才执行回调函数。执行完回调函数后不会再次执行。定时器2是一个自动重载定时器,定时周期为5个滴答周期。 它在t1启动,回调函数t5、t11、t16等时刻执行。
软件定时器的状态
软件计时器可以处于以下两种状态:
- 休眠态
休眠的软件定时器存在于内存中,可以通过它的句柄使用,但是没有运行,不会执行回调函数。 - 运行态
运行中的软件定时器会在一段时间后执行回调函数,如果是自动重载定时器将会重启,定时周期到达后再次执行回调函数。
下面两个图展示了两种软件定时器的状态转换。
自动重载定期器的状态转换图:
一次性定时器状态转换图:
在任何时候都可以使用xTimerDelete()删除软件定时器。
软件定时器的上下文
软件定时器的回调函数
回调函数是通过C函数实现的,它的原型如下:
void ATimerCallback( TimerHandle_t xTimer );
回调函数应该保持简洁,同时不能进入阻塞状态。
注意:回调函数不能电泳FreeRTOS的API函数,这些函数会使任务进入阻塞态,但是可以调用xQueueReceive()等函数,其中xTicksToWait必须设置为0,不能调用vTaskDelay()等函数。
RTOS守护程序(定时服务)任务
所有软件定时器的回调函数都在RTOS守护程序(定时服务)任务的上下文中执行。守护程序任务是在启动调度程序自动创建的标准FreeRTOS任务,其优先级和堆栈大小分别由configTIMER_TASK_PRIORITY和configTIMER_TASK_STACK_DEPTH设置。在软件定时器的回调函数中不能调用FreeRTOS的API函数,如果调用将会使守护程序任务进入阻塞状态。
定时器命令队列
软件定时器的API函数通过“定时器命令队列”将各命令从调用任务发送到守护程序任务中。
该队列是一个标准的FreeRTOS队列,它在调度程序启动时自动创建,长度由FreeRTOSConfig.h中的configTIMER_QUEUE_LENGTH配置。命令包括启动定时器,停止计时器,重置计时器。下图描述了该队列。
守护程序任务的调度
守护程序任务的调度和其他任务一样,当它是优先级最高的就绪任务时,调度程序将调度它让它进入运行态。下面将分别描述configTIMER_TASK_PRIORITY对调度的影响。
下图展示了守护程序任务的优先级低于调用xTimerStart()函数的任务的优先级时的模式。
图中,任务1的优先级高于守护程序任务的优先级,守护程序任务的优先级高于空闲任务的优先级。
- 在t1时刻
任务1处于运行态,并且守护程序处于阻塞态。如果有命令发送到定时器命令队列或者定时时间达到,守护程序任务将进入就绪态,处理命令或执行回调函数。 - 在t2时刻
任务1调用xTimerStart(),该函数向定时器的命令队列发送启动命令,守护进程任务进入就绪态,但任务1的优先级高于守护进程任务的优先级,所以它不会抢占任务1。 - 在t3时刻
任务1执行完xTimerStart()函数,但任务1并没有还没有进入阻塞态。 - 在t4时刻
任务1进入阻塞态,由于守护进程任务是现在优先级最高的就绪任务,因此它进入运行态,处理任务1发送给它的命令。
注意:软件定时器的启动时间是从启动命令发送到队列的时间开始计算,而不是守护进程处理该命令的时间。 - 在t5时刻
守护进程任务处理完命令,因此它再次进入阻塞态,等待命令或定时时间到达。此时空闲任务是优先级最高的就绪任务,所以他将进入运行态。
下图描述了守护进程任务的优先级高于调用xTimerStart()的任务的优先级的情况。
图中守护进程任务的优先级高于任务1的优先级,任务1的优先级高于空闲任务的优先级。
- 在t1时刻
任务1处于运行态,守护进程任务处于阻塞态。 - 在t2时刻
任务1调用xTimerStart()函数,它向队列中发送了定时器启动命令,守护进程进入就绪态,因为守护进程任务的优先级高于任务1的优先级,所以守护进程任务立即进入运行态,处理队列中的命令。 - 在t3时刻
守护进程任务完成队列中的命令处理,因此它进入阻塞态,任务1重新进入运行态。 - 在t4时刻
任务1执行完xTimerStart()函数,然后继续执行其他操作。 - 在t5时刻
任务1进入阻塞态,空闲任务开始执行。
软件定时器的使用
xTimerCreate()
在使用软件定时器之前必须先创建它,可以在启动调度程序前创建,也可以在启动调度程序后创建。xTimerCreate()的原型如下:
TimerHandle_t xTimerCreate( const char * const pcTimerName,
TickType_t xTimerPeriodInTicks,
UBaseType_t uxAutoReload,
void * pvTimerID,
TimerCallbackFunction_t pxCallbackFunction );
参数:
- pcTimerName
软件定时器的名称,内核中不使用它,仅用于调试。 - xTimerPeriodInTicks
定时周期,可以使用pdMS_TO_TICKS() 转换。 - uxAutoReload
设置为pdTRUE表示该定时器为自动重载定时器,设置为pdFALSE该定时器为一次性定时器。 - pvTimerID
每个软件定时器都有一个ID值,他是一个void指针,可以由应用程序决定它的用途。pvTimerID表示该定时器ID的初始值。 - pxCallbackFunction
定时器的回调函数指针。
返回值:
- 非NULL,定时器创建成功,返回的是该定时器的句柄。
- NULL,定时器创建失败,可能是没有足够内存。
xTimerStart()
xTimerStart()用于启动处于休眠态的软件定时器,或重置(重新启动)处于运行态的软件定时器。 xTimerStop()用于停止处于运行态的软件计时器。 可以在启动调度程序之前调用xTimerStart(),但该操作会在调度程序启动之前启动定时器。
注意:不能在中断函数中调用xTimerStart(),中断安全版为xTimerStartFromISR() 。
xTimerStart()的原型:
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
参数:
- xTimer
定时器的句柄。 - xTicksToWait
该参数与入队API( xQueueSendToBack()等)中的参数作用相同。
返回值: - pdPASS
启动定时器成功。 - pdFALSE
启动定时器失败,命令队列为满,同时设定的等待时间也到达。
创建与启动定时器的示例
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++;
}
/* 定时的周期 */
#define mainONE_SHOT_TIMER_PERIOD pdMS_TO_TICKS( 3333 )
#define mainAUTO_RELOAD_TIMER_PERIOD pdMS_TO_TICKS( 500 )
int main( void )
{
TimerHandle_t xAutoReloadTimer, xOneShotTimer;
BaseType_t xTimer1Started, xTimer2Started;
/* 创建一次性定时器 */
xOneShotTimer = xTimerCreate(
/* 定时器的名称 */
"OneShot",
/* 定时周期 */
mainONE_SHOT_TIMER_PERIOD,
/* 设置为pdFALSE表示一次性定时器 */
pdFALSE,
/* ID初始为0 */
0,
/* 回调函数 */
prvOneShotTimerCallback );
/* 创建自动重载定时器 */
xAutoReloadTimer = xTimerCreate("AutoReload",
mainAUTO_RELOAD_TIMER_PERIOD,
/* 设置为pdTRUE表示自动重载定时器 */
pdTRUE,
0,
prvAutoReloadTimerCallback );
/* Check the software timers were created. */
if( ( xOneShotTimer != NULL ) && ( xAutoReloadTimer != NULL ) )
{
/* 启动两个定时器 */
xTimer1Started = xTimerStart( xOneShotTimer, 0 );
xTimer2Started = xTimerStart( xAutoReloadTimer, 0 );
/* 如果两个定时器都启动成功,则启动调度程序 */
if( ( xTimer1Started == pdPASS ) && ( xTimer2Started == pdPASS ) )
{
vTaskStartScheduler();
}
}
for( ;; );
}
运行结果:
xTimerChangePeriod()
调用xTimerChangePeriod()函数可以更改软件计时器的周期。如果更改的定时器已经处于运行态,将会重新计算定时器的到期时间,重新计算的起始时间与调用该函数的时间有关,而不是定时器最开始的启动时间,如果更改的定时器处于休眠状态,则定时器将启动。
注意:不要在中断中使用xTimerChangePeriod(),而应该使用中断安全的版本xTimerChangePeriodFromISR()。
xTimerChangePeriod()的原型如下:
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
TickType_t xNewTimerPeriodInTicks,
TickType_t xTicksToWait );
参数:
- xTimer
定时器句柄。 - xNewTimerPeriodInTicks
定时器新的定时周期,可以使用pdMS_TO_TICKS()转换时间。 - xTicksToWait
该参数与xTimerStart()的xTicksToWait类似。
返回值:
- pdPASS
更改成功。 - pdFALSE
更改失败,命令队列为满,并且设定的xTicksToWait时间也到达。
xTimerReset()
重置定时器表示重新启动定时器,定时器的到期时间将重新计算,重新计算的起始时间为调用该函数的时间,而不是定时器的启动时间。该函数也有中断安全版,因此不能在中断中调用该函数,而应调用中断安全版。
函数原型如下:
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );
函数的返回值和参数与xTimerStart()函数的返回值和参数类似。
定时器ID
每个软件定时器都有一个ID,它可以被应用程序用于任意用途,ID存储在void中,因此可以用于存放整数值,或指向任何对象。在创建软件定时器时为ID分配初始值,之后可以使用vTimerSetTimerID()更行ID值,或使用pvTimerGetTimerID()查询ID值。这两个API与其他软件定时器的API不同,它们是直接访问软件定时器,不会向定时器队列发送命令。
vTimerSetTimerID()
void vTimerSetTimerID( const TimerHandle_t xTimer, void *pvNewID );
参数:
- xTimer
软件定时器的句柄。 - pvNewID
新的ID的。
pvTimerGetTimerID()
void *pvTimerGetTimerID( TimerHandle_t xTimer );
参数:
- xTimer
软件定时器的句柄。
返回值:
- 定时器ID的指针。
使用示例
static void prvTimerCallback( TimerHandle_t xTimer )
{
TickType_t xTimeNow;
uint32_t ulExecutionCount;
/* 获取执行回调函数的定时器的ID,并将其转换为uint32_t整数*/
ulExecutionCount = ( uint32_t ) pvTimerGetTimerID( xTimer );
ulExecutionCount++;
/* 设置新的ID值 */
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 );
}
}
}
/* 定时的周期 */
#define mainONE_SHOT_TIMER_PERIOD pdMS_TO_TICKS( 3333 )
#define mainAUTO_RELOAD_TIMER_PERIOD pdMS_TO_TICKS( 500 )
int main( void )
{
TimerHandle_t xAutoReloadTimer, xOneShotTimer;
BaseType_t xTimer1Started, xTimer2Started;
/* 创建一次性定时器 */
xOneShotTimer = xTimerCreate(
/* 定时器的名称 */
"OneShot",
/* 定时周期 */
mainONE_SHOT_TIMER_PERIOD,
/* 设置为pdFALSE表示一次性定时器 */
pdFALSE,
/* ID初始为0 */
0,
/* 回调函数 */
prvTimerCallback );
/* 创建自动重载定时器 */
xAutoReloadTimer = xTimerCreate("AutoReload",
mainAUTO_RELOAD_TIMER_PERIOD,
/* 设置为pdTRUE表示自动重载定时器 */
pdTRUE,
0,
prvTimerCallback );
/* Check the software timers were created. */
if( ( xOneShotTimer != NULL ) && ( xAutoReloadTimer != NULL ) )
{
/* 启动两个定时器 */
xTimer1Started = xTimerStart( xOneShotTimer, 0 );
xTimer2Started = xTimerStart( xAutoReloadTimer, 0 );
/* 如果两个定时器都启动成功,则启动调度程序 */
if( ( xTimer1Started == pdPASS ) && ( xTimer2Started == pdPASS ) )
{
vTaskStartScheduler();
}
}
for( ;; );
}
运行结果: