FreeRTOS --(9)任務管理之啓動調度器

目錄

1、vTaskStartScheduler

2、xPortStartScheduler

3、vPortSetupTimerInterrupt

4、prvStartFirstTask

5、vPortSVCHandler


 

在使用 FreeRTOS 的時候,一般的,先創建若干任務,但此刻任務並沒有被調度起來,僅僅是創建了,如果想要真正的跑起來,那麼還需要調用讓調度器跑起來的函數:

vTaskStartScheduler

典型的用法是:

xTaskCreate(.."task_1"..);
xTaskCreate(.."task_2"..);
xTaskCreate(.."task_3"..);

vTaskStartScheduler();

// Never reach here 
DUMP_ERROR();

現在就來看看 vTaskStartScheduler 具體做了些什麼;

 

1、vTaskStartScheduler

vTaskStartScheduler 的實現在 task.c 中:

void vTaskStartScheduler( void )
{
BaseType_t xReturn;

    /* Add the idle task at the lowest priority. */
    #if( configSUPPORT_STATIC_ALLOCATION == 1 )
    {
        StaticTask_t *pxIdleTaskTCBBuffer = NULL;
        StackType_t *pxIdleTaskStackBuffer = NULL;
        uint32_t ulIdleTaskStackSize;

        /* The Idle task is created using user provided RAM - obtain the
        address of the RAM then create the idle task. */
        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
    {
        /* The Idle task is being created using dynamically allocated RAM. */
        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() should only be called if the user
        definable macro FREERTOS_TASKS_C_ADDITIONS_INIT() is defined, as that is
        the only macro called by the function. */
        #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
        {
            freertos_tasks_c_additions_init();
        }
        #endif

        /* Interrupts are turned off here, to ensure a tick does not occur
        before or during the call to xPortStartScheduler().  The stacks of
        the created tasks contain a status word with interrupts switched on
        so interrupts will automatically get re-enabled when the first task
        starts to run. */
        portDISABLE_INTERRUPTS();

        #if ( configUSE_NEWLIB_REENTRANT == 1 )
        {
            /* Switch Newlib's _impure_ptr variable to point to the _reent
            structure specific to the task that will run first. */
            _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
        }
        #endif /* configUSE_NEWLIB_REENTRANT */

        xNextTaskUnblockTime = portMAX_DELAY;
        xSchedulerRunning = pdTRUE;
        xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;

        /* If configGENERATE_RUN_TIME_STATS is defined then the following
        macro must be defined to configure the timer/counter used to generate
        the run time counter time base.   NOTE:  If configGENERATE_RUN_TIME_STATS
        is set to 0 and the following line fails to build then ensure you do not
        have portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() defined in your
        FreeRTOSConfig.h file. */
        portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();

        traceTASK_SWITCHED_IN();

        /* Setting up the timer tick is hardware specific and thus in the
        portable interface. */
        if( xPortStartScheduler() != pdFALSE )
        {
            /* Should not reach here as if the scheduler is running the
            function will not return. */
        }
        else
        {
            /* Should only reach here if a task calls xTaskEndScheduler(). */
        }
    }
    else
    {
        /* This line will only be reached if the kernel could not be started,
        because there was not enough FreeRTOS heap to create the idle task
        or the timer task. */
        configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
    }

    /* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,
    meaning xIdleTaskHandle is not used anywhere else. */
    ( void ) xIdleTaskHandle;
}

在 vTaskStartScheduler 函數中,首先通過 xTaskCreate 創建了一個 Idle 任務,優先級爲最低 0;(關於 Idle 任務,後面專門來講);

調用 portDISABLE_INTERRUPTS(); 關閉全局中斷,因爲後面要初始化 TICK 中斷;

接着初始化了全局變量:

xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE;
xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;

下一個未阻塞的任務時間爲 0xFFFF_FFFF;調度器啓動的標誌位爲 TRUE,代表調度器已經初始化,Tick 的計數器爲 0;

 

2、xPortStartScheduler

接着調用 xPortStartScheduler 來配置和體系結構相關調度,這裏主要是一些和處理器相關的寄存器(比如 SYSTICK 等);這裏還是以 Cortex-M3 作爲例子,xPortStartScheduler 實現在 port.c 文件:

/* Constants required to check the validity of an interrupt priority. */
#define portFIRST_USER_INTERRUPT_NUMBER     ( 16 )
#define portNVIC_IP_REGISTERS_OFFSET_16     ( 0xE000E3F0 )
#define portAIRCR_REG                       ( * ( ( volatile uint32_t * ) 0xE000ED0C ) )
#define portMAX_8_BIT_VALUE                 ( ( uint8_t ) 0xff )
#define portTOP_BIT_OF_BYTE                 ( ( uint8_t ) 0x80 )
#define portMAX_PRIGROUP_BITS               ( ( uint8_t ) 7 )
#define portPRIORITY_GROUP_MASK             ( 0x07UL << 8UL )
#define portPRIGROUP_SHIFT                  ( 8UL )

BaseType_t xPortStartScheduler( void )
{
    #if(configASSERT_DEFINED == 1 )
    {
        volatile uint32_t ulOriginalPriority;
        /* 中斷優先級寄存器0: PRI_0 */
        volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) (portNVIC_IP_REGISTERS_OFFSET_16 +portFIRST_USER_INTERRUPT_NUMBER );
        volatile uint8_t ucMaxPriorityValue;
 
        /* 這一大段代碼用來確定一個最高ISR優先級,在這個ISR或者更低優先級的ISR中可以安全的調用以FromISR結尾的API函數.*/
       
        /* 保存中斷優先級值,因爲下面要覆寫這個寄存器(PRI_0) */
       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;
 
        /* 將PRI_0寄存器的值復原*/
        *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;
}

首先,如果定義 configASSERT_DEFINED 了的話,那麼先:

volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) (portNVIC_IP_REGISTERS_OFFSET_16 +portFIRST_USER_INTERRUPT_NUMBER );

