打印

UCOS系统启动时的堆栈跟踪----从启动分析到任务的切换

[复制链接]
5264|10
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本人很少在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_STK_SIZE-1],TASK_PRIO1);

        OSStart();
     return 0;
}


现在我们开始逐个分析。看看任务的切换导致的PSP是如何指来指去的。
main过程中的堆栈是在PSP0下进行入栈和出栈工作。
各种Init();
这个函数没有什么必要的解释,这是板级初始化函数,也不存在中断,所以一直是在PSP0的堆栈下进行工作。
接着下一个:OSInit();
这个函数虽然是内核中的函数,我们不必理会,只需要知道这个函数是在PSP0的堆栈进行调用的。况且,该函数内部没有什么特殊的部分(我说的是中断或者其他会改变SP指向的行为);只是在初始化OS的一些必要内容。
现在我们需要看一下这个函数:
OSTaskCreate(Task1,(void *)0,&task_stk1[TASK_STK_SIZE-1],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, [R0]

    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, [R0]

    LDR     R0, =NVIC_INT_CTRL                           ; Trigger the PendSV exception (causes context switch)触发中断PendSV
     LDR     R1, =NVIC_PENDSVSET                                                                        ;0x10000000
     STR     R1, [R0]

    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, [R1]
     STR     R0, [R1]                                            ; 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, [R1]
     STRB    R2, [R0]

    LDR     R0, =OSTCBCur                                       ; OSTCBCur  = OSTCBHighRdy;
     LDR     R1, =OSTCBHighRdy
     LDR     R2, [R1]
     STR     R2, [R0]                             ;这里注意了,R2实际存放的是最高级任务块的首地址,由于任务块是结构,
                                      ;[R2]也就存放着这个结构的第一个元素OSTCBStkPtr,也就是该任务的堆栈指针。
     LDR     R0, [R2]                                            ; 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, [R2] ; 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 会不会产生中断溢出报警的问题。谢谢大家了。
在敲字的时候我也没检查,可能有很多敲错的地方,请谅解,另外,其中如有错误的地方请大方指出。以供大家相互学习。因为我也是个菜鸟。没毛的菜鸟。

相关帖子

沙发
jacksult| | 2013-1-11 10:04 | 只看该作者
大神,好崇拜啊!

使用特权

评论回复
板凳
yyql008| | 2013-1-18 16:44 | 只看该作者
不会的!网上流传的CM3的UCOS不是标准移植,用的别东西.所以你看混了.
PSP仅仅是影响到PendSV里面的那个调转,真正切入任务PSP还是用的真正的栈顶所指位置.
第一次PSP如何赋值的?并不是0,第一次切入任务是从任务控制块的  *OSTCBStkPtr的成员中取堆栈指针,并给PSP赋值的(看看os_tcb结构体)
系统是这么把堆栈指针放入OSTCBStkPtr的.看堆栈初始化的函数OSTaskStkInit的返回值为psp,就是堆栈指针,(注意这里的psp不是CM3的PSP,是UCOS里面的返回值,它也正好叫这个名字....碰巧了),然后执行下一个函数OS_TCBInit,
该函数把psp放入OSTCBStkPtr中.

然后执行完PendSV的BX      LR这句中断返回.中断返回会把堆栈里面的堆栈指针弹给PSP的.也就是说真是使用PSP,是通过 PendSV的BX      LR的中断返回,
给PSP赋值的.

使用特权

评论回复
地板
liu_yongguang| | 2013-6-23 17:20 | 只看该作者
看了有点头绪了,还是要仔细研究一下

使用特权

评论回复
5
HORSE7812| | 2013-8-17 14:55 | 只看该作者
学习

使用特权

评论回复
6
greadber| | 2013-8-17 21:35 | 只看该作者
是这段代码把你搞混了吧,其实cortex m0/3的本身就是用PSP及MSP,只不过系统复位后默认使用MSP,如果你不是有意去设置的话,系统整个运行都使用MSP都没有问题!

UCOS  TCB结构的第一个数据就是存放任务本任务的SP(PSP/MSP都可以),任务第一次中断Pendsv前,所使用的指针是MSP,当第一次startOS 中断pendsv后,系统运行在hardle模式下,所用的sp指针还是MSP。

至于为什么中断后,PSP指针为0,这一点我不明白,有明白的出来说一下!

PendSV_Handler
    CPSID   I                                                   ; Prevent interruption during context switch
    MRS     R0, PSP                                             ; PSP is process stack pointer 如果在用PSP堆栈,则可以忽略保存寄存器,参考CM3权威中的双堆栈-白菜注
    CBZ     R0, PendSV_Handler_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, [R1]
    STR     R0, [R1]

以上这段代码就是把当前任务的SP保存到当前任务TCB的首地址(OSTCBCur->OSTCBStkPtr ),完成任务所有状态数据的存储!这是上文保存


下面这段代码就是切换高优先级的任务SP到PSP,通过 ORR     LR, LR, #0x04     这条指令通知CPU退出中断后使用PSP指针,这是下文切换
                                
    LDR     R0, =OSPrioCur                                      ; OSPrioCur = OSPrioHighRdy;
    LDR     R1, =OSPrioHighRdy
    LDRB    R2, [R1]
    STRB    R2, [R0]
    LDR     R0, =OSTCBCur                                       ; OSTCBCur  = OSTCBHighRdy;
    LDR     R1, =OSTCBHighRdy
    LDR     R2, [R1]
    STR     R2, [R0]
    LDR     R0, [R2]                                            ; 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

使用特权

评论回复
7
greadber| | 2013-8-17 21:49 | 只看该作者
这种方法比较坑爹,MSP完全跟踪不到!

当中断嵌套的时候,8个寄存器就会压栈到MSP了.

所以,跟踪MSP的状态是必要的:

使用特权

评论回复
8
liu568chen|  楼主 | 2013-9-13 16:26 | 只看该作者
yyql008 发表于 2013-1-18 16:44
不会的!网上流传的CM3的UCOS不是标准移植,用的别东西.所以你看混了.
PSP仅仅是影响到PendSV里面的那个调转, ...

老大:帮忙解释一下么?我对PSP赋值为0后的中断入栈不是很清楚,因为入栈的时候需要将指针地址放在PSP中,可是这是的PSP已经为0了,再进行入栈就溢出了(虽然这一次的入栈没有意义)。老大解释一下。

使用特权

评论回复
9
yyql008| | 2013-9-17 10:43 | 只看该作者
liu568chen 发表于 2013-9-13 16:26
老大:帮忙解释一下么?我对PSP赋值为0后的中断入栈不是很清楚,因为入栈的时候需要将指针地址放在PSP中 ...

没什么好解释的的,STM32上电后运行在特权级模式的,不使用PSP.
PSP那里置为0,它不起任何作用的,仅仅是个标示符.你用别的数代替0都可以的!!!
只有单片机设置运行在线程模式下,PSP里的值才起作用.

使用特权

评论回复
10
yyql008| | 2013-9-17 11:31 | 只看该作者
          假设系统有A,B,C三个任务.  现在运行A任务,要切换到B任务.系统这样操作的:

把A的现场保存起来(寄存器入栈,最后把堆栈指针保存到该任务块的OSTCBStkPtr中),然后从B任务块读取B的OSTCBStkPtr,给堆栈指针赋值,然后通过堆栈指针依

次弹出寄存器(B的现场),恢复现场.  保护现场和恢复现场都是针对任务而言的,也是必须的!
     
        系统运行前每个任务都调用会调用堆栈初始化函数的,假设堆栈初始化之后的状态下,A,B,C的3个任务堆栈状态是A0,B0,C0,A的优先级最高.

系统先运行A,自然是要切进A任务(是从main函数切到A),然后调用PendSV_Handler.现在保存不保存现场呢,不保存!,因为是从main函数切到A任务,不存在保护现

场问题.
        之后任何时候调用PendSV_Handler,PSP都不为0的,自然都会保存现场,因为那时系统已经运行的,发生的切换都是某个任务切换到另一个任务.
必须把前面的任务的现场保存起来的.

      PSP为0不会溢出的,因为那时单片机运行在特权模式下(运行特权,线程模式是由哪个寄存器控制的,忘记了),PSP为0不导致BUG,因为那时PSP没有作用.
   
这单片机有2个堆栈指针,看你如何使用就是了.也可以用MSP不使用PSP移植UCOS的.官方移植使用了PSP,这样带一些操作系统味道,其实作用微乎其微.

使用特权

评论回复
评分
参与人数 1威望 +1 收起 理由
liu568chen + 1 赞一个!
11
liu568chen|  楼主 | 2013-9-17 23:50 | 只看该作者
yyql008 发表于 2013-9-17 10:43
没什么好解释的的,STM32上电后运行在特权级模式的,不使用PSP.
PSP那里置为0,它不起任何作用的,仅仅是个标 ...

多谢,原来我一直以为在指令进入main函数时,堆栈使用的是PSP。我心中的疑团算是解决。非常感谢。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

1

主题

18

帖子

0

粉丝