RTOS任务切换原理与实现

曾今只是使用过移植好的RTOS进行任务开发,对其实现的底层原理一直一知半解,正好接触到了李述桐老师的课程以及一些网上的资料,让我对实时操作系统的原理有了更深的理解,特此把一些原理和思考记录下来和大家一起分享,同时若有理解上的错误还恳请大家能够及时指正。

RTOS的任务切换简单来说就是分为两步(1、保存当前任务案发现场  2、将当前案发现场布置成下一运行任务的案发现场)。所谓的案发现场其实可以理解为当前所有的内核寄存器的状态。

1、保存当前任务案发现场     

将当前任务内核寄存器的值保存下来,以M3内核为例,在Pendsv中断中处理如下函数,具体的代码实现如下一份是李述桐老师的代码,一份ucosII的内核源码。

MRS     R0, PSP                   // 获取当前任务的堆栈指针
CBZ     R0, PendSVHandler_nosave  // 如果是系统的第一个运行任务,不需要保存
STMDB   R0!, {R4-R11}    //我们需要将除异常自动保存的寄存器之外的其它寄存器自动保存起来{R4, R11}
LDR     R1, =currentTask //保存好后,将最后的堆栈顶位置,保存到currentTask->stack处 
LDR     R1, [R1] //由于stack处在结构体stack处的开始位置处,显然currentTask和stack在内存中的起始
STR     R0, [R1] //将最后的堆栈顶位置,保存到currentTask->stack处   
PendSVHandler_nosave
 
    MRS     R0, PSP                        ; PSP is process stack 
    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]                       ; R0 is SP of process being switched out
PendSV_Handler_Nosave

两份源码其实是一样的,STMDB是每写一个单元前,地址先自减4再写;SUBS R0, R0, #0x20将R0 - 0x20 则R0与PSP之间空出8个单位(每个单位4个字节),再将寄存器的R4-R11存入空出的8个单位。这样我们就将当前的案发现场保存到了我们的栈中,同时还将我们保存R4的位置给保存到了currentTask->stack。可是为什么只保存R4-R11寄存器,M3内核中还和其余的内核寄存器不用保存吗?因为其余的内核寄存会在进入中断前由硬件自动压入栈中保存(这个位置可变因为在程序运行过程中堆栈指针会变,硬件会直接将这些寄存器值压入当前堆栈指针指的位置上去),紧接着中断程序保存R4-R11。所以xPSR, PC, LR, R12以及R3‐R0,R4-R11的绝对位置是不固定的,但是这些内核寄存器之间的相对位置是固定的。

2、将当前案发现场布置成下一运行任务的案发现场

LDR     R0, =currentTask    // 取currentTask的地址到R0
LDR     R1, =nextTask       // 取nextTask的地址到R1 
LDR     R2, [R1] 
STR     R2, [R0]            //这小段代码就将nextTask的栈顶指针赋给currentTask的栈顶指针
LDR     R0, [R2]   // 然后,从currentTask中加载stack,这样好知道从哪个位置取出CPU寄存器恢复运行
LDMIA   R0!, {R4-R11}// 恢复{R4, R11}为什么只恢复了这么点,因为其余在退出PendSV时,硬件自动恢复
MSR     PSP, R0                   // 最后,恢复真正的堆栈指针到PSP  
ORR     LR, LR, #0x04 // 标记下返回标记,指明在退出LR时,切换到PSP堆栈中(PendSV使用的是MSP)
BX      LR                  // 最后返回,此时任务就会从堆栈中取出LR值,恢复到上次运行的位置
    PUSH    {R14}                  ; Save LR exc_return value
    LDR     R0, =OSTaskSwHook      ; OSTaskSwHook();
    BLX     R0 
    POP     {R14}                  //这小段代码就是执行OSTaskSwHook这个C的函数

    LDR     R0, =OSPrioCur         ; OSPrioCur = OSPrioHighRdy;
    LDR     R1, =OSPrioHighRdy
    LDRB    R2, [R1]
    STRB    R2, [R0]               //这小段代码就将OSPrioHighRdy赋给OSPrioCur

    LDR     R0, =OSTCBCur          ; OSTCBCur  = OSTCBHighRdy;
    LDR     R1, =OSTCBHighRdy
    LDR     R2, [R1]
    STR     R2, [R0]               //这小段代码就将OSTCBHighRdy->SP赋给OSPrioCur->SP

    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
    BX      LR                     ; Exception return will restore remaining context

