FreeRTOS 第七章 任务详解

本文详细探讨了Cortex-M4微控制器中任务堆栈初始化、寄存器处理和任务切换的底层机制,涉及pxTopOfStack与pxCurrentTCB的关键作用。
摘要由CSDN通过智能技术生成

之前文章分析过任务相关的创建和切换,但是理解的还不够深刻。

以cortexM4为例。

堆栈初始化

StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack,
                                     TaskFunction_t pxCode,
                                     void * pvParameters )
{
    /* Simulate the stack frame as it would be created by a context switch
     * interrupt. */

    /* Offset added to account for the way the MCU uses the stack on entry/exit
     * of interrupts, and to ensure alignment. */
    pxTopOfStack--;

    *pxTopOfStack = portINITIAL_XPSR;                                    /* xPSR */
    pxTopOfStack--;
    *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
    pxTopOfStack--;
    *pxTopOfStack = ( StackType_t ) prvTaskExitError;                    /* LR */

    /* Save code space by skipping register initialisation. */
    pxTopOfStack -= 5;                            /* R12, R3, R2 and R1. */
    *pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */

    /* A save method is being used that requires each task to maintain its
     * own exec return value. */
    pxTopOfStack--;
    *pxTopOfStack = portINITIAL_EXC_RETURN;

    pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */

    return pxTopOfStack;
}

创建完任务后会进行堆栈初始化。

为什么要进行堆栈初始化?

因为任务运行需要依赖硬件寄存器,而且需要知道任务的入口地址,所以这些信息必须存起来。这样当执行任务的时候就知道从哪开始了。

从大部分文章看,都会描述到有的寄存器是硬件自动恢复的,有的需要手动恢复。那么对于任务堆栈的初始化,这时还没有异常,所以都需要手动的将寄存器存放到堆栈里。这个函数其实并不能体现出自动和手动恢复。因为都是手动存放的。

存放后如下:

初始化之后pxTopOfStack已经指向了空闲堆栈。后面task运行用到的栈就从这里开始入栈出栈。以上的内容是不会被破坏的。

这里需要注意R15(PC)存放的就是task函数。这样在寄存器恢复后pc就等于task函数。这样运行的时候就从task函数开始了。这样task不就可以运行起来了吗。同时,R0存放的是task参数。

堆栈初始化返回的是pxTopOfStack,赋给了TCB。

 第一个任务

上面堆栈已经完成了初始化。

prvStartFirstTask是启动第一个task。

__asm void prvStartFirstTask( void )
{
/* *INDENT-OFF* */
    PRESERVE8

    /* Use the NVIC offset register to locate the stack. */
    ldr r0, =0xE000ED08
    ldr r0, [ r0 ]
    ldr r0, [ r0 ]
    /* Set the msp back to the start of the stack. */
    msr msp, r0

    /* Clear the bit that indicates the FPU is in use in case the FPU was used
     * before the scheduler was started - which would otherwise result in the
     * unnecessary leaving of space in the SVC stack for lazy saving of FPU
     * registers. */
    mov r0, #0
    msr control, r0
    /* Globally enable interrupts. */
    cpsie i
    cpsie f
    dsb
    isb
    /* Call SVC to start the first task. */
    svc 0
    nop
    nop
/* *INDENT-ON* */
}

0xE000ED08是SCB_VTOR寄存器的地址,里面存放的是向量表的起始地址,即MSP的地址。向量表的地址通常是从内部flash的起始地址开始存放。将0xE000ED08这个立即数加载到R0,然后将0xE000ED08所指向的地址再给R0。一般都是flash的起始地址。然后从起始地址获取msp内容,赋值给msp。这样就完成主堆栈初始化。

完成之后就触发svc异常,在svc里启动第一个任务。

__asm void vPortSVCHandler( void )
{
/* *INDENT-OFF* */
    PRESERVE8

    /* Get the location of the current TCB. */
    ldr r3, =pxCurrentTCB
    ldr r1, [ r3 ]
    ldr r0, [ r1 ]
    /* Pop the core registers. */
    ldmia r0!, {r4-r11,r14}
    msr psp, r0
    isb
    mov r0, #0
    msr basepri, r0
    bx r14
/* *INDENT-ON* */
}

pxCurrentTCB是一个全局变量,用于指向正在或即将运行的任务。

假设按照上图方式存储。

加载pxCurrentTCB的地址(0xa0000000)到r3。r3的内存就是pxCurrentTCB在内存中的位置。因为指针也是一个变量。

加载pxCurrentTCB(0xb0000000)到r3。这时r3的内容就是pxCurrentTCB指向的内存地址。

加载pxCurrentTCB指向的任务控制块(0xc0000000)到r0,任务控制块的第一个成员就是栈顶指针,所以此时r0等于栈顶指针。

 既然R0等于pxTopOfStack,而pxTopOfStack又指向了空闲堆栈开始处。既然要运行任务,首先就要恢复寄存器的值。

 

以R0位基地址,将栈中向上增长的8个字节的内容加载到CPU寄存器r4~r11,同时R0也会自增。

