在上一篇文章中,我详细分析了FreeRTOS中上下文切换:基于Cortex-M的RTOS上下文切换详解及FreeRTOS实例
但是第一个任务没有上下文,它是怎么运行的呢?
如果我们没有创建任务的话,系统也有一个空闲任务用来调度,这里不对这个进行分析。
首先,我们知道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调度,这不属于本文讨论的范围接着就是任务调度了,来看看上电后的第一个任务具体是怎么调度的。
vTaskStartScheduler();/* 创建空闲任务 */xReturn = xTaskCreate(prvIdleTask,...)/* 关闭中断 */portDISABLE_INTERRUPTS();/* 开始任务调度 */xPortStartScheduler();/* 该函数中主要是初始化一些常量并打开PendSV和Systick中断:略 *//* 开始第一个任务 */vPortStartFirstTask();
可以看到最后进入到vPortStartFirstTask函数中:
vPortStartFirstTask/* 初始化NVIC的VTOR寄存器,来重定位中断向量表 */ldr r0, =0xE000ED08ldr r0, [r0]ldr r0, [r0]/* 中断向量表中的第一个字为MSP的初始值 */msr msp, r0/* 清除CONTROL寄存器,其中第三位FPCA表示FP扩展,将其关闭 */mov r0, #0msr control, r0/* Call SVC to start the first task. *//* 将PRIMASK设置为0,表示关闭NMI和Hardfault异常 */cpsie i/* 将FAULTMASK设置为0,表示关闭NMI异常 */cpsie fdsbisb/* 触发SVC异常程序,其中0在异常处理函数中没用到,随便传一个立即数即可 */svc 0
SVC异常处理函数如下:
vPortSVCHandler:/* Get the location of the current TCB. */ldr r3, =pxCurrentTCBldr r1, [r3]ldr r0, [r1]/* Pop the core registers. */ldmia r0!, {r4-r11, r14}msr psp, r0isbmov r0, #0msr basepri, r0bx 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中设置的:
xTaskCreateprvInitialiseNewTask/* 假设没打开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 switchinterrupt. *//* Offset added to account for the way the MCU uses the stack on entry/exitof 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 itsown 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堆栈