FreeRTOS内核源码解读之-------系统启动(三)

前面文章两篇文章介绍了FreeRTOS的启动过程,但是有些问题还没有解决,在本篇文章中将会逐一解决。
首先,在《FreeRTOS内核源码解读之-------系统启动(一)》中提到Cortex-M4内核中两个不同的栈指针寄存器MSP和PSP。对于不具有嵌入式OS的应用,可以在操作中只使用MSP栈指针寄存器;对于含有嵌入式OS(就像FreeRTOS)应用,异常处理(包括内核状态下)使用的是MSP,对于应用任务使用的是PSP。每一个应用任务都有自己的栈空间,那么上面这种机制是怎么实现的呢?
还有FreeRTOS是一个多任务运行的操作系统,那么当一个任务正在运行时,会被优先级更高的任务或者中断打断,FreeRTOS是怎么保证被打断的任务在重新运行时不会出现问题?换句话说,如何实现保护现场、任务切换等?
本文从这两个问题触发,具体阐述FreeRTOS内核启动过程,在下一篇介绍FreeRTOS任务调度。

  • 问题引入
  • Cortex-M4对于多任务运行的硬件架构支持特性
    一、问题引入
    通过上一篇文章《FreeRTOS内核源码解读之-------系统启动()》分析,我们知道FreeRTOS最后调用prvStartFirstTask产生一个SVC中断,产生SVC中断之后,在函数vPortSVCHandler进行有FreeRTOS内核态到用户态的切换。两个函数的代码如下:
__asm void prvStartFirstTask( void )
{
	PRESERVE8
	    /* Cortext-M3硬件中,0xE000ED08地址处为VTOR(向量表偏移量)寄存器,存储向量表起始地址*/
    ldr r0, =0xE000ED08    
    ldr r0, [r0]
    /* 取出向量表中的第一项,向量表第一项存储主堆栈指针MSP的初始值*/
    ldr r0, [r0]   
    /* 将堆栈地址存入主堆栈指针 */
    msr msp, r0
    /* 使能全局中断*/
    cpsie i
    cpsie f
    dsb
    isb
    /* 调用SVC启动第一个任务 */
    svc 0
    nop
    nop
}
__asm void vPortSVCHandler( void )
{
	PRESERVE8
	ldr	r3, =pxCurrentTCB
	ldr r1, [r3]
	ldr r0, [r1]
	ldmia r0!, {r4-r11, r14}
	msr psp, r0
	isb
	mov r0, #0
	msr	basepri, r0
	bx r14
}

首先FreeRTOS从内核态切换到用户态时,是怎样切换MSP和PSP栈指针的呢?还有FreeRTOS是怎样找到应用任务的栈以及用户编写的应用程序的呢?
在函数prvStartFirstTask主要做的内容如下:
1)将堆栈地址存入主栈指针寄存器,在中断向量表中第一项是堆栈地址;
2)开中断;
3)产生一个SVC中断。
在函数prvStartFirstTask中设置了MSP主堆栈指针。
在函数vPortSVCHandler可以看到对寄存器进行了赋值,对PSP进行了赋值、然后进行程序跳转。那么猜测关于上面的疑问可以从这几句来寻求答案。
二、应用任务栈帧
在函数vPortSVCHandler中会看到这么一句 ldr r3, =pxCurrentTCB ,pxCurrentTCB变量存放的是当前需要运行任务的控制块。代码:

ldr	r3, =pxCurrentTCB
	ldr r1, [r3]
	ldr r0, [r1]

具体作用是什么的?
首先看任务控制块中的内容(去掉各种条件编译):

typedef struct tskTaskControlBlock
{
	volatile StackType_t	*pxTopOfStack;
	ListItem_t			xStateListItem;
	ListItem_t			xEventListItem;	
	UBaseType_t			uxPriority;	
	StackType_t			*pxStack;
	char				pcTaskName[ configMAX_TASK_NAME_LEN ];
} tskTCB;

去掉条件编译感觉好清爽,这里只是关注第一个成员变量,为了填充一下版面,请允许我这么做。关于任务控制块各成员变量的讲解,请关注我的另一篇文章 《FreeRTOS内核源码解读之-------任务创建》 。由此可知 ldr r3, =pxCurrentTCB 其实就是 ldr r3, =pxTopOfStack。那么,最后就是使得r0寄存器保存任务栈指针。
那么,任务栈里面存放的是什么呢?由于任务第一次运行,那么需要去任务创建函数中寻找一点蛛丝马迹。
任务创建函数调用过程如下:

xTaskCreate->prvInitialiseNewTask->pxPortInitialiseStack

从这里看出任务创建函数xTaskCreate,最终会调用pxPortInitialiseStack。函数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_EXEC_RETURN;

	pxTopOfStack -= 8;	/* R11, R10, R9, R8, R7, R6, R5 and R4. */

	return pxTopOfStack;
}

代码不是很多,就一句一句的分析一下吧。
1)*pxTopOfStack = portINITIAL_XPSR:栈顶保存了xPSR,且它的值为portINITIAL_XPSR,0x01000000。其实就是一个初始状态,其中的1表示Thumb状态。因为Cortex-M只有Thumb状态。
2)*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK : portSTART_ADDRESS_MASK=0xfffffffe,pxCode保存的是任务函数的函数指针,就是我们用户编写的任务函数,这句就是保存任务跳转时的跳转地址,这里要求必须半字或者字对齐,因此需要&portSTART_ADDRESS_MASK。
3)*pxTopOfStack = ( StackType_t ) prvTaskExitError :该数值是赋值给LR寄存器,保存任务出错处理函数指针,一般不会执行这个函数,因为我们的任务都是一个不能返回的死循环,因此不会执行。
4)pxTopOfStack -= 5:留出空间用于保存寄存器 R12, R3, R2 and R1。
5)*pxTopOfStack = ( StackType_t ) pvParameters:保存任务函数的参数。
6)*pxTopOfStack = portINITIAL_EXEC_RETURN:处理器进入异常处理或者中断模式的时候,连接寄存器的数值会被更新为portINITIAL_EXEC_RETURN,当我们使用BX或者LDR这类指令时,会将该数值写入程序寄存器,是用来触发异常返回机制。
7)pxTopOfStack -= 8:保存r4~r11。
通过上面分析,我们大体画出任务栈中保存的数据如下:
在这里插入图片描述那么我们在回过来看一下函数vPortSVCHandler具体做了哪些事情:
1)将任务栈保存的R11-R4内容赋给寄存器R11-R4;
2)将portINITIAL_EXEC_RETURN赋给寄存器R14,主要是用于触发中断返回,也就是下面 bx r14 指令;
3)pvParameters 赋值给PSP寄存器。
那么现在就需要解决FreeRTOS运行用户任务时,MSP到PSP切换的问题。玄妙之处就是portINITIAL_EXEC_RETURN,上面说了将portINITIAL_EXEC_RETURN=0xfffffffd写入到R14寄存器(LR)之后,我们调用bx r14指令之后就会产生中断返回,通过查阅资料得到如下图:
在这里插入图片描述正是写入的是0xfffffffd,所以中断返回的时候是的处理器进入处理模式,并且使用进程栈PSP寄存器。
总结: 本文主要解决了SVC中断处理函数中具体执行内容,和任务创建过程中任务栈中存放的数据。以及如何从MSP栈指针切换到PSP栈指针。下一篇文章将对FreeRTOS中任务切换进行讲解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章