根據 Cortex-M3 的數據手冊可以知道,這個地方獲取到的是:NVIC 中斷優先級寄存器的基地址,Cortex-M3 的 NVIC 中斷優先級寄存器定義如下:

CM3 中斷優先級寄存器組  (0xE000_E400 ~ 0xE000_E4EF)
Name Access Base Address Reset Value Description
PRI_0 R/W 0xE000_E400 0 (8 bits) 外中斷 #0 的優先級
PRI_1 R/W 0xE000_E401 0 (8 bits) 外中斷 #1 的優先級
..... R/W ..... 0 (8 bits) .....
PRI_239 R/W 0xE000_E4EF 0 (8 bits) 外中斷 #239 的優先級

這裏注意一下,每個優先級 8 bit;

這裏,首先將中斷 0 優先級讀出來,放置到 ulOriginalPriority

在往這個 PRI_0 優先級寄存器中寫全 1,也就是 0xFF;

再將這個寄存器讀出來讀到 ucMaxPriorityValue 中;

這樣做的目的是爲了判斷這個中斷優先級寄存器哪些 bit 是可寫(可配置)的;這個和處理器對這方面的定義相關;

在 Cortex-M3 處理器上,NVIC 中斷控制器支持中斷優選級配置,它分爲了組優先級和組內優先級概念,雖然看起來每個優先級使用 8 bits 來表示,看似最大配置到 0xFF,也就是 255,其實不然,因爲引入了組優先級和組內優先級的概念,讓這個 8 bits 配置得有點玄機;

優先級組也叫搶佔優先級,組內優先級也叫子優先級;搶佔優先級高的中斷可以嵌套搶佔優先級低的,同樣搶佔優先級的中斷,則比較的是子優先級;

Cortex-M3 只是處理器的架構,實際上,芯片公司在利用這種架構實現芯片設計的時候,優先級組和組內優先級,並不是這 8 個 bit 都用到了,因爲大量的優先級會增加 NVIC 的複雜度;所以,一般的,具體芯片,PRI_0 的 8 bit 只會用到一部分的 bit,其中中用幾個 bit 來表示搶佔優先級,用幾個 bit 來表示子優先級,這個是可以配置的;具體一點的話,比如:

一款 Cortex-M3 內核做的處理器,它在設計的時候,就定義了 PRI_x 優先級的 8 bit,只有高 3 bit 有效:

一款 Cortex-M3 內核做的處理器,它在設計的時候,就定義了 PRI_x 優先級的 8 bit,只有高 4 bit 有效:

比如ST的STM32F1xx和F4xx只使用了這個8位中的高4位[7:4],低四位取零,這樣2^4=16,只能表示16級中斷嵌套。

那麼用高 4 bit 表示優先級,那麼這 4 個 bit 哪幾個 bit 代表搶佔優先級?哪幾個 bit 代表子優先級呢?

