zoukankan      html  css  js  c++  java
  • μC/OS-III---I笔记12---任务管理

    任务管理
    任务切换应该算是UCOS最基本的部分,首先保存当前任务寄存器的内容到当前任务的堆栈:接着弹出即将进行的任务的堆栈内容到寄存器中然后就是按寄存器内容执行,这个过程成为上下文切换。任务堆栈在创建任务之前都应该是定义好了的,堆栈实际上是两个不同的概念堆和栈,这里的堆栈实际上是指栈,栈的存放规则是后进先出,在任务切换是栈是用来保存局部变量,传递参数,保存任务状态也就是任务切换前的寄存器值的。

    对于Cortex—M3内核的堆栈
    从内核权威指南里可以知道,Cortex—M3有两个堆栈指针,一个MSP主堆栈指针,还有一个PSP进程堆栈指针。在逻辑地址上他们都是R13寄存器,同一时刻只有一个可见,就如同STM32里的RXBUFF与TXBUFF寄存器一样。在handle模式(运行异常服务程序或者中断程序时的状态)下只能使用MSP,程序的默认也是使用MSP指针,且中间只要没有人为切换会一直使用MSP,常见的裸板(前后台)跑程序时就一直只用了MSP指针。

    参考M3权威指南可以看到,堆栈指针的切换有两种方法
    1.修改CONTROL寄存器的1位,设置的具体如图所示:

    2.在异常返回时通过修改LR(R14)寄存器的位2也可以切换模式。寄存器R14在调用程序时时是用以保存调用之前的下一条指令的地址用以调用返回,子程序返回只要跳转到LR寄存器保存的地址就可以实现调用返回。执行异常服务程序之前,返回地址也会被压入堆栈,可能是MSP也可能是PSP。异常返回时会自动出栈恢复异常前的状态,这是普通终端服务程序的任务切换。进入异常服务程序后LR摇身一变成为EXC_RETURN寄存器,它的各个为的做一从指导手册可以看出,通过修改其中的位2就可以切换返回时的堆栈指针。

    对于中断的自动压栈参考官方指南就知道压入了什么按什么顺序压栈的,指南中如此解释假设入栈开始时, SP的值为N,则在入栈后,堆栈内部的变化如表表示:

    这是硬件自动完成的,原文中这样解释为什么不压栈R4-R11这几个寄存器组,“为啥袒护R0‐R3以及R12呢, R4‐R11就是下等公民?原来,在ARM上,有一套的C函数调用标准约定(《 C/C++ Procedure Call Standard for the ARM Architecture》,AAPCS, Ref5)。个中原因就在它上面:它使得中断服务例程能用C语言编写,编译器优先使用被入栈的寄存器来保存中间结果(当然,如果程序过大也可能要用到R4‐R11,此时编译器负责生成代码来push它们”。

    再看任务创建函数时似乎明白了一点,OSTaskStkInit()函数中

    *(--p_stk) = (CPU_STK)0x01000000u;                            /* xPSR                                                   */
        *(--p_stk) = (CPU_STK)p_task;                                 /* Entry Point                                            */
        *(--p_stk) = (CPU_STK)OS_TaskReturn;                          /* R14 (LR)                                               */
        *(--p_stk) = (CPU_STK)0x12121212u;                            /* R12                                                    */
        *(--p_stk) = (CPU_STK)0x03030303u;                            /* R3                                                     */
        *(--p_stk) = (CPU_STK)0x02020202u;                            /* R2                                                     */
        *(--p_stk) = (CPU_STK)p_stk_limit;                            /* R1                                                     */
        *(--p_stk) = (CPU_STK)p_arg;                                  /* R0 : argument                                          */
        *(--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                                                     */

    第二行实际是对应PC指针,所以他指向函数开头,OS_TaskReturn就对应LR函数返回的地址,这是UCOS系统为了防止用户没有将任务写称死循环为了怎家系统健壮性而有意引导程序进入死循环的保护代码详细如下:

    void  OS_TaskReturn (void)
    {
        OS_ERR  err;
        OSTaskReturnHook(OSTCBCurPtr);                          /* Call hook to let user decide on what to do             */
    #if OS_CFG_TASK_DEL_EN > 0u
        OSTaskDel((OS_TCB *)0,                                  /* Delete task if it accidentally returns!                */
                  (OS_ERR *)&err);
    #else
        for (;;) {
            OSTimeDly((OS_TICK )OSCfg_TickRate_Hz,
                      (OS_OPT  )OS_OPT_TIME_DLY,
                      (OS_ERR *)&err);
        }
    #endif
    }

    当一个任务被创建后他就置于就绪列表该有的位置等待任务调度时被执行,任务调度是系统的核心,查看任务调度函数好像很简单基本就是一些参数检查等,但最后一句才是导火索OS_TASK_SW();通过查看他的函数内容

    OSCtxSw
        LDR     R0, =NVIC_INT_CTRL                                  ; Trigger the PendSV exception (causes context switch)
        LDR     R1, =NVIC_PENDSVSET
        STR     R1, [R0]
        BX      LR

    实际就是置位了一个PENDSVS中断。然后在中断函数内进行了一些操作。

    结合M3权威指南,这里实际上是软件置位了PendSV_Handler中断,这是M3内核的一个特殊的中断事件它可以被悬起,在有比它中断优先级更高的中断在请求时系统会先执行其他中断,因为对于实时系统中断是一种实时行要求最高的任务,因此在UCOS里PendSV_Handler通常被配置为优先级最低的中断从而保证在PendSV_Handler的任务切换不会在中断请求时进行。继续上面的内容在OSSched()最后悬起了PendSV_Handler中断,在没有其他中断执行的时候这个中断函数将会执行,函数如下:

    PendSV_Handler
        CPSID   I                                            ; 关闭总中断
        MRS     R0, PSP                                      ; 取PSP到R0,此时硬件已近压栈了上图中断压栈的寄存器,且SP指针也指向最后
        CBZ     R0, PendSVHandler_nosave                     ; 如果是第一次任务调度直接跳转到PendSVHandler_nosave                     
    
        ;使用FPU才会有的压栈,暂时不管.
        TST        R14, #0X10
        IT        EQ
        VSTMDBEQ R0!,{S16-S31}
        
        
        SUBS    R0, R0, #0x20                                 ; 将SP指针向下移动8*4个字节,即32 即0X20,然后保存R4-R11到栈
        STM     R0, {R4-R11}
        ;将sp现在的指向位置值给当前任务的OSTCBCurPtr->OSTCBStkPtr元素,用于恢复调用时出栈 
        LDR     R1, =OSTCBCurPtr                              ; OSTCBCurPtr->OSTCBStkPtr = SP;
        LDR     R1, [R1]
        STR     R0, [R1]                                      ; R0 is SP of process being switched out
    
                                                              ; 到这里当前任务现场已近保存了
    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, =OSTCBCurPtr                            ; OSTCBCurPtr = OSTCBHighRdyPtr;
        LDR     R1, =OSTCBHighRdyPtr
        LDR     R2, [R1]
        STR     R2, [R0]
                                                ;到这里已经取出了下一个即将调度的任务的任务控制块                                        
                                     ;sp从新任务块取出,然后弹出之前保存的R4
    -R11到对应的寄存器 LDR R0, [R2] ; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr; LDM R0, {R4-R11} ; Restore r4-11 from new process stack ADDS R0, R0, #0x20         ;将Sp指针人为移动到上次中断自动压栈的地方 ;;使用FPU才会有的压栈,暂时不管. TST R14, #0x10 IT EQ VLDMIAEQ R0!, {S16-S31} MSR PSP, R0 ; Load PSP with new process SP ORR LR, LR, #0x04 ; 保证EXC_RETURN(LR)寄存器的2位值 CPSIE I BX LR ; 中断自动返回,其中也包含了出栈数据到寄存器的过程 END

    其实深入理解,任务调度其实最关键的就是在PendSV_Handler服务函数里进行了一个“偷梁换柱”的过程,在以往的中断返回过程中,sp指针在函数过程中不会改变,因此会返回到中断前的位置继续执行,但在PendSV_Handler里认为的将Sp指针换成了下一个任务的sp指针值,然后CPU会依然保持以往的中断返回过程,但其实返回的地方就是我们要调度去的位置,到此任务调度结束。其中最为精妙的是OSTCBCurPtr数据结构的设计让汇编可以很方便的访问结构体内存放sp值的位置。

    同时UCOS还有个中断级的任务调度,实际了解内核机制的人一看就懂,因为这个宏和普通任务调度是相同的,这是内核的一个“中断咬尾”机制。手册上给的解释是当处理器在响应某异常时,如果又发生其它异常,但它们优先级不够高,则被阻塞——这个我们已经知道。那么在当前的异常执行返回后,系统处理悬起的异常时,倘若还是先POP然后又把POP出来的内容PUSH回去,这不成了砸锅炼铁再铸锅,白白浪费CPU时间,正因此, CM3不会傻乎乎地POP这些寄存器,而是继续使用上一个异常已经PUSH好的成果,消灭了这种铺张浪费。这么一来,看上去好像后一个异常把前一个的尾巴咬掉了,前前后后只执行了一次入栈/出栈操作。

    简单的理解就是,本该PendSV进行的入栈操作,此时是借用了其他比他优先级更高的任务的现成“功劳”,这是一个硬件机制的程序不用单独控制,因此你可以看到两个宏是一样的。同时对应的有一个晚到(的高优先级)异常机制,这个机制是之当一个低优先级的中断压栈完成时且未执行其他服务函数时,此时又有高优先级的中断请求,此时高优先级中断任务会借用地优先级任务的现成成果(已经压栈好的数据)然后直接执行自己的服务函数,从而省去寄存器压栈的过程,这两个机制都是设计出来加快中断服务响应的。接下来还有一些任务相关的代码都是非常简单的。

  • 相关阅读:
    http数据返回值
    刷新 返回
    微信平台上遇到的bug
    iscroll修改
    iscroll
    a标签的herf和click事件
    ios9+xcode7 适配笔记
    xsd、wsdl生成C#类的命令行工具使用方法
    xcode更新,想想也是醉了
    关于#define预处理指令的一个问题
  • 原文地址:https://www.cnblogs.com/w-smile/p/7985670.html
Copyright © 2011-2022 走看看