【freertos】005-啟動排程器分析

李柱明發表於2022-03-30

前言

本節主要講解啟動排程器。

這些都是與硬體相關,所以會分兩條線走:posix和cortex m3。

原文:李柱明部落格:https://www.cnblogs.com/lizhuming/p/16076476.html

5.1 排程器的基本概念

5.1.1 排程器

排程器就是使用相關的排程演算法來決定當前需要執行的任務。

排程器特點:

  1. 排程器可以區分就緒態任務和掛起任務。
  2. 排程器可以選擇就緒態中的一個任務,然後啟用它。
  3. 不同排程器之間最大的區別就是如何分配就緒態任務間的完成時間。

嵌入式實時作業系統的核心就是排程器和任務切換:

  • 排程器的核心就是排程演算法。
  • 任務切換是基於硬體核心架構實現。

5.1.2 搶佔式排程

搶佔式排程:

  • 每個任務都被分配了不同的優先順序,搶佔式排程器會獲得就緒列表中優先順序最高的任務,並執行這個任務。
  • 在FreeRTOS系統中除了中斷處理函式、排程器上鎖部分的程式碼和禁止中斷的程式碼是不可搶佔的之外,系統的其他部分都是可以搶佔的。

5.1.3 時間片排程

最常用的的時間片排程演算法就是Round-robin排程演算法,這種排程演算法可以用於搶佔式或者合作式的多工中。

實現Round-robin排程演算法需要給同優先順序的任務分配一個專門的列表,用於記錄當前就緒的任務,併為每個任務分配一個時間片。

當任務就緒連結串列中最高優先順序中存在兩個以上的任務時,當前執行的任務耗盡時間片後,當前連結串列的下一個任務到執行態,把當前任務重新插入到當前優先順序就緒連結串列尾部。

使用時間片排程需要在FreeRTOSConfig.h檔案中使能巨集定義:#defineconfigUSE_TIME_SLICING 1

需要注意的是,freertos時間片不能隨意的設定時間為多少個tick,只能預設一個tick。

5.2 cortex m3架構的三個異常

在Cortex-M3架構中,FreeRTOS為了任務啟動和任務切換使用了三個異常:SVC、PendSV和SysTick。

對應三個異常回撥:

#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
#define vPortSVCHandler SVC_Handler

注意:Cortex-M的優先順序數值越大其優先順序越低。

5.2.1 SVC

SVC(系統服務呼叫,亦簡稱系統呼叫)用於任務啟動。所以只被呼叫一次。

有些作業系統不允許應用程式直接訪問硬體,而是通過提供一些系統服務函式,使用者程式使用SVC發出對系統服務函式的呼叫請求,以這種方法呼叫它們來間接訪問硬體,它就會產生一個SVC異常。

在該異常回撥裡啟動第一個任務。

5.2.2 PendSV

PendSV(可掛起系統呼叫)用於完成任務切換。

該異常可以像普通的中斷一樣被掛起的,它的最大特性是如果當前有優先順序比它高的中斷在執行,PendSV會延遲執行,直到高優先順序中斷執行完畢,這樣子產生的PendSV中斷就不會打斷其他中斷的執行。

在該異常的回撥函式裡執行任務切換。

5.2.3 SysTick

SysTick用於產生系統節拍時鐘。

每次systick異常產生都會檢查是否需要任務排程,如果需要,則出發PendSV異常即可。

5.3 啟動排程器

5.3.1 啟動排程器描述

啟動排程器使用API函式vTaskStartScheduler()

該函式會:

  • 建立一個空閒任務;
  • 建立軟體定時器任務;
  • 初始化一些靜態變數;
  • 會初始化系統節拍定時器並設定好相應的中斷;
  • 啟動第一個任務。

啟動排程器,硬體相關是呼叫xPortStartScheduler()

5.3.2 建立空閒任務

空閒任務時在啟動排程器時建立的,該任務不能阻塞,建立空閒任務是為了不讓系統退出,因為系統一旦啟動就必須佔有任務。

空閒任務主體主要是做一些系統記憶體的清理工作、進入休眠或者低功耗操作等操作。

建立空閒任務,也分兩種方式,取決於是否開啟靜態記憶體分配巨集configSUPPORT_STATIC_ALLOCATION

5.3.2.1 靜態記憶體建立

參考前面任務基礎相關的文章便可知,靜態記憶體建立任務需要使用者提供任務控制塊和任務棧空間。

由於空閒任務是核心API建立的,所以使用者需要通過指定的函式vApplicationGetIdleTaskMemory()提供這些資訊。

實現程式碼如下:

/* 如果開啟了靜態記憶體功能,建立空閒任務就按靜態記憶體建立 */
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
    {
        StaticTask_t * pxIdleTaskTCBBuffer = NULL;
        StackType_t * pxIdleTaskStackBuffer = NULL;
        uint32_t ulIdleTaskStackSize;

        /* 獲取空閒任務的任務控制塊地址、任務棧地址、任務棧大小這三個引數。
        	這個API是有使用者實現 */
        vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
        /* 建立空閒任務,使用最低優先順序*/
        xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
                                             configIDLE_TASK_NAME,
                                             ulIdleTaskStackSize,
                                             ( void * ) NULL,
                                             portPRIVILEGE_BIT,
                                             pxIdleTaskStackBuffer,
                                             pxIdleTaskTCBBuffer );

        if( xIdleTaskHandle != NULL )
        {
            xReturn = pdPASS;
        }
        else
        {
            xReturn = pdFAIL;
        }
    }
#endif /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */

5.3.2.2 動態記憶體建立

動態記憶體建立空閒任務,直接使用xTaskCreate()實現即可。

#if ( configSUPPORT_STATIC_ALLOCATION != 1 )
{
    /* 動態記憶體方式建立空閒任務 */
    xReturn = xTaskCreate( prvIdleTask,
                           configIDLE_TASK_NAME,
                           configMINIMAL_STACK_SIZE,
                           ( void * ) NULL,
                           portPRIVILEGE_BIT,
                           &xIdleTaskHandle );
}
#endif /* configSUPPORT_STATIC_ALLOCATION */

5.3.3 建立軟體定時器任務

軟體定時器元件功能,後面會詳細分析,這裡只做簡單說明

和建立空閒任務一個道理。

前提條件時需要配置configUSE_TIMERS開啟軟體定時器功能。

建立軟體定時器內容整合在xTimerCreateTimerTask()API內部了,其實現和建立空閒任務一樣的。

通過巨集configSUPPORT_STATIC_ALLOCATION區分靜態和動態記憶體建立。

5.3.3.1 初始化軟體定時器元件內容

呼叫prvCheckForValidListAndQueue()API初始化定時連結串列和建立定時器通訊服務佇列。

5.3.3.2 靜態記憶體建立

通過使用者實現的vApplicationGetTimerTaskMemory()API獲取軟體定時器任務控制塊和任務棧資訊。

#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
{
    StaticTask_t * pxTimerTaskTCBBuffer = NULL;
    StackType_t * pxTimerTaskStackBuffer = NULL;
    uint32_t ulTimerTaskStackSize;

    /* 獲取軟體定時器任務的任務控制塊地址、任務棧地址、任務棧大小這三個引數。
        	這個API是有使用者實現 */
    vApplicationGetTimerTaskMemory( &pxTimerTaskTCBBuffer, &pxTimerTaskStackBuffer, &ulTimerTaskStackSize );
    /* 建立軟體定時器任務 */
    xTimerTaskHandle = xTaskCreateStatic( prvTimerTask,
                                          configTIMER_SERVICE_TASK_NAME,
                                          ulTimerTaskStackSize,
                                          NULL,
                                          ( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,
                                          pxTimerTaskStackBuffer,
                                          pxTimerTaskTCBBuffer );

    if( xTimerTaskHandle != NULL )
    {
        xReturn = pdPASS;
    }
}
#endif

5.3.3.3 動態記憶體建立

動態記憶體建立軟體定時器任務,直接使用xTaskCreate()實現即可。

#if ( configSUPPORT_STATIC_ALLOCATION != 1 )
{
    /* 動態記憶體方式建立軟體定時器任務 */
    xReturn = xTaskCreate( prvTimerTask,
                           configTIMER_SERVICE_TASK_NAME,
                           configTIMER_TASK_STACK_DEPTH,
                           NULL,
                           ( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,
                           &xTimerTaskHandle );
}
#endif /* configSUPPORT_STATIC_ALLOCATION */

5.3.4 排程器中的使用者函式

在啟動排程器時,核心執行使用者插入一個函式呼叫,一般用於啟動排程器標識處理。

指定函式:freertos_tasks_c_additions_init()

使能巨集:FREERTOS_TASKS_C_ADDITIONS_INIT

/* freertos_tasks_c_additions_init 函式由使用者定義,用於啟動排程器時呼叫一次 */
#ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
{
    freertos_tasks_c_additions_init();
}
#endif

5.3.5 CPU利用率統計配置

如果使用者配置了portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()巨集函式,在排程器啟動時需要呼叫。

該函式一般是重置定時器起始值,搭配portGET_RUN_TIME_COUNTER_VALUE()巨集函式實現執行時間統計功能。

可以參考李柱明部落格:cpu利用率統計後面可能會有獨立章節描述該功能的實現

在啟動排程器中的程式碼:

/* 如果巨集configGENERATE_RUN_TIME_STATS被定義,表示使用執行時間統計功能,則下面這個巨集必須被定義,用於初始化一個基礎定時器/計數器.*/
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();

5.3.6 posix啟動排程器分析

原始碼分析:

  • 啟動排程:xPortStartScheduler()
  • 利用程式實時定時器實現系統滴答:prvSetupTimerInterrupt()
  • 利用執行緒通訊實現啟動第一個任務:vPortStartFirstTask()
  • 在第一次初始化任務棧時會跑該函式(只跑一次):prvSetupSignalsAndSchedulerPolicy()

5.3.6.1 啟動排程器

在介面層,啟動排程呼叫xPortStartScheduler()

  • 獲取執行緒ID;
  • 配置系統滴答時鐘;
  • 啟動第一個任務;
  • 等待使用者呼叫vPortEndScheduler()關閉排程。
  • 系統排程求關閉後需要刪除和釋放啟動排程器時建立的空閒任務和軟體定時器任務。
  • 恢復主執行緒型號掩碼。
portBASE_TYPE xPortStartScheduler( void )
{
    int iSignal;
    sigset_t xSignals;

    /* 獲取當前執行緒ID */
    hMainThread = pthread_self();

    /* 設定系統計時器以按要求的頻率生成滴答中斷 */
    prvSetupTimerInterrupt();

    /* 開啟第一個任務. */
    vPortStartFirstTask();

    /* 等待使用者呼叫關閉排程器 vPortEndScheduler() 這個API發出的訊號 */
    sigemptyset( &xSignals );
    sigaddset( &xSignals, SIG_RESUME );

    /* 等待關閉排程器的訊號 */
    while ( !xSchedulerEnd )
    {
        sigwait( &xSignals, &iSignal ); 
    }

    /* 刪除Idle任務並釋放其資源 */
#if ( INCLUDE_xTaskGetIdleTaskHandle == 1 )
    vPortCancelThread( xTaskGetIdleTaskHandle() );
#endif

#if ( configUSE_TIMERS == 1 )
    /* 刪除軟體定時器任務並釋放其資源 */
    vPortCancelThread( xTimerGetTimerDaemonTaskHandle() );
#endif /* configUSE_TIMERS */

    /* 恢復原始訊號掩模 */
    (void)pthread_sigmask( SIG_SETMASK, &xSchedulerOriginalSignalMask,  NULL );

    return 0;

5.3.6.2 實現滴答時鐘

利用程式實時定時器實現系統滴答:prvSetupTimerInterrupt()

採用posix標準下的getitimer()setitimer()API去實現。

在程式裡使用ITIMER_REAL計數器實現系統滴答時鐘。

  • posix標準下,每個程式都會維護三個域的定時器,當前使用的ITIMER_REAL是程式實時定時器。
void prvSetupTimerInterrupt( void )
{
    struct itimerval itimer;
    int iRet;

    /* 用當前的定時器資訊初始化結構 */
    iRet = getitimer( ITIMER_REAL, &itimer );
    if ( iRet )
    {
        prvFatalError( "getitimer", errno );
    }

    /* 設定定時器事件之間的時間間隔. */
    itimer.it_interval.tv_sec = 0;
    itimer.it_interval.tv_usec = portTICK_RATE_MICROSECONDS;

    /* 設計初始值 */
    itimer.it_value.tv_sec = 0;
    itimer.it_value.tv_usec = portTICK_RATE_MICROSECONDS;

    /* 重置定時器. */
    iRet = setitimer( ITIMER_REAL, &itimer, NULL );
    if ( iRet )
    {
        prvFatalError( "setitimer", errno );
    }

    /* 獲取納秒值 */
    prvStartTimeNs = prvGetTimeNs();
}

5.3.6.3 啟動第一個任務

利用執行緒通訊實現啟動第一個任務:vPortStartFirstTask()

原理在前面posix模擬器設計說過。

利用執行緒型號實現執行緒的啟停從而實現任務切換。

先獲取執行緒控制程式碼:

void vPortStartFirstTask( void )
{
    /* 獲取當前任務的執行緒控制程式碼 */
    Thread_t *pxFirstThread = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() );
    /* 啟動第一個任務. */
    prvResumeThread( pxFirstThread );
}

發訊號給下一個需要跑的執行緒,讓其啟動,這樣就進入了freertos世界嘞:

static void prvResumeThread( Thread_t *xThreadId )
{
    /* 如果當前執行緒不是接下來要跑的執行緒 */
    if ( pthread_self() != xThreadId->pthread )
    {
        /* 傳送事件啟動新的執行緒 */
        event_signal(xThreadId->ev);
    }
}

void event_signal( struct event * ev )
{
    pthread_mutex_lock( &ev->mutex );
    ev->event_triggered = true; // 解除阻塞的標記
    pthread_cond_signal( &ev->cond ); // 傳送訊號給需要啟動的執行緒,讓其解除阻塞
    pthread_mutex_unlock( &ev->mutex );
}

那還需要停止當前執行緒嘞,完成這些時後,回進入等待結束排程器事件而阻塞:(程式碼在xPortStartScheduler()中)

/* 等待使用者呼叫關閉排程器 vPortEndScheduler() 這個API發出的訊號 */
sigemptyset( &xSignals );
sigaddset( &xSignals, SIG_RESUME );

/* 等待關閉排程器的訊號 */
while ( !xSchedulerEnd )
{
    sigwait( &xSignals, &iSignal ); 
}

5.3.7 cortex m3啟動排程器分析

啟動排程器:xPortStartScheduler()

SVC異常啟動第一個任務:vPortSVCHandler()

5.3.7.1 基本知識

  1. cortex m的雙堆疊指標MSP和PSP的切換。

  2. 硬體出入棧和軟體出入棧。

    1. 硬體出入棧:異常時,硬體會完成部分必要暫存器的出入棧。
    2. 軟體出入棧:由於硬體壓棧資訊對保護上下文不夠,需要軟體出入棧完成其它CPU暫存器的出入棧。

5.3.7.2 cortex m3的啟動排程器的基本內容

  1. 把PendSV和SysTick設定為最低優先順序的中斷。

  2. 啟動滴答定時器。

  3. 啟動第一個任務。通過SVC異常方式。

    1. 重置MSP堆疊指標。

    2. 使能全域性中斷。

    3. 觸發SVC異常。進入SVC異常。

      1. 獲取pxCurrentTCB值,即是當前需要跑的任務控制程式碼。
      2. 通過任務控制程式碼獲取任務控制塊,通過任務控制塊獲取任務棧頂。
      3. 軟體出棧。
      4. 更新棧頂指標到PSP。
      5. 修改R14暫存器,使異常退出時,進入執行緒模式,使用PSP棧指標。
      6. 退出異常。硬體自動使用PSP出棧。

至此,系統已經啟動,進入freertos世界。

5.3.7.3 FromISR中斷保護配置

在freertos中會看到FromISR字尾的API,這些API執行環境不一樣,一般用於中斷回撥中使用,要求不能阻塞,快進快出。

這些API不能在中斷保護外的中斷回撥中使用,取決於巨集configMAX_SYSCALL_INTERRUPT_PRIORITY

所以需要配置進出臨界能遮蔽中斷的優先順序級別,優先順序等於或低於 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中斷能被臨界API遮蔽,可呼叫FromISR字尾的API。

先了解下幾個巨集:(數值越小,中斷優先順序越高)

  • configLIBRARY_LOWEST_INTERRUPT_PRIORITY:定義SysTick與PendSV的中斷優先順序。
  • configKERNEL_INTERRUPT_PRIORITY:配置SysTick與PendSV的中斷優先順序到暫存器。
  • configMAX_SYSCALL_INTERRUPT_PRIORITY:定義freertos系統可控最大中斷優先順序。
  • configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY:用於配置basepri暫存器的,當 basepri 設定為某個值的時候,會讓系統不響應比該優先順序低的中斷,而優先順序比之更高
    的中斷則不受影響。這樣,freertos可以通過控制basepri值來控制部分中斷,實現中斷保護。
#if ( configASSERT_DEFINED == 1 )
{
    volatile uint32_t ulOriginalPriority;
    volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
    volatile uint8_t ucMaxPriorityValue;

    /* 確定可以呼叫ISR安全FreeRTOS API函式的最大優先順序。
        ISR安全函式是以“FromISR”結尾的。
        FreeRTOS維護獨立的執行緒和ISR API函式,以確保進入中斷儘可能快和簡單。
        儲存將要被破壞的中斷優先順序值。 */
    ulOriginalPriority = *pucFirstUserPriorityRegister;

    /* 確定可用的優先順序位數。首先寫所有可能的位。 */
    *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
    /* 把值讀回來看看,因為無效的優先順序位讀出位0,讀出有多少個1就知道有多少位優先順序。 */
    ucMaxPriorityValue = *pucFirstUserPriorityRegister;

    /* 核心中斷優先順序應該設定為最低優先順序。 */
    configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );

    /* 對最大系統呼叫優先順序使用相同的掩碼。 */
    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;
    }

    #ifdef __NVIC_PRIO_BITS
        {
            /* 檢查定義優先順序位數的CMSIS配置,該配置與實際從硬體查詢的優先順序位數相匹配。 */
            configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
        }
    #endif

    #ifdef configPRIO_BITS
        {
            /* 檢查定義優先順序位數的FreeRTOS配置,該配置與從硬體實際查詢的優先順序位數匹配。 */
            configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
        }
    #endif

    /* 將優先順序組的值移回它在AIRCR暫存器中的位置 */
    ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
    ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

    /* 將中斷的中斷優先順序暫存器恢復到原來的值 */
    *pucFirstUserPriorityRegister = ulOriginalPriority;
}
#endif /* configASSERT_DEFINED */

5.3.7.4 配置PendSV和SysTick中斷優先順序

PendSV用於切換任務;

SysTick用於系統節拍。

這兩個都配置為最低優先順序。

這樣任務切換不會打斷某個中斷服務程式,中斷服務程式也不會被延遲,有利於系統穩定。

而且SysTick是硬體定時器,響應可能會延遲,都是系統事件不會有偏差。

 /* 將PendSV和SysTick設定為最低優先順序的中斷 */
portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

5.3.7.5 啟動滴答定時器

呼叫vPortSetupTimerInterrupt()實現。

5.3.7.6 啟動第一個任務

呼叫prvStartFirstTask()實現。

啟動第一個任務:

  • 先使能全域性中斷;
  • 觸發進入SVC異常回撥;
  • 在SVC回撥切入第一個任務。
__asm void prvStartFirstTask( void )
{
    PRESERVE8 /* 當前棧需按照 8 位元組對齊 */
    /* 在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 暫存器的地址,裡面存放的是向量表的起始地址,即 MSP 的地址 */
    ldr r0, =0xE000ED08 /* 將 0xE000ED08 這個立即數載入到暫存器 R0 */
    ldr r0, [ r0 ] /* 將 0xE000ED08 地址中的值,也就是向量表的實際地址載入到 R0 */
    ldr r0, [ r0 ] /* 根據向量表實際儲存地址,取出向量表中的第一項,向量表第一項儲存主堆疊指標 MSP 的初始值 */

    /* 將msp設定回堆疊的開始 */
    msr msp, r0
    /* 使能全域性中斷 */
    cpsie i
    cpsie f
    dsb
    isb
    /* 觸發SVC異常開啟動第一個任務. */
    svc 0
    nop
    nop
/* *INDENT-ON* */
}

