1. 程式人生 > >SMC-RTOS任務切換,棧空間初始化(基於CM3,CM4核心)

SMC-RTOS任務切換,棧空間初始化(基於CM3,CM4核心)

棧空間初始化

CM3核心是小端格式的,棧也是滿減棧,下面是任務TCB初始化的時候任務棧空間的初始化
(這部分內容與CM3核心緊密相連,需要讀者非常熟悉CM3堆疊機制(MSP PSP雙堆疊機制等),異常機制等處理)

/**
 * This function will initialize thread stack
 *
 * @param tentry     [the entry of thread]
 * @param parameter  [the parameter of entry]
 * @param stack_addr [the beginning stack address]
 *
 * @return
stack address */
smc_stack_t *smc_thread_stack_init(void (*entry)(void *parameter), void *parameter, smc_stack_t *stack_addr) { /* Align the stack to 8-bytes 主要是在CM3響應異常自動入棧的時候,強制SP對齊到雙字上*/ /*且是滿減棧,並沒有先自減1,之後每次在棧裡面存入一個數據就會先自減1(滿減棧操作)*/
stack_addr = (smc_stack_t *)SMC_ALIGN_DOWN((smc_stack_t)stack_addr, 8); /*順序是xPSR R15 R14 R12 R3-R0 R11-R4,由於是初始化,在後來OS執行起來之後,xPSR R15 R14 R12 R3-R0 都是自動入棧和出棧的*/ /*任務切換函式只需要根據SP來儲存上文的R11-R4,並根據TCB的第一個元素來恢復下文的R11-R4即可*/ *(--stack_addr) = (smc_stack_t)(1 << 24); /* xPSR */
*(--stack_addr) = (smc_stack_t)entry; /* R15 (PC) (Entry Point) */ *(--stack_addr) = (smc_stack_t)0; /* R14 (LR) */ *(--stack_addr) = (smc_stack_t)0; /* R12 */ *(--stack_addr) = (smc_stack_t)0; /* R3 */ *(--stack_addr) = (smc_stack_t)0; /* R2 */ *(--stack_addr) = (smc_stack_t)0; /* R1 */ *(--stack_addr) = (smc_stack_t)parameter; /* R0 : argument */ /* Remaining registers saved on process stack*/ /*之前的暫存器在每次出棧和入棧的時候會根據PSP還是MSP自動出棧入棧,之後的暫存器在任務切換的時候需要手動地入棧和出棧*/ *(--stack_addr) = (smc_stack_t)0; /* R11 */ *(--stack_addr) = (smc_stack_t)0; /* R10 */ *(--stack_addr) = (smc_stack_t)0; /* R9 */ *(--stack_addr) = (smc_stack_t)0; /* R8 */ *(--stack_addr) = (smc_stack_t)0; /* R7 */ *(--stack_addr) = (smc_stack_t)0; /* R6 */ *(--stack_addr) = (smc_stack_t)0; /* R5 */ *(--stack_addr) = (smc_stack_t)0; /* R4 */ /*這個返回值就是SP的任務棧指標(滿減棧),返回後由任務TCB的第一個元素任務SP來儲存這個值*/ /*在TCB的第一個元素來儲存sp指標還有一個關鍵原因,就是在任務切換的時候,是根據TCB的優先順序來的,在切換的新的優先順序後*/ /*直接從TCB第一個元素地址取值給PC就可以繼續上次任務切換的斷點繼續執行*/ return stack_addr; }

任務切換

在任務切換過程中是用pendsv異常來實現的,為啥要用pendsv來實現任務切換呢?

在RTOS效能指標有一個是中斷延遲時間,比如在IRQ10執行的時候systick來了,將會搶佔IRQ10,如果嘗試在systick中切換任務將會導致IRQ延期執行(而且延期時間較長),這對於中斷函式來說是不能容忍的,還有在systick中任務切換後在中斷活躍狀態下返回執行緒模式將會導致處理器的錯誤。

後來許多OS在一些處理器上的操作是,在systick中檢測如果嵌套了中斷,則這次就不任務切換,在下次systick任務切換,就可以儘快執行玩systick異常,減少中斷延遲。這種方法弊端也極其明顯,任務切換可能會被拖一個tick的時間,如果連續每個tick都是搶佔其他中斷的,就拖得時間更長了,如果中斷源中斷頻率跟tick頻率相近就會共振,長時間任務切換不能完成,顯然問題很嚴重。

pendsv的出現就是來完美解決這個問題的,其實就是延遲任務切換的過程,等到沒有中斷的時候立刻執行pendsv異常(需要設定pendsv優先順序最低)。這樣既可以防止在systick中直接任務切換導致中斷延遲較大問題,又可以防止單純地檢測搶佔中斷就不進行任務切換等待下一次的任務切換延時甚至是sysick任務切換與中斷的共振。

在systick_handler中置位置pendsv