恢复部分的两份源码其实原理也是一样的,都是将先前保存的R4-R11的内核寄存器中的值再赋值到R4-R11中去。并且PSP指针指向新任务的堆栈指针去,好让硬件弹出其余自动保存的内核寄存器的位置是正确的,但是需要注意的是保存案发现场的时候xPSR, PC, LR, R12以及R3‐R0由硬件自动压入适当的堆栈 ,这个顺序要和退出异常时硬件自动弹栈的顺序一致,否则将相应的值恢复到错误的寄存器。所以为了在第一次运行这个任务的时候所有的内核寄存器里面有正确的值,比如一些入口函数地址,函数参数等信息,在堆栈初始化的时候就需要按照这个顺序排布。以下为ucos的源码,李述桐老师的源码和这个差不多就不放了。

OS_STK *OSTaskStkInit (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT16U opt)
{
    OS_STK *stk;
    (void)opt;                                   /* 'opt' is not used, prevent warning                 */
    stk       = ptos;                            /* Load stack pointer                                 */
                                                 /* Registers stacked as if auto-saved on exception    */
    *(stk)    = (INT32U)0x01000000L;             /* xPSR                                               */
    *(--stk)  = (INT32U)task;                    /* Entry Point (PC)                      */
    *(--stk)  = (INT32U)0xFFFFFFFEL;             /* R14 (LR) (init value will cause fault if ever used)*/
    *(--stk)  = (INT32U)0x12121212L;             /* R12                                                */
    *(--stk)  = (INT32U)0x03030303L;             /* R3                                                 */
    *(--stk)  = (INT32U)0x02020202L;             /* R2                                                 */
    *(--stk)  = (INT32U)0x01010101L;             /* R1                                                 */
    *(--stk)  = (INT32U)p_arg;                   /* R0 : argument                                      */
                                                 /* Remaining registers saved on process stack         */
    *(--stk)  = (INT32U)0x11111111L;             /* R11                                                */
    *(--stk)  = (INT32U)0x10101010L;             /* R10                                                */
    *(--stk)  = (INT32U)0x09090909L;             /* R9                                                 */
    *(--stk)  = (INT32U)0x08080808L;             /* R8                                                 */
    *(--stk)  = (INT32U)0x07070707L;             /* R7                                                 */
    *(--stk)  = (INT32U)0x06060606L;             /* R6                                                 */
    *(--stk)  = (INT32U)0x05050505L;             /* R5                                                 */
    *(--stk)  = (INT32U)0x04040404L;             /* R4                                                 */

    return (stk);
}

其中 xPSR=(1<<24),xPSR T 位(第 24 位)置 1,否则第一次执行任务时Fault;PC寄存器的值,放程序的入口地址;R14放任务返回处理函数(系统文件os_task.c中定义);R0 用于传递任务函数的参数(编译器是把任务参数直接放到R0寄存器中去的),因此等于p_arg;其余的值没有什么实际含义只是为了方便调试的时候查看。

3、当系统运行第一个任务时,不需要保存当前任务现场,直接将当前案发现场布置成该任务的初始化状态即OSTaskStkInit后的状态

这也是为什么要在运行第一个任务时要将PSP设置为0然后再触发PendSV异常,通过PSP的值来判断是不是系统运行的第一个任务。

本图摘自李述桐01课堂 

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值