FreeRTOS第一个任务的创建和调度详解(SVC异常)

在上一篇文章中,我详细分析了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-r11r14出栈到系统的r4-r11r14寄存器中,然后把出栈后任务的堆栈地址赋值给psp,最后再开中断(前面调用了portDISABLE_INTERRUPTS()),切换到线程模式运行任务。在该异常处理程序退出时,还将由硬件从psppopr0-r3,r12,LR,PCxPSR到系统对应的寄存器中。这样系统就从第一个任务开始运行了。

问:为什么还要将r14(LR)寄存器出栈?或者说为什么要将它保存在栈中?
在创建任务时,每个任务的LRpxPortInitialiseStack函数初始化为:portINITIAL_EXC_RETURN0xFFFFFFFD,它表示退出异常时进入线程模式并使用PSP堆栈,这是通过最后的bx r14来实现的,它的作用是让硬件知道退出异常时要恢复什么状态。

实际上进入异常时硬件也自动保存了LR,但系统中的第一个任务,也就是第一次进入SVC异常时保存的LRvPortStartFirstTask()的下一跳指令return 0的地址,很明显系统不会执行到return 0。进入异常后,LR表示异常发生之前在使用的堆栈,FreeRTOS进入SVC异常时,它的值为0xFFFFFFF9,表示退出时进入线程模式并使用MSP堆栈(没运行操作系统默认使用MSP),当运行操作系统后,系统将使用PSP(FreeRTOS设置LR0xFFFFFFFD,对应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-r15xPSR是何时保存到第一个任务的堆栈的呢?或者说每个创建的任务的初始堆栈是怎么设置的呢?不难发现,是在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;
}

首先来看看任务的堆栈需要将寄存器按什么顺序保存在堆栈中:
FreeRTOS第一个任务的创建和调度详解(SVC异常)_第1张图片

  • 创建任务时,硬件堆栈需要自己初始化

这个函数中就是一个个来初始化这些寄存器的值并写入任务堆栈中,供SVCPendSV进行调度。

  • xPSRportINITIAL_XPSR宏为0x01000000,bit24位为1表示Thumb状态,其它的状态位为0即可
  • PCpxCode就是创建任务时传入的任务函数地址,其中portSTART_ADDRESS_MASK0xFFFFFFFE,根据Cortex-M的规范,PC地址是按字/半字对齐的,所以最低位总是为0。
    • 但使用bxblx跳转时,应该将最低位置1,表示使用Thumb指令
  • LR:硬件的LR设置为prvTaskExitError函数,但任务应该在一个死循环中不该返回,进入这个函数说明程序出错
  • r12~r4:没用到,写为任意值都行,保持默认值即可
  • r0pvParameters即为创建任务时传入的参数,这里可以在任务执行时传给任务
  • r14:前面有提到,设置为portINITIAL_EXC_RETURN(0xFFFFFFFD),表示退出异常时,进入线程模式并使用PSP堆栈

你可能感兴趣的:(ARM,RTOS,arm)