1. 程式人生 > >FreeRTOS(15)---FreeRTOS 排程器啟動過程分析

FreeRTOS(15)---FreeRTOS 排程器啟動過程分析

FreeRTOS 排程器啟動過程分析

使用FreeRTOS,一個最基本的程式架構如下所示:

int main(void)
{  
    必要的初始化工作;
    建立任務1;
    建立任務2;
    ...
   vTaskStartScheduler();  /*啟動排程器*/
    while(1);   
}

任務建立完成後,靜態變數指標pxCurrentTCB(見《FreeRTOS高階篇2—FreeRTOS任務建立分析》第7節內容)指向優先順序最高的就緒任務。但此時任務並不能執行,因為接下來還有關鍵的一步:啟動FreeRTOS排程器。

排程器是FreeRTOS作業系統的核心,主要負責任務切換,即找出最高優先順序的就緒任務,並使之獲得CPU執行權。排程器並非自動執行的,需要人為啟動它。

API函式vTaskStartScheduler()用於啟動排程器,它會建立一個空閒任務、初始化一些靜態變數,最主要的,它會初始化系統節拍定時器並設定好相應的中斷,然後啟動第一個任務。這篇文章用於分析啟動排程器的過程,和上一篇文章一樣,啟動排程器也涉及到硬體特性(比如系統節拍定時器初始化等),因此本文仍然以Cortex-M3架構為例。

啟動排程器的API函式vTaskStartScheduler()的原始碼精簡後如下所示:

void vTaskStartScheduler( void )
{
BaseType_t xReturn;
StaticTask_t *pxIdleTaskTCBBuffer= NULL;
StackType_t *pxIdleTaskStackBuffer= NULL;
uint16_t usIdleTaskStackSize =tskIDLE_STACK_SIZE;
 
    /*如果使用靜態記憶體分配任務堆疊和任務TCB,則需要為空閒任務預先定義好任務記憶體和任務TCB空間*/
    #if(configSUPPORT_STATIC_ALLOCATION == 1 )
    {
       vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &usIdleTaskStackSize);
    }
    #endif /*configSUPPORT_STATIC_ALLOCATION */
 
    /* 建立空閒任務,使用最低優先順序*/
    xReturn =xTaskGenericCreate( prvIdleTask, "IDLE",usIdleTaskStackSize, ( void * ) NULL, ( tskIDLE_PRIORITY | portPRIVILEGE_BIT), &xIdleTaskHandle,pxIdleTaskStackBuffer,pxIdleTaskTCBBuffer, NULL );
 
    if( xReturn == pdPASS )
    {
        /* 先關閉中斷,確保節拍定時器中斷不會在呼叫xPortStartScheduler()時或之前發生.當第一個任務啟動時,會重新啟動中斷*/
       portDISABLE_INTERRUPTS();
       
        /* 初始化靜態變數 */
       xNextTaskUnblockTime = portMAX_DELAY;
       xSchedulerRunning = pdTRUE;
        xTickCount = ( TickType_t ) 0U;
 
        /* 如果巨集configGENERATE_RUN_TIME_STATS被定義,表示使用執行時間統計功能,則下面這個巨集必須被定義,用於初始化一個基礎定時器/計數器.*/
       portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
 
        /* 設定系統節拍定時器,這與硬體特性相關,因此被放在了移植層.*/
        if(xPortStartScheduler() != pdFALSE )
        {
            /* 如果排程器正確執行,則不會執行到這裡,函式也不會返回*/
        }
        else
        {
            /* 僅當任務呼叫API函式xTaskEndScheduler()後,會執行到這裡.*/
        }
    }
    else
    {
        /* 執行到這裡表示核心沒有啟動,可能因為堆疊空間不夠 */
       configASSERT( xReturn );
    }
 
    /* 預防編譯器警告*/
    ( void ) xIdleTaskHandle;
}

這個API函式首先建立一個空閒任務,空閒任務使用最低優先順序(0級),空閒任務的任務控制代碼存放在靜態變數xIdleTaskHandle中,可以呼叫API函式xTaskGetIdleTaskHandle()獲得空閒任務控制代碼。

如果任務建立成功,則關閉中斷(排程器啟動結束時會再次使能中斷的),初始化一些靜態變數,然後呼叫函式xPortStartScheduler()來啟動系統節拍定時器並啟動第一個任務。因為設定系統節拍定時器涉及到硬體特性,因此函式xPortStartScheduler()由移植層提供,不同的硬體架構,這個函式的程式碼也不相同。

對於Cortex-M3架構,函式xPortStartScheduler()的實現如下所示:

