FreeRTOS --(12)任務管理之任務切換

現在創建任務(xTaskCreate)、啓動調度器(vTaskStartScheduler),任務控制(xTaskDelay),以及Tick 中斷(xPortSysTickHandler),都分析完成了,SysTick,PendSV 中斷已經使能,接下來第一個任務便可以自由的奔跑;等待下一次 SysTick 來臨(1ms 後),調度器工作;

 

1、xPortSysTickHandler

SysTick 觸發後,會調用到它的 ISR 函數 xPortSysTickHandler,這個函數的實現和處理器體系架構相關,定義在 port.c:

void xPortSysTickHandler( void )
{
    /* The SysTick runs at the lowest interrupt priority, so when this interrupt
    executes all interrupts must be unmasked.  There is therefore no need to
    save and then restore the interrupt mask value as its value is already
    known - therefore the slightly faster vPortRaiseBASEPRI() function is used
    in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
    vPortRaiseBASEPRI();
    {
        /* Increment the RTOS tick. */
        if( xTaskIncrementTick() != pdFALSE )
        {
            /* A context switch is required.  Context switching is performed in
            the PendSV interrupt.  Pend the PendSV interrupt. */
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
    }
    vPortClearBASEPRIFromISR();
}

 

由於之前配置的 SysTick 的優先級爲最低,所以此刻便有可能其他中斷介入打斷,所以這裏配置一下 BASEPRI 寄存器,來防止被打斷;在最後調用 vPortClearBASEPRIFromISR() 來恢復;

SysTick Handler 中調用 xTaskIncrementTick 來判斷是否需要進行上下文切換,如果需要進行上下文切換(也就是返回 pdTRUE)的話,那麼通過調用:

portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT

往 NVIC 中手動拉起一個 PendSV 中斷;

先看看 xTaskIncrementTick 的內部邏輯在《FreeRTOS --(11)任務管理之系統節拍》中已經詳細描述,它返回的是一個是否需要調度的標誌,如果返回了 pdTRUE,則代表需要調度,拉起 PendSV;

當然,不僅僅是 SysTick 會引發上下文切換,主動調用 portYIELD 也會拉起 PendSV 使得上下文切換:

/* Scheduler utilities. */
#define portYIELD()                                                             \
{                                                                               \
    /* Set a PendSV to request a context switch. */                             \
    portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;                             \
                                                                                \
    /* Barriers are normally not required but do ensure the code is completely  \
    within the specified behaviour for the architecture. */                     \
    __dsb( portSY_FULL_READ_WRITE );                                            \
    __isb( portSY_FULL_READ_WRITE );                                            \
}
 

2、xPortPendSVHandler

在 PendSV 被拉起來後,如果當前沒有其他中斷正在執行的話,就會走到 xPortPendSVHandler,這個 ISR 在 port.c 中,我們以 Cortex-M3 爲例,是一段彙編代碼:

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

    PRESERVE8

    mrs r0, psp                 /* 獲取進入PendSV的ISR之前的任務的 PSP. */
    isb                         /* 刷指令流水線. */

    ldr r3, =pxCurrentTCB       /* 獲取進入PendSV的ISR之前的任務的 pxCurrentTCB */
    ldr r2, [r3]

    stmdb r0!, {r4-r11}         /* Save the remaining registers. */
    str r0, [r2]                /* Save the new top of stack into the first member of the TCB. */

    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
}

還好,這裏的彙編代碼都是常用的指令,理解起來並不困難,只是有一些細節需要特別注意,那麼接下來就逐行分析:

0、切記,這裏已經是 PendSV 的 ISR,此刻,Cortex-M3 的硬件已經完成了對 xPSR、PC、LR、R12、R0-R3 的入棧

1、首先還是 PRESERVE8 的 8字節對齊操作;

2、既然是 PendSV 的 ISR,那麼所有的通用寄存器都可以被我們使用,首先使用 MRS 指令,獲取到當前的 PSP;(注意,在 ISR 中使用的是 MSP 堆棧指針,此刻獲得的 PSP 是什麼樣的呢?

 ​

3、獲取進入 ISR 之前的 pxCurrentTCB,並保存到 R2 中;

4、手動入棧 R4~R11,在異常發生(PendSV 的時候,硬件已經自動入棧了 xPSR、PC、LR、R12、R0-R3 ,不過此刻的 R4~R11 還未改變,我們通過 stmdb 指令,從 R0 開始,將 R4~R11 手動入棧,在上下文切換之前爲任務保存完整的棧信息)

stmdb r0!, {r4-r11}

這裏是以 R0 的位置開始(任務的 PSP),順序保存 R4~R11,並增加 R0;所以在執行完這條指令後,任務的堆棧信息變爲:

 ​

5、此刻 R0 以及被更新到了 R11 的位置,此刻的 R2 是 pxCurrentTCB,還記得麼,pxCurrentTCB 的第一個指針叫做 pxTopOfStack 保存的是任務的堆棧棧頂的指針變量也就是 PSP,接下來通過 STR 指令,將之前的任務的 pxTopOfStack 更新到現在 R0 的位置,也就是 R11,完成了手動保存並更新了上一個任務的堆棧;

str r0, [r2]

 ​

6、上一個任務已經處理完畢,接下來便是選擇下一個任務了:

stmdb sp!, {r3, r14}

使用 stmdb 將 R3 和 R14(也就是 LR)入棧(注意,此刻的 sp 指的是 MSP,主堆棧指針);爲啥只對這兩個寄存器入棧呢?因爲即將調用 C 函數,此刻的 R3 保存了 pxCurrentTCB 指針的地址,這個值在函數調用後還要用到,而 R14 就是 LR,在函數調用的時候,將被重寫覆蓋;

7、通過配置 CM3 的 BASEPRI 來開啓臨界區:

mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY   
msr basepri, r0

8、通過 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

            /* Add the amount of time the task has been running to the
            accumulated time so far.  The time the task started running was
            stored in ulTaskSwitchedInTime.  Note that there is no overflow
            protection here so count values are only valid until the timer
            overflows.  The guard against negative values is to protect
            against suspect run time stat counter implementations - which
            are provided by the application, not the kernel. */
            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 )
        {
            /* Switch Newlib's _impure_ptr variable to point to the _reent
            structure specific to this task.
            See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
            for additional information. */
            _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
        }
        #endif /* configUSE_NEWLIB_REENTRANT */
    }
}