SVC回撥:

  • 通過pxCurrentTCB獲取當前需要跑的第一個任務控制塊;
  • 獲取該任務棧頂地址;
  • 從棧頂地址軟體出棧;(下文恢復)
  • 更新棧頂地址到PSP;
  • 雙堆疊指標從MSP轉用PSP;
  • 異常返回,硬體會根據PSP棧出棧,完成下文恢復,進入freertos第一個任務。
__asm void vPortSVCHandler( void )
{
/* *INDENT-OFF* */
    PRESERVE8

    ldr r3, = pxCurrentTCB   /* 載入 pxCurrentTCB 的地址到 r3. */
    ldr r1, [ r3 ] /* 載入 pxCurrentTCB 到 r3. 而任務控制塊的第一個成員就是任務棧頂指標。 */
    ldr r0, [ r1 ]           /* 任務控制塊的第一個成員就是棧頂指標,所以此時 r0 等於棧頂指標 */
    ldmia r0 !, { r4 - r11 } /* 軟體出棧部分,r4-r11暫存器出棧 */
    msr psp, r0 /* 將新的棧頂指標 r0 更新到 psp,任務執行的時候使用的堆疊指標是psp. */
    isb
    mov r0, # 0 /* 將暫存器 r0 清 0 */
    msr basepri, r0 /* 設定 basepri 暫存器的值為 0,即開啟所有中斷。basepri 是一箇中斷遮蔽暫存器,大於等於此暫存器值的中斷都將被遮蔽。Cortex-M的優先順序數值越大其優先順序越低。 */
    orr r14, # 0xd /* 向 r14 暫存器最後 4 位按位或上0x0D。退出異常時使用程式堆疊指標 PSP 完成出棧操作並返回後進入任務模式、返回 Thumb 狀態 */
    bx r14 /* 異常返回,這個時候出棧使用的是 PSP 指標,自動將棧中的剩下內容載入到 CPU 暫存器: xPSR,PC(任務入口地址),R14,R12,R3,R2,R1,R0。PSP 的值也將更新,即指向任務棧的棧頂 */
/* *INDENT-ON* */
}

5.3.7.7 啟動第一個任務後的任務棧情況

該圖片源自野火

附件

vTaskStartScheduler()