這個由另一個寄存器的值說了算,SCB->AIRCR[10:8](0xE000_ED00) 寄存器的 PRIGROUP 的值說了算;

#define NVIC_PriorityGroup_0          ((u32)0x700) /* 0 bits for pre-emption priority 4 bits for subpriority */
#define NVIC_PriorityGroup_1          ((u32)0x600) /* 1 bits for pre-emption priority 3 bits for subpriority */
#define NVIC_PriorityGroup_2          ((u32)0x500) /* 2 bits for pre-emption priority 2 bits for subpriority */
#define NVIC_PriorityGroup_3          ((u32)0x400) /* 3 bits for pre-emption priority 1 bits for subpriority */
#define NVIC_PriorityGroup_4          ((u32)0x300) /* 4 bits for pre-emption priority 0 bits for subpriority */
SCB->AIRCR[10:8]
Group AIRCR[10:8] Value PRI_x bit[7:4] 分配情況 分配結果
0 3‘b111 0:4 0位搶佔優先級,4位響應優先級
1 3‘b110 1:3 1位搶佔優先級,3位響應優先級
2 3‘b101 2:2 2位搶佔優先級,2位響應優先級
3 3‘b100 3:1 3位搶佔優先級,1位響應優先級
4 3‘b011 4:0 4位搶佔優先級,0位響應優先

Cortex-M3 中斷優先級數值越大,表示優先級越低。而 FreeRTOS 的任務優先級則與之相反:優先級數值越大的任務,優先級越高。

好了,言歸正傳,這裏應該說清楚爲何要寫進去 0xF,在讀出來,就是因爲這 8bit 並不是全部都用了,這樣便可以得到最大支持的優先級個數 ucMaxPriorityValue

接着計算最大優先級組的值,從讀出來有效的 PRI_0 的最高位開始判斷(因爲)優先級組和子優先級是從最高位開始;

接着配置 PendSV 和 SysTick 的優先級爲最低:

	/* Make PendSV and SysTick the lowest priority interrupts. */
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

 

3、vPortSetupTimerInterrupt

調用 vPortSetupTimerInterrupt 配置 SysTick,這個是操作系統的心跳,和體系架構相關:

void vPortSetupTimerInterrupt( void )
{
    /* Calculate the constants required to configure the tick interrupt. */
    #if( configUSE_TICKLESS_IDLE == 1 )
    {
        ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );
        xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;
        ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
    }
    #endif /* configUSE_TICKLESS_IDLE */

    /* Stop and clear the SysTick. */
    portNVIC_SYSTICK_CTRL_REG = 0UL;
    portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;

    /* Configure SysTick to interrupt at the requested rate. */
    portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
    portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}

STM32 的 SysTick 是一個向下計數的計數器,可以配置產生 Tick 中斷;

configSYSTICK_CLOCK_HZ 定義了 CPU 的時鐘頻率,需要和處理器同步;

configTICK_RATE_HZ 定義了 Tick 來的頻率,比如:

configTICK_RATE_HZ爲100,則系統節拍時鐘週期爲10ms,設置宏configTICK_RATE_HZ爲1000,則系統節拍時鐘週期爲1ms

太頻繁的 Tick 中斷會導致過頻繁的上下文切換,增加系統負擔,過於長的上下文切換,會導致任務響應不及時;

典型的,STM32 的 configTICK_RATE_HZ 爲 1000,也就是 1ms 一次 Tick 中斷;

然後配置寄存器,使能了 SysTick,使能了 SysTick 中斷;

接着初始化了嵌套深度:

uxCriticalNesting = 0;

 

4、prvStartFirstTask

最後調用了 prvStartFirstTask(); 啓動第一個任務,它的實現使用匯編寫的:

__asm void prvStartFirstTask( void )
{
    PRESERVE8
 
    /* Cortext-M3硬件中,0xE000ED08 地址處爲VTOR(向量表偏移量)寄存器,存儲向量表起始地址*/
    /* 將 0xE000ED08 加載到 R0 */
    ldr r0, =0xE000ED08    
    /* 將 0xE000ED08 中的值,也就是向量表的實際地址加載到 R0 */
    ldr r0, [r0]
    /* 根據向量表實際存儲地址,取出向量表中的第一項,向量表第一項存儲主堆棧指針MSP的初始值*/
    ldr r0, [r0]   
 
    /* 將堆棧地址寫入主堆棧指針 */
    msr msp, r0
    /* 使能全局中斷*/
    cpsie i
    cpsie f
    dsb
    isb
    /* 調用SVC啓動第一個任務 */
    svc 0
    nop
    nop
}

