天天看點

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

前言

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

這些都是與硬體相關,是以會分兩條線走: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 啟動第一個任務後的任務棧情況

(該圖檔源自野火)

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

附件

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
}