void vTaskStartScheduler( void )
{
    BaseType_t xReturn;

    /* 如果開啟了靜態記憶體功能,建立空閒任務就按靜態記憶體建立 */
    #if ( configSUPPORT_STATIC_ALLOCATION == 1 )
        {
            StaticTask_t * pxIdleTaskTCBBuffer = NULL;
            StackType_t * pxIdleTaskStackBuffer = NULL;
            uint32_t ulIdleTaskStackSize;

            /* 獲取空閒任務的任務控制塊地址、任務棧地址、任務棧大小這三個引數。
            	這個API是有使用者實現 */
            vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
            /* 建立空閒任務,使用最低優先順序*/
            xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
                                                 configIDLE_TASK_NAME,
                                                 ulIdleTaskStackSize,
                                                 ( void * ) NULL,
                                                 portPRIVILEGE_BIT,
                                                 pxIdleTaskStackBuffer,
                                                 pxIdleTaskTCBBuffer );

            if( xIdleTaskHandle != NULL )
            {
                xReturn = pdPASS;
            }
            else
            {
                xReturn = pdFAIL;
            }
        }
    #else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */
        {
            /* 動態記憶體方式建立空閒任務 */
            xReturn = xTaskCreate( prvIdleTask,
                                   configIDLE_TASK_NAME,
                                   configMINIMAL_STACK_SIZE,
                                   ( void * ) NULL,
                                   portPRIVILEGE_BIT,
                                   &xIdleTaskHandle );
        }
    #endif /* configSUPPORT_STATIC_ALLOCATION */

    #if ( configUSE_TIMERS == 1 )
        {
            if( xReturn == pdPASS )
            {
                xReturn = xTimerCreateTimerTask();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
    #endif /* configUSE_TIMERS */

    if( xReturn == pdPASS )
    {
        /* freertos_tasks_c_additions_init 函式由使用者定義,用於啟動排程器時呼叫一次 */
        #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
            {
                freertos_tasks_c_additions_init();
            }
        #endif

        /* 先關閉中斷,確保節拍定時器中斷不會在呼叫xPortStartScheduler()時或之前發生。當第一個任務啟動時,會重新啟動中斷*/
        portDISABLE_INTERRUPTS();

        #if ( configUSE_NEWLIB_REENTRANT == 1 )
            {
                /* 略 */
                _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
            }
        #endif /* configUSE_NEWLIB_REENTRANT */

        /* 初始化靜態變數 */
        xNextTaskUnblockTime = portMAX_DELAY;
        xSchedulerRunning = pdTRUE;
        xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;

        /* 如果巨集configGENERATE_RUN_TIME_STATS被定義,表示使用執行時間統計功能,則下面這個巨集必須被定義,用於初始化一個基礎定時器/計數器.*/
        portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();

        traceTASK_SWITCHED_IN();

        /* 設定系統節拍定時器,這與硬體特性相關,因此被放在了移植層.*/
        if( xPortStartScheduler() != pdFALSE )
        {
            /* 如果排程器正確執行,則不會執行到這裡,函式也不會返回*/
        }
        else
        {
            /* 僅當任務呼叫API函式xTaskEndScheduler()後,會執行到這裡.*/
        }
    }
    else
    {
        /* 執行到這裡表示核心沒有啟動,可能因為堆疊空間不夠 */
        configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
    }

    /* 預防編譯器警告*/
    ( void ) xIdleTaskHandle;
    ( void ) uxTopUsedPriority;
}

posix:xPortStartScheduler()

portBASE_TYPE xPortStartScheduler( void )
{
    int iSignal;
    sigset_t xSignals;

    /* 獲取當前執行緒ID */
    hMainThread = pthread_self();

    /* 設定系統計時器以按要求的頻率生成滴答中斷 */
    prvSetupTimerInterrupt();

    /* 開啟第一個任務. */
    vPortStartFirstTask();

    /* 等待使用者呼叫關閉排程器 vPortEndScheduler() 這個API發出的訊號 */
    sigemptyset( &xSignals );
    sigaddset( &xSignals, SIG_RESUME );

    /* 等待關閉排程器的訊號 */
    while ( !xSchedulerEnd )
    {
        sigwait( &xSignals, &iSignal ); 
    }

    /* Cancel the Idle task and free its resources */
#if ( INCLUDE_xTaskGetIdleTaskHandle == 1 )
    vPortCancelThread( xTaskGetIdleTaskHandle() );
#endif

#if ( configUSE_TIMERS == 1 )
    /* Cancel the Timer task and free its resources */
    vPortCancelThread( xTimerGetTimerDaemonTaskHandle() );
#endif /* configUSE_TIMERS */

    /* Restore original signal mask. */
    (void)pthread_sigmask( SIG_SETMASK, &xSchedulerOriginalSignalMask,  NULL );

    return 0;
}

posix:prvSetupTimerInterrupt()

void prvSetupTimerInterrupt( void )
{
    struct itimerval itimer;
    int iRet;

    /* 用當前的定時器資訊初始化結構 */
    iRet = getitimer( ITIMER_REAL, &itimer );
    if ( iRet )
    {
        prvFatalError( "getitimer", errno );
    }

    /* 設定定時器事件之間的時間間隔. */
    itimer.it_interval.tv_sec = 0;
    itimer.it_interval.tv_usec = portTICK_RATE_MICROSECONDS;

    /* 設計初始值 */
    itimer.it_value.tv_sec = 0;
    itimer.it_value.tv_usec = portTICK_RATE_MICROSECONDS;

    /* 重置定時器. */
    iRet = setitimer( ITIMER_REAL, &itimer, NULL );
    if ( iRet )
    {
        prvFatalError( "setitimer", errno );
    }

    /* 獲取納秒值 */
    prvStartTimeNs = prvGetTimeNs();
}

posix:vPortStartFirstTask()

void vPortStartFirstTask( void )
{
    /* 獲取當前任務的執行緒控制程式碼 */
    Thread_t *pxFirstThread = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() );

    /* 啟動第一個任務. */
    prvResumeThread( pxFirstThread );
}

