FreeRTOS基础五:软件定时器
软件定时器简介
软件定时器的作用:在指定的时间到来时执行指定的函数,或者以某个频率周期性地执行某个函数。被执行的函数叫做软件定时器回调函数。
软件定时器由FreeRTOS内核实现,不需要硬件支持。软件定时器只有在软件定时器回调函数被调用时才需要占用CPU时间。
在FreeRTOS应用程序中使用软件定时器,则需要:
软件定时器回调函数
软件定时器回调函数的原型如下:
//参数xTimer :因定时到期而调用这个回调函数的定时器的句柄
void ATimerCallback( TimerHandle_t xTimer );
忠告:软件定时器回调函数是在软件定时器任务中被执行的,这个任务是在vTaskStartScheduler()函数内部由内核自动创建的。不要在回调函数中使用一些导致任务阻塞的函数或代码,例如vTaskDelay(),否则会导致FreeRTOS后台任务进入到阻塞状态。而且应该尽量让定时器回调函代码简洁高效快速执行。
软件定时器任务
软件定时器任务现在最准确的叫法应该叫做FreeRTOS后台任务(FreeRTOS daemon task),因为早期的FreeRTOS专门用这个任务去实现软件定时器,而随着FreeRTOS版本更新,这个任务也用于执行一些其他的操作。本文为了便于理解都叫软件定时器任务。
在FreeRTOS中,可以创建多个软件定时器,所有的软件定时器回调函数都执行在软件定时器任务上下文环境中。
软件定时器任务在调度器启动时由内核自动创建,它是一个标准的FreeRTOS任务。当调用vTaskStartScheduler()来开启调度器后,这个函数内部使用xTimerCreateTimerTask()来创建软件定时器任务。
软件定时器任务的任务优先级和栈深度是在FreeRTOSConfig.h中通过configTIMER_TASK_PRIORITY和configTIMER_TASK_STACK_DEPTH这两个宏配置的。
软件定时器的属性
软件定时器的类型
如下图所示,Timer1是单次触发定时器,定时时间为6个tick;Timer2是自动重装定时器,定时时间为5个tick。两个定时器都在t1时刻启动。Timer1的回调函数在t7时刻执行一次就不再执行了,而Timer2的回调函数则会以5个tick的固定周期反复执行。
软件定时器的状态
如下图所示,单次触发定时器和自动重装定时器主要的区别在于,当定时到期后,自动重装定时器会执行回调函数并再次进入到运行态,而单次触发定时器则会执行回调函数并进入到休眠状态。
软件定时器命令队列
软件定时器的相关API本质上是操作队列的API。当FreeRTOS调度器启动时,内核除了自动创建软件定时器任务外,还会自动创建一个软件定时器命令队列。在用户任务中使用软件定时器相关的API,例如启动定时器,停止定时器,复位定时器,本质上就是通过这个队列来向软件定时器任务发送相关消息。
软件定时器命令队列的长度在FreeRTOSConfig.h中使用configTIMER_QUEUE_LENGTH来定义。
所以软件定时器大部分情况下阻塞状态,只有当用户发送了一个软件定时器命令或者一个软件定时器定时器到期需要执行回调函数时,才会被唤醒进入就绪状态。
那么为什么说不能在定时器回调函数中使用让软件定时器阻塞的代码呢?因为软件定时器任务的状态只应该由内核或者定时器API来控制而不能由用户代码控制,这样才能保证软件定时器的能正常使用。
软件定时器的使用
创建软件定时器
使用xTimerCreate()函数来创建一个软件定时器对象,并返回它的句柄。创建好的软件定时器的初始状态为休眠状态。可以在FreeRTOS调度器运行之前创建软件定时器对象,也可以在一个任务中创建软件定时器对象。
TimerHandle_t xTimerCreate( const char * const pcTimerName,
TickType_t xTimerPeriodInTicks,
UBaseType_t uxAutoReload,
void * pvTimerID,
TimerCallbackFunction_t pxCallbackFunction );
参数pcTimerName:软件定时器的名称,仅仅用于调试目的,内核不使用这个参数。
参数xTimerPeriodInTicks:软件定时器的定时周期,也就是从定时器启动后多少个tick后定时到期。开发者可以使用pdMS_TO_TICKS()来将毫秒转换为tick数。
参数uxAutoReload:pdTRUE来创建自动重装定时器;pdFALSE来创建单次定时器。
参数pvTimerID:定时器ID。当多个软件定时器使用同一个回调函数时,此参数非常有用。如果不需要使用则传递NULL即可。
参数pxCallbackFunction:回调函数的指针
返回值:返回NULL代表没有足够的堆内存来创建当前定时器;否则返回当前定时器的句柄。
启动软件定时器
使用xTimerStart() 来让一个处于休眠状态的定时器开始运行,也可以让一个处于运行状态的定时器重新开始计数运行。可以在调度器运行之前调用xTimerStart(),但是只有当调度器运行后,定时器才能开始运行。
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer, TickType_t xTicksToWait );
参数xTimer:定时器的句柄。
参数xTicksToWait:xTimerStart()函数本质上是向软件定时器队列中放一个启动定时器的命令,这个参数值就是当队列满时,调用这个函数的任务阻塞并等待队列有空闲空间可以接收新的指令时阻塞的tick数。如果参数为0,则当队列满时,这个函数会立刻返回而不阻塞调用者任务。如果参数为portMAX_DELAY(需要在FreeRTOSConfig.h中将INCLUDE_vTaskSuspend定义为1),则当队列一直满时等待时间为永久等待并阻塞。如果在调度器运行之前调用xTimerStart(),则此参数作用失效,等价于参数为0。
返回值:当定时器启动指令成功发送到软件定时器队列时,返回pdPASS。如果xTicksToWait不为0,则调用者任务可能会阻塞,但是在阻塞时间超时前命令队列有空闲空间使得指令成功发送了,则也是会返回pdPASS的。当定时器启动指令没有发送到软件定时器队列时,返回pdFAIL。如果xTicksToWait不为0,则调用者任务可能一直阻塞,直到阻塞时间超时,命令队列也没有空闲空间,则返回pdFAIL。
复位软件定时器
函数xTimerReset()用于复位一个已经存在的软件定时器对象。如果软件定时器正在运行,则调用后软件定时器将立刻从新开始一个定时周期,从头开始计时。如果定时器处于休眠状态,则调用效果和调用xTimerStart()的一样。所以调用xTimerReset()成功后会确保这个定时器一定是处于运行状态。
//参数xTimer:软件定时器的句柄
//参数xTicksToWait:参见xTimerStart()函数的解释
//返回值:参见xTimerStart()函数的解释
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );
停止软件定时器
使用xTimerStop()来停止一个正在运行的软件定时器。调用xTimerStop()成功后会确保这个定时器一定是非运行状态(休眠状态)。
//参数xTimer:软件定时器的句柄
//参数xTicksToWait:参见xTimerStart()函数的解释
//返回值:参见xTimerStart()函数的解释
BaseType_t xTimerStop( TimerHandle_t xTimer,
TickType_t xTicksToWait);
修改软件定时器定时周期
使用xTimerChangePeriod()来修改一个已经存在的软件定时器的定时周期。无论之前定时器的状态是什么,此函数调用成功后,定时器将变为运行态,开始运行。
//参数xTimer:软件定时器的句柄
//参数xNewPeriod:新的定时周期,以tick为单位。
//参数xTicksToWait:参见xTimerStart()函数的解释
//返回值:参见xTimerStart()函数的解释
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xTicksToWait);
删除软件定时器
使用xTimerDelete()函数来删除一个已经存在的软件定时器,软件定时器可以在任何时间被删除。
//参数xTimer:软件定时器的句柄
//参数xTicksToWait:参见xTimerStart()函数的解释
//返回值:参见xTimerStart()函数的解释
BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );
定时器ID
在使用软件xTimerCreate()来创建软件定时器时需要指定一个void*类型的TimerID参数,所以可以使用整数值作为ID参数,或者任何类型的指针(字符串指针,函数指针等)。
定时器ID可以当做区分不同定时器的标志,因为多个定时器实例可以使用同一个定时器回调函数,这个时候在回调函数中可以读取定时器的ID来区别到底是哪个定时器到期了。
可以使用vTimerSetTimerID()来修改软件定时器的ID,也可以使用pvTimerGetTimerID()来读取一个软件定时器的ID。这两个函数直接作用在定时器对象上,而不需要通过软件定时器队列来工作。
//参数xTimer:软件定时器定时器句柄
//参数pvNewID:新的ID值
void vTimerSetTimerID( const TimerHandle_t xTimer, void *pvNewID );
//参数xTimer:软件定时器定时器句柄
//返回:软件定时器的ID值
void *pvTimerGetTimerID( TimerHandle_t xTimer );
软件定时器的使用心得
1、软件定时器的最小定时周期等于一个tick周期。如果你需要更短的定时周期则考虑使用硬件定时器。
2、软件定时器的精度一般没有硬件定时器的高,但基本可以满足一般定时需求。提高软件定时器任务的优先级为最高可以提高软件定时器的精度。
3、软件定时器一般用于处理一些逻辑不复杂的,简短的定时任务,它的优点是简单方便,占用的系统资源低,缺点是不够灵活,扩展性不强,当业务逻辑复杂时,考虑使用单独的任务去做。
4、不能在软件定时器回调函数中执行让软件定时器任务阻塞的代码。要尽量保持回调函数简短高效执行。
5、软件定时器可以开多个,理论上只要FreeRTOS的堆内存足够,就可以继续创建新的软件定时器。
6、软件定时器的相关API同样有两个版本,普通版本和中断安全版本(FromISR),使用时需要注意在中断函数中只能使用中断安全版本的。