前面有了创建任务、启动调度器、任务控制,接下来便开始分析一个 Tick 到来之后,FreeRTOS 即将有什么行为;
在启动调度器的时候,就已经配置好了 SysTick,它作为 OS 的心跳,每隔一个固定周期来一次 SysTick 中断,来驱动 OS 做事(任务调度);
以 STM32 为例,定义的 configTICK_RATE_HZ 为 1000,由《FreeRTOS --(9)任务管理之启动调度器》得知,系统节拍时钟周期为1ms;
不同的处理器结构可能有所区别,所以他是需要移植的部分,在 port.c 中 xPortSysTickHandler:
void xPortSysTickHandler( void )
{
/* The SysTick runs at the lowest interrupt priority, so when this interrupt
executes all interrupts must be unmasked. There is therefore no need to
save and then restore the interrupt mask value as its value is already
known - therefore the slightly faster vPortRaiseBASEPRI() function is used
in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
vPortRaiseBASEPRI();
{
/* Increment the RTOS tick. */
/* 如果返回值标记了任务切换,即有优先级高的任务 */
if( xTaskIncrementTick() != pdFALSE )
{
/* A context switch is required. Context switching is performed in
the PendSV interrupt. Pend the PendSV interrupt. */
/* 设置PendSV中断位 */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
vPortClearBASEPRIFromISR();
}
开头调用 vPortRaiseBASEPRI();结尾调用 vPortClearBASEPRIFromISR(); 是为了创造临界区;
调用了 xTaskIncrementTick;如果返回 pdTRUE 则代表要进行任务切换,那么就手动拉起 PendSV;否则不进行上下文切换;
接下来看下 xTaskIncrementTick 做了什么,大概猜测是选最高优先级的,并且在 Ready 链表的任务投入运行:
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
/* Called by the portable layer each time a tick interrupt occurs.
Increments the tick then checks to see if the new tick value will cause any
tasks to be unblocked. */
traceTASK_INCREMENT_TICK( xTickCount );
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
/* Minor optimisation. The tick count cannot change in this
block. */
const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
/* Increment the RTOS tick, switching the delayed and overflowed
delayed lists if it wraps to 0. */
xTickCount = xConstTickCount;
if( xConstTickCount == ( TickType_t ) 0U ) /*lint !e774 'if' does not always evaluate to false as it is looking for an overflow. */
{
taskSWITCH_DELAYED_LISTS();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* See if this tick has made a timeout expire. Tasks are stored in
the queue in the order of their wake time - meaning once one task
has been found whose block time has not expired there is no need to
look any further down the list. */
if( xConstTickCount >= xNextTaskUnblockTime )
{
for( ;; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
/* The delayed list is empty. Set xNextTaskUnblockTime
to the maximum possible value so it is extremely
unlikely that the
if( xTickCount >= xNextTaskUnblockTime ) test will pass
next time through. */
xNextTaskUnblockTime = portMAX_DELAY; /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
break;
}
else
{
/* The delayed list is not empty, get the value of the
item at the head of the delayed list. This is the time
at which the task at the head of the delayed list must
be removed from the Blocked state. */
pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); /*lint !e9079 void * is used as this macro is used with timers and co-routines too. Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
if( xConstTickCount < xItemValue )
{
/* It is not time to unblock this item yet, but the
item value is the time at which the task at the head
of the blocked list must be removed from the Blocked
state - so record the item value in
xNextTaskUnblockTime. */
xNextTaskUnblockTime = xItemValue;
break; /*lint !e9011 Code structure here is deedmed easier to understand with multiple breaks. */
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* It is time to remove the item from the Blocked state. */
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
/* Is the task waiting on an event also? If so remove
it from the event list. */
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* Place the unblocked task into the appropriate ready
list. */
prvAddTaskToReadyList( pxTCB );
/* A task being unblocked cannot cause an immediate
context switch if preemption is turned off. */
#if ( configUSE_PREEMPTION == 1 )
{
/* Preemption is on, but a context switch should
only be performed if the unblocked task has a
priority that is equal to or higher than the
currently executing task. */
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
}
}
}
/* Tasks of equal priority to the currently running task will share
processing time (time slice) if preemption is on, and the application
writer has not explicitly turned time slicing off. */
#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 )
{
/* Guard against the tick hook being called when the pended tick
count is being unwound (when the scheduler is being unlocked). */
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;
/* The tick hook gets called at regular intervals, even if the
scheduler is locked. */
#if ( configUSE_TICK_HOOK == 1 )
{
vApplicationTickHook();
}
#endif
}
return xSwitchRequired;
}
这个函数有点长,但是其实思路是非常清晰易懂的:
1、因为 FreeRTOS 支持挂起调度器,也就是调用 vTaskSuspendAll 后,RTOS 在每个 Tick 来临的时候,不在调度任务进行上下文切换;所以,每次进入 xTaskIncrementTick 的时候,要判断调度器是否被挂起;
2、如果允许调度,首先增加当前的计数器的计数:xTickCount;
3、增加完 xTickCount 后,判断计数器是否溢出,如果溢出了,那么调用 taskSWITCH_DELAYED_LISTS 来交换 pxDelayedTaskList 和 pxOverflowDelayedTaskList (为了解决xTickCount溢出问题,FreeRTOS使用了两个延时列表:xDelayedTaskList1 和 xDelayedTaskList2。并使用延时列表指针pxDelayedTaskList和溢出延时列表指针pxOverflowDelayedTaskList分别指向上面的延时列表1和延时列表2(在创建任务时将延时列表指针指向延时列表)。这两个延时列表指针变量和两个延时列表变量都是在tasks.c中定义的静态局部变量)
/* pxDelayedTaskList and pxOverflowDelayedTaskList are switched when the tick
count overflows. */
#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(); \
}
这两个链表专门为了处理计数器溢出而存在;一旦溢出,就交换,OS 始终取的是 pxDelayedTaskList 中的 Delay Task,在挂接任务的时候,判断时钟计数器,看是否需要往 pxOverflowDelayedTaskList 上面挂;
4、对比当前的时间 xConstTickCount 和下一个阻塞在时间上的任务的时间 xNextTaskUnblockTime 大小,查看阻塞时间是否到期,xNextTaskUnblockTime 是一个全局变量,记录着下一个最近的任务阻塞时间;
5、如果阻塞时间到期,那么首先判断当前的 Delay 链表是否为空,如果为空,则说明没有阻塞在时间上的任务,将 xNextTaskUnblockTime 赋值为最大 portMAX_DELAY,直接退出;
6、如果阻塞时间到期,而且 pxDelayedTaskList 链表不为空,那么取出 pxDelayedTaskList 链表的第一个元素(注意,往 pxDelayedTaskList 链表中插入 Item 的时候,是用 vListInsert ,根据唤醒时间有序插入的,即前面放置的是 Delay 时间最小的,后面是 Delay 大的)的时间,和当前的时间 xConstTickCount 进行比对,看看是否超期,如果没有超期,那么将其更新到下一个唤醒时间 xNextTaskUnblockTime 中,退出;如果到期,那么将其从 pxDelayedTaskList 链表中移除(如果在等 Event 也同时从 Event 中移除),将其添加到 ReadyList(prvAddTaskToReadyList),
/*
* Place the task represented by pxTCB into the appropriate ready list for
* the task. It is inserted at the end of the list.
*/
#define prvAddTaskToReadyList( pxTCB ) \
traceMOVED_TASK_TO_READY_STATE( pxTCB ); \
taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority ); \
vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) ); \
tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )
7、如果支持抢占的话(不支持抢占的 RTOS 是没有灵魂的),就要判断解除阻塞的任务和当前的任务的优先级,哪个更高,如果高于当前的任务优先级,那么 xSwitchRequired 设置为 pdTRUE,表示要进行一次上下文切换;
8、循环步骤 4 到 7,也就是对 pxDelayedTaskList 链表中的元素进行遍历,直到 pxDelayedTaskList 为空,或者有元素的运行时间还未到,还需要继续阻塞;
9、此刻,该到期的任务,已经全部从 pxDelayedTaskList 链表移动到了 pxReadyTasksLists 中,对应优先级的地方;
10、如果定义了抢占(configUSE_PREEMPTION),同时也定义了同一个优先级轮转调度(configUSE_TIME_SLICING) 的话呢(普通情况下,这两个都需要定义,不然没有灵魂),只要当前的任务所在的 pxReadyTasksLists 链表中,包含不止一个待运行的任务,就要去轮转调度另一个任务执行;所以 xSwitchRequired 设置为 pdTRUE;
11、如果应用层定义了 configUSE_TICK_HOOK,那么会调用 vApplicationTickHook 钩子;
12、如果定义了抢占(configUSE_PREEMPTION),而且 xYieldPending 也是 pdTRUE 的时候,也会设置 xSwitchRequired 设置为 pdTRUE,强制去进行上下文切换,
xYieldPending 这个变量什么时候会被设置称为 pdTRUE?
对于队列以及使用队列机制的信号量、互斥量等,在中断服务程序中调用了这些API函数,将任务从阻塞中解除,则需要调用函数xTaskRemoveFromEventList()将任务的事件列表项从事件列表中移除。在移除事件列表项的过程中,会判断解除的任务优先级是否大于当前任务的优先级,如果解除的任务优先级更高,会将变量xYieldPending设置为pdTRUE。在下一次系统节拍中断服务函数中,触发一次任务切换;
if(pxUnblockedTCB->uxPriority > pxCurrentTCB->uxPriority)
{
/*任务具有更高的优先级,返回pdTRUE。告诉调用这个函数的任务,它需要强制切换上下文。*/
xReturn= pdTRUE;
/*带中断保护的API函数的都会有一个参数参数"xHigherPriorityTaskWoken",如果用户没有使用这个参数,这里设置任务切换标志。在下个系统中断服务例程中,会检查xYieldPending的值,如果为pdTRUE则会触发一次上下文切换。*/
xYieldPending= pdTRUE;
}