FreeRTOS 软件定时器

FreeRTOS 软件定时器

FreeRTOS 也提供了定时器功能,不过是软件定时器,软件定时器的精度肯定没有硬件定时器那么高,但是对于普通的精度要求不高的周期性处理的任务来说够了。当 MCU 的硬件定时器不够的时候就可以考虑使用 FreeRTOS 的软件定时器

软件定时器简介

软件定时器允许设置一段时间,当设置的时间到达之后就执行指定的功能函数,被定时器调用的这个功能函数叫做定时器的回调函数。回调函数的两次执行间隔叫做定时器的定时周期,简而言之,当定时器的定时周期到了以后就会执行回调函数。

在实时操作系统(RTOS)中,软件定时器的设计通常是为了在特定的时间点执行一段代码,而这段代码是通过定时器的回调函数来实现的。定时器服务任务(或称为定时器守护任务)是一个特殊的任务,负责管理和执行所有软件定时器的回调函数。这里面涉及到几个关键的概念和原因,解释为什么在定时器回调函数中调用可能会阻塞任务的API函数是不合适的:

  1. 任务优先级

定时器服务任务通常运行在一个相对较低的优先级。如果在定时器的回调函数中调用了阻塞API(如vTaskDelay()或访问队列和信号量的阻塞调用),这将导致定时器服务任务被阻塞。因为定时器服务任务负责处理所有定时器的回调,其被阻塞意味着所有的定时器回调都会被延迟执行,这会影响到系统中其他定时器的准确性和可靠性。

  1. 资源共享和死锁

在定时器回调函数中调用可能导致阻塞的API,尤其是那些涉及到资源访问(如队列、信号量等)的API,可能会引起死锁。因为定时器服务任务和其他任务可能会同时试图访问同一资源,如果没有合适的同步机制,这就可能导致死锁情况的发生。

  1. 实时性

实时操作系统的一个核心目标是保证任务的实时性,即任务能够在规定的时间内完成。在定时器回调中调用阻塞API会违背这一原则,因为它会使得定时器服务任务无法及时响应其他定时器的到期事件。这样不仅会影响当前定时器的准时性,还会影响到系统中其他定时器的准时性。

  1. 系统的可预测性

在定时器回调中使用阻塞API会降低系统的可预测性。RTOS设计之初就是为了提供可预测的行为,即在任何给定的情况下,系统的行为都是可以预测的。定时器服务任务如果被阻塞,系统的响应时间会变得不可预测,这对于需要严格时间控制的实时系统来说是不可接受的。

  1. 效率问题

最后,即使不考虑上述问题,定时器回调中调用阻塞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: 软件定时器停止失败,命令发送失败。
相关推荐
智商偏低2 小时前
单片机之helloworld
单片机·嵌入式硬件
青牛科技-Allen3 小时前
GC3910S:一款高性能双通道直流电机驱动芯片
stm32·单片机·嵌入式硬件·机器人·医疗器械·水泵、
森焱森5 小时前
无人机三轴稳定控制(2)____根据目标俯仰角,实现俯仰稳定化控制,计算出升降舵输出
c语言·单片机·算法·架构·无人机
白鱼不小白5 小时前
stm32 USART串口协议与外设(程序)——江协教程踩坑经验分享
stm32·单片机·嵌入式硬件
S,D6 小时前
MCU引脚的漏电流、灌电流、拉电流区别是什么
驱动开发·stm32·单片机·嵌入式硬件·mcu·物联网·硬件工程
芯岭技术9 小时前
PY32F002A单片机 低成本控制器解决方案,提供多种封装
单片机·嵌入式硬件
youmdt9 小时前
Arduino IDE ESP8266连接0.96寸SSD1306 IIC单色屏显示北京时间
单片机·嵌入式硬件
嘿·嘘9 小时前
第七章 STM32内部FLASH读写
stm32·单片机·嵌入式硬件
Meraki.Zhang9 小时前
【STM32实践篇】:I2C驱动编写
stm32·单片机·iic·驱动·i2c
几个几个n12 小时前
STM32-第二节-GPIO输入(按键,传感器)
单片机·嵌入式硬件