FreeRTOS --(13)任務管理之空閒任務

創建完畢任務,啓動調度器,任務控制,系統 SysTick 來臨後判斷是否需上下文切換;

如果沒有其他任務執行的情況下,FreeRTOS 的 Idle 任務將被調度投入運行;

在啓動調度器的時候,Idle 任務就被創建了,優先級爲最低 0;

void vTaskStartScheduler( void )
{
.....................
xReturn = xTaskCreate(  prvIdleTask,
                        configIDLE_TASK_NAME,
                        configMINIMAL_STACK_SIZE,
                        ( void * ) NULL,
                        portPRIVILEGE_BIT,
                        &xIdleTaskHandle );
.....................
}

當某時刻所有優先級高於 Idle 任務的任務處於被阻塞或者部分被掛起的狀態,此刻調度器會調度 Idle 任務運行,它的執行函數爲:

/*
 * -----------------------------------------------------------
 * The Idle task.
 * ----------------------------------------------------------
 *
 * The portTASK_FUNCTION() macro is used to allow port/compiler specific
 * language extensions.  The equivalent prototype for this function is:
 *
 * void prvIdleTask( void *pvParameters );
 *
 */
static portTASK_FUNCTION( prvIdleTask, pvParameters )
{
    /* Stop warnings. */
    ( void ) pvParameters;

    /** THIS IS THE RTOS IDLE TASK - WHICH IS CREATED AUTOMATICALLY WHEN THE
    SCHEDULER IS STARTED. **/

    /* In case a task that has a secure context deletes itself, in which case
    the idle task is responsible for deleting the task's secure context, if
    any. */
    portALLOCATE_SECURE_CONTEXT( configMINIMAL_SECURE_STACK_SIZE );

    for( ;; )
    {
        /* See if any tasks have deleted themselves - if so then the idle task
        is responsible for freeing the deleted task's TCB and stack. */
        prvCheckTasksWaitingTermination();

        #if ( configUSE_PREEMPTION == 0 )
        {
            /* If we are not using preemption we keep forcing a task switch to
            see if any other task has become available.  If we are using
            preemption we don't need to do this as any task becoming available
            will automatically get the processor anyway. */
            taskYIELD();
        }
        #endif /* configUSE_PREEMPTION */

        #if ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) )
        {
            /* When using preemption tasks of equal priority will be
            timesliced.  If a task that is sharing the idle priority is ready
            to run then the idle task should yield before the end of the
            timeslice.

            A critical region is not required here as we are just reading from
            the list, and an occasional incorrect value will not matter.  If
            the ready list at the idle priority contains more than one task
            then a task other than the idle task is ready to execute. */
            if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ tskIDLE_PRIORITY ] ) ) > ( UBaseType_t ) 1 )
            {
                taskYIELD();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        #endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) ) */

        #if ( configUSE_IDLE_HOOK == 1 )
        {
            extern void vApplicationIdleHook( void );

            /* Call the user defined function from within the idle task.  This
            allows the application designer to add background functionality
            without the overhead of a separate task.
            NOTE: vApplicationIdleHook() MUST NOT, UNDER ANY CIRCUMSTANCES,
            CALL A FUNCTION THAT MIGHT BLOCK. */
            vApplicationIdleHook();
        }
        #endif /* configUSE_IDLE_HOOK */

        /* This conditional compilation should use inequality to 0, not equality
        to 1.  This is to ensure portSUPPRESS_TICKS_AND_SLEEP() is called when
        user defined low power mode implementations require
        configUSE_TICKLESS_IDLE to be set to a value other than 1. */
        #if ( configUSE_TICKLESS_IDLE != 0 )
        {
        TickType_t xExpectedIdleTime;

            /* It is not desirable to suspend then resume the scheduler on
            each iteration of the idle task.  Therefore, a preliminary
            test of the expected idle time is performed without the
            scheduler suspended.  The result here is not necessarily
            valid. */
            xExpectedIdleTime = prvGetExpectedIdleTime();

            if( xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP )
            {
                vTaskSuspendAll();
                {
                    /* Now the scheduler is suspended, the expected idle
                    time can be sampled again, and this time its value can
                    be used. */
                    configASSERT( xNextTaskUnblockTime >= xTickCount );
                    xExpectedIdleTime = prvGetExpectedIdleTime();

                    /* Define the following macro to set xExpectedIdleTime to 0
                    if the application does not want
                    portSUPPRESS_TICKS_AND_SLEEP() to be called. */
                    configPRE_SUPPRESS_TICKS_AND_SLEEP_PROCESSING( xExpectedIdleTime );

                    if( xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP )
                    {
                        traceLOW_POWER_IDLE_BEGIN();
                        portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime );
                        traceLOW_POWER_IDLE_END();
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                ( void ) xTaskResumeAll();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        #endif /* configUSE_TICKLESS_IDLE */
    }
}

Idle 任務也是一個無限循環:

1、調用 prvCheckTasksWaitingTermination() 判斷是否有需要 Task 自己刪除自己,如果有,那麼在 Idle 任務中來回收這種類型的場景:

static void prvCheckTasksWaitingTermination( void )
{

    /** THIS FUNCTION IS CALLED FROM THE RTOS IDLE TASK **/

    #if ( INCLUDE_vTaskDelete == 1 )
    {
        TCB_t *pxTCB;

        /* uxDeletedTasksWaitingCleanUp is used to prevent taskENTER_CRITICAL()
        being called too often in the idle task. */
        while( uxDeletedTasksWaitingCleanUp > ( UBaseType_t ) 0U )
        {
            taskENTER_CRITICAL();
            {
                pxTCB = listGET_OWNER_OF_HEAD_ENTRY( ( &xTasksWaitingTermination ) ); /*lint !e9079 void * is used as this macro is used with timers and co-routines too.  Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
                ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
                --uxCurrentNumberOfTasks;
                --uxDeletedTasksWaitingCleanUp;
            }
            taskEXIT_CRITICAL();

            prvDeleteTCB( pxTCB );
        }
    }
    #endif /* INCLUDE_vTaskDelete */
}

如果支持任務刪除,而且有需要被刪除的任務的話,進入臨界區,取出要被刪除的任務,更新當前任務個數和待刪除任務個數,退出臨界區,並調用 prvDeleteTCB() 接口來刪除任務的資源,其實就是調用了 vPortFree( pxTCB->pxStack ); 和 vPortFree( pxTCB ); 來釋放任務的 TCB 結構和堆棧;

2、如果定義了 configUSE_PREEMPTION 爲 1(支持搶佔),同時 configIDLE_SHOULD_YIELD 也爲 1 (如果有與 Idle 任務相同優先級的任務,並且處於 Ready 狀態,那麼 Idle 任務將爲其讓路)的情況,Idle 任務直接調用 taskYIELD(); 引發一次調度,放棄 CPU;

3、如果使能了 configUSE_IDLE_HOOK,也就是用戶的 Idle 鉤子函數,則調用 vApplicationIdleHook;

4、如果使能了 configUSE_TICKLESS_IDLE,就意味着要進入低功耗場景,當然,既然都調用 Idle 任務了,進入低功耗理所應當;這裏的 Tickless 的含義是:進入低功耗後,Systick 不在來中斷,因爲 Tick 心跳很頻繁的話,處理器很快就被喚醒了,失去了低功耗的意義;

5、調用 prvGetExpectedIdleTime 獲取距離下一個最近的阻塞任務的執行時間,與 當前的時間做減法,得到最大的可以進入低功耗的時間,當然這裏只能判斷阻塞在時間上的任務,對於事件,我們並不知道什麼時候會來,也許是中斷激活事件,不過這樣要求中斷能夠喚醒處理器,否則中斷無法得到及時處理,那麼 RTOS 的實時任務的運行也得不到實時的保證;

6、如果獲取得到的最大進入低功耗的時間 xExpectedIdleTime 大於了我們配置的期望睡眠的最小時間,也就是滿足進入低功耗的條件,那麼掛起調度器(因爲馬上要進入 Tickless),調用 portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime ); 進入低功耗;

7、portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime ) 的實現是和處理器相關,因爲是帶 port 前綴,每種處理器進入低功耗的方式不盡相同,即便是同一種處理器,進入低功耗也有幾種模式,所以這裏交給處理器相關 port.c 去實現;

8、喚醒後,一般的,原地繼續執行,調用 xTaskResumeAll 恢復調度器;

針對 Cortex-M3 進入睡眠部分:

#define portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime ) \
vPortSuppressTicksAndSleep( xExpectedIdleTime )

調用到了 vPortSuppressTicksAndSleep(xExpectedIdleTime)入參是期待睡眠的時間;也就是 Tick 個數:

#if( configUSE_TICKLESS_IDLE == 1 )

    __weak void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
    {
    uint32_t ulReloadValue, ulCompleteTickPeriods, ulCompletedSysTickDecrements;
    TickType_t xModifiableIdleTime;

        /* Make sure the SysTick reload value does not overflow the counter. */
        /* 睡眠的最大值不能夠超過處理器支持的最大值 */
        if( xExpectedIdleTime > xMaximumPossibleSuppressedTicks )
        {
            xExpectedIdleTime = xMaximumPossibleSuppressedTicks;
        }

        /* Stop the SysTick momentarily.  The time the SysTick is stopped for
        is accounted for as best it can be, but using the tickless mode will
        inevitably result in some tiny drift of the time maintained by the
        kernel with respect to calendar time. */
        /* 禁止 SysTick */
        portNVIC_SYSTICK_CTRL_REG &= ~portNVIC_SYSTICK_ENABLE_BIT;

        /* Calculate the reload value required to wait xExpectedIdleTime
        tick periods.  -1 is used because this code will execute part way
        through one of the tick periods. */
        /* 計算將要配置到 SYSTICK  用於喚醒的值 */
        ulReloadValue = portNVIC_SYSTICK_CURRENT_VALUE_REG + ( ulTimerCountsForOneTick * ( xExpectedIdleTime - 1UL ) );
        if( ulReloadValue > ulStoppedTimerCompensation )
        {
            ulReloadValue -= ulStoppedTimerCompensation;
        }

        /* Enter a critical section but don't use the taskENTER_CRITICAL()
        method as that will mask interrupts that should exit sleep mode. */
        /* 關閉中斷 */
        __disable_irq();
        __dsb( portSY_FULL_READ_WRITE );
        __isb( portSY_FULL_READ_WRITE );

        /* If a context switch is pending or a task is waiting for the scheduler
        to be unsuspended then abandon the low power entry. */
        /* 再次 Check 有沒有非 Idle 狀態待執行的任務 */
        if( eTaskConfirmSleepModeStatus() == eAbortSleep )
        {
            /* Restart from whatever is left in the count register to complete
            this tick period. */
            /* 重新配置 SysTICK 終止睡眠流程 */
            portNVIC_SYSTICK_LOAD_REG = portNVIC_SYSTICK_CURRENT_VALUE_REG;

            /* Restart SysTick. */
            portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;

            /* Reset the reload register to the value required for normal tick
            periods. */
            portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL;

            /* Re-enable interrupts - see comments above __disable_irq() call
            above. */
            /* 打開中斷 */
            __enable_irq();
        }
        else
        {
            /* Set the new reload value. */
            /* 將 SysTick 的時間配置爲之前計算好的時間 */
            portNVIC_SYSTICK_LOAD_REG = ulReloadValue;

            /* Clear the SysTick count flag and set the count value back to
            zero. */
            /* 清除當前 SysTick 的值 */
            portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;

            /* Restart SysTick. */
            /* 開啓 SysTick */
            portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;

            /* Sleep until something happens.  configPRE_SLEEP_PROCESSING() can
            set its parameter to 0 to indicate that its implementation contains
            its own wait for interrupt or wait for event instruction, and so wfi
            should not be executed again.  However, the original expected idle
            time variable must remain unmodified, so a copy is taken. */
            xModifiableIdleTime = xExpectedIdleTime;
            configPRE_SLEEP_PROCESSING( xModifiableIdleTime );
            /* 執行 WFI 睡眠 */
            if( xModifiableIdleTime > 0 )
            {
                __dsb( portSY_FULL_READ_WRITE );
                __wfi();
                __isb( portSY_FULL_READ_WRITE );
            }
            /* 此處爲喚醒 */
            configPOST_SLEEP_PROCESSING( xExpectedIdleTime );

            /* Re-enable interrupts to allow the interrupt that brought the MCU
            out of sleep mode to execute immediately.  see comments above
            __disable_interrupt() call above. */
            /* 因爲可能是其他中斷喚醒的 WFI,立馬開啓中斷,進入 ISR */
            __enable_irq();
            __dsb( portSY_FULL_READ_WRITE );
            __isb( portSY_FULL_READ_WRITE );

            /* Disable interrupts again because the clock is about to be stopped
            and interrupts that execute while the clock is stopped will increase
            any slippage between the time maintained by the RTOS and calendar
            time. */
            /* 關閉中斷,做處理 */
            __disable_irq();
            __dsb( portSY_FULL_READ_WRITE );
            __isb( portSY_FULL_READ_WRITE );
            
            /* Disable the SysTick clock without reading the 
            portNVIC_SYSTICK_CTRL_REG register to ensure the
            portNVIC_SYSTICK_COUNT_FLAG_BIT is not cleared if it is set.  Again, 
            the time the SysTick is stopped for is accounted for as best it can 
            be, but using the tickless mode will inevitably result in some tiny 
            drift of the time maintained by the kernel with respect to calendar 
            time*/
            /* 重新配置 SysTick */
            portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT );

            /* Determine if the SysTick clock has already counted to zero and
            been set back to the current reload value (the reload back being
            correct for the entire expected idle time) or if the SysTick is yet
            to count to zero (in which case an interrupt other than the SysTick
            must have brought the system out of sleep mode). */
            if( ( portNVIC_SYSTICK_CTRL_REG & portNVIC_SYSTICK_COUNT_FLAG_BIT ) != 0 )
            {
                uint32_t ulCalculatedLoadValue;

                /* The tick interrupt is already pending, and the SysTick count
                reloaded with ulReloadValue.  Reset the
                portNVIC_SYSTICK_LOAD_REG with whatever remains of this tick
                period. */
                ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL ) - ( ulReloadValue - portNVIC_SYSTICK_CURRENT_VALUE_REG );

                /* Don't allow a tiny value, or values that have somehow
                underflowed because the post sleep hook did something
                that took too long. */
                if( ( ulCalculatedLoadValue < ulStoppedTimerCompensation ) || ( ulCalculatedLoadValue > ulTimerCountsForOneTick ) )
                {
                    ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL );
                }

                portNVIC_SYSTICK_LOAD_REG = ulCalculatedLoadValue;

                /* As the pending tick will be processed as soon as this
                function exits, the tick value maintained by the tick is stepped
                forward by one less than the time spent waiting. */
                ulCompleteTickPeriods = xExpectedIdleTime - 1UL;
            }
            else
            {
                /* Something other than the tick interrupt ended the sleep.
                Work out how long the sleep lasted rounded to complete tick
                periods (not the ulReload value which accounted for part
                ticks). */
                ulCompletedSysTickDecrements = ( xExpectedIdleTime * ulTimerCountsForOneTick ) - portNVIC_SYSTICK_CURRENT_VALUE_REG;

                /* How many complete tick periods passed while the processor
                was waiting? */
                ulCompleteTickPeriods = ulCompletedSysTickDecrements / ulTimerCountsForOneTick;

                /* The reload value is set to whatever fraction of a single tick
                period remains. */
                portNVIC_SYSTICK_LOAD_REG = ( ( ulCompleteTickPeriods + 1UL ) * ulTimerCountsForOneTick ) - ulCompletedSysTickDecrements;
            }

            /* Restart SysTick so it runs from portNVIC_SYSTICK_LOAD_REG
            again, then set portNVIC_SYSTICK_LOAD_REG back to its standard
            value. */
            portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
            portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
            vTaskStepTick( ulCompleteTickPeriods );
            portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL;

            /* Exit with interrpts enabled. */
            __enable_irq();
        }
    }

