UCOS系统启动时的堆栈跟踪----从启动分析到任务的切换
本人很少在BBS上发帖,因为还是个菜鸟,无法达到帮人解惑的高度。同时我发这个帖子,里面有我的一些分析过程,也有一个遇到的疑问,请大家帮忙解惑。以供大家学习讨论。谢谢!疑问的部分字体中已经标出。另外半夜了,也该睡了。
UCOS在main函数中启动时,我们最关注的是这个堆栈是怎么分配到各自的任务堆栈中进行跳转的呢?
跳来跳去的堆栈值,有点让我们头晕目眩,不知所为。
要处理这个问题,首先要明白STM32平台对堆栈的操作方式,因为CM3平台有两个堆栈指针,但是同时间只有一个能够被用于堆栈:MSP和PSP。
MSP Main SP 特权访问下会自动切换到该堆栈指针下进行工作。
PSP Process SP 在线程程序下自动切换到该堆栈指针下进行工作。
简单一点说,MSP运用于中断下的堆栈(其实并不完全正确)。PSP运用于常规的用户程序堆栈。
STM3平台在复位后进行执行指令时,属于特权模式,运用的是MSP,当我们在startup.s中执行时,分别进行MSP、PSP的初始化,其实flash第0条地址下存放的就是MSP的数据,当所有配置都完成后,处理器才会切换到PSP状态下。并逐步跳转到main函数下。现在我们明白。其实我们常常看到的main函数内部就是线程模式下。此时我将main函数下的SP记作PSP0,这个是main函数及各个任务运用的PSP指向RAM区间不一样。需要区分我们后面所说到的任务堆栈SP。我们会把任务函数下的SP记作PSP1、PSP2...PSPx。
现在我们开始跟踪并分析main的运行过程下的PSP。
我们还是补充一点中断是怎么一个过程,因为这个对入栈极为重要。
当用户级状态下程序执行时有中断发生,则会在PSP的状态下自动进行8个寄存器。然后PSP就不动了。同时切换动MSP做栈指针,同时进行中断函数的执行,函数结尾时,运用PSP进行出栈,并返回到任务函数的的中断处执行。就这么简单。
UCOS的main函数都有比较固定的模式:
int main(void)
{
各种Init();
OSInit();
OSTaskCreate(Task1,(void *)0,&task_stk1,TASK_PRIO1);
OSStart();
return 0;
}
现在我们开始逐个分析。看看任务的切换导致的PSP是如何指来指去的。
main过程中的堆栈是在PSP0下进行入栈和出栈工作。
各种Init();
这个函数没有什么必要的解释,这是板级初始化函数,也不存在中断,所以一直是在PSP0的堆栈下进行工作。
接着下一个:OSInit();
这个函数虽然是内核中的函数,我们不必理会,只需要知道这个函数是在PSP0的堆栈进行调用的。况且,该函数内部没有什么特殊的部分(我说的是中断或者其他会改变SP指向的行为);只是在初始化OS的一些必要内容。
现在我们需要看一下这个函数:
OSTaskCreate(Task1,(void *)0,&task_stk1,TASK_PRIO1);
这个也是内核中的一个函数,按理说我们没有必要重视,因为这也是正常的函数调用。但是我们还是需要详细拆解一下这个函数。里头有两个特殊的调用:
psp = OSTaskStkInit(task, p_arg, ptos, 0); /* Initialize the task's stack */
err = OS_TCBInit(prio, psp, (OS_STK *)0, 0, 0, (void *)0, 0);
前一个函数的任务堆栈初始化函数;后一个函数的任务控制块初始化函数;说到这儿我们就把前面所引出的任务堆栈说出来了。首先来分析OSTaskStkInit函数。
这个函数对于STM32平台来说很简单,也就是在利用前面在全局变量的情况下使用的OS_STK数组。头顶作为堆栈的栈顶。当然我们还需要模拟一下任务在第一次入栈的情况,也就是说需要模拟一次入栈。这里我说一下任务的调度是在什么情况下发生的,是在中断中发生的,也就是在PendSV中发生的,所以这次的入栈是根据中断的情况模拟入栈,我们需要保存CPU中寄存器,首先保存中断自行入栈的部分,R15(PC),xPSR,..R3..R0之类的,然后我们在手工保存R4-R11。记住:总共16个寄存器,模拟中断保存的8个寄存器,以及自己手工保存的8个寄存器。现在任务堆栈就OK了。堆栈用的向下增长的方式,即满递减的方式入栈。看到这个函数的task参数不,其实这个task地址就是要放在堆栈中的那个PC的位置下。等待用这个栈做为中断返回的时候就把这个task返回到PC中。任务就切换完成。
好了啰嗦了这么多,就是想要理解这个堆栈是怎么建立的,我们有一组专门的数组,并模拟进入了一次入栈手续。最后一个入栈的数据的地址就是我们的PSP。由于是任务堆栈,我们取名叫PSPx。这个地址是赋值给psp的。我们还需要把这个psp存放在该任务控制块中,也就是任务控制块的第一个元素。
其实任务控制块的初始化没啥好讲解的。也即是这些参数放到任务控制块中,尤为要注意的是这个psp。有没有注意到这个psp就是前面函数的返回值,它是我们作为堆栈的SP的值,也即是堆栈的地址,我们把这个记录到任务控制块中。
好了现在把这两内部函数介绍完成。退出OSTaskCreate函数。注意,前面这些都是我在做准备,并没有真实用的任务中的PSPx作为堆栈,在main函数中仍然是由PSP0做主。
现在接着往下运行OSStart()。
这个函数其实也没啥要说的。主要关注内部最后一个函数:OSStartHighRdy();这个函数是由汇编编写。深藏于os_cpu_a.asm中。
OSStartHighRdy
LDR R0, =NVIC_SYSPRI14 ; Set the PendSV exception priority 0xE000ED22
LDR R1, =NVIC_PENDSV_PRI ;0xFF设置Pen大SV为最低255级
STRB R1,
MOVS R0, #0 ; Set the PSP to 0 for initial context switch call这里不是很懂。
MSR PSP, R0
LDR R0, =OSRunning ; OSRunning = TRUE
MOVS R1, #1
STRB R1,
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)触发中断PendSV
LDR R1, =NVIC_PENDSVSET ;0x10000000
STR R1,
CPSIE I ; Enable interrupts at processor level使能中断。
OSStartHang
B OSStartHang
这个函数在执行过程中应该会很顺利,但是我有一个地方不懂,问什么要置PSP为0;PSP置0 的话中断产生时不会发生入栈溢出吗,PSP为0了后,中断是怎么入栈的啊?可是这个PSP为0 很重要,因为在PendSV 中判断这个PSP是否为0来确定系统是否为第一次调用任务切换。请大家帮忙解释一下这个PSP为0的疑惑。
这个函数执行到最后,就停在OSStartHang处了。目的就是不让他出去。到这儿,我们剩下的工作就是等待PendSV中断。我也顺便插一句,在中断没有产生前,这仍是在main函数中执行,SP用的仍然是PSP0的堆栈。一旦中断产生并发生任务调度,这个main函数就没用了,相同的PSP的堆栈也跟着废了。
好吧,PendSV如期而至:
这个函数看似简单,我当时一口气把读完了,发现没什么不寻常的地方。后来回去琢磨,又发现看不懂了。直到一个晚上没睡着,第二天上班的时候还是心不在焉的想这事。后来无意中想通了。
OS_CPU_PendSVHandler
CPSID I ; Prevent interruption during context switch
MRS R0, PSP ; PSP is process stack pointer
CBZ R0, OS_CPU_PendSVHandler_nosave ; Skip register save the first time
SUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stack
STM R0, {R4-R11}
LDR R1, =OSTCBCur ; OSTCBCur->OSTCBStkPtr = SP;
LDR R1,
STR R0, ; R0 is SP of process being switched out
; At this point, entire context of process has been saved
OS_CPU_PendSVHandler_nosave
PUSH {R14} ; Save LR exc_return value
LDR R0, =OSTaskSwHook ; OSTaskSwHook();
BLX R0
POP {R14}
LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy;
LDR R1, =OSPrioHighRdy
LDRB R2,
STRB R2,
LDR R0, =OSTCBCur ; OSTCBCur= OSTCBHighRdy;
LDR R1, =OSTCBHighRdy
LDR R2,
STR R2, ;这里注意了,R2实际存放的是最高级任务块的首地址,由于任务块是结构,
;也就存放着这个结构的第一个元素OSTCBStkPtr,也就是该任务的堆栈指针。
LDR R0, ; R0 is new process SP; SP = OSTCBHighRdy->OSTCBStkPtr;
LDM R0, {R4-R11} ; Restore r4-11 from new process stack
ADDS R0, R0, #0x20
MSR PSP, R0 ; Load PSP with new process SP
ORR LR, LR, #0x04 ; Ensure exception return uses process stack
CPSIE I
BX LR ; Exception return will restore remaining context
END
我来解释一下这个函数,这个函数就是用来任务调度的过程函数,因为能够用C实现的部分都在OS源代码中实现了,就差一个任务切换的函数,这个函数我们至少要读两回,在前面我们分析OSStart()时,在它的结尾有个OSStartHighRdy();OSStartHighRdy()的结尾只是把中断使能了。然后停在结尾处。目的就是在等待我们的中断调度。
好吧,中断来了,接着系统运行的顺序,注意任务堆栈是怎么切换进去的。任务是怎么切换的。
在中断来的时候需要做一些入栈操作,虽然我这个时候仍然用的是PSP0堆栈,但是实际寄存器中的PSP值被改写为0 了。这个时候我完全不知道它怎么入栈的,虽然我们不要这次的入栈内容,因为我们从来没想过要再次切换到main函数里头去。但是这个过程是免不了。好吧我忽略了,同时,希望大神没帮忙解惑一下。
在该程序的运行过程中,用的是MSP堆栈,硬件操作与PSP没一毛钱的关系,切记。当然你可以用指令操作PSP。
该程序的第一个指令就是把PSP传送到R0中,注意,前面我们已经将PSP置0 ,下一条指令为检测R0的是否为0,为0则跳转到OS_CPU_PendSVHandler_nosave处,不为0的话就不跳转,接着往下执行。
根据我们之前分析的状况,PSP为0,则R0为0,则需要进行跳转。
在OS_CPU_PendSVHandler_nosave处接下往下走指令,这四行不看,是直接执行Hook函数的。没意思。接下来的四行执行OSPrioCur = OSPrioHighRdy;也没啥讲的,再接下来的四行是为了执行OSTCBCur= OSTCBHighRdy;更没啥讲的。但是这里头的那句:
LDR R0, ; R0 is new process SP; SP = OSTCBHighRdy->OSTCBStkPtr;
需要明白是啥意思。也就是说此时的R0是存放着PSPx的值,但PSPx在任务创建的时候就已经初始化堆栈了(看前面解释)。
我重新说一下任务堆栈的初始化:它存放着16个寄存器的内容,是完全模拟中断入栈的情况存放的。由于R0现在已经指向该任务堆栈最后一个入栈的内容。
执行:LDM R0, {R4-R11}时,出栈R4-R11
执行:ADDS R0, R0, #0x20;R0往上曾8个字空间,也就是8个寄存器被出栈了。
执行:MSR PSP, R0 ; 这里就强大了,PSP现在指向了模拟中断是的那8个入栈的内容。
现在在中断退出时PSP做出完美出栈。任务切换完成。
以上是系统的第一次任务切换。
如我们在系统中执行第N次任务切换呢?这又是一个怎么中断过程呢?我们还需要分析这个PendSV函数。还记得那个PSP的检测是否为0吗?因为当进行了N次切换时,PSP的内容已经不为0了,PendSV函数中不需要跳转。
有空大家可以分析一下。写了这么多,要休息了。
另外请大家帮忙解惑一下我这里提到那个PSP为0 会不会产生中断溢出报警的问题。谢谢大家了。
在敲字的时候我也没检查,可能有很多敲错的地方,请谅解,另外,其中如有错误的地方请大方指出。以供大家相互学习。因为我也是个菜鸟。没毛的菜鸟。
等了很多天,就没有一个能解惑的?我太悲哀了。
页:
[1]