static void prvResumeThread( Thread_t *xThreadId )
{
    /* 如果當前執行緒不是接下來要跑的執行緒 */
    if ( pthread_self() != xThreadId->pthread )
    {
        /* 傳送事件啟動新的執行緒 */
        event_signal(xThreadId->ev);
    }
}

void event_signal( struct event * ev )
{
    pthread_mutex_lock( &ev->mutex );
    ev->event_triggered = true; // 解除阻塞的標記
    pthread_cond_signal( &ev->cond ); // 傳送訊號給需要啟動的執行緒,讓其解除阻塞
    pthread_mutex_unlock( &ev->mutex );
}

cortex m3:xPortStartScheduler()

BaseType_t xPortStartScheduler( void )
{
    #if ( configASSERT_DEFINED == 1 )
        {
            volatile uint32_t ulOriginalPriority;
            volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
            volatile uint8_t ucMaxPriorityValue;

            /* 確定可以呼叫ISR安全FreeRTOS API函式的最大優先順序。
                ISR安全函式是以“FromISR”結尾的。
                FreeRTOS維護獨立的執行緒和ISR API函式,以確保進入中斷儘可能快和簡單。
                儲存將要被破壞的中斷優先順序值。 */
            ulOriginalPriority = *pucFirstUserPriorityRegister;

            /* 確定可用的優先順序位數。首先寫所有可能的位。 */
            *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
            /* 把值讀回來看看,因為無效的優先順序位讀出位0,讀出有多少個1就知道有多少位優先順序。 */
            ucMaxPriorityValue = *pucFirstUserPriorityRegister;

            /* 核心中斷優先順序應該設定為最低優先順序。 */
            configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );

            /* 對最大系統呼叫優先順序使用相同的掩碼。 */
            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;
            }

            #ifdef __NVIC_PRIO_BITS
                {
                    /* 檢查定義優先順序位數的CMSIS配置,該配置與實際從硬體查詢的優先順序位數相匹配。 */
                    configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
                }
            #endif

            #ifdef configPRIO_BITS
                {
                    /* 檢查定義優先順序位數的FreeRTOS配置,該配置與從硬體實際查詢的優先順序位數匹配。 */
                    configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
                }
            #endif

            /* 將優先順序組的值移回它在AIRCR暫存器中的位置 */
            ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
            ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

            /* 將中斷的中斷優先順序暫存器恢復到原來的值 */
            *pucFirstUserPriorityRegister = ulOriginalPriority;
        }
    #endif /* configASSERT_DEFINED */

    /* 將PendSV和SysTick設定為最低優先順序的中斷 */
    portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

    /* 啟動滴答定時器。注意,當前全域性中斷是關閉的,在啟動第一個任務時會開啟。 */
    vPortSetupTimerInterrupt();

    /* 初始化為第一個任務準備的關鍵巢狀計數。 */
    uxCriticalNesting = 0;

    /* 啟動第一個任務。 */
    prvStartFirstTask();

    /* 啟動排程器後時不會跑到這裡的 */
    return 0;
}

cortex m3:prvStartFirstTask()

