FreeRTOS系列第28篇---系统节拍时钟分析
扫描二维码
随时随地手机看文章
关注、星标公众号,直达精彩内容ID:技术让梦想更伟大整理:李肖遥
操作系统的运行是由系统节拍时钟驱动的。在FreeRTOS中,我们知道系统延时和阻塞时间都是以系统节拍时钟周期为单位。在配置文件
FreeRTOSConfig.h
,改变宏configTICK_RATE_HZ
的值,可以改变系统节拍时钟的中断频率,也间接的改变了系统节拍时钟周期(T=1/f)
。比如设置宏configTICK_RATE_HZ
为100,则系统节拍时钟周期为10ms,设置宏configTICK_RATE_HZ
为1000,则系统节拍时钟周期为1ms。系统节拍中断服务程序会调用函数xTaskIncrementTick()
来完成主要工作,如果该函数返回值为真(不等于pdFALSE),说明处于就绪态任务的优先级比当前运行的任务优先级高。这会触发一次PendSV中断,进行上下文切换。我们重点看一下函数xTaskIncrementTick()
做了哪些事情,以及什么情况下返回真值。1.调度器正常情况
调度器正常(没有挂起),即变量uxSchedulerSuspended
的值为pdFALSE。变量uxSchedulerSuspended
是定义在tasks.c文件中的静态变量,记录调度器运行状态。当调用API函数vTaskSuspendAll()
挂起调度器时,会将变量uxSchedulerSuspended
增1。所以变量uxSchedulerSuspended
为真时,表示调度器被挂起。调度器正常情况下,首先将变量xTickCount
增1。变量xTickCount
也是在tasks.c文件中定义的静态变量,它在启动调度器时被清零,在每次系统节拍时钟发生中断后加1,用来记录系统节拍时钟中断的次数。内核会将所有阻塞的任务跟这个变量比较,以判断是否超时(超时意味着可以解除阻塞)。变量xTickCount
的数据类型跟具体硬件有关,32位架构硬件一般是无符号32位变量、8位或16位架构一般是无符号16位变量。即便是32位变量,xTickCount
累加到0xFFFFFFFF后也会溢出。因此,在程序中要判断变量xTickCount
是否溢出。如果溢出(xTickCount
为0),则调用宏taskSWITCH_DELAYED_LISTS()
交换延时列表指针和溢出延时列表指针。这个牵扯的有点广,我们慢慢说明。为了解决xTickCount
溢出问题,FreeRTOS使用了两个延时列表:xDelayedTaskList1
和xDelayedTaskList2
。并使用延时列表指针pxDelayedTaskList
和溢出延时列表指针pxOverflowDelayedTaskList
分别指向上面的延时列表1和延时列表2(在创建任务时将延时列表指针指向延时列表)。顺便说一下,上面的两个延时列表指针变量和两个延时列表变量都是在tasks.c中定义的静态局部变量。比如我们使用API延时函数vTaskDelay( xTicksToDelay )
将任务延时xTicksToDelay
个系统节拍周期,延时函数会以当前的系统节拍中断次数xTickCount
为参考,这个值加上参数规定的延时时间xTicksToDelay
,即xTickCount xTicksToDelay
,就是下次唤醒任务的时间。xTickCount xTicksToDelay
会被记录到任务TCB中,随着任务一起挂接到延时列表。如果内核判断出xTickCount xTicksToDelay
溢出(大于32位可以表示的最大值),就将当前任务挂接到列表指针pxOverflowDelayedTaskList
指向的列表中,否则就挂接到列表指针pxDelayedTaskList
指向的列表中。任务按照延时时间,顺序的插入到延时列表中。所以当系统节拍中断次数计数器xTickCount
溢出时,必须将延时列表指针pxDelayedTaskList
和溢出延时列表指针pxOverflowDelayedTaskList
交换以便正确处理延时的任务。宏taskSWITCH_DELAYED_LISTS()
的代码如下所示:#definetaskSWITCH_DELAYED_LISTS() \
{ \
List_t *pxTemp \
\
/* The delayed tasks list should beempty when the lists are switched. */ \
configASSERT( ( listLIST_IS_EMPTY( pxDelayedTaskList) ) ); \
\
pxTemp = pxDelayedTaskList; \
pxDelayedTaskList = pxOverflowDelayedTaskList; \
pxOverflowDelayedTaskList = pxTemp; \
xNumOfOverflows ; \
prvResetNextTaskUnblockTime \
}
这段代码完成两部分工作,第一是将延时列表指针pxDelayedTaskList
和溢出延时列表指针pxOverflowDelayedTaskList
交换;第二是调用函数prvResetNextTaskUnblockTime()
重新获取下一次解除阻塞的时间,这个时间保存在静态变量xNextTaskUnblockTime
中,该变量也是定义在tasks.c中。下面检查延时列表任务是否到期时,会用到这个变量。接下来函数会检查延时列表,查看延时的任务是否到期。前面我们说过,延时的任务根据延时时间先后,顺序的插入到延时列表中,延时时间短的在前,延时时间长的在后,并且下一个要被唤醒任务的时间数值保存在变量xNextTaskUnblockTime
中。所以使用xTickCount
与xNextTaskUnblockTime
比较就可以知道是否有任务可以被唤醒。if( xConstTickCount >=xNextTaskUnblockTime )
{
/* 延时的任务到期,需要被唤醒 */
}
如果任务被唤醒,则将任务从延时列表中删除,重新加入就绪列表。如果新加入就绪列表的任务优先级大于当前任务优先级,则会触发一次上下文切换。FreeRTOS支持多个任务共享同一个优先级,如果设置为抢占式调度(宏configUSE_PREEMPTION
设置为1)并且宏configUSE_TIME_SLICING
也为1(或未定义),则相同优先级的多个任务间进行任务切换。最后还会调用时间片钩子函数vApplicationTickHook()
。可以看到时间片钩子函数是在中断服务函数中调用的,所以这个钩子函数必须简洁、不可以调用不带中断保护的API函数。2.调度器挂起情况
如果调度器挂起,正在执行的任务会一直继续执行,内核不再调度(意味着当前任务不会被切换出去),直到该任务调用了xTaskResumeAll()
函数。在调度器挂起阶段内,FreeRTOS使用静态变量uxPendedTicks
记录挂起期间,系统节拍中断的次数。当调用恢复调度器函数xTaskResumeAll()
时,会执行uxPendedTicks
次本函数(xTaskIncrementTick()
)。变量uxPendedTicks
同样是在tasks.c中定义的。3.自动任务切换
函数的最后几行代码颇让人难以理解,其中局部变量xSwitchRequired
是本函数的返回值,在文章开始也说过:“如果该函数返回值为真,说明处于就绪态任务的优先级高于当前运行任务的优先级,则会触发一次PendSV中断,进行上下文切换”,现在如果变量xYieldPending
为真,则返回值也会为真,函数结束后会进行上下文切换。这个变量xYieldPending
的作用是什么?又是在什么时候被赋值为真呢?还真要从头说起。if( xYieldPending != pdFALSE )
{
xSwitchRequired = pdTRUE;
}
带中断保护的API函数,都会有一个参数pxHigherPriorityTaskWoken
。如果API函数导致一个任务解锁,并且解锁的任务优先级高于当前运行的任务,则API函数将*pxHigherPriorityTaskWoken
设置成pdTRUE。在中断退出前,老版本的FreeRTOS需要手动触发一次任务切换。比如在《 FreeRTOS系列第15篇---使用任务通知实现命令行解释器》一文中,我们在串口接收中断中调用了带中断保护的API函数vTaskNotifyGiveFromISR()
,在函数执行完后,会使用代码portYIELD_FROM_ISR(xHigherPriorityTaskWoken)
判断参数xHigherPriorityTaskWoken
是否为真,为真则手动强制上下文切换。BaseType_txHigherPriorityTaskWoken = pdFALSE;
/*收到一帧数据,向命令行解释器任务发送通知*/
vTaskNotifyGiveFromISR(xCmdAnalyzeHandle,