简介
软件定时器优缺点?
FreeRTOS软件定时器特点
软件定时器的上下文
守护任务
守护任务的调度
例子 1:守护任务的优先性级较低:
例子 2:守护任务的优先性级较高
回调函数
定时器的回调函数的原型如下:void ATimerCallback( TimerHandle_t xTimer );
软件定时器的函数
创建软件定时器API函数
回调函数的类型是:
删除
动态分配的定时器,不再需要时可以删除掉以回收内存。删除函数原型如下:
启动/停止
启动定时器就是设置它的状态为运行态(Running、Active)。停止定时器就是设置它的状态为冬眠(Dormant),让它不能运行。涉及的函数原型如下:
修改周期
定时器 ID
软件定时器的一般使用
我们这里不去分析软件定时器的一般使用,我们讲方法,具体可以自己去根据知识点自己写一些代码自己玩一下,我这里推荐韦东山老师,大家可以去看一下他的实战视频,有教你怎么去使用软件定时器。
软件定时器的内部机制
创建软件定时器
函数逻辑分析:
首先,函数内部会通过
pvPortMalloc
分配内存来创建一个Timer_t
结构体(假设Timer_t
是一个定时器结构体或类型)的实例pxNewTimer
。如果内存分配成功(即
pxNewTimer != NULL
),则调用prvInitialiseNewTimer
函数,用传入的参数来初始化新创建的定时器pxNewTimer
。如果系统支持静态分配(
configSUPPORT_STATIC_ALLOCATION
等于 1),则设置pxNewTimer->ucStaticallyAllocated
为pdFALSE
,表示该定时器是动态分配的。最后,函数返回指向新创建定时器的指针
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的问题的时候,才能更好的去解决问题。