UCOS任务切换

任务切换

堆栈初始化

由于抢占式任务的需要,每个任务需要有自己的任务堆栈,在任务初始化函数OSTaskCreate中,通过传递p_stk_base来指出创建的那部分数组空间,最终在OSTaskStkInit中初始化该数组所储存的最初的数据:通用寄存器的值。由于通用寄存器的内容涉及到程序运行的各个过程,相关内容参考寄存器,此外,由于任务堆栈是从大地址到小的增长方式,所以寄存器的值实际上是放置在任务堆栈的末尾,然后从末尾一点点增加其他的数据的,这一点不要混淆。

CPU_STK  *OSTaskStkInit (OS_TASK_PTR    p_task,
                         void          *p_arg,
                         CPU_STK       *p_stk_base,
                         CPU_STK       *p_stk_limit,
                         CPU_STK_SIZE   stk_size,
                         OS_OPT         opt)
{
    CPU_STK  *p_stk;
		
	
		/*数组地址由小到大增长,即数组末端的地址最大*/

    (void)opt;                                              /* Prevent compiler warning                               */

    p_stk = &p_stk_base[stk_size];                          /* Load stack pointer 获取任务堆栈的栈顶(堆栈中的最大地址)  */
                                                            /* Registers stacked as if auto-saved on exception        */
    *--p_stk = (CPU_STK)0x01000000u;                        /* xPSR,程序状态寄存器,包含条件,中断等白标志位           */
    *--p_stk = (CPU_STK)p_task;                             /* Entry Point,R15寄存器实际作用是指向下一个运行的代码位置,所以此出指向任务的实际代码位置,类似于函数指针*/
    *--p_stk = (CPU_STK)OS_TaskReturn;                      /* R14 (LR)  是链接寄存器,用来储存当前函数结束时范返回的位置*/
    *--p_stk = (CPU_STK)0x12121212u;                        /* R12 是内部调用暂时寄存器                                */
    *--p_stk = (CPU_STK)0x03030303u;                        /* R3到R0作为传入的参数寄存器                              */
    *--p_stk = (CPU_STK)0x02020202u;                        /* R2                                                     */
    *--p_stk = (CPU_STK)p_stk_limit;                        /* R1                                                     */
    *--p_stk = (CPU_STK)p_arg;                              /* R0 : argument                                          */
                                                            /* Remaining registers saved on process stack             */
    *--p_stk = (CPU_STK)0x11111111u;                        /* R11                                                    */
    *--p_stk = (CPU_STK)0x10101010u;                        /* R10                                                    */
    *--p_stk = (CPU_STK)0x09090909u;                        /* R9                                                     */
    *--p_stk = (CPU_STK)0x08080808u;                        /* R8                                                     */
    *--p_stk = (CPU_STK)0x07070707u;                        /* R7                                                     */
    *--p_stk = (CPU_STK)0x06060606u;                        /* R6                                                     */
    *--p_stk = (CPU_STK)0x05050505u;                        /* R5                                                     */
    *--p_stk = (CPU_STK)0x04040404u;                        /* R4                                                     */

    return (p_stk);
}

寄存器在进入中断的过程中xPSR,R0-R3,R12,R14,R15会由中断自行保存,是最先被保存的,所以放在最上面。而后的部分要代码手动保存,主要是R4-R11。
参考下图
任务堆栈模型

PendSV_Handler

当系统调用OS_TASK_SW()或者OSIntCtxSw()时,都将执行NVIC_INT_CTRL = NVIC_PENDSVSET,其功能是触发一次PendSV,对于PendSV,在分析其内容前先了解下为什么使用PendSv而不是其他的中断异常。
首先,PendSv是Cortex-m中优先级最低的中断异常,这意味着任何其他的中断异常都可以抢占它,同时在进入PendSV处理函数后就关闭了中断CPSID I参考,也就是在响应PendSv的过程中不会有其他中断去抢占任务切换的过程,对于PendSv的分析,在不同的抢占式系统中不一样,但可以总结如下,系统存在一个最高优先级操作SVC,在运行该操作时无法被抢占,而后,当tick_task任务运行到需要进行任务切换时,系统将挂起一个pendsv,当所有的异常都处理结束时才会进行pendsv_handle,保证了每次任务切换的过程能被及时执行。
其示例如下:
pendSV

流程:

  1. 任务A呼叫SVC来请求任务切换(例如,等到某些工作完成);
  2. OS接收到请求,做好上下文切换的准备,并且悬起一个PendSV异常;
  3. 当CPU退出SVC后,它立即进入PendSV,从而执行上下文切换;
  4. 当PendSV执行完毕后,将返回到任务B,同时进入线程模式;
  5. 发生了一个中断,并且中断服务程序已开始执行;
  6. 在ISR执行过程中,发生SysTick异常,并且抢占了该ISR;
  7. OS执行必要的操作,然后悬起PendSV异常以作好上下文切换的准备;
  8. 当SysTick退出后,回到先前被抢占的ISR中,ISR继续执行;
  9. ISR执行完毕并退出后,PendSV服务程序开始执行,并且在里面执行上下文切换;
  10. 当PendSV执行完毕后,回到任务A,同时系统再次进入线程模式。

