在上一篇文章中,我详细分析了FreeRTOS中上下文切换:基于Cortex-M的RTOS上下文切换详解及FreeRTOS实例
但是第一个任务没有上下文,它是怎么运行的呢?
1 创建任务
如果我们没有创建任务的话,系统也有一个空闲任务用来调度,这里不对这个进行分析。
首先,我们知道pxCurrentTCB
指向当前运行任务的TCB,所以我们先看看哪里设置了pxCurrentTCB
,流程如下
xTaskCreate
/* 初始化TCB内容 */
prvInitialiseNewTask
/* 将TCB加入ReadyList */
prvAddNewTaskToReadyList
prvAddNewTaskToReadyList
的大概逻辑如下:
if( pxCurrentTCB == NULL )
{
pxCurrentTCB = pxNewTCB;
...
}
else
{
if( xSchedulerRunning == pdFALSE )
{
if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority )
{
pxCurrentTCB = pxNewTCB;
}
...
}
...
}
prvAddTaskToReadyList( pxNewTCB );
也就是说如果pxCurrentTCB
为空,则直接将新创建的任务赋值给pxCurrentTCB
,如果不为空且还没有开始任务调度,则判断当前创建任务的优先级是否比pxCurrentTCB
中任务的优先级高,若是则更改pxCurrentTCB
。
- 若任务已经开始调度,就将任务加入
readyList
中,交给Systick
调度,这不属于本文讨论的范围
2 开始调度
接着就是任务调度了,来看看上电后的第一个任务具体是怎么调度的。
vTaskStartScheduler();
/* 创建空闲任务 */
xReturn = xTaskCreate(prvIdleTask,...)
/* 关闭中断 */
portDISABLE_INTERRUPTS();
/* 开始任务调度 */
xPortStartScheduler();
/* 该函数中主要是初始化一些常量并打开PendSV和Systick中断:略 */
/* 开始第一个任务 */
vPortStartFirstTask();
可以看到最后进入到vPortStartFirstTask
函数中:
- 函数实现的具体内容见注释
vPortStartFirstTask
/* 初始化NVIC的VTOR寄存器,来重定位中断向量表 */
ldr r0, =0xE000ED08
ldr r0, [r0]
ldr r0, [r0]
/* 中断向量表中的第一个字为MSP的初始值 */
msr msp, r0
/* 清除CONTROL寄存器,其中第三位FPCA表示FP扩展,将其关闭 */
mov r0, #0
msr control, r0
/* Call SVC to start the first task. */
/* 将PRIMASK设置为0,表示关闭NMI和Hardfault异常 */
cpsie i
/* 将FAULTMASK设置为0,表示关闭NMI异常 */
cpsie f
dsb
isb
/* 触发SVC异常程序,其中0在异常处理函数中没用到,随便传一个立即数即可 */
svc 0
SVC异常处理函数如下:
vPortSVCHandler:
/* Get the location of the current TCB. */
ldr r3, =pxCurrentTCB
ldr r1, [r3]
ldr r0, [r1]
/* Pop the core registers. */
ldmia r0!, {r4-r11, r14}
msr psp, r0
isb
mov r0, #0
msr basepri, r0
bx r14
上面程序的意思就是将pxCurrentTCB
的第一个参数,即第一个运行任务的堆栈指针pxTopOfStack
加载到r0
中,然后将任务中堆栈里的r4-r11
和r14
出栈到系统的r4-r11
和r14
寄存器中,然后把出栈后任务的堆栈地址赋值给psp
,最后再开中断(前面调用了portDISABLE_INTERRUPTS()
),切换到线程模式运行任务。在该异常处理程序退出时,还将由硬件从psp
中pop
出r0-r3
,r12
,LR
,PC
和xPSR
到系统对应的寄存器中。这样系统就从第一个任务开始运行了。
问:为什么还要将r14
(LR
)寄存器出栈?或者说为什么要将它保存在栈中?
在创建任务时,每个任务的LR
被pxPortInitialiseStack
函数初始化为:portINITIAL_EXC_RETURN
即0xFFFFFFFD
,它表示退出异常时进入线程模式并使用PSP堆栈,这是通过最后的bx r14
来实现的,它的作用是让硬件知道退出异常时要恢复什么状态。
实际上进入异常时硬件也自动保存了LR
,但系统中的第一个任务,也就是第一次进入SVC
异常时保存的LR
是vPortStartFirstTask()
的下一跳指令return 0
的地址,很明显系统不会执行到return 0
。进入异常后,LR
表示异常发生之前在使用的堆栈,FreeRTOS进入SVC
异常时,它的值为0xFFFFFFF9
,表示退出时进入线程模式并使用MSP
堆栈(没运行操作系统默认使用MSP
),当运行操作系统后,系统将使用PSP
(FreeRTOS设置LR
为0xFFFFFFFD
,对应SVC
异常程序的ldmia r0!, {r4-r11, r14}
中出栈给r14
)。
一旦开始运行一个任务之后,每次进入异常硬件保存的LR
都是0xFFFFFFFD
了,因为FreeRTOS的任务都是在使用PSP
堆栈,进入异常前的状态都是一样的。在后续任务的上下文切换的PendSV
中断中也有压入r14
:
xPortPendSVHandler
/* 进入时LR=0xFFFFFFFD,它是被SVC异常最后的bl r14修改的 */
...
stmdb r0!, {r4-r11, r14}
...
bl vTaskSwitchContext
...
ldmia r0!, {r4-r11, r14}
...
这里将r14
压栈再出栈的原因和SVC
中的出栈不同,这里是因为后面调用了函数vTaskSwitchContext
,会修改LR
为其下一条指令的值,所以需要保存r14
的值。
最后还有一个问题没有解决:r0-r15
和xPSR
是何时保存到第一个任务的堆栈的呢?或者说每个创建的任务的初始堆栈是怎么设置的呢?不难发现,是在pxPortInitialiseStack
中设置的:
xTaskCreate
prvInitialiseNewTask
/* 假设没打开StackOverflow检测和MPU */
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
现在来看看pxPortInitialiseStack
具体做了什么事:
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
/* Simulate the stack frame as it would be created by a context switch
interrupt. */
/* Offset added to account for the way the MCU uses the stack on entry/exit
of interrupts, and to ensure alignment. */
pxTopOfStack--;
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR */
/* Save code space by skipping register initialisation. */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
/* A save method is being used that requires each task to maintain its
own exec return value. */
pxTopOfStack--;
*pxTopOfStack = portINITIAL_EXC_RETURN;
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
return pxTopOfStack;
}
首先来看看任务的堆栈需要将寄存器按什么顺序保存在堆栈中:
- 创建任务时,硬件堆栈需要自己初始化
这个函数中就是一个个来初始化这些寄存器的值并写入任务堆栈中,供SVC
或PendSV
进行调度。
xPSR
:portINITIAL_XPSR
宏为0x01000000
,bit24位为1表示Thumb状态,其它的状态位为0即可PC
:pxCode
就是创建任务时传入的任务函数地址,其中portSTART_ADDRESS_MASK
为0xFFFFFFFE
,根据Cortex-M的规范,PC
地址是按字/半字对齐的,所以最低位总是为0。- 但使用
bx
或blx
跳转时,应该将最低位置1,表示使用Thumb
指令
- 但使用
LR
:硬件的LR
设置为prvTaskExitError
函数,但任务应该在一个死循环中不该返回,进入这个函数说明程序出错r12~r4
:没用到,写为任意值都行,保持默认值即可r0
:pvParameters
即为创建任务时传入的参数,这里可以在任务执行时传给任务r14
:前面有提到,设置为portINITIAL_EXC_RETURN
(0xFFFFFFFD
),表示退出异常时,进入线程模式并使用PSP
堆栈