RISC-V架构与FreeRTOS任务栈变化

栈的基本概念

在FreeRTOS中,每个任务有一个全局的tskTCB实例,pxCurrentTCB指针指向的是正在运行的任务实例,有三个和栈相关的变量pxTopOfStack和pxStack,pxEndOfStack。

  • pxTopOfStack指向当前堆栈栈顶,随着进栈出栈,pxTopOfStack指向的位置是会变化的;

  • pxStack指向当前堆栈的起始位置,一经分配后,堆栈起始位置就固定了,不会被改变了。

  • pxEndOfStack 标记着栈结束位置,任务创建完成后就固定了

adress:0+ulStackDepth       ------------------- pxEndOfStack(栈底)
                            |XXXXXXXXXXXXXXXXXX|
adress:0+x                  --------------------pxTopOfStack(栈顶)
                            |                  |
                            |                  |
                            |                  |
adress:0                    ------------------- pxStack

任务栈空间开辟

xTaskCreate 
   1. 分配一个栈空间
    /* Allocate space for the stack used by the task being created. */
    pxStack = pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) );
    pxNewTCB->pxStack = pxStack;
    2. 初始化新任务
    prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
        2.1 更新pxTopOfStack
        pxTopOfStack = &( pxNewTCB->pxStack[ ulStackDepth - ( uint32_t ) 1 ] );
        pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
        2.2 初始化栈
        pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
            初始化栈后,pxNewTCB->pxTopOfStack = pxTopOfStack - Context Size

初始化栈

pxPortInitialiseStack:
#if defined(CONFIG_ARCH_RISCV_FPU) && !defined(CONFIG_SAVE_RISCV_FPU_CONTEXT_IN_TCB)
    addi a0, a0, -(FPU_CONTEXT_SIZE)
    /* set fpu rounding mode field to 0(Round to Nearest, ties to Even) when create new thread,
     * otherwise it\'s possible that the rounding mode filed is invalid and
     * cause the illegal instruction exception when restore the fcsr.
     */
    store_x x0, FPU_CTX_FCSR_OFFSET(a0)
#endif
    addi a0, a0, -portWORD_SIZE
    addi a0, a0, -portWORD_SIZE
    addi a0, a0, -portWORD_SIZE
    addi a0, a0, -portWORD_SIZE
    csrr t0, mstatus                    /* Obtain current mstatus value. */
    andi t0, t0, ~0x8                   /* Ensure interrupts are disabled when the stack is restored within an ISR.  Required when a task is created after the schedulre has been started, otherwise interrupts would be disabled anyway. */
    addi t1, x0, 0x188                  /* Generate the value 0x1880, which are the MPIE and MPP bits to set in mstatus. */
    slli t1, t1, 4
    or t0, t0, t1                       /* Set MPIE and MPP bits in mstatus value. */

#ifdef CONFIG_SAVE_RISCV_FPU_CONTEXT_IN_TCB
    /* set FS filed(in thread stack) to initial state for skip save/restore FPU context operation */
    li t1, SR_FS
    li t2, SR_FS_INITIAL
    not t3, t1
    and t0, t0, t3
    or t0, t0, t2
#endif

    addi a0, a0, -portWORD_SIZE
    store_x t0, 0(a0)                   /* mstatus onto the stack. */
    addi a0, a0, -(22 * portWORD_SIZE)  /* Space for registers x11-x31. */
    store_x a2, 0(a0)                   /* Task parameters (pvParameters parameter) goes into register X10/a0 on the stack. */
    addi a0, a0, -(6 * portWORD_SIZE)   /* Space for registers x5-x9. */
    la   t0, portTASK_RETURN_ADDRESS
    store_x t0, 0(a0)                   /* Return address onto the stack, could be portTASK_RETURN_ADDRESS */
    addi t0, x0, portasmADDITIONAL_CONTEXT_SIZE /* The number of chip specific additional registers. */
chip_specific_stack_frame:              /* First add any chip specific registers to the stack frame being created. */
    beq t0, x0, 1f                      /* No more chip specific registers to save. */
    addi a0, a0, -portWORD_SIZE         /* Make space for chip specific register. */
    store_x x0, 0(a0)                   /* Give the chip specific register an initial value of zero. */
    addi t0, t0, -1                     /* Decrement the count of chip specific registers remaining. */
    j chip_specific_stack_frame         /* Until no more chip specific registers. */
