软件定时器管理
章节介绍和范围
软件计时器用于调度功能在将来的设定时间执行,或以固定频率定期执行。由软件定时器执行的函数称为软件定时器的回调函数。
软件计时器由FreeRTOS内核实现,并受FreeRTOS内核的控制。它们不需要硬件支持,也与硬件计时器或硬件计数器无关。
请注意,根据FreeRTOS使用创新设计以确保最高效率的理念,软件计时器不会使用任何处理时间,除非软件计时器回调函数实际正在执行。
软件计时器功能是可选的。要包括软件计时器功能,请执行以下操作:
将FreeRTOS源文件
FreeRTOS/Source/timers.c
构建为项目的一部分在
FreeRTOSConfig.h
中将configUSE_TIMERS
设置为1。
范围
本章旨在让读者更好地了解以下内容:
软件定时器的特性与任务特性的比较。
RTOS后台任务。
计时器命令队列。
单次软件定时器和周期性软件定时器之间的区别。
如何创建、启动、重置和更改软件计时器的周期。
软件定时器回调函数
软件计时器回调函数被实现为C函数。它们唯一的特别之处是它们的原型,它必须返回void,并将软件计时器的句柄作为其唯一的参数。清单72演示了回调函数原型。
清单72.软件计时器回调函数原型
软件计时器回调函数自始至终执行,并以正常方式退出。它们应该保持简短,并且不能进入阻塞状态。
注意:正如将看到的,软件计时器回调函数在启动FreeRTOS调度程序时自动创建的任务的上下文中执行。 因此,软件计时器回调函数决不能调用会导致调用任务进入阻塞状态的FreeRTOS API函数,这一点至关重要。 可以调用xQueueReceive()之类的函数,但前提是该函数的xTicksToWait参数(指定函数的阻塞时间)设置为0。 调用vTaskDelay()之类的函数是不对的,因为调用vTaskDelay()会始终将调用任务置于阻塞状态。
软件计时器的属性和状态
软件计时器的周期
软件计时器的‘周期’是软件计时器启动和软件计时器的回调函数执行之间的时间
单次计时器和自动重新加载计时器
有两种类型的软件计时器:
单次计时器 一旦启动,一次性定时器将只执行其回调函数一次。一次性计时器可以手动重新启动,但不会自行重新启动。
自动重新加载计时器 一旦启动,自动重新加载计时器将在每次到期时重新启动,从而定期执行其回调函数。
图38显示了单次定时器和自动重新加载定时器之间的行为差异。虚线垂直线标记计时中断发生的时间。
参考图38:
计时器1 定时器1是具有6个滴答周期的一次性定时器。它在时间t1启动,因此它的回调函数在6个刻度之后,即时间t7执行。由于定时器1是一次性定时器,其回调函数不会再次执行。
计时器2 定时器2是具有5个滴答周期的自动重新加载定时器。它在时间t1启动,因此它的回调函数在时间t1之后每5个节拍执行一次。在图38中,这是时间t6、t11和t16。
软件计时器状态
软件计时器可以处于以下两种状态之一:
休眠:存在休眠的软件计时器,可以由其句柄引用,但不在运行,因此其回调函数将不会执行
运行:正在运行的软件定时器,将在自该软件定时器进入运行状态,或自该软件定时器上次被重置以来经过与其周期相等的时间之后执行其回调功能。
图39和图40分别显示了自动重新加载定时器和单次定时器在休眠和运行状态之间可能的转换。这两个图的关键区别在于定时器到期后进入的状态;自动重新加载定时器执行其回调函数,然后重新进入运行状态,一次性定时器执行其回调函数,然后进入休眠状态。
xTimerDelete()接口函数的作用是:删除计时器。可以随时删除计时器。
软件定时器的上下文
RTOS守护(计时器服务)任务
所有软件计时器回调函数都在同一RTOS守护进程(或“计时器服务”)任务的上下文中执行[1]。
[1]. 该任务过去被称为“计时器服务任务”,因为最初它只用于执行软件计时器回调函数。现在同一任务也用于其他目的,因此它被称为“RTOS守护程序任务”的更一般的名称。
守护程序任务,是在启动调度程序时,自动创建的标准FreeRTOS任务。其优先级和堆栈大小分别由configTIMER_TASK_PRIORITY
和configTIMER_TASK_STACK_DEPTH
编译时间配置常量设置。这两个常量都在FreeRTOSConfig.h中定义。
软件计时器回调函数不得调用会导致调用任务进入阻塞状态的FreeRTOS API函数,否则将导致守护程序任务进入阻塞状态。
计时器命令队列
软件计时器API函数将命令从调用任务发送到称为“计时器命令队列”的队列上的守护程序任务。这如图41所示。命令的例子包括“启动定时器”、“停止定时器”和“重置定时器”。
计时器命令队列是在启动调度程序时自动创建的标准FreeRTOS队列。定时器命令队列的长度由FreeRTOSConfig.h中的configTIMER_QUEUE_LENGTH
编译时间配置常量设置。
守护进程任务调度
守护程序任务与任何其他FreeRTOS任务一样进行调度;当守护程序任务是能够运行的最高优先级任务时,它只会处理命令或执行计时器回调函数。图42和图43演示了configTIMER_TASK_PRIORITY
设置如何影响执行模式
图42显示了当守护程序任务的优先级低于调用xTimerStart()
API函数的任务的优先级时的执行模式
参照图42,其中任务1的优先级高于守护程序任务的优先级,并且守护程序任务的优先级高于空闲任务的优先级:
t1时刻 :任务1处于RUNNING状态,守护程序任务处于BLOCKED状态。 守护程序任务将脱离阻塞状态,如果一个命令被发送到计时器命令队列,在这种情况下,它将处理命令。或者如果软件计时器超时,在这种情况下,它将执行软件计时器的回调函数。
t2时刻:任务1调用
xTimerStart()
。xTimerStart()
向计时器命令队列发送命令,使守护程序任务离开阻塞状态。 任务1的优先级高于守护程序任务的优先级,因此守护程序任务不会抢占任务1。 任务1仍处于Running状态,守护程序任务已离开BLOCKED状态,进入READY状态。t3时刻:任务1完成
xTimerStart()
API函数的执行。 任务1从函数开始到函数结束执行xTimerStart()
,而不离开运行状态。t4时刻:任务1调用导致其进入阻塞状态的API函数。守护程序任务现在是处于就绪状态的最高优先级任务,因此调度程序选择守护程序任务作为进入运行状态的任务。然后,守护程序任务开始处理任务1发送到计时器命令队列的命令。 注意:正在启动的软件计时器将到期的时间,是从向计时器命令队列发送“启动计时器”命令开始计算的,而不是从守护程序任务从计时器命令队列接收到“启动计时器”命令的时间计算的。
t5时刻:守护程序任务已完成对任务1发送给它的命令的处理,并尝试从计时器命令队列接收更多数据。计时器命令队列为空,因此守护程序任务重新进入阻塞状态。如果将命令发送到计时器命令队列,或者如果软件计时器超时,则守护程序任务将再次离开阻塞状态。空闲任务现在是处于就绪状态的最高优先级任务,因此调度程序选择空闲任务作为要进入运行状态的任务。
图43显示了类似于图42所示的场景,但是这一次守护程序任务的优先级高于调用xTimerStart()
的任务的优先级。
参照图43,其中守护任务的优先级高于任务1的优先级,任务1的优先级高于空闲任务的优先级: 1. t1时刻 和之前一样,任务1正在运行态,守护任务在阻塞态。
t2时刻 任务1调用
xTimerStart()
。xTimerStart()
向计时器命令队列发送命令,使守护程序任务离开阻塞状态。守护程序任务的优先级高于任务1的优先级,因此调度器选择守护程序任务作为进入运行状态的任务。 任务1在完成执行xTimerStart()
函数之前被守护程序任务抢占,现在处于就绪状态。守护程序任务开始处理任务1发送到定时器命令队列的命令。t3时刻 守护程序任务已完成对任务1发送给它的命令的处理,并尝试从计时器命令队列接收更多数据。 计时器命令队列为空,因此守护程序任务重新进入阻塞状态。 任务1现在是处于就绪状态的最高优先级任务,因此调度程序选择任务1作为要进入运行状态的任务。
t4时刻 任务1在完成执行
xTimerStart()
函数之前被守护程序任务抢占,并且只有在重新进入运行状态后才退出(从)xTimerStart()
。t5时刻 任务1调用导致其进入阻塞状态的API函数。空闲任务现在是处于就绪状态的最高优先级任务,因此调度程序选择空闲任务作为要进入运行状态的任务。
在图42所示的场景中,任务1向计时器命令队列发送命令与守护进程任务接收和处理命令之间经过了一段时间。在图43所示的场景中,在Task1从发送命令的函数返回之前,守护进程任务已经接收并处理了Task1发送给它的命令。
发送到计时器命令队列的命令包含时间戳。时间戳用于说明从应用程序任务发送的命令到守护程序任务正在处理的同一命令之间经过的任何时间。例如,如果发送“启动计时器”命令来启动周期为10个滴答的计时器,则时间戳用于确保计时器是在命令发送后10个滴答超时,而不是在命令被守护进程处理之后10个滴答超时。
创建和开始一个软件定时器
xTimerCreate()API函数
FreeRTOS V9.0.0还包括xTimerCreateStatic()
函数,该函数分配在编译时静态创建计时器所需的内存:软件计时器必须先显式创建,然后才能使用。
软件计时器由TimerHandle_t
类型的变量引用。xTimerCreate()
用于创建软件计时器,并返回TimerHandle_t
以引用其创建的软件计时器。软件计时器在休眠状态下创建。
可以在调度器运行之前创建软件计时器,也可以在调度器启动后从任务创建软件计时器。第0节介绍了使用的数据类型和命名约定。
清单73. xTimerCreate()API函数原型
表27. xTimerCreate()参数和返回值
xTimerStart()API函数
xTimerStart()
用于启动处于休眠状态的软件定时器,或重置(重新启动)处于运行状态的软件定时器。xTimerStop()
用于停止处于运行状态的软件计时器。停止软件计时器与将计时器转换到休眠状态相同。
可以在调度程序启动之前调用xTimerStart()
,但是当这样做时,软件计时器直到调度程序启动时才会实际启动。
注意:切勿从中断服务例程调用xTimerStart()。应该使用中断安全版本xTimerStartFromISR()来代替它。
清单74. xTimerStart()API函数原型
表28. xTimerStart()参数和返回值
此示例创建并启动一个一次性计时器和一个自动重新加载计时器-如清单75所示。
清单75. 创建并启动示例13中使用的计时器
计时器的回调函数在每次被调用时只打印一条消息。清单76中显示了一次性计时器回调函数的实现。自动重新加载计时器回调函数的实现如清单77所示。
清单76. 示例13中的一次性定时器使用的回调函数
清单77. 示例13中的自动重新加载计时器使用的回调函数
执行此示例将生成如图44所示的输出。图44显示了自动重新加载计时器的回调函数以500个滴答的固定周期执行(清单75中的mainAUTO_RELOAD_TIMER_PERIOD
设置为500),当滴答计数为3333时,一次性计时器的回调函数只执行一次(清单75中的MainOne_Shot_Timer_Period
设置为3333)。
定时器ID
每个软件计时器都有一个ID,它是应用程序编写器可以出于任何目的使用的标记值。ID存储在空指针(void*)
中,因此可以直接存储整数值、指向任何其他对象或用作函数指针。
创建软件计时器时会为ID分配初始值-之后可以使用vTimerSetTimerID()
API函数更新ID,并使用pvTimerGetTimerID()
API函数进行查询。
与其他软件计时器API函数不同,vTimerSetTimerID()
和pvTimerGetTimerID()
直接访问软件计时器-它们不向计时器命令队列发送命令。
vTimerSetTimerID()API函数
清单78. vTimerSetTimerID()API函数原型
表 29. vTimerSetTimerID() 参数
pvTimerGetTimerID()API函数
清单79pvTimerGetTimerID()API函数原型
表30. pvTimerGetTimerID()参数和返回值
示例14.使用回调函数参数和软件定时器ID
可以将相同的回调函数分配给多个软件计时器。完成后,回调函数参数用于确定哪个软件计时器过期。
示例13使用了两个单独的回调函数;一个回调函数由OneShot计时器使用,另一个回调函数由自动重新加载计时器使用。示例14创建与示例13创建的功能类似的功能,但将单个回调函数分配给两个软件计时器。
示例14使用的main()函数与示例13使用的main()函数几乎相同,唯一的区别是创建软件计时器的位置。清单80显示了这种差异,其中prvTimerCallback()
用作两个计时器的回调函数。
清单80. 创建示例14中使用的计时器
prvTimerCallback()
将在任一计时器超时时执行。prvTimerCallback()
的实现使用函数的参数来确定调用它是因为一次性计时器过期,还是因为自动重新加载计时器过期。
prvTimerCallback()
还演示了如何将软件计时器ID用作特定于计时器的存储;每个软件计时器在其自己的ID中保存其过期次数的计数,并且自动重新加载计时器在第五次执行时使用该计数停止自身。
prvTimerCallback()
的实现如清单79所示
清单81. 示例14中使用的计时器回调函数
示例14产生的输出如图45所示。可以看到,自动重新加载计时器只执行五次。
更改定时器的周期
每个官方FreeRTOS端口都提供了一个或多个示例项目。大多数示例项目都是在运行中不断自检,LED用于提供项目状态的可视反馈;如果自检总是通过,则LED缓慢闪烁,如果自检失败,则LED快速闪烁。
一些示例项目在任务中执行自检,并使用vTaskDelay()
函数控制LED的切换速率。其他示例项目在软件计时器回调函数中执行自检,并使用计时器的周期来控制LED的切换速率。
xTimerChangePeriod()API函数
使用xTimerChangePeriod()
函数更改软件计时器的周期。
如果xTimerChangePeriod()
用于更改已在运行的计时器的周期,则该计时器将使用新的周期值重新计算其到期时间。重新计算的过期时间是相对于调用xTimerChangePeriod()
的时间,而不是相对于最初启动计时器的时间。
如果使用xTimerChangePeriod()
来更改处于休眠状态的计时器(未运行的计时器)的周期,则计时器将计算到期时间,并转换到运行状态(计时器将开始运行)。
注意:切勿从中断服务例程调用xTimerChangePeriod()
。应该使用中断安全版本xTimerChangePerodFromISR()
来代替它。
清单82. xTimerChangePeriod()API函数原型
表31 xTimerChangePeriod()参数和返回值
清单83 展示了包含自检的FreeRTOS例程是怎么在软件定时器的回调函数中使用 xTimerChangePeriod()
在自检失败时提高LED闪烁速度的。执行自检的软件定时器被称为“检查定时器”。
清单83.使用xTimerChangePeriod()
重置一个定时器
重置软件计时器意味着重新启动计时器;计时器的超时时间相对于计时器重置的时间被重新计算,而不是根据计时器最初启动的时间。图46演示了这一点,它显示了一个以6为周期的计时器,在最终到期并执行他的回调函数之前,重启两次的过程。
参考图46:
定时器1在时间t1启动。它的周期为6,因此它执行回调函数的时间最初计算为T7,即启动后的6个滴答。
定时器1在到达时间T7之前,也就是在它到期并执行其回调函数之前被重置。定时器1在时间t5被重置,因此它将执行其回调函数的时间被重新计算为t11,即它被重置后的6个滴答。
定时器1在时间t11之前再次重置,因此在其到期并执行其回调函数之前再次复位。定时器1在时间t9被重置,因此它将执行其回调函数的时间被重新计算为t15,这是它上次被重置后的6个滴答。
定时器1不会再次复位,因此它在时间t15到期,并且相应地执行其回调函数。
xTimerReset() API函数
使用xTimerReset()
API函数重置计时器。
xTimerReset()
还可用于启动处于休眠状态的计时器。
注意:切勿从中断服务例程调用xTimerReset()
。应该使用中断安全版本xTimerResetFromISR()
来代替它。
清单84. xTimerReset()API函数原型
表32. xTimerReset()参数和返回值
示例15.重置软件计时器
此示例模拟手机上的背光行为。背光:
按下某个键时打开。
如果在特定时间段内按下更多键,则保持打开状态。
如果在特定时间段内没有按下键,则自动关闭。
使用一次性软件计时器来实现此行为:
按下按键时打开[模拟]背光,在软件计时器的回调函数中关闭[模拟]背光。
每次按键按下,软件定时器重置。
因此,需要按下按键防止背光熄灭的时间等于软件定时器的周期;如果在计时器到期之前没有通过按键重置软件计时器,则执行计时器的回调功能,并且关闭背光。
xSimulatedBacklightOn
变量保存背光状态。xSimulatedBacklightOn
设置为pdTRUE表示背光打开,设置为pdFALSE表示背光关闭。
软件计时器回调函数如清单85所示。
清单85. 示例15中使用的一次性计时器的回调函数
示例15创建一个任务来轮询键盘[1]。清单86显示了该任务,但是出于下一段中描述的原因,清单86并不是最佳设计的代表。
[1].打印到Windows控制台和从Windows控制台读取密钥都会导致执行Windows系统调用。Windows系统调用,包括使用Windows控制台、磁盘或TCP/IP堆栈,可能会对FreeRTOS Windows端口的行为产生不利影响,通常应该避免。
使用FreeRTOS允许您的应用程序是事件驱动的。事件驱动设计非常高效地使用处理时间,因为处理时间仅在事件已发生时使用,并且处理时间不会浪费在轮询尚未发生的事件上。实施例15不能由事件驱动,因为在使用FreeRTOS Windows端口时处理键盘中断是不切实际的,因此必须使用效率低得多的轮询技术。如果清单86是一个中断服务例程,那么将使用xTimerResetFromISR()
代替xTimerReset()
。
清单86. 示例15中用于重置软件计时器的任务
执行示例15时产生的输出如图47所示。参考图47:
第一次按键发生在滴答计数为812的时候。当时打开了背光,启动了一次计时器。
当滴答计数为1813、3114、4015和5016时,出现了进一步的按键。所有这些按键都会导致计时器在计时器到期之前被重置。
计时器在滴答计数为10016时超时。当时背光是关着的。
在图47中可以看到,计时器有5000个滴答的周期;在上次按下一个键之后恰好5000个滴答地关闭了背光,所以在最后一次重置计时器之后有5000个滴答。
Last updated