前言
在使用freertos的时候,我们都知道在创建了一系列任务之后,启用调度器,系统就可以帮我们管理任务,分配资源。本文主要对调度器的原理进行剖析,从vTaskStartScheduler()函数开始,一探究竟。
freertos版本:9.0.0
启动调度器
vTaskStartScheduler()
vTaskStartScheduler()用于开启调度器,具体代码如下:
void vTaskStartScheduler( void ){
BaseType_t xReturn;
xReturn = xTaskCreate( prvIdleTask,
"IDLE",configMINIMAL_STACK_SIZE,
( void * ) NULL,
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
&xIdleTaskHandle );
if( xReturn == pdPASS )
{
portDISABLE_INTERRUPTS();
if( xPortStartScheduler() != pdFALSE )
{
}
}
}
代码已做删减(只显示了核心部分,下同)。可以看到在开启调度器的时候,vTaskStartScheduler()主要做了三件事:创建空闲任务;关闭中断,确保后续工作不会被Systick打断;同时调用xPortStartScheduler()(此处以ARM_CM3内核为例)
xPortStartScheduler()
xPortStartScheduler()函数代码如下:
BaseType_t xPortStartScheduler( void )
{
extern void vPortStartFirstTask( void );
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
vPortSetupTimerInterrupt();
uxCriticalNesting = 0;
prvPortStartFirstTask();
prvTaskExitError();
return 0;
}
}
第一步是对系统Systick中断和PendSV中断进行优先级设置,均设置为最低( 为什么要设置Systick中断和PendSV中断最低优先级),第二步使能Systick中断(但是中断不会发生,因为之前关闭了中断),最后调用vPortStartFirstTask()。
执行第一个任务
vPortStartFirstTask()
vPortStartFirstTask()用于执行第一个任务。
static void prvPortStartFirstTask( void )
{
__asm volatile(
" ldr r0, =0xE000ED08 \n"
" ldr r0, [r0] \n"
" ldr r0, [r0] \n"
" msr msp, r0 \n"
" cpsie i \n"
" cpsie f \n"
" dsb \n"
" isb \n"
" svc 0 \n"
" nop \n"
);
}
代码分析:
ldr r0, =0xE000ED08
ldr r0, [r0]
ldr r0, [r0]
msr msp, r0
这4步的目的是给主堆栈的栈顶指针msp赋初值,具体操作步骤如图:
主堆栈指针就指向了栈顶0x200008DB。
cpsie i
cpsie f
之后开启中断和异常,让下面的SVC中断能够响应
svc 0
产生系统调用服务号为0的SVC中断
SVC中断服务函数
vPortSVCHandler()函数代码如下:
void vPortSVCHandler( void )
{
__asm volatile (
" ldr r3, pxCurrentTCBConst2 \n"
" ldr r1, [r3] \n"
" ldr r0, [r1] \n"
" ldmia r0!, {r4-r11} \n"
" msr psp, r0 \n"
" isb \n"
" mov r0, #0 \n"
" msr basepri, r0 \n"
" orr r14, #0xd \n"
" bx r14 \n"
" \n"
" .align 4 \n"
"pxCurrentTCBConst2: .word pxCurrentTCB \n"
);
}
代码分析:
ldr r3, pxCurrentTCBConst2
ldr r1, [r3]
ldr r0, [r1]
此部分代码是将pxCurrentTCBConst2这个任务控制块指向的第一个成员的值赋值给r0。从下图任务控制块的结构体可以看到,第一个成员是栈顶指针。
ldmia r0!, {r4-r11}
msr psp, r0
以r0 为基地址,将栈中向上增长的8个字的内容加载到CPU寄存
器r4~r11,同时r0 也会跟着自增。并将自增后的r0赋值给psp,如下图所示:
mov r0, #0
msr basepri, r0
basepri寄存器置0,打开所有中断
orr r14, #0xd
bx r14
当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回用户级。在SVC中断服务里面,使用的是MSP堆栈指针,是处在特权级。关于msp和psp可参考这篇文章:双堆栈…的区别
退出中断,由于此时sp指针使用任务指针psp,所以在进行中断退出的出栈操作时,是以psp指针指向地址开始出栈。这一部分均由硬件完成,相应寄存器会被置位,比如PC指针会更新成新任务的入口地址。
在我们创建任务时,会有初始化任务控制块和任务堆栈的操作。这时候再看看任务堆栈的初始化过程,和上面对比,很多问题都会迎刃而解。
任务堆栈初始化:
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
pxTopOfStack--;
*pxTopOfStack = portINITIAL_XPSR;
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) portTASK_RETURN_ADDRESS;
pxTopOfStack -= 5;
*pxTopOfStack = ( StackType_t ) pvParameters;
pxTopOfStack -= 8;
return pxTopOfStack;
}
至此,pc指针指向任务的函数地址,sp指针(此时为psp)指向任务栈的栈顶,第一个任务成功运行。
任务切换
创建完了第一个任务,我们看到之后没有代码了,那操作系统如何对其他任务调度呢?
不要忘了之前的Systick和PendSV两个中断,因为之前关中断以及SVC执行的缘故,他们一直没有起作用。SVC中断执行完之后,Systick开始执行。
Systick中断
Systick中断使能
在开启调度器的时候,我们调用了vPortSetupTimerInterrupt()来使能系统时钟Systick,代码如下:
__attribute__(( weak )) void vPortSetupTimerInterrupt( void )
{
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}
主要工作是设置Systick定时器的中断周期和使能Systick中断
Systick中断服务函数
中断使能后,接下来的所有关于任务切换的工作都由Systick发起。
void xPortSysTickHandler( void )
{
portDISABLE_INTERRUPTS();
{
if( xTaskIncrementTick() != pdFALSE )
{
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
portENABLE_INTERRUPTS();
}
xPortSysTickHandler()函数首先进行关中断处理,然后调用xTaskIncrementTick()函数
xTaskIncrementTick()函数代码如下:
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
const TickType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;
if( xConstTickCount == ( TickType_t ) 0U )
{
taskSWITCH_DELAYED_LISTS();
}
if( xConstTickCount >= xNextTaskUnblockTime )
{
for( ;; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
xNextTaskUnblockTime = portMAX_DELAY;
break;
}
else
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
if( xConstTickCount < xItemValue )
{
xNextTaskUnblockTime = xItemValue;
break;
}
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
prvAddTaskToReadyList( pxTCB );
#if ( configUSE_PREEMPTION == 1 )
{
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
}
#endif
}
}
}
}
else
{
++uxPendedTicks;
}
#if ( configUSE_PREEMPTION == 1 )
{
if( xYieldPending != pdFALSE )
{
xSwitchRequired = pdTRUE;
}
}
#endif
return xSwitchRequired;
}
代码分析:
const TickType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;
这是代码的第一部分,负责系统时基计数。每进入Systick中断一次,xTickCount值+1。例如Systick中断1ms一次,则xTickCount的值就代表系统运行的毫秒数。当然xTickCount也会有溢出的时候,这视处理器而定。溢出了则从0计数,所以对于长时间的,精确的时间统计,我们会选择其他方案。
if( xConstTickCount == ( TickType_t ) 0U )
{
taskSWITCH_DELAYED_LISTS();
}
这是代码第二部分。这里涉及到了任务的延时列表。freertos采用延时列表的机制来管理阻塞任务,通过给任务设置延时来让任务进入阻塞状态。例如:任务延时A:60ms,B:70ms,C:120ms,系统会按照当前时基值设置任务的解锁时间(假设xTickCount=40)。所以,任务的解锁时间为A:40+60=100ms,B:40+70=110ms,C:40+120=160ms。同时,系统将这些任务按照解锁时间将其插入延时列表,并按顺序排列,值最小的任务代表最先被解锁加入就绪列表,就会被插入到列表最前面。如下图所示:
现在再来谈上面代码的含义。因为Systick存在溢出问题,比如16位系统,会在xTickCount=65535时加一溢出。为了解决这一问题,freertos采用了延时列表和延时溢出列表,比如Systick的溢出值为140,A,B任务由于解锁时间均大于xTickCount,所以都会加入延时列表;而C任务由于40+120=20(溢出)<xTickCount,会被放进溢出延时列表。如下图:
延时列表中的A,B任务执行完后,延时列表为空。在xTickCount溢出后,xTickCount从0开始计数,并交换两列表(指针指向C所在的列表),等C任务解锁并执行。值得注意的是,此时C所在的列表变为延时列表,而刚刚A,B所在的变为溢出列表。这就是freertos对Systick溢出采用的解决办法。
if( xConstTickCount >= xNextTaskUnblockTime )
{
for( ;; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
xNextTaskUnblockTime = portMAX_DELAY;
break;
}
else
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
if( xConstTickCount < xItemValue )
{
xNextTaskUnblockTime = xItemValue;
break;
}
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
prvAddTaskToReadyList( pxTCB );
#if ( configUSE_PREEMPTION == 1 )
{
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
}
#endif
}
}
}
}
else
{
++uxPendedTicks;
}
这是代码第三部分。它判断是否有任务解锁,其中xNextTaskUnblockTime代表最近的任务解锁时间。如果有任务需要解锁,就获取延时列表头部的任务TCB,同时获取其xItemValue值(就是任务解锁时间),之后将其移出延时列表,插入就绪列表,在判断它的优先级是否最高,最高则切换任务,否则不切换。通过这样的操作,任务才能再次被调度。
当然还有一点需要注意。以上步骤是在for循环里面实现的,它的退出条件如下:
if( xConstTickCount < xItemValue )
{
xNextTaskUnblockTime = xItemValue;
break;
}
之所以出现遍历多次的情况,是因为同一时间有可能有多个任务需要解锁,也就是说它们的解锁时间是相同的,这种情况不能忽略。
#if ( configUSE_PREEMPTION == 1 )
{
if( xYieldPending != pdFALSE )
{
xSwitchRequired = pdTRUE;
}
}
#endif
这是代码的最后部分。这里涉及到一个全局变量xYieldPending,它是任务切换的标志,会在其它函数中调用,比如vTaskNotifyGiveFromISR(),还有之后的vTaskSwitchContext()。因为有些任务不是在xTaskIncrementTick()中解除阻塞的,而是在其他函数中解除。他们将xYieldPending置位,以达到任务切换的目的。
至此,xTaskIncrementTick()函数结束。它的返回值会作为任务切换的依据,从上面代码的分析可知,任务只有在以下两种情况下才会自动切换:有任务解锁并且其优先级最高,或者xYieldPending置位。
PendSV中断
PendSV中断通过将PENDSV位置位来触发,此前已经在Systick中断服务函数中出现过。
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
PendSV中断服务函数
xPortPendSVHandler()代码如下:
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp
isb
ldr r3, =pxCurrentTCB
ldr r2, [r3]
stmdb r0!, {r4-r11}
str r0, [r2]
stmdb sp!, {r3, r14}
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
bl vTaskSwitchContext
mov r0, #0
msr basepri, r0
ldmia sp!, {r3, r14}
ldr r1, [r3]
ldr r0, [r1]
ldmia r0!, {r4-r11}
msr psp, r0
isb
bx r14
nop
}
整个代码分为两部分:保存上文和切换下文
保存上文部分:
mrs r0, psp
isb
ldr r3, =pxCurrentTCB
ldr r2, [r3]
stmdb r0!, {r4-r11}
str r0, [r2]
整个过程和SVC中断差不多。
R0=psp,R3=pxCurrentTCB,R2=pxCurrentTCB->TopOfStack(任务块第一个成员为栈顶地址),保存它们是为了之后使用。
因为在进入PendSV中断时,硬件已经自动将一些寄存器入栈了,所以此时psp的指向如下图所示,只需要将R4-R11手动入栈即可。
此时的R0指向栈顶,将其保存到R2中,更新当前任务栈的栈顶,方便以后任务切换时调用。
中间有一行代码值得注意:
stmdb sp!, {r3, r14}
这行代码的作用时保存R3,R14寄存器的值到堆栈中。在中断中执行时,需使用主堆栈,栈顶指针为msp。之所以要保存到主堆栈中,是因为中断退出的时候,使用的是msp进行出栈操作,而执行vTaskSwitchContext()会让R14的值发生改变,所以需要入栈保护。
至于R3,他在入栈前存的是pxCurrentTCB的地址,执行vTaskSwitchContext()不能确定会不会改变R3的值,所以也入栈保护。
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
关中断保护,因为之后要操作全局变量pxCurrentTCB。
切换下文部分:
bl vTaskSwitchContext
mov r0, #0
msr basepri, r0
ldmia sp!, {r3, r14}
ldr r1, [r3]
ldr r0, [r1]
ldmia r0!, {r4-r11}
msr psp, r0
isb
bx r14
nop
这里首先执行vTaskSwitchContext()函数。
vTaskSwitchContext()函数代码如下:
void vTaskSwitchContext( void )
{
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
{
xYieldPending = pdTRUE;
}
else
{
xYieldPending = pdFALSE;
taskSELECT_HIGHEST_PRIORITY_TASK();
}
}
首先判断调度器是否挂起,再决定是否对xYieldPending进行置位。如果挂起了,则置位xYieldPending,相当于下次开启时会强制进行一次任务切换。如果没挂起,则执行taskSELECT_HIGHEST_PRIORITY_TASK()函数。
taskSELECT_HIGHEST_PRIORITY_TASK()函数代码如下:
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority = uxTopReadyPriority; \
\
\
while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \
{ \
configASSERT( uxTopPriority ); \
--uxTopPriority; \
} \
\
\
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
uxTopReadyPriority = uxTopPriority; \
}
此函数用于选择优先级最高的任务,全局变量pxCurrentTCB就是在这里修改的。因为优先级最高的任务列表不一定就有任务存在(任务阻塞被暂时移除),所以要遍历出此刻有任务存在列表的最高优先级,然后调用listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) )函数,注意此处传入的参数为pxCurrentTCB。
listGET_OWNER_OF_NEXT_ENTRY()代码如下:
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \
{ \
List_t * const pxConstList = ( pxList ); \
\
\
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
{ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
} \
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \
}
因为同一优先级下可能存在多个任务,freertos的处理方法是使用时间片,从代码可以很容易看出,每进入一次这个函数,( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext都会执行一次,作用是切换到同优先级的下一个任务,让同优先级的每一个任务都能拥有相同的执行时间。
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;
之后就是最核心的修改全局变量pxCurrentTCB,它作为参数传递给了pxTCB。至此,pxCurrentTCB更新,指向优先级最高任务。
接下来回到汇编,回到切换下文的内容。
mov r0, #0
msr basepri, r0
ldmia sp!, {r3, r14}
ldr r1, [r3]
ldr r0, [r1]
ldmia r0!, {r4-r11}
msr psp, r0
isb
bx r14
nop
首先是开中断,修改完了pxCurrentTCB要立即打开,不然会影响系统的实时性。之后的操作与入栈如出一辙,值得注意的是,之前R3,R14入栈的作用就在这里体现了,R3始终指向pxCurrentTCB的地址,R14保存的进入中断之前的处理器模式和堆栈指针。
到此,freertos的任务调度内核函数剖析结束。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)