1:
    addi a0, a0, -portWORD_SIZE
    store_x a1, 0(a0)                   /* mret value (pxCode parameter) onto the stack. */ 
    ret
    .endfunc

经过pxPortInitialiseStack初始化后存储布局如下:

第一个任务栈恢复

xPortStartFirstTask:

#if( portasmHAS_SIFIVE_CLINT != 0 )
    /* If there is a clint then interrupts can branch directly to the FreeRTOS
    trap handler.  Otherwise the interrupt controller will need to be configured
    outside of this file. */
    la t0, freertos_risc_v_trap_handler
    csrw mtvec, t0
#endif /* portasmHAS_CLILNT */

    load_x  t0, pxCurrentTCB            /* 加载当前TCB */
    load_x  sp, 0 * portWORD_SIZE( t0 )     /* 获取当前TCB的栈空间*/

    load_x  x1, 0 * portWORD_SIZE( sp )     /* 加载返回地址,即任务入口函数 */
    load_x  x6, 3 * portWORD_SIZE( sp )     /* t1 */
    load_x  x7, 4 * portWORD_SIZE( sp )     /* t2 */
    load_x  x8, 5 * portWORD_SIZE( sp )     /* s0/fp */
    load_x  x9, 6 * portWORD_SIZE( sp )     /* s1 */
    load_x  x10, 7 * portWORD_SIZE( sp )    /* a0 */
    load_x  x11, 8 * portWORD_SIZE( sp )    /* a1 */
    load_x  x12, 9 * portWORD_SIZE( sp )    /* a2 */
    load_x  x13, 10 * portWORD_SIZE( sp )   /* a3 */
    load_x  x14, 11 * portWORD_SIZE( sp )   /* a4 */
    load_x  x15, 12 * portWORD_SIZE( sp )   /* a5 */
    load_x  x16, 13 * portWORD_SIZE( sp )   /* a6 */
    load_x  x17, 14 * portWORD_SIZE( sp )   /* a7 */
    load_x  x18, 15 * portWORD_SIZE( sp )   /* s2 */
    load_x  x19, 16 * portWORD_SIZE( sp )   /* s3 */
    load_x  x20, 17 * portWORD_SIZE( sp )   /* s4 */
    load_x  x21, 18 * portWORD_SIZE( sp )   /* s5 */
    load_x  x22, 19 * portWORD_SIZE( sp )   /* s6 */
    load_x  x23, 20 * portWORD_SIZE( sp )   /* s7 */
    load_x  x24, 21 * portWORD_SIZE( sp )   /* s8 */
    load_x  x25, 22 * portWORD_SIZE( sp )   /* s9 */
    load_x  x26, 23 * portWORD_SIZE( sp )   /* s10 */
    load_x  x27, 24 * portWORD_SIZE( sp )   /* s11 */
    load_x  x28, 25 * portWORD_SIZE( sp )   /* t3 */
    load_x  x29, 26 * portWORD_SIZE( sp )   /* t4 */
    load_x  x30, 27 * portWORD_SIZE( sp )   /* t5 */
    load_x  x31, 28 * portWORD_SIZE( sp )   /* t6 */

    load_x  x5, 29 * portWORD_SIZE( sp )    /* Initial mstatus into x5 (t0) */
    addi x5, x5, 0x08                       /* Set MIE bit so the first task starts with interrupts enabled - required as returns with ret not eret. */
    csrrw  x0, mstatus, x5                  /* Interrupts enabled from here! */
    load_x  x5, 2 * portWORD_SIZE( sp )     /* Initial x5 (t0) value. */

    addi    sp, sp, portCONTEXT_SIZE         /*销毁栈空间,SP已经更新*/
#if 0
    load_x      t0, pxCurrentTCB        /* Load pxCurrentTCB. */
    store_x     sp, 0( t0 )             /* Write sp to first TCB member. */
#endif
    更新栈顶指针,上面两句不执行也行,因为任务执行时,sp的值已经更新了,任务用的是SP,
    而不是pxCurrentTCB->pxTopOfStack。
    ret
    .endfunc
/*-----------------------------------------------------------*/

任务切换栈变化

