1、软件定时器概念
软件定时器用于在未来的某个设定时刻安排功能的执行,或以固定的频率定期执行某个功能。软件定时器执行的函数称为软件定时器的回调函数。
软件定时器由 FreeRTOS 内核实现并受其控制。它们不需要硬件支持,并且与硬件定时器或硬件计数器无关。
软件定时器不使用任何处理时间,除非软件定时器回调函数实际正在执行。
软件定时器功能是可选择的:1、要包含 FreeRTOS/Source/timers.c文件到你的项目中去。2、 在FreeRTOSConfig.h文件中,设置宏configUSE_TIMERS 为 1 。
2、软件定时器的运行机制
2.1 组成
软件定时器的功能组成的实质是两个部件:
一个是,定时器命令队列(Timer Command Queue);
一个是,RTOS守护进程任务(RTOS Deamon Task),该任务过去被称为“计时器服务任务(Timer Service task)”,因为最初它只用于执行软件计时器回调函数。现在,相同的任务也用于其他目的,因此它被称为“RTOS守护程序任务”这一更通用的名称。
2.2 创建
软件定时器的运行部件必须是由系统创建的。具体是在调度程序vTaskStartSchedule()函数运行时,创建定时器的命令队列,以及处理该队列的RTOS守护进程任务。该任务一般优先级是大于1,高于其它系统自动创建的任务,例如系统自动创建的空闭任务。
针对守护进程的优先级,是可以设置的,在标准的FreeRTOS中,是在FreeRTOSConfig.h中的宏 configTIMER_TASK_PRIORITY进行设置,而在ESP-IDF for VSCode中,使用menuconfig去设置。设置选项如下:
2.3 运行
这样,每个tick中断,调度器会去调度RTOS守护进程任务,并在该任务里确定哪个定时器到期,并执行哪个定时器对应的回调函数,以处理定时器到期任务。
此时,如果有高于RTOS守护进程任务的其它任务正在执行,则会出现延迟处理定时器到期任务的情况。或者如果定时器对应的回调函数里有长时间运行的行为,比如出现了阻塞,则也会影响其它的定时器到期任务。
注意事项:
因此,实际在编程时,如果有用到定时器,一定要注意, 以上这两种情况。即高于优先级数值1的其它优先级任务一定要有阻塞机制,使系统可以正常及时的调度RTOS守护进程。
另一个注意事项就是定时器的回调函数决不能进入阻塞状态。软件定时器回调函数在 FreeRTOS 调度程序启动时自动创建的RTOS守护进程任务中运行。因此,软件定时器回调函数绝对不能调用会导致调用程序进入阻塞的 FreeRTOS API 函数。可以调用xQueueReceive()等函数,但前提是函数的xTicksToWait参数(指定函数的阻塞时间)设置为0。不可以调用vTaskDelay()等函数,调用vTaskDelay() 将始终将调用任务置于阻塞状态。
3、软件定时器的属性和状态
3.1 定时器的周期
软件定时器的“周期period”是指软件定时器启动和软件定时器回调函数执行之间的时间。
3.2 定时器的类型
在FreeRTOS中,软件定时器有两种类型:
1、One-shot timers (一次性定时器)
一旦启动,一次性定时器将只执行一次回调函数。一次性定时器可以手动重新启动,但不会自动重新启动。
2、Auto-reload timers (自动重载定时器)
一旦启动,自动重载定时器将在每次到期时重新启动,从而定期执行其回调函数。
3.3 定时器的状态
用户创建的软件定时器,在任何时刻,都会处于两种状态之一:
1、Dormant 睡眠状态
计时器处于睡眠状态时,可以通过其句柄引用它,但由于计时器是未运行状态,因此其回调函数不会执行。
2、Running 运行状态
运行中的软件定时器,在定时时间到后会执行其回调函数。
状态切换的示意图
4、软件定时器的回调函数原型
在timers.h中,软件定时器的回调函数的原型定义如下:
软件计时器回调函数被实现为C函数。它们唯一特别的地方是它们的原型,它必须返回void,并将软件计时器的句柄作为其唯一参数。
5、定时器的使用
5.1 创建定时器xTimeCreate()
概述:
创建一个新的软件计时器,并返回一个句柄,通过该句柄可以引用创建的软件计时器。
每个软件计时器都需要少量RAM来保持计时器的状态。如果使用xTimerCreate()创建软件计时器,则会从FreeRTOS堆自动分配此RAM。如果使用xTimerCreateStatic()创建软件计时器,则RAM由应用程序编写器提供,这需要额外的参数,但允许在编译时静态分配RAM。
创建计时器不会启动计时器运行。xTimerStart()、xTimerReset()、xDimerStartFromISR()、xMimerResetFromISR)、xTimerChangePeriod()和xTimerChangePeriodFromISR API函数都可以用于启动计时器运行。
参数
参数 | 解释 |
---|---|
pcTimerName | 分配给计时器的纯文本名称,纯粹是为了帮助调试。 |
xTimerPeriod | 计时器周期。计时器周期以tick周期的倍数指定。pdMS_TO_TICKS()宏可用于将以毫秒为单位的时间转换为以tick为单位的。例如,如果计时器必须在100个刻度后过期,则xNewPeriod可以直接设置为100。或者,如果计时器必须在500ms后过期,则xNewPeriod可以设置为pdMS_to_TICKS(500),前提是configTICK_RATE_HZ小于或等于1000。 |
uxAutoReload | 设置为pdTRUE以创建自动重载计时器。设置为pdFALSE以创建一次性计时器。一旦启动,自动重载计时器将以xTimerPeriod参数设置的频率重复过期。一旦启动,一次性计时器将仅过期一次。一次性计时器到期后可以手动重新启动 |
pvTimerID | 分配给正在创建的计时器的标识符。稍后可以使用vTimerSetTimerID()API函数更新标识符。如果将同一个回调函数分配给多个计时器,则可以在回调函数内检查计时器标识符,以确定哪个计时器实际过期。此外,计时器标识符可用于在调用计时器的回调函数之间存储值。 |
pxCallbackFunction | 计时器过期时要调用的函数。回调函数必须具有TimerCallbackFunction_t 定义的原型。所需的原型如上第4点所述。 |
返回值
返回值 | 解释 |
---|---|
NULL | 无法创建软件计时器,因为可用的FreeRTOS堆内存不足,无法成功分配计时器数据结构。 |
NULL以外的其它值 | 已成功创建软件计时器,返回的值是可以引用创建的软件计时器的句柄。 |
注意事项
在FreeRTOSConfig.h中,configUSE_TIMERS和configSUPPORT_DYNAMIC_ALLOCATION必须都设置为1,以便xTimerCreate()可用。
如果未定义,configSUPPORT_DYNAMIC_ALLOCATION将默认为1。
5.2 启动定时器xTimerStart()
描述
启动计时器运行。xTimerStartFromISR()是一个可以从中断服务例程调用的等效函数。
如果计时器尚未运行,则计时器将根据调用xTimerStart()的时间计算到期时间。
如果计时器已经在运行,则xTimerStart()在功能上等同于xTimerReset()。
参数
参数 | 解释 |
---|---|
xTimer | 要重置、启动或重新启动的计时器。 |
xTicksToWait | 计时器功能不是由核心FreeRTOS代码提供的,而是由计时器服务(或守护程序)任务提供的。FreeRTOS计时器API将命令发送到名为计时器命令队列的队列上的计时器服务任务。xTicksToWait指定任务应保留在“阻止”中的最长时间状态以等待计时器命令队列上的空间变为可用(如果队列已满)。 |
返回值
返回值 | 解释 |
---|---|
pdPASS | pdPASS返回值,表明启动命令已成功发送到计时器命令队列。如果指定了阻塞时间(xTicksToWait不为零),则pdPASS的返回值说明在阻塞时间到期之前,就把数据写入队列了。实际处理命令的时间取决于计时器服务任务相对于系统中其他任务的优先级,尽管计时器的到期时间与实际调用xTimerStart()的时间有关。计时器服务任务的优先级由configTIMER_task_priority配置常量设置。 |
pdFAIL | 启动命令未发送到计时器命令队列,因为队列已满。如果指定了阻塞时间(xTicksToWait不为零),则一直到阻塞到期,数据也没有被发送的定时器命令队列中。 |
5.3 终止定时器xTimerStop()
参数
参数 | 解释 |
---|---|
xTimer | 要被终止的定时器句柄 |
xTicksToWait | 计时器功能不是由核心FreeRTOS代码提供的,而是由计时器服务(或守护程序)任务提供的。FreeRTOS计时器API将命令发送到名为计时器命令队列的队列上的计时器服务任务。xTicksToWait指定任务应保留在“阻止”中的最长时间状态以等待计时器命令队列上的空间变为可用(如果队列已满)。 |
返回值
返回值 | 解释 |
---|---|
pdPASS | pdPASS返回值,表明启动命令已成功发送到计时器命令队列。如果指定了阻塞时间(xTicksToWait不为零),则pdPASS的返回值说明在阻塞时间到期之前,就把数据写入队列了。实际处理命令的时间取决于计时器服务任务相对于系统中其他任务的优先级,尽管计时器的到期时间与实际调用xTimerStart()的时间有关。计时器服务任务的优先级由configTIMER_task_priority配置常量设置。 |
pdFAIL | 启动命令未发送到计时器命令队列,因为队列已满。如果指定了阻塞时间(xTicksToWait不为零),则一直到阻塞到期,数据也没有被发送的定时器命令队列中。 |
5.4 定时器重置xTimerReset()
描述
重新启动计时器。xTimerResetFromISR()是一个可以从中断服务例程调用的等效函数。
如果计时器已经在运行,那么计时器将重新计算其到期时间,以与调用xTimerReset()的时间相对应。
如果计时器未运行,则计时器将计算与调用xTimerReset()的时间相关的到期时间,计时器将开始运行。在这种情况下,xTimerReset()在功能上等同于xTimerStart()。
如果在调度程序启动之前调用xTimerReset(),则在调度程序开始之前,计时器不会开始运行,并且计时器的到期时间将与调度程序启动的时间相关
参数
参数 | 解释 |
---|---|
xTimer | 要被重启的定时器句柄 |
xTicksToWait | 计时器功能不是由核心FreeRTOS代码提供的,而是由计时器服务(或守护程序)任务提供的。FreeRTOS计时器API将命令发送到名为计时器命令队列的队列上的计时器服务任务。xTicksToWait指定任务应保留在“阻止”中的最长时间状态以等待计时器命令队列上的空间变为可用(如果队列已满)。 |
返回值
返回值 | 解释 |
---|---|
pdPASS | 重置命令已成功发送到计时器命令队列。如果指定了阻塞时间(xTicksToWait不为零),则阻塞到期之前,reset命令已成功写入队列。实际处理命令的时间取决于计时器服务任务相对于系统中其他任务的优先级,尽管计时器的到期时间与实际调用xTimerReset()的时间有关。计时器服务任务的优先级由configTIMER_task_priority配置常量设置 |
pdFAIL | 重置命令未发送到计时器命令队列,因为队列已满 |
5.5 实例
在本例中,当按下一个键时,LCD背光将打开。如果5秒过去而没有按下一个按键,则LCD背光将由一个一次性计时器关闭。以下是一段伪代码,用于说明之前几个函数的用法。
5.5 定时器停止运行xTimerStop()
描述
停止一个定时器的运行。
参数
参数 | 解释 |
---|---|
xTimer | 需要被停止的定时器句柄 |
xTicksToWait | 计时器功能不是由核心FreeRTOS代码提供的,而是由计时器服务(或守护程序)任务提供的。FreeRTOS计时器API将命令发送到名为计时器命令队列的队列上的计时器服务任务。xTicksToWait指定如果队列已满,任务应保持在“阻止”状态以等待计时器命令队列上的可用空间的最长时间 |
返回值
返回值 | 解释 |
---|---|
pdPASS | stop命令已成功发送到计时器命令队列。如果指定了阻塞时间(xTicksToWait不为零),则阻塞到期之前,stop命令已成功写入队列。 |
pdFAIL | stop命令未发送到计时器命令队列,因为队列已满 |
5.6 删除定时器xTimerDelete()
描述
删除定时器。被删除的定时器,必须是已用xTimerCreate()创建的了 。
参数
参数 | 解释 |
---|---|
xTimer | 将要被删除的定时器的句柄 |
xTicksToWait | 计时器功能不是由核心FreeRTOS代码提供的,而是由计时器服务(或守护程序)任务提供的。FreeRTOS计时器API将命令发送到名为计时器命令队列的队列上的计时器服务任务。xTicksToWait指定任务应保留在“阻止”中的最长时间状态以等待计时器命令队列上的空间变为可用(如果队列已满)。 |
返回值
返回值 | 解释 |
---|---|
pdPASS | 删除命令已成功发送到计时器命令队列。如果指定了阻塞时间(xTicksToWait不为零),则阻塞到期之前,delete命令已成功写入队列。 |
pdFAIL | 删除命令未发送到计时器命令队列,因为队列已满 |
5.7 改变定时器周期
如果使用xTimerChangePeriod()更改已在运行的定时器的周期,则计时器将使用新的周期值重新计算其到期时间。然后,重新计算的到期时间将与调用xTimerChangePeriod()的时间相关,而不是与定时器最初启动的时间相关。
如果使用xTimerChangePeriod()更改尚未运行的定时器的周期,则计时器将使用新的周期值计算到期时间,定时器将开始运行。
参数
参数 | 解释 |
---|---|
xTimer | 将要被改变的周期的定时器句柄 |
xNewPeriodxTimer | 参数引用的计时器的新周期。计时器周期以tick周期的倍数指定。pdMS_TO_TICKS()宏可用于将以毫秒为单位的时间转换为以tick为单位的。例如,如果计时器必须在100个tick后过期,则xNewPeriod可以直接设置为100。或者,如果计时器必须在500ms后过期,则xNewPeriod可以设置为pdMS_to_TICKS(500),前提是configTICK_RATE_HZ小于或等于1000。 |
xTicksToWait | 计时器功能不是由核心FreeRTOS代码提供的,而是由定时器提供的服务(守护程序)任务。FreeRTOS计时器API将命令发送到名为定时器命令队列的队列上的计时器服务任务。xTicksToWait指定在队列已满的情况下,任务应保持在“阻止”状态以等待计时器命令队列上的可用空间的最长时间。 |
返回值
返回值 | 解释 |
---|---|
pdPASS | 更改周期命令已成功发送到计时器命令队列。 |
pdFAIL | 更改周期命令未发送到计时器命令队列,因为队列已满 |
6 Timer ID
6.1 定时器ID的概念
在定时器的结构体中,有一个void * pvTimerID这么一个类型。见下图。
每个软件定时器都有一个ID,这是一个能让程序开发者用于任何目的的标签值。这个ID被存储于void*指针,所以能存入一
个整型值直接指向任何对象或像函数指针一样使用。
当软件定时器被创建时,分配给ID一个初始化的值,(在xTimerCreate()函数的第四个参数)之后,可以使用vTimerSetTimerID()函数更新ID值,并用pvTimerGetTimerID()来查询ID。
从上面的定时器结构体可以看出。该ID值是内嵌在定时器结构体内的,因此,对该值的操作,不像其它软件定时器API函数,vTimerSetTimerID() 和 pvTimerGetTimerID()是直接访问软件定时器,而不用发送命令到定时器命令队列。
6.2 定时器ID的应用场景
由于这个数据是一个void *的类型。因此,我们可以非常灵活的使用它,并用这个值给定时器的其它行为传递任何数据。比如,这个值可以是一个32位的任意类型的数据。也可以是一个指针,指向一个更复杂的数据结构,可以是一个函数指针,用于向定时器传递一个当下对应的函数指针。
因此,这是一个非常灵活的设定。我们可以用于任何目的和我们想要的场景。比如,可以把这个void *当成一个整型变量,用以维护一个对应该定时器的计数器,记录定时器的运行次数。也可以把这个指针指向一个更为复杂的数据结构,为这个定时器维护一个专有的数据结构用于更复杂的工作。
如果将同一个回调函数分配给多个计时器,则可以在回调函数内部检查计时器标识符,以确定哪个计时器实际过期。
计时器标识符还可以用于在调用计时器的回调函数之间在计时器中存储数据。
6.3 定时器ID的初始化
在xTimerCreate()创建定时器时,我们就需要初始化该定时器ID了。这个从创建函数的定义中就可以清楚:
如果无需要该值,则我们一般初始化为NULL。
6.4 设置定时器ID vTimerSetTimerID()
创建计时器时,会为定时器分配标识符(定时器ID),可以使用vTimerSetTimerID()API函数随时更改。
参数
参数 | 解释 |
---|---|
xTimer | 需要被更新标识符ID的定时器的句柄 |
pvNewID | 计时器标识符将设置为的值。 |
6.5 查询定时器ID值
返回分配给计时器的标识符(定时器ID)。创建计时器时,会为计时器分配一个标识符,可以使用vTimerSetTimerID()API函数进行更新。有关详细信息,请参见xTimerCreate()API函数。
如果将同一个回调函数分配给多个计时器,则可以在回调函数内部检查计时器标识符,以确定哪个计时器实际过期。
此外,计时器的标识符可用于在调用计时器的回调函数之间存储值
参数
xTimer : 将要被查询的定时器句柄
返回值
定时器ID值。
6.6 实例
实例1
实例2
同一个回调函数可以分配给多个软件定时器。完成后,回调函数参数用于确定哪个软件计时器到期。
本示例14创建了两个定时器,但为两个软件定时器分配了同一个回调函数。其中prvTimerCallback()用作两个计时器的回调函数。
prvTimerCallback() 将在任一计时器到期时执行。 prvTimerCallback() 的实现使用函数的参数来确定调用它是因为一
次性计时器到期,还是因为自动重载计时器到期。
prvTimerCallback() 还演示了如何使用软件定时器 ID 作为定时器特定的存储;每个软件定时器都会在自己的 ID 中
记录它已过期的次数,自动重载定时器会在第五次执行时使用该计数自行停止。
7 总结
Timer 是FreeRTOS中不依赖硬件的一个软件定时器。机制上由三个部分组成,一个是Timer结构体,用于存放定时器的具体参数及必要数据结构。二是系统定时器守护进程任务,用于具体处理定时器到期检测以及定时器回调函数的运行,三是软件定时器命令队列,用于向守护进程任务传递定时器的处理命令。
定时器守护进程任务与定时器命令队列由系统在调度程序xTaskStartSchedule()启动时自动创建。
编程人员在使用定时器时。必须首先创建定时器对象(即数据结构)。明确定时器的周期,定时器的类型(一次性的或者是自动重载的),Timer ID等,回调函数。
然后由开发者在过程中用定时器API来向命令队列发送具体的定时器命令,如xTimerStart(),xTimerStop(),xTimerDelete()等。这些命令由定时器守护进程任务在适当时候执行。这里的适当时候指调度程序根据守护进程任务的优先级进行调度运行。因此守护进程的优先级是必须由开发人员具体调整 的。在FreeRTOSConfig.h中的宏 configTIMER_TASK_PRIORITY进行设置。系统默认该宏为1。