prvStartFirstTask();
如何启动一个任务, 这是第一步, 这个函数是 vTaskStartScheduler ( ) --> xPortStartScheduler ( ) --> prvStartFirstTask ( ) 这一连串的嵌套的函数
来看汇编代码.
这里的汇编的语法其实都不重要我们只需要明白这个函数分别实现了哪些功能这才是最重要的.
__asm void prvStartFirstTask( void ) {
PRESERVE8 /* 8字节对齐 */
ldr r0, =0xE000ED08 /* 0xE000ED08 为 VTOR 地址 */
ldr r0, [ r0 ] /* 获取 VTOR 的值 */
ldr r0, [ r0 ] /* 获取 MSP 的初始值 */
msr msp, r0 /* 初始化MSP *
cpsie i /*开启全局中断*/
spsie f
dsb
isb
svc 0 /*使用SVC中断函数进行后序操作*/
nop
nop
}
这里这个函数的功能其实就两个, 开启全局中断和初始化MSP, 下面来具体介绍.
- 与C语言对接
PRESERVE8 /* 8字节对齐 */
汇编是很死板的, 这里的需要手动声明8字节与C程序对齐才能更方便的让C程序访问其声明的存储空间(这里是栈)
- 初始化MSP
ldr r0, =0xE000ED08 /* 0xE000ED08 为 VTOR 地址 */
ldr r0, [ r0 ] /* 获取 VTOR 的值 */
ldr r0, [ r0 ] /* 获取 MSP 的初始值 */
msr msp, r0 /* 初始化MSP */
看到这里相比大家会有两个问题: VTOR的地址怎么是0xE000ED08?, MSP又是啥玩意儿?
我们先来解答这些问题, 再来看看函数实现了哪些功能.
-
- VTOR的地址
#define portNVIC_VTOR_ADDRESS ( ( volatile uint32_t * ) 0xE000ED08 )
这里定义了VTOR的地址, 而VTOR是啥呢?
简单来说VTOR是中断函数的门, 我们打开start_stm32xxxxxx.s文件一般都是查询对应的中断函数, 这些中断函数的地址都被放在了一个内存空间中, 如果说这个内存空间是中断的集合, 那VTOR就是程序寻找中断的入口, 也就是门 VTOR是Vector Table Offset Register的缩写, 翻译过来就是向量表偏移寄存器, 它的作用就是存储向量表的偏移地址, 向量表是存储中断向量的地方, 每个中断向量表都存储了对应中断的入口地址, 当发生中断时, CPU就会根据这个向量表找到对应中断的入口地址, 然后跳转到这个地址执行中断服务程序.
b. 那这个值是怎么来的呢?
首先中断函数是从0x00000000开始按照栈进行存储的, 这很符合直觉, 我们知道我们在操作栈的时候一般都是从栈顶开始逐步访问的, 欸, 这里的VTOR就是栈顶的地址, 可以供我们访问栈内部的数据, 不过你又怎么知道以后会存入多少的数据在栈里面呢? 所以这个值是当前环境下的栈顶地址.
c. MSP又是什么?
如果说VTOR是中断函数的门, 那MSP就是钥匙, 只有它才能被外部调用从而访问栈内的数据. 在MCU中, 是使用一个名为SP指针来指向栈顶(最后一个入栈的元素地址)从而访问数据的. 这里可以看出SP指针是实时更新的.
对于ARM Cortex-M系列处理器 有两个栈, 一个是用来存储主程序产生的临时变量 用PSP作为SP指针来访问, 另一个是用来存储中断服务程序产生的临时变量 用MSP来作为SP指针来访问.
那么话说回来这个函数对这两个变量做了什么呢? 首先获取地址不用说, 我们来看看初始化MSP是什么作用.
初始化就是重定向MSP, 将其指向栈底. 没了钥匙也就不能访问了和没有没啥区别, 当然, 在之后我们其实也不会再使用了, 因为调度器在启动了第一个任务之后就不会在回来看了
- 开启全局中断
cpsie i /*开启全局中断*/
spsie f
dsb
isb
在**xPortStartScheduler()**中, 也就是调用此函数之前我们把中断关了, 这里我们需要开启中断, 这里是SVC中断, 来完成后序的操作. 接下来我们来看看中断函数中有什么吧.
vPortSVCHandler()
先来浏览一下代码:
__asm void vPortSVCHandler(void){
//8字节对齐
PRESERVE8
//获取任务栈的地址
ldr r1, =pxCurrentTCB //得到TCB_t的结构体
ldr r2, [r1] //得到结构体第一个成员
ldr r3, [r2] //得到结构体第一个成员的地址
//模拟出栈
ldmia r0 !, {r4 - r11,r14}
msr psp, r0
isb
//使能全部的中断
mov r0, # 0
msr basepri
//使用PSP指针切换到任务函数
orr r14, # 0xd
bx r14
}
首先第一个对齐不用说, 我们从第二行开始.
- 获取任务栈的地址
ldr r1, =pxCurrentTCB //得到TCB_t的结构体
ldr r2, [r1] //得到结构体第一个成员
ldr r3, [r2] //得到结构体第一个成员的地址
pxCurrentTCB是当前处于就绪队列优先级最高的任务的TCB_t结构体的地址, 而TCB_t结构体中第一个成员就是栈的地址, 所以这里我们获取的就是当前任务栈的地址.
这里我们可以跳转到xCreateTask()函数中 和 TCB_t结构体中查看一下.
TCB_t 结构体第一个成员
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack;
//...
}TCB_t
xCreateTask()中调用的prvInitialiseNewTask(...)函数末尾
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
pxPortInitialiseStack()函数中
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
pxTopOfStack--;
*pxTopOfStack = portINITIAL_XPSR;
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) prvTaskExitError;
pxTopOfStack -= 5;
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
return pxTopOfStack;
}
你看这里其实就很明显了, 存一个数据 栈顶动一下
- 模拟出栈
ldmia r0 !, {r4 - r11}
msr psp, r0
isb
这里我们模拟出栈, 将任务栈中的数据加载到CPU寄存器中, 这样才能确保执行相应的任务.
这里的主要就两件事:
-
- ldmia 不断的将数据出栈, 顺序是从低地址往高地址
- msr psp, r0 将PSP指针指向任务函数的参数地址
- 使能全部的中断
mov r0, # 0
msr basepri
这里的主要操作是basepri = r0 = 0, 即给basepri寄存器写0, 可以开启全局的中断
- 返回r14
bx r14
r14一般来说用于保存函数的返回地址, 但是在这个中断当中又有所不同.
这里指向的则是EXC_RETURN
这是一个4byte即32位寄存器, 在这里我们直接说结论, 是用于跳转到任务函数中.
这一节写起来是真的很费劲, 涉及许多寄存器的知识, 写起来我并没有全盘的把握,我只能把我所理解的写给大家看, 所以如果大家看到这里有什么的问题的欢迎指出或者私信。