4.FreeRTOS调度器的启动简易分析
FreeRTOS调度器的启动简易分析
- 架构:Cortex-M3
- 版本:FreeRTOS V9.0.0
- 前言:上一篇我分析了关于一个任务的创建过程,既然创建了任务,自然是要用。那么FreeRTOS中对于任务的切换,调度器发挥着巨大的作用,这是一个核心。
1.从函数vTaskStartScheduler
入手
便于分析我简化了代码:
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
/* Add the idle task at the lowest priority. */
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
....
....
...
}
#else
{
/* The Idle task is being created using dynamically allocated RAM. */
xReturn = xTaskCreate( prvIdleTask,
"IDLE", configMINIMAL_STACK_SIZE,
( void * ) NULL,
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
&xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
#if ( configUSE_TIMERS == 1 )
{
if( xReturn == pdPASS )
{
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TIMERS */
if( xReturn == pdPASS )
{
/* Interrupts are turned off here, to ensure a tick does not occur
before or during the call to xPortStartScheduler(). The stacks of
the created tasks contain a status word with interrupts switched on
so interrupts will automatically get re-enabled when the first task
starts to run. */
portDISABLE_INTERRUPTS();
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
/* Switch Newlib's _impure_ptr variable to point to the _reent
structure specific to the task that will run first. */
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif /* configUSE_NEWLIB_REENTRANT */
xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE;
xTickCount = ( TickType_t ) 0U;
/* If configGENERATE_RUN_TIME_STATS is defined then the following
macro must be defined to configure the timer/counter used to generate
the run time counter time base. */
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
/* Setting up the timer tick is hardware specific and thus in the
portable interface. */
if( xPortStartScheduler() != pdFALSE )
{
/* Should not reach here as if the scheduler is running the
function will not return. */
}
else
{
/* Should only reach here if a task calls xTaskEndScheduler(). */
}
}
else
{
/* This line will only be reached if the kernel could not be started,
because there was not enough FreeRTOS heap to create the idle task
or the timer task. */
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
/* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,
meaning xIdleTaskHandle is not used anywhere else. */
( void ) xIdleTaskHandle;
}
- 用动态创建任务的方式,创建了一个空闲任务
- 创建以及初始化定时器任务
- 最后调用了
xPortStartScheduler
我简化了一些代码xPortStartScheduler
/*
* See header file for description.
*/
BaseType_t xPortStartScheduler( void )
{
#if( configASSERT_DEFINED == 1 )
{
...
...
...
}
#endif /* conifgASSERT_DEFINED */
/* Make PendSV and SysTick the lowest priority interrupts. */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* Start the timer that generates the tick ISR. Interrupts are disabled
here already. */
vPortSetupTimerInterrupt();
/* Initialise the critical nesting count ready for the first task. */
uxCriticalNesting = 0;
/* Start the first task. */
prvStartFirstTask();
/* Should not get here! */
return 0;
}
首先是我在分析List的时候讲过关于SVC和PendSV优先级的赋值情况,实际上这两个优先级是最低的。然后是启动软件定时器vPortSetupTimerInterrupt()
,设置临界区嵌套深度uxCriticalNesting
为0,调用prvStartFirstTask()
开启第一个任务。
函数如下:
__asm void prvStartFirstTask( void )
{
PRESERVE8
/* Use the NVIC offset register to locate the stack. */
ldr r0, =0xE000ED08
ldr r0, [r0]
ldr r0, [r0]
/* Set the msp back to the start of the stack. */
msr msp, r0
/* Globally enable interrupts. */
cpsie i
cpsie f
dsb
isb
/* Call SVC to start the first task. */
svc 0
nop
nop
}
首先,从中断向量表偏移寄存器0xE000ED08
中取出向量表的偏移地址,向量表的前四个字节就是主堆栈地址,把地址传给MSP,开启总中断,调用svc 0
,触发ISR。当进入SVC异常服务例程时,CPU处于特权模式了。在特权模式下就允许操作只有特权模式下能操作的硬件。
接下来看SVC异常服务例程:
__asm void vPortSVCHandler( void )
{
PRESERVE8
ldr r3, =pxCurrentTCB /* Restore the context. */
ldr r1, [r3] /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
ldr r0, [r1] /* The first item in pxCurrentTCB is the task top of stack. */
ldmia r0!, {r4-r11} /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
msr psp, r0 /* Restore the task stack pointer. */
isb
mov r0, #0
msr basepri, r0
orr r14, #0xd
bx r14
}
pxCurrentTCB的前四个字节就是pxTopOfStack
,我前面分析了很久的pxTopOfStack
并没有浪费,因为我知道,这个指针指向的位置,就是存放任务现场的地址。取出pxTopOfStack
后,按照Cortex-M3的出栈顺序,取出栈中各个寄存器的值。最开始,R0存放的是pxTopOfStack
的值,执行手动出栈R4~R11的之后,R0的值比pxTopOfStack
多0x20,然后把R0赋值到PSP,此时执行了bx r14之后,PSP会再次出栈xPSR、PC、LR、R12、R3~R0,PSP又会再增加0x20,也就是到了任务执行代码的时候,PSP就会比pxTopOfStack
大0x40。如图:
最后调用:
orr r14, #0xd
bx r14
根据Cortex-M3权威手册上的说明,R14的bit0为1表示返回thumb状态,bit1和bit2分别表示返回后sp用msp还是psp、以及返回到特权模式还是用户模式。R14存储的是返回的地址,这里的返回地址一定就是某个任务的函数指针。返回时,但并不是简简单单返回,当或上0xd时,就是要让CPU进入到线程模式、Thumb状态,当线程模式时,才会使用PSP。
这么看来一切都通了:创建任务的时候,把任务当成正在运行的任务,然后把任务现场统统入栈到pxTopOfStack
,在启动调度器时,又把pxTopOfStack
保存的现场统统出栈,最后设置R14的低三位,使得CPU进入用户模式和跳到某个任务的执行代码中。