在調度器沒被掛起的情況下,這個函數中,主要通過調用 taskSELECT_HIGHEST_PRIORITY_TASK 來選取最高優先級的任務,這個函數有兩個實現,主要是看是否有定義 configUSE_PORT_OPTIMISED_TASK_SELECTION 這個宏

#if ( configUSE_PORT_OPTIMISED_TASK_SELECTION == 0 )

    #define taskSELECT_HIGHEST_PRIORITY_TASK()                                                          \
    {                                                                                                   \
    UBaseType_t uxTopPriority = uxTopReadyPriority;                                                     \
                                                                                                        \
        /* Find the highest priority queue that contains ready tasks. */                                \
        while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )                           \
        {                                                                                               \
            configASSERT( uxTopPriority );                                                              \
            --uxTopPriority;                                                                            \
        }                                                                                               \
                                                                                                        \
        /* listGET_OWNER_OF_NEXT_ENTRY indexes through the list, so the tasks of                        \
        the same priority get an equal share of the processor time. */                                  \
        listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );           \
        uxTopReadyPriority = uxTopPriority;                                                             \
    } /* taskSELECT_HIGHEST_PRIORITY_TASK */

    /*-----------------------------------------------------------*/

#else /* configUSE_PORT_OPTIMISED_TASK_SELECTION */

    #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 ] ) );       \
    }

#endif /* configUSE_PORT_OPTIMISED_TASK_SELECTION */

在沒有定義 configUSE_PORT_OPTIMISED_TASK_SELECTION 這個宏的時候,選擇最高優先級任務的方式是通過遍歷的方式來獲取最高優先級的任務;

如果定義了 configUSE_PORT_OPTIMISED_TASK_SELECTION 這個宏的時候,就調用 portGET_HIGHEST_PRIORITY 來獲得最高優先級任務鏈表,他是和處理器架構體系相關的,也就是說,你的硬件如果支持捷徑,那麼就用你的方法獲取最高優先級;在 Cortex-M3 中,是有一個 CLZ 指令,這個指令用來計算一個變量從最高位開始的連續零的個數,比如,uxTopReadyPriority爲 0x09(二進制爲:0000 0000 0000 0000 0000 0000 0000 1001),即bit3和bit0爲1,表示存在優先級爲0和3的就緒任務。則__clz( (uxTopReadyPriority)的值爲28,uxTopPriority =31-28=3,即優先級爲3的任務是就緒態最高優先級任務。下面的代碼跟通用方法一樣,調用宏listGET_OWNER_OF_NEXT_ENTRY獲取最高優先級列表中的下一個列表項,並從該列表項中獲取任務TCB指針賦給變量pxCurrentTCB。

所以在 CM3 的 portmacro.h 中:

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

但是定義 configUSE_PORT_OPTIMISED_TASK_SELECTION 這個需要注意一點,它不能夠支持優先級的個數超過 32 個,因爲 CLZ 最大就處理 32 bit,所以這個表示優先級的 bitmap 最多 32,也就是優先級限定在 32 個以內;

9、獲取到最大優先級的任務後,將其賦值給了 pxCurrentTCB,返回彙編代碼;

10、離開臨界區

    mov r0, #0
    msr basepri, r0

爲何此時能夠離開臨界區呢?因爲前一個任務已經完好的保存,下一個任務也已經選擇出來;此刻被高優先級中斷嵌套,其實不在有影響,只是上下文切換的時間被延時;

11、將 R3 和 R14 出棧,R3 存儲了 pxCurrentTCB 地址,R14 存儲了進入 ISR 那刻的 LR(0xFFFF_FFFD);

12、此刻 R3 指向的 pxCurrentTCB 地址,已經是更新到最高優先級的 TCB 的地址,TCB 的第一個元素是這個任務的棧頂指針 pxTopOfStack,獲取該任務的棧頂指針到 R0:

ldr r1, [r3]
ldr r0, [r1]	

13、既然獲得到了下一個即將被調度的任務的棧頂指針,那麼首先是將它的 R4~R11 出棧(這部分要手動做,因爲硬件只會去做xPSR、PC、LR、R12、R0-R3);

ldmia r0!, {r4-r11}

此刻便可以更新 PSP了,更新 PSP 後,如果返回 ISR 的話,硬件會自動出棧 xPSR、PC、LR、R12、R0-R3;

msr psp, r0
isb

最後調用 bx R14 返回中斷服務程序,完成整個調度:

bx r14

此刻硬件便會將 xPSR、PC、LR、R12、R0-R3 自動出棧,下一個任務跑起來了;

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章