#endif /* #if configUSE_TICKLESS_IDLE */

內容不少,慢慢看即可:

0、入參是期望睡眠的 Tick 數目,這裏不用這麼抽象,比如配置的 Tick 週期爲 1ms,那麼這裏就是 ms 數;

這裏有 3 個全局變量需要說明一下:

/*
 * The number of SysTick increments that make up one tick period.
 */
#if( configUSE_TICKLESS_IDLE == 1 )
    static uint32_t ulTimerCountsForOneTick = 0;
#endif /* configUSE_TICKLESS_IDLE */

/*
 * The maximum number of tick periods that can be suppressed is limited by the
 * 24 bit resolution of the SysTick timer.
 */
#if( configUSE_TICKLESS_IDLE == 1 )
    static uint32_t xMaximumPossibleSuppressedTicks = 0;
#endif /* configUSE_TICKLESS_IDLE */

/*
 * Compensate for the CPU cycles that pass while the SysTick is stopped (low
 * power functionality only.
 */
#if( configUSE_TICKLESS_IDLE == 1 )
    static uint32_t ulStoppedTimerCompensation = 0;
#endif /* configUSE_TICKLESS_IDLE */

在開啓調度器,配置 SysTick 的時候,這 3 個全局變量被賦值初始化:

#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 */