来源

在ucos中,在每次退出最后一个嵌套层或者主动执行OSSched()时,会最终触发OSIntCtxSw/OS_TASK_SW,两者都将触发PendSv。PendSV_Handler处理cpu寄存器的代码如下,主要是保存了cpu寄存器中与当前的任务相关的数据并切换到新任务的环境的过程,分析过程涉及汇编(ARM),以下假设任务A,B。

  1. 初始化触发PendSV
    从头开始分析,在第一次进入PendSV_Handler前,系统是通过OSStartHighRdy进行最后的环境设置后触发了PendSV,其中包括初始化PSP寄存器为0的操作。在这以后的任务切换(触发PendSv异常)都是任务和中断的自发行为。

    OSStartHighRdy
        LDR     R0, =NVIC_SYSPRI4                                  ; Set the PendSV exception priority
        LDR     R1, =NVIC_PENDSV_PRI
        STRB    R1, [R0]
    
        MOVS    R0, #0                                              ; Set the PSP to 0 for initial context switch call,初始化PSP寄存器为0
        MSR     PSP, R0
    
        LDR     R0, =OS_CPU_ExceptStkBase                           ; Initialize the MSP to the OS_CPU_ExceptStkBase
        LDR     R1, [R0]
        MSR     MSP, R1    
    
        LDR     R0, =NVIC_INT_CTRL                                  ; Trigger the PendSV exception (causes context switch)
        LDR     R1, =NVIC_PENDSVSET                                 ;触发一次PenSv,这也是最初的Pensv
        STR     R1, [R0]
    
        CPSIE   I                                                   ; Enable interrupts at processor level
    
    OSStartHang
        B       OSStartHang                                         ; Should never get here
    
  2. 初入PendSv
    PendSV_Handler的完整过程如下,需要提醒:汇编在没有使用跳出当前汇编Bx Lr或者End指令等其他跳出或结束指令时会继续不停地运行后面的代码。,另外PSP,MSP都作为SP的物理寄存器,当系统进入不同的状态时会进行切换。

    PendSV_Handler
        CPSID   I                                                   ; Prevent interruption during context switch
        MRS     R0, PSP                                             ; PSP is process stack pointer,将PSP的值保存在R0中,然后判断R0是否为0来查看
        CBZ     R0, PendSVHandler_nosave                            ; Skip register save the first time,判断psp是不是0,零则跳转后面的代码(第一次任务切换时psp为0)
    
        SUBS    R0, R0, #0x20                                       ; Save remaining regs r4-11 on process stack
        STM     R0, {R4-R11}
    
        LDR     R1, =OSTCBCurPtr                                    ; OSTCBCurPtr->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
    PendSVHandler_nosave
        PUSH    {R14}                                               ; Save LR exc_return value,暂存R14的值,后面对OSTaskSwHook()的操作会影响到R14寄存器
        LDR     R0, =OSTaskSwHook                                   ; OSTaskSwHook();将OSTaskSwHook函数地址保存到R0
        BLX     R0													;
        POP     {R14}												;恢复R14的值
    
        LDR     R0, =OSPrioCur                                      ; OSPrioCur   = OSPrioHighRdy;
        LDR     R1, =OSPrioHighRdy                                  
        LDRB    R2, [R1]                                            
        STRB    R2, [R0]                                            ; 
                                                                    
        LDR     R0, =OSTCBCurPtr                                    ; OSTCBCurPtr = OSTCBHighRdyPtr;R0=&OSTCBCurPtr
        LDR     R1, =OSTCBHighRdyPtr                                ;R1=&OSTCBHighRdyPtr;
        LDR     R2, [R1]                                            ;R2=*R1=OSTCBHighRdyPtr;
        STR     R2, [R0]											;此时R2的值还是OSTCBHighRdyPtr的值,OSTCBHighRdyPtr是指向新tcb的指针,即此时R2存着指向新TCB的值,TCB结构的第一个对象CPU_STK *StkPtr,就是堆栈指针,那么R2=OSTCBHighRdyPtr=&StkPtr;   
    
        LDR     R0, [R2]                                            ; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr;R0=*R2=*&StkPtr=StkPtr,StkPtr为指向任务堆栈栈顶的指针,即SP
        LDM     R0, {R4-R11}                                        ; Restore r4-11 from new process stack,通过获取的任务堆栈的栈顶取出的值按顺序赋值给R4-R11寄存器,也就实现了非自动保存非自动恢复类寄存器的数据更新
        ADDS    R0, R0, #0x20                                       ;由于R4-R11相应的堆栈数据已经从任务堆栈中写入对应的寄存器,需要将栈顶SP指向存放R0寄存器的位置
        MSR     PSP, R0                                             ; Load PSP with new process SP,将刚获取的栈顶SP设置为新的任务堆栈PSP,这样当跳出中断时,系统会根据PSP恢复R0-R3,R12-R15,xPSR。读者需要明白,这类寄存器的恢复和保存在进入异常和出异常的过程中只要提供正确的堆栈指针就能够实现。
        ORR     LR, LR, #0x04                                       ; Ensure exception return uses process stack
        CPSIE   I
        BX      LR                                                  ; Exception return will restore remaining context
    
        END
    

    第一次进入PendSv,pendsv进入后的第一步就是判断psp是否为0,通过CBZ R0, PendSVHandler_nosave判断,由于OSStartHighRdy的初始化工作,PSP的值为0,此时将跳转到PendSVHandler_nosave继续执行。
    具体汇编和注释在上面的PendSVHandler_nosave中,阅读PendSVHandler_nosave后你会发现,除了进行必要的指针更新(OSPrioCur、OSTCBCurPtr)之外,就是实现恢复寄存器值的功能,通过上面的代码,会发现只恢复了R4-R11的寄存器值,那么其他寄存器呢,这里需要展开一个CM3的知识点

    在发生中断异常前,Cortex-M3都会自动保存一半的处理器上下文,并在从中断返回时恢复相同的上下文。即在进入中断前,系统会使用当前的堆栈指针sp(psp)保存必要的寄存器(PSR,PC,LR(R14),R12,R3,R2,R1,R0),然后在进入中断后,系统默认会改用handler下的MSP堆栈指针。在MSP下,通过获取堆栈里面的堆栈值,间接地修改堆栈环境,更新堆栈指针SP(PSP),最后在退出中断异常时,系统会切换回PSP指针。所以,在PendSVC的ISR中,我们为了保存环境,需要手动保存R4-R11,并更新堆栈指针(PSP)。对于PSP和MSP的内容,参考CM3基础>MSP&PSP;

    回到初入PendSv,当第一次进入PendSv时,由于任务堆栈对应的任务实际上还没有运行,r0-r3的参数,r15的pc,r14的返回寄存器和r12,都以初始化任务堆栈OSTaskStkInit时的值为原始值,同时R4-R11也象征性地保存,会发现OSTaskStkInit中对寄存器初始化的值有部分其实是随便写的😂,当这个寄存器被使用时,实际上其内容会被刷新,OSTaskStkInit里除了赋值了重要寄存器(如R15)相应的函数地址等重要参数的值外,其他的寄存器值只是便于调试设置的(如R12=0x12121212)。

  3. 任务切换
    假设任务A要抢占任务B时,其经过的过程如下。
    外部源触发任务切换,运行PendSV_Handler。
    系统在进入PendSV_Handler之前,已经通过PSP(B)保存能够自动保存的寄存器(R0-R3,R12…),此时PSP(B)刚好指向R0的位置(注意不是等于,只是指向),然后将堆栈指针切换为MSP,同之前一样,判断psp指针是否为0,由于不是初始状态,PSP保存着B任务的栈顶不为0,此时代码继续执行,如下注释内容

    PendSV_Handler
        CPSID   I                                                   ; Prevent interruption during context switch
        MRS     R0, PSP                                             ; 将PSP(B)的值保存在R0中,然后判断R0是否为0来查看,此时R0=PSP(B)
        CBZ     R0, PendSVHandler_nosave                            ; 判断PSP(B)是否为0,由于初始化结束,PSP(B)不为0,固不跳转PendSVHandler_nosave
    
        SUBS    R0, R0, #0x20                                       ;在上面,已知R0=PSP(B),通过R0计算需要储存R4-R11时PSP(B)的值,即自减0x20,然后从该值开始开始,
        STM     R0, {R4-R11}                                        ;将R4-R11的值保存在B的任务堆栈(这部分不理解请对照OSTaskStkInit里面初始化任务堆栈的顺序)。
                                                                    ;每个寄存器占用4个字节,r4-r11共8个寄存器,即4*8=32(dec)个偏移量,转16进制即0x20。这样就把任务B的相关寄存器保存好。还差更新PSP(B)的值。
    
        LDR     R1, =OSTCBCurPtr                                    ; R1=&OSTCBCurPtr
        LDR     R1, [R1]                                            ; R1=*&OSTCBCurPtr=OSTCBCurPtr
        STR     R0, [R1]                                            ; *OSTCBCurPtr=OSTCBStkPtr=R0,OSTCBStkPtr在此时还是指向B任务堆栈栈顶
                                                                    ;解析:当B任务的所有寄存器保存好之后,还需要更新B任务堆栈指针PSP(B),即更新栈顶,更新的PSP(B)保存在哪里呢?实际上就保存在B任务的TCB块里面。下次切换回B任务时直接取就是了。
    PendSVHandler_nosave
        ...                                                                 
    

    以上就完成了对B任务相关寄存器的保存,后面就是切换到A任务中,这和上面初始化进入第一个堆栈的过程是一样的,都是使用PendSVHandler_nosave。

参考
一步步写STM32 OS【三】PendSV与堆栈操作
Cortex-M3 SVC与PendSV
cortex-M3 的SVC、PendSV异常,与操作系统(ucos实时系统)
从零开始写一个操作系统(三) —— 任务切换器
uCOS在任务切换时做了什么以及任务切换汇编代码分析

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值