FreeRTOS软件定时器(software timer)及内部机制

简介

 软件定时器优缺点? 

FreeRTOS软件定时器特点 

 

定时器运行情况示例如下:
        Timer1:它是一次性的定时器,在 t1 启动,周期是 6 Tick 。经过 6 个tick 后,在 t7执行回 调函数。它的回调函数只会被执行一次,然后该定时器进入冬眠状态。
        Timer2:它是自动加载的定时器,在 t1 启动,周期是 5 Tick 。每经过 5个 tick 它的回调函数都被执行,比如在 t6 t11 t16 都会执行。

软件定时器的上下文 

守护任务

        要理解软件定时器 API 函数的参数,特别是里面的 xTicksToWait ,需要知道定时器执行的过程。  
        FreeRTOS 中有一个 Tick 中断,软件定时器基于 Tick 来运行。在哪里执行定时器函数?第一印象就是在 Tick 中断里执行:   
        --在 Tick 中断中判断定时器是否超时。
        --如果超时了,调用它的回调函数。
FreeRTOS RTOS ,它不允许在内核、在中断中执行不确定的代码:如果定时器函数很耗时,会影响整个系统。所以,FreeRTOS 中,不在 Tick 中断中执行定时器函数。
        在哪里执行?在某个任务里执行,这个任务就是:RTOS Damemon Task RTOS 守护任务。以前被称为"Timer server" ,但是这个任务要做并不仅仅是定时器相关,所以改名为:RTOS Damemon Task。当 FreeRTOS 的配置项 configUSE_TIMERS 被设置为 1 时,在启动调度器时,会自动创建 RTOS Damemon Task
        我们自己编写的任务函数要使用定时器时,是通过" 定时器命令队列 "(timer command
queue) 和守护任务交互,如下图所示:
守护任务的 优先级为: configTIMER_TASK_PRIORITY ;定时器命令队列的长度为configTIMER_QUEUE_LENGTH。可以自行去代码里面查找对应值。

守护任务的调度 

        守护任务的调度,跟普通的任务并无差别。当守护任务是当前优先级最高的就绪态任务时,它就可以运行。它的工作有两类:
        1、处理命令:从命令队列里取出命令、处理。
        2、执行定时器的回调函数。
        能否及时处理定时器的命令、能否及时执行定时器的回调函数,严重依赖于守护任务
的优先级。下面使用 2 个例子来演示。
        

例子 1:守护任务的优先性级较低:

t1 Task1 处于运行态,守护任务处于阻塞态。
守护任务在这两种情况下会退出阻塞态切换为就绪态:命令队列中有数据、某个定时器超时了。
至于守护任务能否马上执行,取决于它的优先级。 
t2 Task1 调用 xTimerStart()
要注意的是, xTimerStart() 只是把 "start timer" 的命令发给 " 定时器命令队列 " ,使得守护任务退出阻塞态。在本例中,Task1 的优先级高于守护任务,所以守护任务无法抢占 Task1
t3 Task1 执行完 xTimerStart() 但是定时器的启动工作由守护任务来实现,所以 xTimerStart() 返回并不表示定时器已经被启动了。
t4: Task1 由于某些原因进入阻塞态,现在轮到守护任务运行。守护任务从队列中取出"start timer" 命令,启动定时器。
t5 :守护任务处理完队列中所有的命令,再次进入阻塞态。 Idel 任务时优先级最高的就绪态任务,它执行。
注意:假设定时器在后续某个时刻 tX 超时了,超时时间是"tX-t2",而非"tX-t4",从 xTimerStart()函数被调用时算起。

 

例子 2:守护任务的优先性级较高 

t1 Task1 处于运行态,守护任务处于阻塞态。守护任务在这两种情况下会退出阻塞态切换为就绪态:命令队列中有数据、某个定时器超时了。至于守护任务能否马上执行,取决于它的优先级。
t2 Task1 调用 xTimerStart(), 要注意的是, xTimerStart() 只是把 "start timer" 的命令发给 " 定时器命令队列 " ,使得守护任务退出阻塞态。在本例中,守护任务的优先级高于 Task1 ,所以守护任务抢占 Task1 ,守护任务开始处理命令队列。Task1 在执行 xTimerStart() 的过程中被抢占,这时它无法完成此函数。
t3 :守护任务处理完命令队列中所有的命令,再次进入阻塞态。此时 Task1 是优先级最高的就绪态任务,它开始执行。
t4 Task1 之前被守护任务抢占,对 xTimerStart() 的调用尚未返回。现在开始继续运行次函数、返回。
t5 Task1 由于某些原因进入阻塞态,进入阻塞态。 Idel 任务时优先级最高的就绪态任务,它执行。

 回调函数

