软件定时器能够让函数在未来设定的时间执行。由定时器执行的函数称为定时器回调函数。从定时器启动到其回调函数执行之间的时间 被称为定时器的周期。简而言之, 当定时器的周期到期时,定时器的回调函数会被执行。
请注意,在使用软件定时器之前,必须明确地创建它。
定时器服务任务(主要)利用现有 FreeRTOS 功能, 允许在对应用程序的可执行二进制文件的大小造成影响最小的情况下, 将定时器功能添加到应用程序中。
回调函数
FreeRTOS 的定时器实现不从中断上下文中执行定时器回调函数, 不消耗任何处理时间, 除非定时器实际上已经过期,不给 tick 中断增加任何处理开销,并且在中断被禁用时不走行任何链接列表结构体。
FreeRTOS 定时器的回调函数并不会直接在中断服务程序(ISR)中运行,而是在定时器服务任务的上下文中执行。
定时器没有到期时,它只需要记录剩余的时间。
只有当定时器到期后,才会触发定时器服务任务执行定时器回调函数。
在 tick 中断中,仅简单地更新 tick 计数,定时器到期后,将任务转移到定时器服务任务中处理。
中断被禁用时,尽量不执行复杂的链表操作,确保系统的实时性和稳定性。
定时器回调函数在定时器服务任务的上下文中执行不从中断上下文中执行。因此,定时器回调函数永远不试图阻塞是至关重要的 。
如果某个定时器回调函数执行了阻塞操作(例如死循环),定时器服务任务将被阻塞,导致以下问题:
其他定时器的回调函数无法被及时执行。
定时器服务任务无法处理新到期的定时器。
可能阻塞其他依赖于定时器服务任务的功能,例如动态软件定时器的创建或删除。
例如,定时器回调函数在访问队列或信号量时,不得调用 vTaskDelay()、 vTaskDelayUntil(),也不得指定非零阻塞时间。
定时器服务/守护进程任务和定时器命令队列
定时器是一个不属于核心 FreeRTOS 内核的可选功能,由 定时器服务任务(定时器守护进程任务)提供。
FreeRTOS 提供了一组与定时器相关的 API 函数。其中许多函数使用标准 FreeRTOS 队列向定时器服务任务发送命令。用于此目的的队列称为定时器命令队列。“定时器命令队列” 专用于 FreeRTOS 定时器实现,无法直接访问。
下图演示了这种情景。左边的代码表示 一个函数,属于用户应用程序的一部分, 并由作为同一用户应用程序的一部分创建的任务调用。右边的代码表示 定时器服务任务实现。 定时器命令队列将用户应用任务和定时器服务任务连接在一起(定时器 API 将任务添加到定时器任务队列中,定时器服务程序从任务队列中获取命令并执行)。 在此演示案例中,应用程序代码 调用 xTimerReset() API 函数。其结果是复位命令会发送到定时器命令队列中, 再由定时器服务任务来处理。应用程序代码 只会调用 xTimerReset() API 函数,不会(也无法)直接 访问定时器命令队列。
配置
要使 FreeRTOS 软件计时器 API 在应用程序中可用, 只需:
- 将 FreeRTOS/Source/timers.c 源文件添加到项目中,以及
- 在应用程序 FreeRTOSConfig.h 头文件中定义下表详述的常量。
常量
configUSE_TIMERS
设置为 1 以包括定时器功能。当 configUSE_TIMERS 设置为 1 时, RTOS 定时器服务任务将在调度器启动时自动创建。
configTIMER_TASK_PRIORITY
设置定时器服务任务的优先级。与所有任务一样,定时器服务任务可以 在 0 到 (configMAX_PRIORITIES - 1) 之间的任何优先级运行。
需要仔细选择此数值,以满足应用程序的要求。例如,如果 定时器服务任务被设置为系统中最高优先级的任务, 则发送到定时器服务任务(当调用定时器 API 函数时)和过期的定时器的命令都将立即得到处理。 相反,如果定时器服务任务被赋予低优先级, 则发送到定时器服务任务和过期定时器的命令将不会被处理, 直到定时器服务任务成为能够运行的最高优先级任务。然而,值得注意的是,定时器到期时间是相对于发送命令的时间计算的, 而不是相对于处理命令的时间计算的。
configTIMER_QUEUE_LENGTH
这设置了定时器命令队列在任一时间可以容纳的未处理命令的最大数量 。定时器命令队列可能已满的原因包括:
- 在 RTOS 调度器启动之前(即创建定时器服务任务之前) 进行多次定时器 API 函数调用。
- 从中断服务程序 (ISR) 进行多次(中断安全)定时器 API 函数调用。
- 从优先级高于定时器服务任务的任务进行多次定时器 API 函数调用。
configTIMER_TASK_STACK_DEPTH
设置分配给定时器服务任务的堆栈大小(以字为单位,而不是以字节为单位)。
定时器回调函数在定时器服务任务的上下文中执行。因此,定时器服务任务的堆栈要求 取决于定时器回调函数的堆栈要求。
定时器运行机制
定时器创建:用户通过 xTimerCreate() 函数创建一个定时器,并为其指定一个时间间隔和回调函数。
启动定时器:定时器通过 xTimerStart() 或 xTimerChangePeriod() 启动。一旦定时器启动,它就开始计时。
计时:软件定时器的计时是基于系统的 tick 中断。每次 tick 中断发生时,FreeRTOS 会检查所有定时器,看是否有定时器已到期。如果有到期的定时器,它会将其移到待处理队列中,等待定时器服务任务处理。
定时器回调:所有定时器的回调函数都由定时器服务任务(Timer Service Task)来执行。当定时器到期时,回调函数会在定时器服务任务上下文中执行,而不是在中断上下文中执行。定时器服务任务在
定时器服务任务:定时器服务任务是一个系统级的任务,通常以最低优先级运行。它会定期检查是否有定时器回调需要执行。
定时器 API
xTimerCreate
创建一个新的软件定时器实例, 并返回一个可以引用定时器的句柄。
定时器是在休眠状态下创建的。需要显示的激活使用;
1 | // timers.h |
参数:
pcTimerName
分配给定时器的可读文本名称。这样做纯粹是为了协助调试。 RTOS 内核本身只通过句柄引用定时器,而不是通过其名称。
xTimerPeriod
定时器的周期。周期以滴答为单位,宏 pdMS_TO_TICKS() 可用于 将以毫秒为单位的时间转换为以滴答为单位的时间。
uxAutoReload
如果 uxAutoReload 设置为 pdTRUE, 那么定时器将以 xTimerPeriod 参数设置的频率重复过期。如果 uxAutoReload 设置为 pdFALSE,则此定时器为一次性定时器, 它会在到期后进入休眠状态。
pvTimerID
分配给正在创建的定时器的标识符。通常情况下, 当同一回调函数分配给多个定时器时,该 ID 将用于定时器回调函数, 以识别哪个定时器过期,或者与 vTimerSetTimerID 和 pvTimerGetTimerID()API 函数一起用于在定时器回调函数调用之间保存值。
pxCallbackFunction
定时器到期时调用的函数。回调函数必须具有 TimerCallbackFunction_t 定义的原型,即:
1
void vCallbackFunction( TimerHandle_t xTimer );
返回:
- 如果定时器创建成功, 则返回新创建的定时器的句柄。如果由于剩余的 FreeRTOS 堆不足以分配定时器结构体而无法创建定时器, 则返回 NULL。
xTimerStart
xTimerStart 用于启动之前使用 xTimerCreate API 函数创建的定时器。如果定时器已经启动且已处于活跃状态, 则 xTimerStart()
具有与 xTimerReset API 函数相同的功能。
在 RTOS 调度器启动之前调用 xTimerStart() 是有效的, 但是完成此操作后,直到启动 RTOS 调度器之前,定时器都不会真正启动。
1 | if( xTimerStart(xTimer, xBlockTime ) != pdPASS ) { |
参数:
xTimer
正在启动/重新启动的定时器的句柄。
xBlockTime
在调用 xTimerStart 时队列已满的情况下,调用任务处于阻塞状态以等待启动命令成功发送到定时器命令队列的时间(单位:滴答)。
如果在 RTOS 调度器启动之前就调用 xTimerStart ,则 xBlockTime 将被忽略
返回:
在经过 xBlockTime 个滴答后,启动命令依旧无法发送至定时器命令队列,则返回 pdFAIL
如果能将此命令成功发送到定时器命令队列,则返回 pdPASS
实际处理命令的时间取决于定时器服务/守护进程任务相对于系统中其他任务的优先级,尽管定时器到期时间与实际调用 xTimerStart 的时间有关。
定时器服务/守护进程任务的优先级由 configTIMER_TASK_PRIORITY 配置常量设置。
xTimerStartFromISR
从中断服务例程调用的 xTimerStart() 函数。
1 | if( xTimerStartFromISR( xBacklightTimer,&xHigherPriorityTaskWoken ) != pdPASS ) { |
参数:
xTimer
正在启动/重新启动的定时器的句柄。
pxHigherPriorityTaskWoken
定时器服务/守护进程任务大部分时间都处于“阻塞”状态,等待消息到达定时器命令队列。调用 xTimerStartFromISR() 会将消息写入定时器命令队列,从而让定时器服务/守护进程任务转换为非阻塞状态。
如果调用 xTimerStartFromISR() 导致定时器服务/守护进程任务退出阻塞状态,并且定时器服务/守护进程任务的优先级等于或高于当前执行的任务 (被中断的任务),则 *pxHigherPriorityTaskWoken 将在 xTimerStartFromISR() 函数内部被设置为 pdTRUE。如果 xTimerStartFromISR () 将此值设置为 pdTRUE, 那么应在退出中断之前执行上下文切换。
返回:
如果启动命令无法发送至定时器命令队列,则返回 pdFAIL。如果命令成功发送至定时器命令队列, 则返回 pdPASS。实际处理命令的时间取决于定时器服务/守护进程任务相对于系统中其他任务的优先级, 尽管定时器到期时间是相对于实际调用 xTimerStartFromISR() 的时间而言。定时器服务/守护进程 任务优先级由 configTIMER_TASK_PRIORITY 配置常量设置。
xTimerStop
停止先前使用 xTimerStart()、xTimerReset()、xTimerStartFromISR()、xTimerResetFromISR()、xTimerChangePeriod() 和xTimerChangePeriodFromISR() API 函数创建的定时器。
1 | xTimerStop(xTimer, xBlockTime); |
参数:
xTimer
正在停止的定时器的句柄。
xBlockTime
指定在调用 xTimerStop 时队列已满的情况下,调用任务处于阻塞状态以等待停止命令成功发送到定时器命令队列的时间(单位:滴答)。
返回:
pdFAIL
如果在 xBlockTime 滴答已过之后仍无法向定时器命令队列发送删除命令,则返回 pdFAIL
pdPASS
如果能将此命令成功发送到定时器命令队列,则返回 pdPASS
xTimerDelete
xTimerDelete() 可删除之前使用 xTimerCreate() 函数创建的定时器。
请注意,删除静态分配的定时器时, 在 xTimerIsTimerActive() 指示该定时器处于非活动状态之前,无法重复使用其静态内存。
1 | BaseType_t xTimerDelete(TimerHandle_t xTimer,TickType_t xBlockTime); |
参数:
xTimer
正在删除的定时器的句柄。
xBlockTime
指定在调用 xTimerDelete() 时队列已满的情况下, 调用任务处于阻塞状态以等待删除命令成功发送到定时器命令队列的时间 (单位:滴答)。如果 在 RTOS 调度器启动前调用 xTimerDelete(),xBlockTime 将被忽略。
返回:
- 如果在 xBlockTime 滴答已过之后仍无法向定时器命令队列发送删除命令, 则返回 pdFAIL。
- 如果能将此命令成功发送到定时器命令队列,则返回 pdPASS。实际处理命令的时间 取决于定时器服务/守护进程任务相对于系统中其他任务的优先级 。定时器服务/守护进程任务的优先级由 configTIMER_TASK_PRIORITY 配置常量设置。
XTimerlsTimerActive
查询软件定时器是否处于活动或休眠状态。
如果出现以下情况,定时器将处于休眠状态:
- 已创建但尚未启动
- 这是一个尚未重启的过期的一次性计时器。
xTimerStart()、xTimerReset()、xTimerStartFromISR()、xTimerResetFromISR()、xTimerChangePeriod() 和xTimerChangePeriodFromISR() 函数都可以用于将定时器转换为活跃状态。
1 | BaseType_t xTimerIsTimerActive( TimerHandle_t xTimer ); |
参数:
xTimer
被查询的定时器。
返回:
- 如果定时器处于休眠状态,将返回 pdFALSE。
- 如果定时器处于活动状态,将返回 pdFALSE 以外的值。
实现
1 |
|
问题
一、小技巧
1 | // FreeRTOSConfig.h |
二、定时器回调函数为什么不可以阻塞
定时器回调函数的执行方式: FreeRTOS 定时器回调函数是在定时器管理任务(vTimerTask
)中执行的,这意味着定时器回调函数会在 vTimerTask
任务的上下文中执行。这个任务的优先级通常较低,它会在其他更高优先级的任务(包括中断)执行之后被调度。
阻塞操作: 在 FreeRTOS 中,如果定时器回调函数中调用了 vTaskDelay
、vTaskDelayUntil
或类似的 API 来显式地让自己“阻塞”,或者如果回调函数执行了一个需要较长时间的操作(例如等待 I/O 或同步操作),那么它会占用 vTimerTask
的执行时间,导致其他定时器的回调函数被延迟执行。
任务调度: 如果一个定时器回调函数在执行过程中阻塞了,那么 FreeRTOS 将不会在此期间调度其他定时器回调函数。由于定时器回调函数是由同一个任务 (vTimerTask
) 执行的,如果 vTimerTask
被阻塞,其他定时器的回调也会被延迟直到 vTimerTask
完成当前任务。因此,多个定时器回调函数的并行性可能会受到影响。
三、调用 xTimerDelete 删除了该定时器,然后再次调用 xTimerStart,仍然可以重新启动同一个定时器
?
四、软件定时器启动后是如何计数的
在 FreeRTOS 中,定时器的计时值是通过系统的 Tick 定时器中断来减少的,但这个过程并不会占用额外的时间片,它是在 中断上下文 中执行的。
Tick 定时器中断(也叫做时钟中断)是由硬件定时器触发的,通常会以固定的时间间隔触发(例如,每 1 毫秒)。
每次 Tick 中断发生时,FreeRTOS 会执行一个中断服务例程(ISR),并执行以下几项重要操作:
- 增加 Tick 计数器:每次 Tick 中断时,系统的 Tick 计数器会增加 1。
- 更新定时器的计时值:FreeRTOS 会检查所有软件定时器的计时值(xTimeToExpiration),并在每次 Tick 中断时,将这些定时器的计时值减去 1。