BaseType_t xPortStartScheduler( void )
{
    #if(configASSERT_DEFINED == 1 )
    {
        volatile uint32_tulOriginalPriority;
        /* 中斷優先順序暫存器0:IPR0 */
        volatile uint8_t * constpucFirstUserPriorityRegister = ( uint8_t * ) (portNVIC_IP_REGISTERS_OFFSET_16 +portFIRST_USER_INTERRUPT_NUMBER );
        volatile uint8_tucMaxPriorityValue;
 
        /* 這一大段程式碼用來確定一個最高ISR優先順序,在這個ISR或者更低優先順序的ISR中可以安全的呼叫以FromISR結尾的API函式.*/
       
        /* 儲存中斷優先順序值,因為下面要覆寫這個暫存器(IPR0) */
       ulOriginalPriority = *pucFirstUserPriorityRegister;
 
        /* 確定有效的優先順序位個數. 首先向所有位寫1,然後再讀出來,由於無效的優先順序位讀出為0,然後數一數有多少個1,就能知道有多少位優先順序.*/
        *pucFirstUserPriorityRegister= portMAX_8_BIT_VALUE;
       ucMaxPriorityValue = *pucFirstUserPriorityRegister;
 
        /* 冗餘程式碼,用來防止使用者不正確的設定RTOS可遮蔽中斷優先順序值 */
       ucMaxSysCallPriority =configMAX_SYSCALL_INTERRUPT_PRIORITY &ucMaxPriorityValue;
 
        /* 計算最大優先順序組值 */
       ulMaxPRIGROUPValue =portMAX_PRIGROUP_BITS;
        while( (ucMaxPriorityValue &portTOP_BIT_OF_BYTE ) ==portTOP_BIT_OF_BYTE )
        {
           ulMaxPRIGROUPValue--;
           ucMaxPriorityValue <<= ( uint8_t ) 0x01;
        }
       ulMaxPRIGROUPValue <<=portPRIGROUP_SHIFT;
       ulMaxPRIGROUPValue &=portPRIORITY_GROUP_MASK;
 
        /* 將IPR0暫存器的值復原*/
        *pucFirstUserPriorityRegister= ulOriginalPriority;
    }
    #endif /*conifgASSERT_DEFINED */
 
    /* 將PendSV和SysTick中斷設定為最低優先順序*/
   portNVIC_SYSPRI2_REG |=portNVIC_PENDSV_PRI;
   portNVIC_SYSPRI2_REG |=portNVIC_SYSTICK_PRI;
 
    /* 啟動系統節拍定時器,即SysTick定時器,初始化中斷週期並使能定時器*/
   vPortSetupTimerInterrupt();
 
    /* 初始化臨界區巢狀計數器 */
   uxCriticalNesting = 0;
 
    /* 啟動第一個任務 */
   prvStartFirstTask();
 
    /* 永遠不會到這裡! */
    return 0;
}

從原始碼中可以看到,開始的一大段都是冗餘程式碼。因為Cortex-M3的中斷優先順序有些違反直覺:Cortex-M3中斷優先順序數值越大,表示優先順序越低。而FreeRTOS的任務優先順序則與之相反:優先順序數值越大的任務,優先順序越高。根據官方統計,在Cortex-M3硬體上使用FreeRTOS,絕大多數的問題都出在優先順序設定不正確上。因此,為了使FreeRTOS更健壯,FreeRTOS的作者在編寫Cortex-M3架構移植層程式碼時,特意增加了冗餘程式碼。關於詳細的Cortex-M3架構中斷優先順序設定,參考《FreeRTOS系列第7篇—Cortex-M核心使用FreeRTOS特別注意事項》一文。

在Cortex-M3架構中,FreeRTOS為了任務啟動和任務切換使用了三個異常:SVC、PendSV和SysTick。SVC(系統服務呼叫)用於任務啟動,有些作業系統不允許應用程式直接訪問硬體,而是通過提供一些系統服務函式,通過SVC來呼叫;PendSV(可掛起系統呼叫)用於完成任務切換,它的最大特性是如果當前有優先順序比它高的中斷在執行,PendSV會推遲執行,直到高優先順序中斷執行完畢;SysTick用於產生系統節拍時鐘,提供一個時間片,如果多個任務共享同一個優先順序,則每次SysTick中斷,下一個任務將獲得一個時間片。關於詳細的SVC、PendSV異常描述,推薦《Cortex-M3權威指南》一書的“異常”部分。

這裡將PendSV和SysTick異常優先順序設定為最低,這樣任務切換不會打斷某個中斷服務程式,中斷服務程式也不會被延遲,這樣簡化了設計,有利於系統穩定。