定时器的回调函数的原型如下:void ATimerCallback( TimerHandle_t xTimer ); 

定时器的回调函数是在守护任务中被调用的,守护任务不是专为某个定时器服务的,它还要处理其他定时器。所以,定时器的回调函数不要影响其他人:
1、回调函数要尽快实行,不能进入阻塞状态。
2、不要调用会导致阻塞的 API 函数,比如 vTaskDelay()。
3、可以调用 xQueueReceive() 之类的函数,但是超时时间要设为 0 :即刻返回,不可阻塞。

软件定时器的函数 

根据定时器的状态转换图,就可以知道所涉及的函数:

 

创建软件定时器API函数 

要使用定时器,需要先创建它,得到它的句柄。有两种方法创建定时器:动态分配内存、静态分配内存。函数原型如下:

 

回调函数的类型是: 

删除

动态分配的定时器,不再需要时可以删除掉以回收内存。删除函数原型如下:

 

         定时器的很多 API 函数,都是通过发送"命令"到命令队列,由守护任务来实现。如果队列满了, " 命 令 " 就无法即刻写入队列。我们可以指定一个超时时间xTicksToWait,等待一会。

启动/停止

        启动定时器就是设置它的状态为运行态(Running、Active)。停止定时器就是设置它的状态为冬眠(Dormant),让它不能运行。涉及的函数原型如下:

        注意,这些函数的 xTicksToWait 表示的是,把命令写入命令队列的超时时间。命令队列可能已经满了,无法马上把命令写入队列里,可以等待一会。
         xTicksToWait 不是定时器本身的超时时间,不是定时器本身的 " 周期 " 。创建定时器时,设置了它的周期(period) xTimerStart() 函数是用来启动定时器。
        假设调用 xTimerStart() 的时刻是 tX ,定时器的周期是 n ,那么在 tX+n 时刻定时器的回调函数被调用。如果定时器已经被启动,但是它的函数尚未被执行,再次执行 xTimerStart() 函数相当于执行 xTimerReset() ,重新设定它的启动时间。
复位
        从定时器的状态转换图可以知道,使用 xTimerReset() 函数可以让定时器的状态从冬眠态转换为运行态,相当于使用 xTimerStart() 函数。
        如果定时器已经处于运行态,使用 xTimerReset() 函数就相当于重新确定超时时间。假设调用 xTimerReset() 的时刻是 tX ,定时器的周期是 n ,那么 tX+n 就是重新确定的超时时间。
        复位函数的原型如下:

修改周期 

        从定时器的状态转换图可以知道,使用 xTimerChangePeriod() 函数,处理能修改它的
周期外,还可以让定时器的状态从冬眠态转换为运行态。
        修改定时器的周期时,会使用新的周 期重新计算它的超时时间。 xTimerChangePeriod() 函数的时间 tX ,新的周期是 n ,则 tX+n 就是新的超时时间。
        相关函数的原型如下:

定时器 ID 

软件定时器的一般使用 

        我们这里不去分析软件定时器的一般使用,我们讲方法,具体可以自己去根据知识点自己写一些代码自己玩一下,我这里推荐韦东山老师,大家可以去看一下他的实战视频,有教你怎么去使用软件定时器。

软件定时器的内部机制

创建软件定时器

函数逻辑分析:

  • 首先,函数内部会通过 pvPortMalloc 分配内存来创建一个 Timer_t 结构体(假设 Timer_t 是一个定时器结构体或类型)的实例 pxNewTimer

  • 如果内存分配成功(即 pxNewTimer != NULL),则调用 prvInitialiseNewTimer 函数,用传入的参数来初始化新创建的定时器 pxNewTimer

  • 如果系统支持静态分配(configSUPPORT_STATIC_ALLOCATION 等于 1),则设置 pxNewTimer->ucStaticallyAllocatedpdFALSE,表示该定时器是动态分配的。

  • 最后,函数返回指向新创建定时器的指针 pxNewTimer

返回值分析:

  • 函数返回的是 TimerHandle_t 类型的指针 pxNewTimer,这个指针指向一个新创建的定时器结构体或对象。用户可以使用这个指针来操作和管理这个定时器,比如启动、停止、删除等操作。

 定时器句柄结构体