ulTimerCountsForOneTick :代表了一個 SysTick 配置到寄存器的 Tick 的 Count;換句話來說,就是產生 1ms 的 SysTick 中斷,需要配置給寄存器的值;

xMaximumPossibleSuppressedTicks:代表了在溢出之前,硬件最大支持多少個 Tick;(因爲 Cortex-M3 處理器配置給硬件的 Tick Count 最大是 24 bit 的,所以這裏用 24bit 的全 1 除以 ulTimerCountsForOneTick );

ulStoppedTimerCompensation:代表了一個時鐘補償的因子,這裏是固定的 45;

1、首先判斷期望睡眠的值是否大於了處理器的 xMaximumPossibleSuppressedTicks,如果是,那麼將睡眠的值限定在 xMaximumPossibleSuppressedTicks

2、禁止 SysTick 模塊;

3、計算新的 SysTick 的 Load 值,這裏的原理是,因爲需要讓處理器進入 WFI (Waiting For Interrupt),進入 WFI 後,處理器可以被中斷喚醒,並繼續執行;其實這裏的 Tickless 並不是真的一直關閉了 SysTick ,而是將睡眠的時間配置到了 SysTick 中,所以這裏纔會限制睡眠時間;

ulReloadValue = portNVIC_SYSTICK_CURRENT_VALUE_REG + \
( ulTimerCountsForOneTick * ( xExpectedIdleTime - 1UL ) );