clic_interrupt_handler:
    1. 任务在运行过程中,被中断打断,先分配一块栈帧,保存现场。
    addi sp, sp, -portCONTEXT_SIZE
    store_x x1,  1  * portWORD_SIZE( sp )
    store_x x5,  2  * portWORD_SIZE( sp )
    store_x x6,  3  * portWORD_SIZE( sp )
    store_x x7,  4  * portWORD_SIZE( sp )
    store_x x8,  5  * portWORD_SIZE( sp )
    store_x x9,  6  * portWORD_SIZE( sp )
    store_x x10, 7  * portWORD_SIZE( sp )
    store_x x11, 8  * portWORD_SIZE( sp )
    store_x x12, 9  * portWORD_SIZE( sp )
    store_x x13, 10 * portWORD_SIZE( sp )
    store_x x14, 11 * portWORD_SIZE( sp )
    store_x x15, 12 * portWORD_SIZE( sp )
    store_x x16, 13 * portWORD_SIZE( sp )
    store_x x17, 14 * portWORD_SIZE( sp )
    store_x x18, 15 * portWORD_SIZE( sp )
    store_x x19, 16 * portWORD_SIZE( sp )
    store_x x20, 17 * portWORD_SIZE( sp )
    store_x x21, 18 * portWORD_SIZE( sp )
    store_x x22, 19 * portWORD_SIZE( sp )
    store_x x23, 20 * portWORD_SIZE( sp )
    store_x x24, 21 * portWORD_SIZE( sp )
    store_x x25, 22 * portWORD_SIZE( sp )
    store_x x26, 23 * portWORD_SIZE( sp )
    store_x x27, 24 * portWORD_SIZE( sp )
    store_x x28, 25 * portWORD_SIZE( sp )
    store_x x29, 26 * portWORD_SIZE( sp )
    store_x x30, 27 * portWORD_SIZE( sp )
    store_x x31, 28 * portWORD_SIZE( sp )
    store_x x3,  31 * portWORD_SIZE( sp )
    store_x x4,  32 * portWORD_SIZE( sp )

    addi a0, sp, portCONTEXT_SIZE
    store_x a0,  30 * portWORD_SIZE( sp )

    csrr a0, mepc
    store_x a0,  0 * portWORD_SIZE( sp )
    csrr t0, mscratch
    store_x t0, 33 * portWORD_SIZE( sp )

    csrr t0, mstatus
    store_x t0, 29 * portWORD_SIZE( sp )

    2. 因为分配了一段栈空间,所以需要将栈顶指针更新到pxCurrentTCB->pxTopOfStack
       到这里,这样任务的上下文就保存好了,可以运行中断函数,或切换到其他任务。
    lw      t0, pxCurrentTCB        /* Load pxCurrentTCB. */
    sw      sp, 0( t0 )             /* Write sp to first TCB member. */

    3. 加载中断的栈空间,执行中断处理函数。
    load_x sp, xISRStackTop
    call enter_interrupt_handler
    csrr a0, mcause
    andi a0, a0, 0x7FF           /* If the CLIC support more than 2048 interrupts, we need to use the mask value saved in register */
    call irq_core_handle_root_ic_irq
    call exit_interrupt_handler

    4. 获取任务的控制块,这里的任务可能是原来的任务,也可能是新的任务
    在中断处理函数中,会进行调度,更新pxCurrentTCB,之后获取pxCurrentTCB->pxTopOfStack
    这样就找到了新任务的栈空间
    lw      t1, pxCurrentTCB            /* Load pxCurrentTCB. */
    lw      sp, 0( t1 )                 /* Read sp from first TCB member. */

    5. 栈空间的第一个保存的开始要运行的地址,加载出来写入到mepc中
    load_x  t0, 0  * portWORD_SIZE( sp )
    csrw mepc, t0

    /* Load mstatus with the interrupt enable bits used by the task. */
    load_x  t0, 29 * portWORD_SIZE( sp )
    csrw mstatus, t0

    load_x  t0, 33 * portWORD_SIZE( sp )
    csrw mscratch, t0

    load_x  x1 , 1  * portWORD_SIZE( sp )
    load_x  x5 , 2  * portWORD_SIZE( sp )       /* t0 */
    load_x  x6 , 3  * portWORD_SIZE( sp )       /* t1 */
    load_x  x7 , 4  * portWORD_SIZE( sp )       /* t2 */
    load_x  x8 , 5  * portWORD_SIZE( sp )       /* s0/fp */
    load_x  x9 , 6  * portWORD_SIZE( sp )       /* s1 */
    load_x  x10, 7  * portWORD_SIZE( sp )   /* a0 */
    load_x  x11, 8  * portWORD_SIZE( sp )   /* a1 */
    load_x  x12, 9  * portWORD_SIZE( sp )   /* a2 */
    load_x  x13, 10 * portWORD_SIZE( sp )   /* a3 */
    load_x  x14, 11 * portWORD_SIZE( sp )   /* a4 */
    load_x  x15, 12 * portWORD_SIZE( sp )   /* a5 */
    load_x  x16, 13 * portWORD_SIZE( sp )   /* a6 */
    load_x  x17, 14 * portWORD_SIZE( sp )   /* a7 */
    load_x  x18, 15 * portWORD_SIZE( sp )   /* s2 */
    load_x  x19, 16 * portWORD_SIZE( sp )   /* s3 */
    load_x  x20, 17 * portWORD_SIZE( sp )   /* s4 */
    load_x  x21, 18 * portWORD_SIZE( sp )   /* s5 */
    load_x  x22, 19 * portWORD_SIZE( sp )   /* s6 */
    load_x  x23, 20 * portWORD_SIZE( sp )   /* s7 */
    load_x  x24, 21 * portWORD_SIZE( sp )   /* s8 */
    load_x  x25, 22 * portWORD_SIZE( sp )   /* s9 */
    load_x  x26, 23 * portWORD_SIZE( sp )   /* s10 */
    load_x  x27, 24 * portWORD_SIZE( sp )   /* s11 */
    load_x  x28, 25 * portWORD_SIZE( sp )   /* t3 */
    load_x  x29, 26 * portWORD_SIZE( sp )   /* t4 */
    load_x  x30, 27 * portWORD_SIZE( sp )   /* t5 */
    load_x  x31, 28 * portWORD_SIZE( sp )   /* t6 */

   6.上下文已经恢复,销毁掉栈空间
    addi sp, sp, portCONTEXT_SIZE

   7. 下面两句,可以不执行,因为mret之后就切到任务中运行了,sp已经更新。