1、pcTimerName,类型:const char *,含义:定时器的名称。这个字段不被内核使用,仅用于调试目的,方便开发者识别和调试定时器的用途。
2、xTimerListItem,类型:ListItem_t,含义:标准的链表项,用于将定时器控制结构体链接到内核管理的事件链表中。这是 FreeRTOS 内核中用于事件管理的通用链表项。

3、xTimerPeriodInTicks,类型:TickType_t,含义:定时器的周期,以时钟节拍数(ticks)表示。即定时器每隔多少个节拍就会触发一次。

4、uxAutoReload,类型:UBaseType_t,含义:指示定时器是否自动重新加载的标志。如果设置为 pdTRUE,则定时器在到期后会自动重新启动,即周期性定时器。如果设置为 pdFALSE,则定时器是单次触发的,即一次性定时器。

5、pvTimerID,类型:void *,含义:用于标识定时器的 ID。当多个定时器共享相同的回调函数时,通过这个 ID 可以区分不同的定时器实例。

6、pxCallbackFunction,类型:TimerCallbackFunction_t,含义:定时器到期时调用的回调函数。这个函数会执行用户定义的操作,比如处理定时器事件或触发其他动作。

7、uxTimerNumber,类型:UBaseType_t,含义:用于跟踪定时器的编号,这个编号通常由跟踪工具(如 FreeRTOS+Trace)分配和使用。这个字段仅在配置了跟踪功能 (configUSE_TRACE_FACILITY == 1) 时才可用。

8、ucStaticallyAllocated,类型:uint8_t,含义:静态分配标志。在支持静态分配和动态分配的系统中,如果定时器是静态分配的,则设置为 pdTRUE;否则,设置为 pdFALSE。静态分配的定时器在删除时不会尝试释放其内存。

这里强调一下,xTimerListItem很关键,因为这是链表结构体,通过它把这个定时器放到合适的链表里面。 我标红的每个都很关键,他们是构成软件定时器的核心关键。

启动软件定时器(向定时器队列发送启动命令)

这段代码是一个用于发送定时器命令的函数 xTimerGenericCommand。让我们逐步分析其功能和实现细节:
函数签名和参数解析
BaseType_t xTimerGenericCommand( TimerHandle_t xTimer,
                                 const BaseType_t xCommandID,
                                 const TickType_t xOptionalValue,
                                 BaseType_t * const pxHigherPriorityTaskWoken,
                                 const TickType_t xTicksToWait )


1.参数解析:
2.xTimer:定时器句柄,指向具体的定时器实例。
3.xCommandID:命令ID,用于指示要执行的操作,如启动、停止、重置等。
4.xOptionalValue:可选的数值参数,用于传递额外的信息。
5.pxHigherPriorityTaskWoken:指向一个变量的指针,用于在中断服务程序中指示是否唤醒了比当前任务优先级更高的任务。
6.xTicksToWait:如果队列满了,等待的时间(以时钟节拍为单位)。

函数主要逻辑:

1.配置断言:

   configASSERT( xTimer );

这行代码用于确保 xTimer 不为 NULL,如果为 NULL,将触发配置断言,通常用于调试目的。

2.发送消息给定时器服务任务:

   if( xTimerQueue != NULL )
   {
       // 创建消息
       xMessage.xMessageID = xCommandID;
       xMessage.u.xTimerParameters.xMessageValue = xOptionalValue;
       xMessage.u.xTimerParameters.pxTimer = ( Timer_t * ) xTimer;

       // 根据命令ID决定发送方式
       if( xCommandID < tmrFIRST_FROM_ISR_COMMAND )
       {
           // 任务调度器运行时,通过普通队列发送消息
           if( xTaskGetSchedulerState() == taskSCHEDULER_RUNNING )
           {
               xReturn = xQueueSendToBack( xTimerQueue, &xMessage, xTicksToWait );
           }
           // 任务调度器未运行时,立即发送消息
           else
           {
               xReturn = xQueueSendToBack( xTimerQueue, &xMessage, tmrNO_DELAY );
           }
       }
       else
       {
           // 从中断服务程序中发送消息
           xReturn = xQueueSendToBackFromISR( xTimerQueue, &xMessage, pxHigherPriorityTaskWoken );
       }

       // 跟踪发送的命令
       traceTIMER_COMMAND_SEND( xTimer, xCommandID, xOptionalValue, xReturn );
   }
   else
   {
       // 没有定时器队列的覆盖测试
       mtCOVERAGE_TEST_MARKER();
   }


3.首先检查 xTimerQueue 是否为 NULL,以确保定时器队列存在。


4.创建一个 DaemonTaskMessage_t 类型的 xMessage 并设置其中的各种参数,准备发送给定时器服务任务。