if( ulReloadValue > ulStoppedTimerCompensation )
{
    ulReloadValue -= ulStoppedTimerCompensation;
}

使用當前的 SysTick 的值,加上睡眠的時間乘以每個 Tick 的 Count,計算出即將配置到 SysTick 硬件寄存器的值;然後對補償因子做減法;

4、__disable_irq,關閉中斷,刷指令和數據流水線;

5、判斷是否還有需要被執行的任務,如果有,那麼重新配置 SysTick 還是爲 1ms,並使能 SysTick,開啓中斷,退出睡眠邏輯;

6、如果沒有要被執行的任務,將計算出來最大的睡眠時間 ulReloadValue 配置進 SysTick 計數器寄存器,開啓 SysTick,此刻的 SysTick 便是睡眠的時間;

7、進入 WFI 睡眠;

8、如果有中斷,則對 WFI 原地喚醒,繼續執行,這裏可能是 SysTick 的 IRQ 喚醒,也可能是其他中斷喚醒;

9、__enable_irq,開啓中斷,因爲可能是被其他 IRQ 喚醒,這裏需要立馬執行 ISR;

10、__disable_irq,關閉中斷,刷指令和數據流水線,因爲下面的配置不允許被打斷;

11、重新配置 SysTick 成爲 OS 的心跳(也就是 1ms),並使能 SysTick;

 

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