#if 0
    load_x      t0, pxCurrentTCB        /* Load pxCurrentTCB. */
    store_x     sp, 0( t0 )             /* Write sp to first TCB member. */
#endif

   8. 返回到任务中运行,
    mret

pxCurrentTCB->pxTopOfStack的变化

  1. pxPortInitialiseStack初始化栈,分配一段栈空间,设置pxCurrentTCB->pxTopOfStack初值。这段栈空间在任务得到运行时会将pxCurrentTCB->pxTopOfStack赋值为sp,接着恢复上下文运行。任务得到运行有两种场景
  • xPortStartFirstTask:这种任务是优先级比较高,系统初始化完成后会挑选第一个任务进行运行,在该函数中会恢复上下文,接着更新sp的值进而销毁栈空间,但是没有更新pxCurrentTCB->pxTopOfStack的值(该值是否更新不影响,sp已经更新了)。
  • 其他任务让出得到调度:这种场景是得到调度机会,在中断中进行恢复上下文(上面代码的第4点点开始),
  1. 运行过程中被中断打断(可能是中断,也有可能是时间片用完),分配一段栈空间用于保存上下文信息。上面代码第1点就是开辟一段栈空间,进行保存上下文,保存上下文后,因为可能会让出调度,因此需要更新pxCurrentTCB->pxTopOfStack,上述代码的第二点。

为什么只看到第1点中SP开辟了,更新到pxCurrentTCB->pxTopOfStack后,没有见哪里归还,是否有栈泄露?
答:在任务再次得到运行的时候,会进行归还,上面第6点就是,SP进行的增加,释放的栈空间,只是没有写到pxCurrentTCB->pxTopOfStack,而当任务在运行过程中被打断进入中断后,sp是已经释放后的sp,而即使在第1点中开辟空间,也是归还后,再开辟的,所以不存在泄露。

跟踪一个任务运行栈的变化实例

初始化栈

初始化时开辟了一个栈空间,栈帧指向0x6007af78位置。

调度得到运行

恢复上下文