FreeRTOS的学习(一)——STM32上的移植问题
FreeRTOS的学习(二)——任务优先级问题
FreeRTOS的学习(三)——中断机制
FreeRTOS的学习(四)——列表
FreeRTOS的学习(五)——系统延时
FreeRTOS的学习(六)——系统时钟
FreeRTOS的学习(七)——1.队列概念
FreeRTOS的学习(七)——2.队列入队源码分析
FreeRTOS的学习(七)——3.队列出队源码分析
FreeRTOS的学习(八)——1.二值信号量
FreeRTOS的学习(八)——2.计数型信号量
FreeRTOS的学习(八)——3.优先级翻转问题
FreeRTOS的学习(八)——4.互斥信号量
FreeRTOS的学习(九)——软件定时器
FreeRTOS的学习(十)——事件标志组
FreeRTOS的学习(十一)——任务通知
本节介绍的是系统时钟,也就是FreeRTOS的时钟节拍是怎么来的,并且会介绍定时器中断的函数内部实现。
FreeRTOS使用裸机自带的滴答定时器中断,使用其主频或者外部频率作为时钟基准。由于定时器的功能作为FreeRTOS的核心,所以正常情况下必须是一个一直运行着的中断,那么就意味着FreeRTOS庞大的代码量也必须与此中断相互配合,保证实时性和可靠性,因此滴答定时器的中断时间必然是不能太短的,否则将大大影响FreeRTOS的实时性。我们直接来看配置的代码:
void delay_init()
{
u32 reload;
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK);//选择内部时钟 HCLK
fac_us=SystemCoreClock/1000000; //不论是否使用OS,fac_us都需要使用
reload=SystemCoreClock/1000000; //每秒钟的计数次数 单位为M
reload*=1000000/configTICK_RATE_HZ; //根据configTICK_RATE_HZ设定溢出时间
//reload为24位寄存器,最大值:16777216,在72M下,约合0.233s左右
fac_ms=1000/configTICK_RATE_HZ; //代表OS可以延时的最少单位
SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk; //开启SYSTICK中断
SysTick->LOAD=reload; //每1/configTICK_RATE_HZ秒中断一次
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; //开启SYSTICK
}
通过控制SysTick寄存器的几个地址可以实现开启中断,设定中断频率,开启/关闭SysTick的功能。
其中,reload作为中断的频率,其计算过程较为重要,需要根据系统时钟设定。本人使用的是stm32f103,系统主频为72M,那么也就意味着,stm32在1s内可以计数72M个。那么只要控制reload的值,每次计数结束后产生一次中断就可以了。所以引入configTICK_RATE_HZ的变量,用于方便调整滴答定时器的中断频率。具体的计算相对简单,可自行尝试。
FreeRTOS通过全局变量xTickCount的计数以及在SysTick的中断函数里的操作与逻辑之间形成了联系。下面给出了**SysTick_Handler()**的中断内部函数:
void SysTick_Handler(void)
{
if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//判断系统是否已经运行
{
xPortSysTickHandler();
}
}
首先需要判断任务调度是否开始,如果任务调度没有开始,那么调用**xPortSysTickHandler()**也没有实际的意义,该函数是属于FreeRTOS的,故里面的许多变量和函数可能都无法执行。
首先看xPortSysTickHandler的整体函数:
void xPortSysTickHandler( void )
{
//执行SysTick_Handler中断函数时,为了保证在freertos中属于最低优先级的此中断能顺利执行,
//故要关闭FreeRTOS的所有可管理中断,保证系统计数时不被打断。
vPortRaiseBASEPRI(); //关中断
{
if( xTaskIncrementTick() != pdFALSE ) //判断返回值,如果为pdTURE就要进行一次上下文切换
{
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
vPortClearBASEPRIFromISR(); //开中断
}
为了保证计数的正常执行,需要先关闭系统中断,但是值得注意的是,FreeRTOS系统可关闭的中断都是系统级的,一般还有优先级在5以上的几个中断不受其管理,具体可见之前写的中断机制相关内容。那么也就不可避免地,计数会被这种情况打断,至少目前为止,以我学习到的内容来考虑,使用了RTOS就不太适合使用其他高优先级的大段大段主体内容的中断函数了,会对RTOS的运行产生极大的影响。所以系统外的高优先级中断一般都是短且对实时性要求高的,用于诸如一些紧急情况,需要紧急处理的。
接下来就是最重要的一个函数了——xTaskIncrementTick(),该函数有多个功能,除了对计数值的累加以外,还有交换延时列表,确认唤醒任务等。
接下来来看看详细的函数:
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
traceTASK_INCREMENT_TICK( xTickCount );
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) //如果任务调度器没有被挂起
{
//计数+1
const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
xTickCount = xConstTickCount;
if( xConstTickCount == ( TickType_t ) 0U )
{
//在进入计数器中断前的溢出情况存储在延时列表中,当进入此函数发现计数溢出时则交换pxDelayedTaskList和pxOverflowDelayedTaskList
taskSWITCH_DELAYED_LISTS();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( xConstTickCount >= xNextTaskUnblockTime ) //如果计数时间大于下一个在阻塞态要唤醒的任务时,则进行相关操作。
{
for( ; ; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
xNextTaskUnblockTime = portMAX_DELAY;
break;
}
else
{
pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
//再次确认下一个要唤醒的任务是否正确。
if( xConstTickCount < xItemValue )
{
xNextTaskUnblockTime = xItemValue;
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
listREMOVE_ITEM( &( pxTCB->xStateListItem ) );
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
listREMOVE_ITEM( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* Place the unblocked task into the appropriate ready
* list. */
prvAddTaskToReadyList( pxTCB );
#if ( configUSE_PREEMPTION == 1 )
{
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
}
}
}
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */
#if ( configUSE_TICK_HOOK == 1 )
{
if( xPendedTicks == ( TickType_t ) 0 )
{
vApplicationTickHook();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TICK_HOOK */
#if ( configUSE_PREEMPTION == 1 )
{
if( xYieldPending != pdFALSE )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
}
else //如果任务调度器被挂起
{
++xPendedTicks; //在任务调度器被挂起时进行节拍计数
#if ( configUSE_TICK_HOOK == 1 )
{
vApplicationTickHook();
}
#endif
}
return xSwitchRequired;
}
可以看到上面函数相对还是比较长的。看的时候可以根据每个if语句去分类,先看最外部的if判断:
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) //如果任务调度器没有被挂起
{
//省略内部代码
}
else //如果任务调度器被挂起
{
++xPendedTicks; //在任务调度器被挂起时进行节拍计数
#if ( configUSE_TICK_HOOK == 1 )
{
vApplicationTickHook();
}
#endif
}
return xSwitchRequired;
}
可以看到,当任务调度器被挂起时,FreeRTOS的节拍数就不是通过xTickCount计数了,而是先用xPendedTicks替代,并且在恢复任务调度器时,候就会调用 uxPendedTicks 次函数xTaskIncrementTick(),这样xTickCount 就会恢复,并且那些应该取消阻塞的任务都会取消阻塞。就相当于间隔了一段时间后,任务调度器从之前的状态继续开始工作,所以使用一个新的变量也是有原因的。具体恢复调度器的代码可见下面的代码:
BaseType_t xTaskResumeAll( void )
{
TCB_t *pxTCB = NULL;
BaseType_t xAlreadyYielded = pdFALSE;
configASSERT( uxSchedulerSuspended );
taskENTER_CRITICAL();
/************************************************************************/
/****************************省略部分代码********************************/
/************************************************************************/
UBaseType_t uxPendedCounts = uxPendedTicks;
if( uxPendedCounts > ( UBaseType_t ) 0U )
{
//do-while()循环体,循环次数为 uxPendedTicks
do{
if( xTaskIncrementTick() != pdFALSE )
{
xYieldPending = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
--uxPendedCounts;//变量减一
} while( uxPendedCounts > ( UBaseType_t ) 0U );
uxPendedTicks = 0;//循环执行完毕,uxPendedTicks 清零
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/************************************************************************/
/****************************省略部分代码********************************/
/************************************************************************/
taskEXIT_CRITICAL();
return xAlreadyYielded;
}
另外值得说明的是,上面的代码是新版的,旧版FreeRTOS的else语句内还多一个判断条件,如下:
#if ( configUSE_PREEMPTION == 1 )
{
if( xYieldPending != pdFALSE )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
对于抢占式内核进行了判断,如果存在高优先级任务要抢占情况的话,就要进行上下文切换。但是新版并没有这段函数。一开始我也比较疑惑,我第一感觉是没有必要,因为此时任务调度器已经挂起了,不会有其他任务运行,包括中断。后来我发现对抢占式内核的判断放在了任务调度器没有被挂起的判断条件里了,这样的话我觉得是合理的。即每个中断都判断是否有抢占的情况,及时进行任务切换,具体的函数可见下文的步骤9。
接下来看前面的if语句内部的函数,下面再次给出这个判断条件内部的函数:
const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
xTickCount = xConstTickCount;
if( xConstTickCount == ( TickType_t ) 0U )
{
taskSWITCH_DELAYED_LISTS(); //在进入计数器中断前的溢出情况存储在延时列表中,当进入此函数发现计数溢出时则交换pxDelayedTaskList和pxOverflowDelayedTaskList
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( xConstTickCount >= xNextTaskUnblockTime ) //如果计数时间大于下一个在阻塞态要唤醒的任务时,则进行相关操作。
{
for( ; ; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
xNextTaskUnblockTime = portMAX_DELAY;
break;
}
else
{
pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
//再次确认下一个要唤醒的任务是否正确。
if( xConstTickCount < xItemValue )
{
xNextTaskUnblockTime = xItemValue;
break; /*lint !e9011 Code structure here is deemed easier to understand with multiple breaks. */
}
else
{
mtCOVERAGE_TEST_MARKER();
}
listREMOVE_ITEM( &( pxTCB->xStateListItem ) );
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
listREMOVE_ITEM( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* Place the unblocked task into the appropriate ready
* list. */
prvAddTaskToReadyList( pxTCB );
#if ( configUSE_PREEMPTION == 1 )
{
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
}
}
}
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */
#if ( configUSE_TICK_HOOK == 1 )
{
if( xPendedTicks == ( TickType_t ) 0 )
{
vApplicationTickHook();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TICK_HOOK */
#if ( configUSE_PREEMPTION == 1 )
{
if( xYieldPending != pdFALSE )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
函数的运行过程分别为一下几步:
1.xConstTickCount+1,也就是xTickCount+1;
2.判断xConstTickCount是否为0,,如果为0,则说明计数器(32位)溢出,需要交换pxDelayedTaskList和pxOverflowDelayedTaskList的内容。这里指的注意的是,如果在进入定时中断前就存在溢出了,也就是在xConstTickCount溢出前,延时列表pxDelayedTaskList就已经溢出了,溢出的部分是放在pxOverflowDelayedTaskList的,这与FreeRTOS的学习(五)——系统延时描述的是一致的,交换两个延时列表内容的操作则是每次进行滴答定时中断时检查的。
taskSWITCH_DELAYED_LISTS函数的详细内容见下面的代码:
#define taskSWITCH_DELAYED_LISTS() \
{ \
List_t * pxTemp; \
\
/* The delayed tasks list should be empty when the lists are switched. */ \
configASSERT( ( listLIST_IS_EMPTY( pxDelayedTaskList ) ) ); \
\
pxTemp = pxDelayedTaskList; \
pxDelayedTaskList = pxOverflowDelayedTaskList; \
pxOverflowDelayedTaskList = pxTemp; \
xNumOfOverflows++; \
prvResetNextTaskUnblockTime(); \
}
上述代码实质上就是对两个延时列表的内容进行了交换。其中prvResetNextTaskUnblockTime函数见下面的代码(附代码说明):
static void prvResetNextTaskUnblockTime( void )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
//延迟列表为空,则给他赋值最大,防止后面运行的if( xConstTickCount >= xNextTaskUnblockTime )成立,从而运行某些操作
xNextTaskUnblockTime = portMAX_DELAY;
}
else
{
//延迟列表里之前有延时的任务,其阻塞时间是存储在列表值xItemValue上的,所以将最新要从延时列表唤醒的任务赋值给xNextTaskUnblockTime。
xNextTaskUnblockTime = listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxDelayedTaskList );
}
}
3.判断xConstTickCount是否大于xNextTaskUnblockTime,也就是下一个马上要从阻塞态唤醒的任务的时间戳。
3.1. 如果判断成立,则进入for循环,延时列表为空时,也就是没有要从阻塞态唤醒的任务,则给xNextTaskUnblockTime赋值为最大的阻塞时间,反之,则将延时列表中排序最前面的任务的控制块给到pxTCB,该任务状态列表中的xItemValue也取出来。并且再次做判断确认下一个要唤醒的任务的xItemValue正确与否。
3.2.确认要唤醒的任务无误后,将该任务从延时列表中移除,并且再次确认是否移除。
4.判断该任务的事件列表的等待时间是否到了(队列,信号量的相关概念),等待某些事件时也会被移入等待列表,如果时间到了,则移除该事件列表。
5.将从其他等待列表中移除好后的任务放入就绪列表中。
6.判断configUSE_PREEMPTION是否为1,也就是是否使能了抢占式内核,如果刚唤醒的任务,也就是刚进入就绪列表的任务优先级高于当前正在运行的任务优先级,则要进行一次上下文切换。
到此唤醒下一个任务的操作就结束了。接下来的宏定义判断是属于任务调度器没有被挂起的情况。
7.判断 ( configUSE_PREEMPTION 是否为1 ) && ( configUSE_TIME_SLICING 是否为1 ) ,即如果在使能了抢占式内核和时间片调度的话,还要处理时间片调度相关的操作。
8.判断configUSE_TICK_HOOK 是否为1,如果使能了时间节拍钩子函数,在xPendedTicks为0,也就是任务调度恢复的情况下,执行时间片钩子函数 vApplicationTickHook(),可由用户自己编写。
9.判断configUSE_PREEMPTION是否为1,即是否有高优先级的任务要抢占任务,如果是,则准备进行上下文切换。
10.返回 xSwitchRequired 的值,xSwitchRequired 保存了是否进行任务切换的信息,如果
为 pdTRUE 的话就需要进行任务切换,pdFALSE 的话就不需要进行任务切换。函数
xPortSysTickHandler()中调用 xTaskIncrementTick()的时候就会判断返回值,并且根据返回值决定
是否进行任务切换。
到这儿,系统时钟的部分也讲完了,总的来说还是收获满满的,系统的滴答定时器中断呢,个人感觉更像是定时检查的功能,主要是列表溢出检查,任务唤醒检查,是否唤醒的任务要进行调度或者抢占式任务要进行调度等功能。期待下一次的博客。