对应如下代码:

ldmia r0!, {r4-r11,r14}

将新的栈顶指针r0更新到psp,任务执行的时候使用的堆栈指针是psp。

msr psp, r0

这里为什么要将r0给到psp,一直用r0恢复剩余的寄存器不行吗?

上面说过,有些寄存器是可以硬件自动恢复的。所以没必要手动恢复。这里将r0给到psp。就是为了能够硬件恢复剩余寄存器。

像r14寄存器最后4位或上0x0d,使得硬件退出时使用psp返回任务模式。上面psp已经赋值了,这样就可以达到硬件恢复了。这时psp指向了任务栈顶。需要注意的是pxTopOfStack还是指向r4处!

pc已经等于task函数了,所以退出中断后就直接运行task了。

看到此会有如下疑问:

1、task开始运行后,局部变量、函数调用等用到的栈空间是从栈顶指针是空闲块开始的。

任务切换

具体是在xPortPendSVHandler()函数里。

__asm void xPortPendSVHandler( void )
{
    extern uxCriticalNesting;
    extern pxCurrentTCB;
    extern vTaskSwitchContext;

/* *INDENT-OFF* */
    PRESERVE8

    mrs r0, psp
    isb
    /* Get the location of the current TCB. */
    ldr r3, =pxCurrentTCB
    ldr r2, [ r3 ]

    /* Is the task using the FPU context?  If so, push high vfp registers. */
    tst r14, #0x10
    it eq
    vstmdbeq r0!, {s16-s31}

    /* Save the core registers. */
    stmdb r0!, {r4-r11, r14}

    /* Save the new top of stack into the first member of the TCB. */
    str r0, [ r2 ]

    stmdb sp!, {r0, r3}
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
    msr basepri, r0
    dsb
    isb
    bl vTaskSwitchContext
    mov r0, #0
    msr basepri, r0
    ldmia sp!, {r0, r3}

    /* The first item in pxCurrentTCB is the task top of stack. */
    ldr r1, [ r3 ]
    ldr r0, [ r1 ]

    /* Pop the core registers. */
    ldmia r0!, {r4-r11, r14}

    /* Is the task using the FPU context?  If so, pop the high vfp registers
     * too. */
    tst r14, #0x10
    it eq
    vldmiaeq r0!, {s16-s31}

    msr psp, r0
    isb
    #ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
        #if WORKAROUND_PMU_CM001 == 1
            push { r14 }
            pop { pc }
            nop
        #endif
    #endif

    bx r14
/* *INDENT-ON* */
}

 

mrs r0, psp

将psp放到r0寄存器。当进入pensvc时,上一个任务的运行环境即:xPSR、PC、R14、R12、R3、R2 、R1、 R0这些寄存器会自动存储到任务栈中。此时psp已经自动指向了r0。并且psp的内容也存放到r0。

 

    ldr r3, =pxCurrentTCB
    ldr r2, [ r3 ]

加载pxCurrentTCB的地址到r3。 加载r3指向的内容到r2,即r2等于pxCurrentTCB。

/* Save the core registers. */
    stmdb r0!, {r4-r11, r14}

 此时r0已经等于psp了,那么就可以以r0为基准,手动恢复剩余的寄存器。并且r0自动递减。

 

此时r0指向r4后面了。 

/* Save the new top of stack into the first member of the TCB. */
    str r0, [ r2 ]

 将r0的值存储到r2指向的内容,r2等于pxCurrentTCB,具体为将r0的值存储到上一个任务的栈顶指针pxTopOfStack。

自持上文的保存就完成了。 

调用vTaskSwitchContext()查询下一个要运行的任务。具体函数内容就不分析了,我们只要知道这个函数可能会更新pxCurrentTCB.

  /* The first item in pxCurrentTCB is the task top of stack. */
    ldr r1, [ r3 ]
    ldr r0, [ r1 ]

 加载r3指向的内容到r1,r3存放的是pxCurrentTCB的地址。加载r1的内容到r0,即下一个要运行的任务的栈顶指针。

 /* Pop the core registers. */
    ldmia r0!, {r4-r11, r14}

以r0为基地址,手动恢复r4~r11 r14寄存器。

msr psp, r0

更新psp的值,等异常退出时,会以psp为基地址,将任务剩下的内容自动加载到cpu寄存器。

bx r14

异常发生时,r14等于oxfffffffd,表示异常返回后进入任务模式,SP以PSP作为堆栈指针出栈,出栈完毕后PSP指向任务的栈顶。并且这时硬件已经自动恢复剩下的寄存器了。注意PC不是task入口了。而是上一次运行的位置。

 

从第一个任务和任务切换来看。

1、pxTopOfStack只会在r11和r4之间移动。

2、psp只会在r0和xPSR之间移动。

所以会有一个疑问,在task运行过程中,形参和函数调用都会使用到栈,但并不会影响psp和pxTopOfStack。 那么task在运行时的栈指针是从哪恢复的,sp指向了psp,这样不就从栈顶位置开始了吗,不应该是从task运行时使用的占空间位置恢复吗?

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

依然@Fantasy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值