【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 啟動第一個任務后的任務棧情況

該圖片源自野火

附件

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
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM