FreeRTOS 软件定时器
FreeRTOS 也提供了定时器功能,不过是软件定时器,软件定时器的精度肯定没有硬件定时器那么高,但是对于普通的精度要求不高的周期性处理的任务来说够了。当 MCU 的硬件定时器不够的时候就可以考虑使用 FreeRTOS 的软件定时器
软件定时器简介
软件定时器允许设置一段时间,当设置的时间到达之后就执行指定的功能函数,被定时器调用的这个功能函数叫做定时器的回调函数。回调函数的两次执行间隔叫做定时器的定时周期,简而言之,当定时器的定时周期到了以后就会执行回调函数。
在实时操作系统(RTOS)中,软件定时器的设计通常是为了在特定的时间点执行一段代码,而这段代码是通过定时器的回调函数来实现的。定时器服务任务(或称为定时器守护任务)是一个特殊的任务,负责管理和执行所有软件定时器的回调函数。这里面涉及到几个关键的概念和原因,解释为什么在定时器回调函数中调用可能会阻塞任务的API函数是不合适的:
- 任务优先级
定时器服务任务通常运行在一个相对较低的优先级。如果在定时器的回调函数中调用了阻塞API(如vTaskDelay()
或访问队列和信号量的阻塞调用),这将导致定时器服务任务被阻塞。因为定时器服务任务负责处理所有定时器的回调,其被阻塞意味着所有的定时器回调都会被延迟执行,这会影响到系统中其他定时器的准确性和可靠性。
- 资源共享和死锁
在定时器回调函数中调用可能导致阻塞的API,尤其是那些涉及到资源访问(如队列、信号量等)的API,可能会引起死锁。因为定时器服务任务和其他任务可能会同时试图访问同一资源,如果没有合适的同步机制,这就可能导致死锁情况的发生。
- 实时性
实时操作系统的一个核心目标是保证任务的实时性,即任务能够在规定的时间内完成。在定时器回调中调用阻塞API会违背这一原则,因为它会使得定时器服务任务无法及时响应其他定时器的到期事件。这样不仅会影响当前定时器的准时性,还会影响到系统中其他定时器的准时性。
- 系统的可预测性
在定时器回调中使用阻塞API会降低系统的可预测性。RTOS设计之初就是为了提供可预测的行为,即在任何给定的情况下,系统的行为都是可以预测的。定时器服务任务如果被阻塞,系统的响应时间会变得不可预测,这对于需要严格时间控制的实时系统来说是不可接受的。
- 效率问题
最后,即使不考虑上述问题,定时器回调中调用阻塞API也是一种效率低下的做法。定时器服务任务被阻塞意味着CPU资源被闲置,这在资源受限的嵌入式系统中是一种浪费。
定时器服务/Daemon 任务
定时器是一个可选的、不属于 FreeRTOS 内核的功能,它是由定时器服务(或 Daemon)任务来提供的。FreeRTOS 提供了很多定时器有关的 API 函数,这些 API 函数大多都使用 FreeRTOS的队列发送命令给定时器服务任务。这个队列叫做定时器命令队列。定时器命令队列是提供给FreeRTOS 的软件定时器使用的,用户不能直接访问!
并且会在某个用户创建的用户任务中调用。图中右侧部分是定时器服务任务的任务函数,定时器命令队列将用户应用任务和定时器服务任务连接在一起。在这个例子中,应用程序调用了函数 xTimerReset(),结果就是复位命令会被发送到定时器命令队列中,定时器服务任务会处理这个命令。应用程序是通过函数 xTimerReset()
间接的向定时器命令队列发送了复位命令,并不是直接调用类似 xQueueSend()这样的队列操作函数发送的。
定时器相关配置
定时器相关配置需要去FreeRTOSconfig.h中去配置对应的宏定义:
1、configUSE_TIMERS
如果要使用软件定时器的话宏 configUSE_TIMERS 一定要设置为 1,当设置为 1 的话定时器服务任务就会在启动 FreeRTOS 调度器的时候自动创建。
2、configTIMER_TASK_PRIORITY
设置软件定时器服务任务的任务优先级,可以为 0~( configMAX_PRIORITIES-1)。优先级一定要根据实际的应用要求来设置。如果定时器服务任务的优先级设置的高的话,定时器命令队列中的命令和定时器回调函数就会及时的得到处理。
3、configTIMER_QUEUE_LENGTH
此宏用来设置定时器命令队列的队列长度。
4、configTIMER_TASK_STACK_DEPTH
此宏用来设置定时器服务任务的任务堆栈大小,单位为字,不是字节!,对于 STM32 来说一个字是 4 字节。由于定时器服务任务中会执行定时器的回调函数,因此任务堆栈的大小一定要根据定时器的回调函数来设置。
单次定时器和周期定时器
单次定时器和周期定时器的区别就是单次定时器只能运行单次,周期定时器根据周期去运转。具体为原子文档中的下图:
复位软件定时器
复位软件定时器的话会重新计算定时周期到达的时间点,这个新的时间点是相对于复位定时器的那个时刻计算的,并不是第一次启动软件定时器的那个时间点。下图 演示了这个过程,Timer1 是单次定时器,定时周期是 5s:
FreeRTOS 提供了两个 API 函数来完成软件定时器的复位:
c
xTimerReset() 复位软件定时器,用在任务中。
xTimerResetFromISR() 复位软件定时器,用在中断服务函数中
函数 xTimerReset()
复位一个软件定时器,此函数只能用在任务中,不能用于中断服务函数!此函数是一个宏,真正执行的是函数 xTimerGenericCommand(),函数原型如下:
c
BaseType_t xTimerReset( TimerHandle_t xTimer,
TickType_t xTicksToWait )
参数:
xTimer: 要复位的软件定时器的句柄。
xTicksToWait: 设置阻塞时间,调用函数 xTimerReset ()开启软件定时器其实就是向定时器命
令队列发送一条 tmrCOMMAND_RESET 命令,既然是向队列发送消息,那
肯定会涉及到入队阻塞时间的设置。
返回值:
pdPASS: 软件定时器复位成功,其实就是命令发送成功。
pdFAIL: 软件定时器复位失败,命令发送失败。
函数 xTimerResetFromISR()
此函数是 xTimerReset()的中断版本,此函数用于中断服务函数中!此函数是一个宏,真正执行的是函数 xTimerGenericCommand(),函数原型如下:
c
BaseType_t xTimerResetFromISR( TimerHandle_t xTimer,
BaseType_t * pxHigherPriorityTaskWoken );
参数:
xTimer: 要复位的软件定时器的句柄。
pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换,这个变量的值函数会自动设置的,用户不用进行
设置,用户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行
一次任务切换。
返回值:
pdPASS: 软件定时器复位成功,其实就是命令发送成功。
pdFAIL: 软件定时器复位失败,命令发送失败。
创建软件定时器
使用软件定时器之前要先创建软件定时器,
c
xTimerCreate() 使用动态方法创建软件定时器。
xTimerCreateStatic() 使用静态方法创建软件定时器。
函数 xTiemrCreate()
此函数用于创建一个软件定时器,所需要的内存通过动态内存管理方法分配。新创建的软件 定 时 器 处 于 休 眠 状 态 , 也 就 是 未 运 行 的 。 函 数 xTimerStart() 、 xTimerReset() 、xTimerStartFromISR() 、 xTimerResetFromISR() 、 xTimerChangePeriod() 和xTimerChangePeriodFromISR()可以使新创建的定时器进入活动状态,此函数的原型如下:
c
TimerHandle_t xTimerCreate(const char * const pcTimerName,
TickType_t xTimerPeriodInTicks,
UBaseType_t uxAutoReload,
void * pvTimerID,
TimerCallbackFunction_t pxCallbackFunction )
参数:
pcTimerName: 软件定时器名字,名字是一串字符串,用于调试使用。
xTimerPeriodInTicks : 软件定时器的定时器周期, 单位是时钟节拍数。可以借助portTICK_PERIOD_MS
将 ms 单位转换为时钟节拍数。举个例子,定时器的周期为 100 个时钟节拍的话,
那么 xTimerPeriodInTicks 就为100,当定时器周期为 500ms 的时候
xTimerPeriodInTicks 就可以设置为(500/ portTICK_PERIOD_MS)。
uxAutoReload: 设置定时器模式,单次定时器还是周期定时器?当
此参数为 pdTRUE的时候表示创建的是周期定时器。如果为 pdFALSE 的
话表示创建的是单次定时器。pvTimerID: 定时器 ID 号,一般情况下
每个定时器都有一个回调函数,当定时器定时周期到了以后就会执行这
个回调函数。但是 FreeRTOS 也支持多个定时器共用同一个回调函数,
在回调函数中根据定时器的 ID 号来处理不同的定时器。pxCallbackFunction:
定时器回调函数,当定时器定时周期到了以后就会调用这个函数。
返回值:
NULL: 软件定时器创建失败。
其他值: 创建成功的软件定时器句柄。
函数 xTimerCreateStatic()
此函数用于创建一个软件定时器,所需要的内存需要用户自行分配。新创建的软件定时器处于休眠状态,也就是未运行的。函数 xTimerStart()、xTimerReset()、xTimerStartFromISR()、xTimerResetFromISR()、xTimerChangePeriod()和 xTimerChangePeriodFromISR()可以使新创建的定时器进入活动状态,此函数的原型如下:
c
TimerHandle_t xTimerCreateStatic(const char * const pcTimerName,
TickType_t xTimerPeriodInTicks,
UBaseType_t uxAutoReload,
void * pvTimerID,
TimerCallbackFunction_t pxCallbackFunction,
StaticTimer_t * pxTimerBuffer )
参数:
pcTimerName: 软件定时器名字,名字是一串字符串,用于调试使用。
xTimerPeriodInTicks : 软件定时器的定时器周期, 单位是时钟节拍数。可以借助
portTICK_PERIOD_MS 将 ms 单位转换为时钟节拍数。举个例子,定
时器的周期为 100 个时钟节拍的话,那么 xTimerPeriodInTicks 就为
100,当定时器周期为 500ms 的时候 xTimerPeriodInTicks 就可以设置
为(500/ portTICK_PERIOD_MS)。
uxAutoReload: 设置定时器模式,单次定时器还是周期定时器?当此参数为 pdTRUE
的时候表示创建的是周期定时器。如果为 pdFALSE 的话表示创建的
是单次定时器。
pvTimerID: 定时器 ID 号,一般情况下每个定时器都有一个回调函数,当定时器定
时周期到了以后就会执行这个回调函数。当时 FreeRTOS 也支持多个
定时器共用同一个回调函数,在回调函数中根据定时器的 ID 号来处
理不同的定时器。
pxCallbackFunction: 定时器回调函数,当定时器定时周期到了以后就会调用这个函数。
pxTimerBuffer: 参数指向一个 StaticTimer_t 类型的变量,用来保存定时器结构体。
返回值:
NULL: 软件定时器创建失败。
其他值: 创建成功的软件定时器句柄。
开启软件定时器
如果软件定时器停止运行的话可以使用 FreeRTOS 提供的两个开启函数来重新启动软件定时器
c
xTimerStart() 开启软件定时器,用于任务中。
xTimerStartFromISR() 开启软件定时器,用于中断中。
函数 xTimerStart()
启动软件定时器,函数 xTimerStartFromISR()是这个函数的中断版本,可以用在中断服务函数中。如果软件定时器没有运行的话调用函数 xTimerStart()就会计算定时器到期时间,如果软件定时器正在运行的话调用函数 xTimerStart()的结果和 xTimerReset()一样。此函数是个宏,真正执行的是函数 xTimerGenericCommand,函数原型如下:
c
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait )
参数:
xTimer: 要开启的软件定时器的句柄。
xTicksToWait: 设置阻塞时间,调用函数 xTimerStart()开启软件定时器其实就是向定时器命令
队列发送一条 tmrCOMMAND_START 命令,既然是向队列发送消息,那肯
定会涉及到入队阻塞时间的设置。
返回值:
pdPASS: 软件定时器开启成功,其实就是命令发送成功。
pdFAIL: 软件定时器开启失败,命令发送失败。
函数 xTimerStartFromISR()
此函数是函数 xTimerStart()的中断版本,用在中断服务函数中,此函数是一个宏,真正执行的是函数 xTimerGenericCommand(),此函数原型如下:
c
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer,
BaseType_t * pxHigherPriorityTaskWoken );
参数:
xTimer: 要开启的软件定时器的句柄。
pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值函数会自动设置的,用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务
函数之前一定要进行一次任务切换。
返回值:
pdPASS: 软件定时器开启成功,其实就是命令发送成功。
pdFAIL: 软件定时器开启失败,命令发送失败。
停止软件定时器
既然有开启软件定时器的 API 函数,那么肯定也有停止软件定时器的函数,FreeRTOS 也提供了两个用于停止软件定时器的 API 函数,
c
xTimerStop() 停止软件定时器,用于任务中。
xTimerStopFromISR() 停止软件定时器,用于中断服务函数中。
函数 xTimerStop()
此函数用于停止一个软件定时器,此函数用于任务中,不能用在中断服务函数中!此函数是一个宏,真正调用的是函数 xTimerGenericCommand(),函数原型如下:
c
BaseType_t xTimerStop ( TimerHandle_t xTimer,
TickType_t xTicksToWait )
参数:
xTimer: 要停止的软件定时器的句柄。
xTicksToWait: 设置阻塞时间,调用函数 xTimerStop()停止软件定时器其实就是向定时器命令
队列发送一条 tmrCOMMAND_STOP 命令,既然是向队列发送消息,那肯定
会涉及到入队阻塞时间的设置。
返回值:
pdPASS: 软件定时器停止成功,其实就是命令发送成功。
pdFAIL: 软件定时器停止失败,命令发送失败。
函数 xTimerStopFromISR()
此函数是 xTimerStop()的中断版本,此函数用于中断服务函数中!此函数是一个宏,真正执行的是函数 xTimerGenericCommand(),函数原型如下:
c
BaseType_t xTimerStopFromISR( TimerHandle_t xTimer,BaseType_t * pxHigherPriorityTaskWoken );
参数:
xTimer: 要停止的软件定时器句柄。
pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值函数会
自动设置的,用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。
当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdPASS: 软件定时器停止成功,其实就是命令发送成功。
pdFAIL: 软件定时器停止失败,命令发送失败。