接下來呼叫函式vPortSetupTimerInterrupt()設定SysTick定時器中斷週期並使能定時器執行這個函式比較簡單,就是設定SysTick硬體的相應暫存器。

再接下來有一個關鍵的函式是prvStartFirstTask(),這個函式用來啟動第一個任務。我們先看一下原始碼:

__asm void prvStartFirstTask( void )
{
    PRESERVE8
 
    /* Cortext-M3硬體中,0xE000ED08地址處為VTOR(向量表偏移量)暫存器,儲存向量表起始地址*/
    ldr r0, =0xE000ED08    
    ldr r0, [r0]
    /* 取出向量表中的第一項,向量表第一項儲存主堆疊指標MSP的初始值*/
    ldr r0, [r0]   
 
    /* 將堆疊地址存入主堆疊指標 */
    msr msp, r0
    /* 使能全域性中斷*/
    cpsie i
    cpsie f
    dsb
    isb
    /* 呼叫SVC啟動第一個任務 */
    svc 0
    nop
    nop
}

程式開始的幾行程式碼用來複位主堆疊指標MSP的值,表示從此以後MSP指標被FreeRTOS接管,需要注意的是,Cortex-M3硬體的中斷也使用MSP指標。之後使能中斷,使用匯編指令svc 0觸發SVC中斷,完成啟動第一個任務的工作。我們看一下SVC中斷服務函式:

__asm void vPortSVCHandler( void )
{
    PRESERVE8
 
    ldr r3, =pxCurrentTCB   /* pxCurrentTCB指向處於最高優先順序的就緒任務TCB */
    ldr r1, [r3]            /* 獲取任務TCB地址 */
    ldr r0, [r1]            /* 獲取任務TCB的第一個成員,即當前堆疊棧頂pxTopOfStack */
    ldmia r0!, {r4-r11}     /* 出棧,將暫存器r4~r11出棧 */
    msr psp, r0             /* 最新的棧頂指標賦給執行緒堆疊指標PSP */
    isb
    mov r0, #0
    msr basepri, r0
    orrr14, #0xd           /* 這裡0x0d表示:返回後進入執行緒模式,從程序堆疊中做出棧操作,返回Thumb狀態*/
    bx r14
}

通過上一篇介紹任務建立的文章,我們已經認識了指標pxCurrentTCB。這是定義在tasks.c中的唯一一個全域性變數,指向處於最高優先順序的就緒任務TCB。我們知道FreeRTOS的核心功能是確保處於最高優先順序的就緒任務獲得CPU許可權,因此可以說這個指標指向的任務要麼正在執行中,要麼即將執行(排程器關閉),所以這個變數才被命名為pxCurrentTCB。

根據《FreeRTOS高階篇2—FreeRTOS任務建立分析》第三節我們可以知道,一個任務建立時,會將它的任務堆疊初始化的像是經過一次任務切換一樣,如圖1-1所示。對於Cortex-M3架構,需要依次入棧xPSR、PC、LR、R12、R3R0、R11R4,其中r11~R4需要人為入棧,其它暫存器由硬體自動入棧。暫存器PC被初始化為任務函式指標vTask_A,這樣當某次任務切換後,任務A獲得CPU控制權,任務函式vTask_A被出棧到PC暫存器,之後會執行任務A的程式碼;LR暫存器初始化為函式指標prvTaskExitError,這是由移植層提供的一個出錯處理函式。

任務TCB結構體成員pxTopOfStack表示當前堆疊的棧頂,它指向最後一個入棧的專案,所以在圖中它指向R4,TCB結構體另外一個成員pxStack表示堆疊的起始位置,所以在圖中它指向堆疊的最開始處。 在這裡插入圖片描述

圖1-1:任務建立後任務堆疊分佈情況

所以,SVC中斷服務函式一開始就使用全域性指標pxCurrentTCB獲得第一個要啟動的任務TCB,從而獲得任務的當前堆疊棧頂指標。先將人為入棧的暫存器R4~R11出棧,將最新的堆疊棧頂指標賦值給執行緒堆疊指標PSP,再取消中斷掩蔽。到這裡,只要發生中斷,就都能夠被響應了。

中斷服務函式通過下面兩句彙編返回。Cortex-M3架構中,r14的值決定了從異常返回的模式,這裡r14最後四位按位或上0x0d,表示返回時從程序堆疊中做出棧操作、返回後進入執行緒模式、返回Thumb狀態。

orr r14, #0xd       
bx r14

執行bx r14指令後,硬體自動將暫存器xPSR、PC、LR、R12、R3~R0出棧,這時任務A的任務函式指標vTask_A會出棧到PC指標中,從而開始執行任務A。

至此,任務vTask_A獲得CPU執行權,排程器正式開始工作。