__asm void prvStartFirstTask( void )
{
/* *INDENT-OFF* */
    PRESERVE8 /* 當前棧需按照 8 位元組對齊 */

    /* 在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 暫存器的地址,裡面存放的是向量表的起始地址,即 MSP 的地址 */
    ldr r0, =0xE000ED08 /* 將 0xE000ED08 這個立即數載入到暫存器 R0 */
    ldr r0, [ r0 ] /* 將 0xE000ED08 地址中的值,也就是向量表的實際地址載入到 R0 */
    ldr r0, [ r0 ] /* 根據向量表實際儲存地址,取出向量表中的第一項,向量表第一項儲存主堆疊指標 MSP 的初始值 */

    /* 將msp設定回堆疊的開始 */
    msr msp, r0
    /* 使能全域性中斷 */
    cpsie i
    cpsie f
    dsb
    isb
    /* 觸發SVC異常開啟動第一個任務. */
    svc 0
    nop
    nop
/* *INDENT-ON* */
}

cortex m3:vPortSVCHandler()

__asm void vPortSVCHandler( void )
{
/* *INDENT-OFF* */
    PRESERVE8

    ldr r3, = pxCurrentTCB   /* 載入 pxCurrentTCB 的地址到 r3. */
    ldr r1, [ r3 ] /* 載入 pxCurrentTCB 到 r3. 而任務控制塊的第一個成員就是任務棧頂指標。 */
    ldr r0, [ r1 ]           /* 任務控制塊的第一個成員就是棧頂指標,所以此時 r0 等於棧頂指標 */
    ldmia r0 !, { r4 - r11 } /* 軟體出棧部分,r4-r11暫存器出棧 */
    msr psp, r0 /* 將新的棧頂指標 r0 更新到 psp,任務執行的時候使用的堆疊指標是psp. */
    isb
    mov r0, # 0 /* 將暫存器 r0 清 0 */
    msr basepri, r0 /* 設定 basepri 暫存器的值為 0,即開啟所有中斷。basepri 是一箇中斷遮蔽暫存器,大於等於此暫存器值的中斷都將被遮蔽。Cortex-M的優先順序數值越大其優先順序越低。 */
    orr r14, # 0xd /* 向 r14 暫存器最後 4 位按位或上0x0D。退出異常時使用程式堆疊指標 PSP 完成出棧操作並返回後進入任務模式、返回 Thumb 狀態 */
    bx r14 /* 異常返回,這個時候出棧使用的是 PSP 指標,自動將棧中的剩下內容載入到 CPU 暫存器: xPSR,PC(任務入口地址),R14,R12,R3,R2,R1,R0。PSP 的值也將更新,即指向任務棧的棧頂 */
/* *INDENT-ON* */
}

cortex m3:xPortPendSVHandler()

__asm void xPortPendSVHandler(void)
{
    extern uxCriticalNesting;
    extern pxCurrentTCB; /* 指向當前啟用的任務 */
    extern vTaskSwitchContext;

    PRESERVE8

    mrs r0, psp     /* PSP內容存入R0 */
    isb /* 指令同步隔離,清流水線 */

    ldr r3, = pxCurrentTCB /* 當前啟用的任務TCB指標存入R2 */
    ldr r2,[r3]

    stmdb r0 !,{r4 - r11} /* 儲存剩餘的暫存器,異常處理程式執行前,硬體自動將xPSR、PC、LR、R12、R0-R3入棧 */
    str r0,[r2] /* 將新的棧頂儲存到任務TCB的第一個成員中 */

    stmdb sp !,{r3, r14} /* 將R3和R14臨時壓入堆疊,因為即將呼叫函式vTaskSwitchContext,呼叫函式時,返回地址自動儲存到R14中,所以一旦呼叫發生,R14的值會被覆蓋,因此需要入棧保護; R3儲存的當前啟用的任務TCB指標(pxCurrentTCB)地址,函式呼叫後會用到,因此也要入棧保護*/
    mov r0,#configMAX_SYSCALL_INTERRUPT_PRIORITY /* 進入臨界區 */
    msr basepri,r0
    dsb /* 資料和指令同步隔離 */
    isb
    bl vTaskSwitchContext /* 呼叫函式,尋找新的任務執行,通過使變數pxCurrentTCB指向新的任務來實現任務切換 */
    mov r0,#0 /* 退出臨界區*/
    msr basepri,r0
    ldmia sp !,
    {r3, r14} /* 恢復R3和R14*/

    ldr r1,[r3] 
    ldr r0, [r1] /* 當前啟用的任務TCB第一項儲存了任務堆疊的棧頂,現在棧頂值存入R0*/
    ldmia r0 !,{r4 - r11} /* 出棧*/
    msr psp,r0
    isb
    bx r14 /* 異常發生時,R14中儲存異常返回標誌,包括返回後進入執行緒模式還是處理器模式、使用PSP堆疊指標還是MSP堆疊指標,當呼叫 bx r14指令後,硬體會知道要從異常返回,然後出棧,這個時候堆疊指標PSP已經指向了新任務堆疊的正確位置,當新任務的執行地址被出棧到PC暫存器後,新的任務也會被執行。*/
    nop
}

相關文章