PRESERVE8 用於 8 字節對齊;

從 0xE000ED08 獲取向量表的偏移,爲啥要獲得向量表呢?因爲向量表的第一個是 MSP 指針!

取 MSP 的初始值的思路是先根據向量表的位置寄存器 VTOR (0xE000ED08) 來獲取向量表存儲的地址;

在根據向量表存儲的地址,來訪問第一個元素,也就是初始的 MSP;

此刻呢,將初始的 MSP 存入到了 R0 中,通過 MSR 指令,寫到 MSP 中:

打個比方,Cortex-M3 處理器,上電默認進入線程的特權模式,使用 MSP 作爲堆棧指針,從上電跑到這裏,經過一系列的函數調用,出棧,入棧,MSP 自然已經不是最開始的初始化的位置,這裏通過 MSR 重新複製了 MSP,豈不是堆棧都沒了麼?是的,因爲這是一條不歸路,代碼跑到這裏,首先不會返回,之前壓棧的內容再也不會用到,所以破壞之前的堆棧也沒關係;其次既然不會用到,那麼豈不是之前的壓棧空間都廢了,如果把 MSP 重新初始化到頭,就 OK 了嘛,大不了就是破壞了堆棧,反正再也回不去啦;

OK,堆棧指針 MSP 刷完,賦予了新的生命,此刻開中斷,開異常,刷流水線;

調用 svc 並傳入系統調用號爲 0 手動拉 SVC 中斷;

 

5、vPortSVCHandler

手動拉了 SVC 中斷,而且開啓了中斷,那麼就會進入它的  ISR:vPortSVCHandler,它的實現也是和處理器體系結構相關,在 port.c 中實現:

__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
    orr r14, #0xd           /* 這裏0x0d表示:返回後進入線程模式,從進程堆棧中做出棧操作,返回Thumb狀態*/
    bx r14
}

首先還是 PRESERVE8 的 8字節對齊操作;

還記得嗎,pxCurrentTCB 指向的是最高優先級的 Ready 狀態的任務指針;

根據 pxCurrentTCB 獲取到對應 TCB 的地址;然後獲取第一個成員變量,也就是當前棧頂地址 pxTopOfStack;這個值在任務分配的時候,就已經計算好,並且模擬的 Cortex-M3 的異常入棧順序,手動入棧了;

使用 LDMIA 指令,以 pxTopOfStack 開始順序出棧,先出 R4~R11(在創建任務的時候,最後入棧的就是這些個),同時 R0 遞增;

將此刻的 R0 賦值給 PSP(因爲彈棧的時候,處理器會按照入棧的順序去取 xPSR、PC、LR、R12、R3、R2、R1、R0,而這些寄存器在我們創建任務的時候已經手動壓棧);

ISB 指令屏障,刷流水線;

將 BASEPRI 寄存器賦值爲 0,也就是允許任何中斷;

ORR 指令時按位或,所以 ORR R14, #0xd 相當於 R14 |= 0xd;這個操作也和體系架構相關,R14 是鏈接寄存器 LR,在 ISR 中(此刻我們在 SVC 的 ISR 中),它記錄了異常返回值 EXC_RETURN(更多細節參考《Cortex-M3 處理器窺探》Chapter 7.4);

因爲當前在 ISR 中還是使用的 MSP,啓動任務後,我們期望在任務執行過程中,處於線程模式,並使用 PSP(前面幾行已經給 PSP 賦值了),所以我們需要將 LR 設計成爲 0xFFFF_FFFD,讓處理器知道返回的時候呢,使用線程模式+PSP堆棧;

最後執行 bx R14,告訴處理器 ISR 完成,需要返回,此刻處理器便會使用 PSP 做爲堆棧指針,進行出棧操作,將xPSR、PC、LR、R12、R3~R0 出棧,初始化的時候,PC 被我們賦值成爲了執行任務的函數的入口,所以呢,就正常跳入到了優先級最高的 Ready 狀態的第一個任務的入口函數了;

處理器相關的部分,可以參考《Cortex-M3 處理器窺探》,創建任務的部分參考《FreeRTOS --(8)任務管理之創建任務

大致的流程如下:

紫色部分,是和體系架構相關的,黑色的是開關中斷的地方,藍色的是 FreeRTOS 的代碼;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章