FreeRTOS任务切换学习

FreeRTOS任务切换学习

所谓任务切换,就是CPU寄存器的切换。假设当由任务A切换到任务B时,主要分为两步:
1:需暂停任务A的执行,并将此时任务A的寄存器保存到任务堆栈,这个过程叫做保存现场;
2:将任务B的各个寄存器值(被存于任务堆栈中)恢复到CPU寄存器中,这个过程叫做恢复现场;
对任务A保存现场,对任务B恢复现场,这个整体的过程称之为:上下文切换。下面要补充几个知识,以便更好理解任务切换。
能够触发PendSV的也就是以下这几种情况:
1:滴答定时器中断
2:执行FreeRTOS提供的相关API函数:portYIELD()

PendSV异常

PendSV(可挂起的系统调用)异常对 OS 操作非常重要,其优先级可以通过编程设置。可以通过将中断控制和状态寄存器 ICSR 的 bit28,也就是 PendSV 的挂起位置 1 来触发 PendSV 中断。与 SVC 异常不同,它是不精确的,因此它的挂起状态可在更高优先级异常处理内设置,且
会在高优先级处理完成后执行。若将 PendSV 设置为最低的异常优先级,可以让 PendSV 异常处理在所有其他中断处理完成后执行,
下面我们直接来边解读程序边理解实现任务切换的过程:

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

/* *INDENT-OFF* */
    PRESERVE8

    mrs r0, psp//读取进程栈指针,保存在寄存器 R0 里面。
    isb

    ldr r3, =pxCurrentTCB /* 得到正在运行指向任务控制块的指针的地址。*/
    ldr r2, [ r3 ]//得到任务控制块的地址

    stmdb r0 !, { r4 - r11 } /* 保存从R4到R11寄存器的值*/
    str r0, [ r2 ] /* 将此时的栈顶指针保存到任务控制块中的首个元素 */

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

    ldr r1, [ r3 ]
    ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */
    ldmia r0 !, { r4 - r11 } /* Pop the registers and the critical nesting count. */
    msr psp, r0
    isb
    bx r14
    nop
/* *INDENT-ON* */
}

xPortPendSVHandler首先我们要明白这个函数PendSV中断。中断中使用的是MSP指针,中断外使用的是PSP指针。具体可以在手册Cortext-M3手册中找到:
在这里插入图片描述
所以自动压栈都是用的PSP指针。并且完成自动压栈后PSP指针指向的位置如下图所示:
在这里插入图片描述
mrs r0, psp所以此时r0寄存器保存的此时指针的位置。
stmdb r0 !, { r4 - r11 }从r0指针指向的位置手动压栈将寄存器R4-R11寄存器的值保存起来。此时r0指针指向的地址如图中所示:

在这里插入图片描述
str r0, [ r2 ] 将此时的栈顶指针保存到任务控制块中的首个元素。以便后面从压栈后的最新指针出开始出栈。
stmdb sp !, { r3, r14 }R14 是连接寄存器(LR)。在一个汇编程序中,你可以把它写作 both LR 和 R14。LR 用于在调用子程序时存储返回地址,R3为任务控制块的地址,为了防止 R3 和 R14 的值被改写,所以这里临时将 R3和 R14 的值先压栈。
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY msr basepri, r0开启临界区,也就是关闭中断。
bl vTaskSwitchContext调用这个函数得到下一个要运行的任务。下面具体来看一下这个函数是如何实现的:

void vTaskSwitchContext( void )
{
    if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
    {
        /* The scheduler is currently suspended - do not allow a context
         * switch. */
        xYieldPending = pdTRUE;
    }
    else
    {
        xYieldPending = pdFALSE;
        traceTASK_SWITCHED_OUT();

        #if ( configGENERATE_RUN_TIME_STATS == 1 )
        {
            #ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
                portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );
            #else
                ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
            #endif
            if( ulTotalRunTime > ulTaskSwitchedInTime )
            {
                pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }

            ulTaskSwitchedInTime = ulTotalRunTime;
        }
        #endif /* configGENERATE_RUN_TIME_STATS */

        /* Check for stack overflow, if configured. */
        taskCHECK_FOR_STACK_OVERFLOW();

        /* Before the currently running task is switched out, save its errno. */
        #if ( configUSE_POSIX_ERRNO == 1 )
        {
            pxCurrentTCB->iTaskErrno = FreeRTOS_errno;
        }
        #endif

        /* Select a new task to run using either the generic C or port
         * optimised asm code. */
        taskSELECT_HIGHEST_PRIORITY_TASK(); /*lint !e9079 void * is used as this macro is used with timers and co-routines too.  Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
        traceTASK_SWITCHED_IN();

        /* After the new task is switched in, update the global errno. */
        #if ( configUSE_POSIX_ERRNO == 1 )
        {
            FreeRTOS_errno = pxCurrentTCB->iTaskErrno;
        }
        #endif

        #if ( ( configUSE_NEWLIB_REENTRANT == 1 ) || ( configUSE_C_RUNTIME_TLS_SUPPORT == 1 ) )
        {
            /* Switch C-Runtime's TLS Block to point to the TLS
             * Block specific to this task. */
            configSET_TLS_BLOCK( pxCurrentTCB->xTLSBlock );
        }
        #endif
    }
}

taskSELECT_HIGHEST_PRIORITY_TASK()这个函数实现找到任务优先级最高的那个,具体实现如下:

 #define taskSELECT_HIGHEST_PRIORITY_TASK()                                                  \
    {                                                                                           \
        UBaseType_t uxTopPriority;                                                              \
                                                                                                \
        /* Find the highest priority list that contains ready tasks. */                         \
        portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );                          \
        configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
        listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );   \
    } /* taskSELECT_HIGHEST_PRIORITY_TASK() */

里面的实现主要又有2个函数,分别是portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority )listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) )

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities )    uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

这个函数获取最高优先级是采用硬件的方法。也就是前导置零指令,这里也需要前面的一个知识点。在这里插入图片描述
就绪表分为多个优先级,就绪表的每个优先级可以容纳多个任务。每个就绪列表都是一个结构体。想要了解这部分可以看之前写的列表和列表项的知识:列表和列表项的知识回顾
在这里插入图片描述
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) )这个函数实现如下所示:

#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )                                           \
    {                                                                                          \
        List_t * const pxConstList = ( pxList );                                               \
        /* 指向List_t类型的常量指针pxConstList,并将其初始化为pxList的值。 */               \
        /* we don't return the marker used at the end of the list.  */                         \
        ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;                           \
        if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
        {                                                                                      \
            ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;                       \
        }                                                                                      \
        ( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;                                         \
    }

当列表中仅有一个任务时候,过程如下图所示:刚开始pxindex指向的是末尾列表项。( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; 这句代码将指针指向列表项1。

 if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
        {                                                                                      \
            ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;                       \
        }  

if判断作用是用来略过末尾列表项的作用。( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; 这句代码作用指向包含此列表项的对象的指针。通常,这个指针指向一个任务控制块(TCB),但也可以指向其他使用列表项的数据结构。这实现了对象和其所属列表项之间的双向链接。
所以此时就得到该任务的任务控制块的地址。
在这里插入图片描述
当该就绪列表有多个任务时候,就要时间片流转了。这部分知识等到学到的时候继续补充。
在这里插入图片描述

mov r0, #0 msr basepri, r0 接下来分析继续执行的汇编代码。这2句汇编代码打开中断。退出临界区。
ldmia sp !, { r3, r14 }恢复寄存器 R3 和 R14 的值。注意,此时 pxCurrentTCB 的值已经改变了,所以读取 R3 所保存的地址处的数据就会发现其值改变了,成为了下一个要运行的任务的任务控制块的地址。
ldr r1, [ r3 ] ldr r0, [ r1 ] 因为R3所保存的是将要运行任务的任务控制块地址。所以r1中得到这个任务控制块,r0在得到栈顶指针。此时:
在这里插入图片描述
栈顶指针指向的位置如上图红色箭头所示:
ldmia r0 !, { r4 - r11 }将栈保存的值加载R4-R11寄存器中。也就是即将运行的任务的现场。
msr psp, r0更新进程栈指针 PSP 的值。此时R0指向的值为:
在这里插入图片描述
然后之后bx r14跳转到要执行的函数。因为R14保存函数返回的地址。执行此行代码以后硬件自动恢复寄存器 R0~R3、R12、LR、PC 和 xPSR 的值,确定异常返回以后应该进入处理器模式还是进程模式,使用主栈指针(MSP)还是进程栈指针(PSP)。很明显这里会进入进程模式,并且使用进程栈指针(PSP),寄存器 PC 值会被恢复为即将运行的任务的任务函数,新的任务开始运行!至此,任务切换成功。

posted @ 2024-04-10 22:11  Bathwind_W  阅读(86)  评论(0编辑  收藏  举报