5.根据 xCommandID 的值判断是通过常规队列发送还是通过中断服务程序发送。


6.如果 xCommandID < tmrFIRST_FROM_ISR_COMMAND,则使用 xQueueSendToBack 发送到队列中。根据任务调度器的状态决定是否等待一段时间。


7.如果 xCommandID >= tmrFIRST_FROM_ISR_COMMAND,则通过 xQueueSendToBackFromISR 从中断服务程序发送,并传递 pxHigherPriorityTaskWoken 变量以指示是否唤醒了更高优先级的任务。


8.最后,记录发送命令的追踪信息。]

9.处理队列不存在的情况:
如果 xTimerQueue 为 NULL,则执行 mtCOVERAGE_TEST_MARKER(),这通常是为了代码覆盖率测试而插入的空操作。

10.返回值:函数最后返回 xReturn,其值表示发送消息的结果,可能是 pdPASS 或 pdFAIL。

在成功写入队列之后,就会去唤醒守护函数。
 

删除/复位/更改周期定时器

        这个操作其实和启动定时器类似,只不过发送的message有区别,之后守护任务会根据message来决定对定时器采取什么操作。

守护函数

        1、当FreeRTOS 的配置项 configUSE_TIMERS 设置为1,在启动任务调度器时,会自动创建软件定时器的服务/守护任务。        

  2、然后会创建prvTimerTask任务(守护任务),这是核心中的核心。           

3、守护任务

1.首先从定时器链表里面获取最快超时的定时器时间。

2.然后去到prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );处理超时的定时器和接收到的命令。

3.prvProcessReceivedCommands(); 清空掉定时器队列。 

prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );

有三种可能:1、处理超时定时器,2、等待定时器超时,然后回到1。3、收到命令而退出。

1、处理一下超时的定时器。

 2、如果没有定时器超时。

vQueueWaitForMessageRestricted( xTimerQueue, ( xNextExpireTime - xTimeNow ), xListWasEmpty ); 

这里就是为什么软件定时器可以实现定时器的功能了,我们通过下次超时的时间和当前的时间相减,就可以算出还有多久,定时器的就超时了,利用队列的性质,如果阻塞相应的时间,就会退出阻塞——定时器超时,这样子,我们就可以去进行定时器超时的处理了。  

prvProcessReceivedCommands( void ) ;(处理定时器命令)

1、看看是不是有定时器的定时时间为0,那我们直接回调。 

2、处理命令。

 

 

 

总结

        我们的软件定时器内部机制就是这么回事,本质还是利用队列的阻塞来实现,我们只要理解实现的大概思路,再去查看源码,就会发现原来如此,但是源码还是很有难度的,我也只讲解了片面(虽然我也只会片面),但是没关系,只有更加深入了解底层,之后面对rtos的问题的时候,才能更好的去解决问题。 

  • 20
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
FreeRTOS 中,软件定时器和硬件定时器是两种不同的定时器实现方式,用于实现任务的定时调度和时间管理。 1. 软件定时器Software Timer): 软件定时器FreeRTOS 提供的一种基于软件定时器机制,通过 FreeRTOS 内核的任务调度器进行管理。软件定时器主要由 `xTimerCreate()`、`xTimerStart()`、`xTimerStop()` 等 API 函数来创建、启动、停止和删除。软件定时器适用于需要在任务中使用的相对较低频率的定时操作。 通过软件定时器,可以创建多个定时器以满足不同任务的需求,并且可以在定时器到期时触发回调函数来执行特定的操作。软件定时器使用 FreeRTOS 的任务调度器进行管理,因此,如果有其他高优先级任务需要执行,软件定时器会在适当的时机被暂停,并在下一个合适的时间点继续执行。 2. 硬件定时器(Hardware Timer): 硬件定时器是嵌入式系统中的硬件设备,可由硬件芯片提供。硬件定时器通常由专用寄存器和计数器组成,可用于生成精确的时间延迟或周期性触发中断。在 FreeRTOS 中,可以将硬件定时器软件定时器结合使用,以提供更精确和高频率的定时操作。 使用硬件定时器需要根据硬件平台和具体的芯片手册进行配置和初始化。一旦硬件定时器设置完成,可以在中断服务程序中处理定时器中断,并在中断处理程序中触发所需的操作。 需要根据具体的应用场景和需求来选择使用软件定时器还是硬件定时器软件定时器适用于相对较低频率和较少精度要求的任务调度,而硬件定时器适用于高频率和精确性要求较高的定时操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值