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;

 

©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页