之前文章分析过任务相关的创建和切换,但是理解的还不够深刻。
以cortexM4为例。
堆栈初始化
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;
}
创建完任务后会进行堆栈初始化。
为什么要进行堆栈初始化?
因为任务运行需要依赖硬件寄存器,而且需要知道任务的入口地址,所以这些信息必须存起来。这样当执行任务的时候就知道从哪开始了。
从大部分文章看,都会描述到有的寄存器是硬件自动恢复的,有的需要手动恢复。那么对于任务堆栈的初始化,这时还没有异常,所以都需要手动的将寄存器存放到堆栈里。这个函数其实并不能体现出自动和手动恢复。因为都是手动存放的。
存放后如下:
初始化之后pxTopOfStack已经指向了空闲堆栈。后面task运行用到的栈就从这里开始入栈出栈。以上的内容是不会被破坏的。
这里需要注意R15(PC)存放的就是task函数。这样在寄存器恢复后pc就等于task函数。这样运行的时候就从task函数开始了。这样task不就可以运行起来了吗。同时,R0存放的是task参数。
堆栈初始化返回的是pxTopOfStack,赋给了TCB。
第一个任务
上面堆栈已经完成了初始化。
prvStartFirstTask是启动第一个task。
__asm void prvStartFirstTask( void )
{
/* *INDENT-OFF* */
PRESERVE8
/* Use the NVIC offset register to locate the stack. */
ldr r0, =0xE000ED08
ldr r0, [ r0 ]
ldr r0, [ r0 ]
/* Set the msp back to the start of the stack. */
msr msp, r0
/* Clear the bit that indicates the FPU is in use in case the FPU was used
* before the scheduler was started - which would otherwise result in the
* unnecessary leaving of space in the SVC stack for lazy saving of FPU
* registers. */
mov r0, #0
msr control, r0
/* Globally enable interrupts. */
cpsie i
cpsie f
dsb
isb
/* Call SVC to start the first task. */
svc 0
nop
nop
/* *INDENT-ON* */
}
0xE000ED08是SCB_VTOR寄存器的地址,里面存放的是向量表的起始地址,即MSP的地址。向量表的地址通常是从内部flash的起始地址开始存放。将0xE000ED08这个立即数加载到R0,然后将0xE000ED08所指向的地址再给R0。一般都是flash的起始地址。然后从起始地址获取msp内容,赋值给msp。这样就完成主堆栈初始化。
完成之后就触发svc异常,在svc里启动第一个任务。
__asm void vPortSVCHandler( void )
{
/* *INDENT-OFF* */
PRESERVE8
/* 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
/* *INDENT-ON* */
}
pxCurrentTCB是一个全局变量,用于指向正在或即将运行的任务。
假设按照上图方式存储。
加载pxCurrentTCB的地址(0xa0000000)到r3。r3的内存就是pxCurrentTCB在内存中的位置。因为指针也是一个变量。
加载pxCurrentTCB(0xb0000000)到r3。这时r3的内容就是pxCurrentTCB指向的内存地址。
加载pxCurrentTCB指向的任务控制块(0xc0000000)到r0,任务控制块的第一个成员就是栈顶指针,所以此时r0等于栈顶指针。
既然R0等于pxTopOfStack,而pxTopOfStack又指向了空闲堆栈开始处。既然要运行任务,首先就要恢复寄存器的值。
以R0位基地址,将栈中向上增长的8个字节的内容加载到CPU寄存器r4~r11,同时R0也会自增。
对应如下代码:
ldmia r0!, {r4-r11,r14}
将新的栈顶指针r0更新到psp,任务执行的时候使用的堆栈指针是psp。
msr psp, r0
这里为什么要将r0给到psp,一直用r0恢复剩余的寄存器不行吗?
上面说过,有些寄存器是可以硬件自动恢复的。所以没必要手动恢复。这里将r0给到psp。就是为了能够硬件恢复剩余寄存器。
像r14寄存器最后4位或上0x0d,使得硬件退出时使用psp返回任务模式。上面psp已经赋值了,这样就可以达到硬件恢复了。这时psp指向了任务栈顶。需要注意的是pxTopOfStack还是指向r4处!
pc已经等于task函数了,所以退出中断后就直接运行task了。
看到此会有如下疑问:
1、task开始运行后,局部变量、函数调用等用到的栈空间是从栈顶指针是空闲块开始的。
任务切换
具体是在xPortPendSVHandler()函数里。
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
/* *INDENT-OFF* */
PRESERVE8
mrs r0, psp
isb
/* Get the location of the current TCB. */
ldr r3, =pxCurrentTCB
ldr r2, [ r3 ]
/* Is the task using the FPU context? If so, push high vfp registers. */
tst r14, #0x10
it eq
vstmdbeq r0!, {s16-s31}
/* Save the core registers. */
stmdb r0!, {r4-r11, r14}
/* Save the new top of stack into the first member of the TCB. */
str r0, [ r2 ]
stmdb sp!, {r0, r3}
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
bl vTaskSwitchContext
mov r0, #0
msr basepri, r0
ldmia sp!, {r0, r3}
/* The first item in pxCurrentTCB is the task top of stack. */
ldr r1, [ r3 ]
ldr r0, [ r1 ]
/* Pop the core registers. */
ldmia r0!, {r4-r11, r14}
/* Is the task using the FPU context? If so, pop the high vfp registers
* too. */
tst r14, #0x10
it eq
vldmiaeq r0!, {s16-s31}
msr psp, r0
isb
#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
#if WORKAROUND_PMU_CM001 == 1
push { r14 }
pop { pc }
nop
#endif
#endif
bx r14
/* *INDENT-ON* */
}
mrs r0, psp
将psp放到r0寄存器。当进入pensvc时,上一个任务的运行环境即:xPSR、PC、R14、R12、R3、R2 、R1、 R0这些寄存器会自动存储到任务栈中。此时psp已经自动指向了r0。并且psp的内容也存放到r0。
ldr r3, =pxCurrentTCB
ldr r2, [ r3 ]
加载pxCurrentTCB的地址到r3。 加载r3指向的内容到r2,即r2等于pxCurrentTCB。
/* Save the core registers. */
stmdb r0!, {r4-r11, r14}
此时r0已经等于psp了,那么就可以以r0为基准,手动恢复剩余的寄存器。并且r0自动递减。
此时r0指向r4后面了。
/* Save the new top of stack into the first member of the TCB. */
str r0, [ r2 ]
将r0的值存储到r2指向的内容,r2等于pxCurrentTCB,具体为将r0的值存储到上一个任务的栈顶指针pxTopOfStack。
自持上文的保存就完成了。
调用vTaskSwitchContext()查询下一个要运行的任务。具体函数内容就不分析了,我们只要知道这个函数可能会更新pxCurrentTCB.
/* The first item in pxCurrentTCB is the task top of stack. */
ldr r1, [ r3 ]
ldr r0, [ r1 ]
加载r3指向的内容到r1,r3存放的是pxCurrentTCB的地址。加载r1的内容到r0,即下一个要运行的任务的栈顶指针。
/* Pop the core registers. */
ldmia r0!, {r4-r11, r14}
以r0为基地址,手动恢复r4~r11 r14寄存器。
msr psp, r0
更新psp的值,等异常退出时,会以psp为基地址,将任务剩下的内容自动加载到cpu寄存器。
bx r14
异常发生时,r14等于oxfffffffd,表示异常返回后进入任务模式,SP以PSP作为堆栈指针出栈,出栈完毕后PSP指向任务的栈顶。并且这时硬件已经自动恢复剩下的寄存器了。注意PC不是task入口了。而是上一次运行的位置。
从第一个任务和任务切换来看。
1、pxTopOfStack只会在r11和r4之间移动。
2、psp只会在r0和xPSR之间移动。
所以会有一个疑问,在task运行过程中,形参和函数调用都会使用到栈,但并不会影响psp和pxTopOfStack。 那么task在运行时的栈指针是从哪恢复的,sp指向了psp,这样不就从栈顶位置开始了吗,不应该是从task运行时使用的占空间位置恢复吗?