void smc_scheduler(void)
{
    if (smc_scheduler_lock_count == 0U) {
        smc_uint32_t status = smc_cpu_disable_interrupt();

        smc_thread_ready = smc_thread_highest_ready();
        smc_cpu_enable_interrupt(status);

        /* if the destination thread is not the same as current thread */
        if (smc_thread_current != smc_thread_ready) {
        /*當前程序 跟 已就緒的最高優先順序程序不一樣就會任務切換*/
                if (smc_scheduler_hook)
                smc_scheduler_hook();
            /* switch to new thread */
            if (smc_interrupt_nest > 0U)
                        /*中斷和非中斷中方式不一樣,這裡其實在CM3平臺的實現是一樣的*/
                smc_thread_intrrupt_switch();
            else
                smc_thread_switch();
        }
    }
}

/*中斷和非中斷方式都是向pendsv控制暫存器中寫一位,使能觸發pendsv延遲暫緩異常*/
/**
 * This function will make context switch.
 *
 * @note [switch not in interrupt]
 *
 */
void smc_thread_switch(void)
{
    smc_mem_write_32(NVIC_INT_CTRL, NVIC_PENDSVSET);
}

/**
 * This function will make context switch.
 *
 * @note [switch in interrupt]
 *
 */
void smc_thread_intrrupt_switch(void)
{
    smc_mem_write_32(NVIC_INT_CTRL, NVIC_PENDSVSET);
}

在其餘中斷執行完成之後就會執行pendsv異常 PendSV_Handler

/**
 * This function will make contex switch
 */
__asm void PendSV_Handler(void)
{
        /*在OS執行起來之後其實剛剛進入systick(也可能是任務A主動放棄CPU,這時候就是pendsv不是systick)的時候A任務的一些暫存器就已經自動入棧了*/
        /*在之後在此排程到A的時候,這些暫存器還會自動出棧根據PSP的值*/
        /*在OS執行起來之前,第一次任務切換的時候,其實在進入pendsv(不是systick)的時候是一些暫存器在MSP中自動入棧*/
    IMPORT smc_thread_current        /*檔案外匯入符號*/
    IMPORT smc_thread_ready

    CPSID   I                        /* Prevent interruption during context switch*/
    /*任務上下文切換期間關閉中斷*/
    LDR R1, =smc_thread_current      /*這裡在系統沒有執行之前,smc_thread_current是空,執行之後指向某個具體的任務TCB*/
    /*結合後面兩句指令,所以在系統沒有執行的時候任務切換就不需要儲存現場,在執行之後任務切換就需要儲存現場*/
    LDR R1, [R1]
    CBZ R1, PendSV_Handler_Nosave    /* skip save R4-R11 for first run user thread */
/*獲取當前任務的棧頂地址,稍後會利用棧頂地址,結合相關操作將R4-R11入棧,其他暫存器在進入handler模式的時候已經入棧了*/

    STMFD R0!, {R4-R11}              /*將R4-R11入棧*/
    STR R0, [R1]                     /* smc_thread_current->sp = PSP */

PendSV_Handler_Nosave
    LDR R0, =smc_thread_current      /* smc_thread_current = smc_thread_ready */
    LDR R1, =smc_thread_ready
    LDR R2, [R1]
    STR R2, [R0]                     /*這就是所謂的任務切換了,就是將smc_thread_current指向smc_thread_ready*/

    LDR R3, [R2]
    LDMFD R3!, {R4-R11}              
    /*恢復B任務的R4-R11暫存器,其他暫存器會在退出pendsv的時候出棧並得到恢復*/
    STR R3, [R2]
    MSR PSP, R3
    ORR LR, LR, #0x04                
    CPSIE   I                        /* 任務切換完成開中斷Prevent interruption during context switch。*/
    /*可能有人會有疑問了,為啥其他地方進入臨界區必須要用那個臨界區的巨集,因為那些地方是可能被中斷的,就是被巢狀,如果直接使用開關中斷,在最外面一層中斷退出的時候就會導致系統中斷全部開啟*/
    /*所以需要用區域性變數來儲存中斷暫存器的一些狀態,而這個pendsv中為啥就可以直接用開關中斷狀態呢,大概就是不能被中斷吧。() */
    BX LR                          /*這個地方並不是函式返回,這裡LR已經被修改為EXEC_RETURN了,詳見CM3權威指南*/
    /*BX LR之後就會導致處理器的模式切換和特權級的切換,切換回原來進入pendsv之前的處理器模式和特權級狀態。一旦模式切換回去,就會自動出棧一些暫存器R0-R3,R12,R14,R15,xPSR*/
    /*PC已經更新了,PSP也已經更新了,其他暫存器也更新了,直接執行切換到的任務斷點(假設原來執行的任務A,現在已經切換到任務B了*/
    /*可能執行幾個tick之後要切換回A,這時候由於剛剛切換到B的時候已經儲存了進入pendsv前的R4-R11了,其實是進入A進入systick之前的狀態)*/
    NOP
}

進入任務切換隻有兩個介面函式

void smc_rtos_scheduler(void);/*第一次任務切換,其中smc_thread_current = NULL;*/
void smc_scheduler(void);/*系統執行後每次任務切換都必須呼叫這個介面*/