07. FreeRTOS的时间管理

一、FreeRTOS的系统时钟节拍

  任务的操作系统需要时钟节拍,FreeRTOS 有一个系统时钟节拍计数器(xTickCount),xTickCount 是一个全局变量,在 tasks.c 文件中有定义,具体的代码如下所示:

PRIVILEGED_DATA static volatile TickType_t xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;

  从上面的代码可以看到, xTickCount 在定义时,被赋了初值,初值由宏定义 configINITIAL_TICK_COUNT 定义,在通常情况下系统使用节拍计数器的初值都是设置为 0,除非在个别特出场合,读者仅需了解系统时钟节拍计数器的初值是可以由用户手动配置的即可。

  FreeRTOS 的系统时钟节拍计数器为全局变量 xTickCount,那么 FreeRTOS 在 SysTick 的中断服务函数中操作系统时钟节拍计数器。在 FreeRTOS 启动任务调度器的过程中会对 SysTick 进行初始化,这里对 SysTick 的配置是会覆盖用户对 SysTick 的配置的,因此最终 SysTick 的配置为 FreeRTOS 对 SysTick 的配置。

  如果单片机配置 SysTick 的时钟源频率与 CPU 的时钟源频率不同,需在 FreeRTOSConfig.h 文件中配置 configSYSTICK_CLOCK_HZ。例如,STM32F1 的单片机,配置 SysTick 的时钟源频率为系统时钟的 8 分频,我们需要在在 FreeRTOSConfig.h 文件中配置 configSYSTICK_CLOCK_HZ

// 定义SysTick时钟频率,当SysTick时钟频率与内核时钟频率不同时才可以定义,单位: Hz,默认: 不定义
#define configSYSTICK_CLOCK_HZ                          (configCPU_CLOCK_HZ / 8)

  既然 FreeRTOS 的系统时钟节拍来自 SysTick,那么 FreeRTOS 系统时钟节拍的处理,自然就是在 SysTick 的中断服务函数中完成的。

/**
 * @brief Systick中断服务函数
 * 
 */
void SysTick_Handler(void)
{
    HAL_IncTick();

    // OS开始跑了,才执行正常的调度处理
    if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
    {
        xPortSysTickHandler();
    }
}

  从上面的代码可以看出,在 SysTick 的中断服务函数中,除了调用函数 HAL_IncTick() 外,还通过函数 xTaskGetSchedulerState() 判断任务调度器是否运行,如果任务调度器运行,那么就调用函数 xPortSysTickHandler() 处理 FreeRTOS 的时钟节拍,及相关事务。

  函数 xPortSysTickHandler() 在 port.c 文件中有定义,具体的代码如下所示:

void xPortSysTickHandler( void )
{
    // 屏蔽所有受 FreeRTOS 管理的中断,
    // 因为 SysTick 的中断优先级设置为最低的中断优先等级,因此需要屏蔽所有受 FreeRTOS 管理的中断
    portDISABLE_INTERRUPTS();
    traceISR_ENTER();
    {
        // 处理系统时钟节拍,并决定是否进行任务切换
        if( xTaskIncrementTick() != pdFALSE )
        {
            traceISR_EXIT_TO_SCHEDULER();

            // 需要进行任务切换, 这是中断控制状态寄存器,以挂起 PendSV 异常
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
        else
        {
            traceISR_EXIT();
        }
    }

    // 取消中断屏蔽
    portENABLE_INTERRUPTS();
}

  从上面的代码可以看出,函数 xPortSysTickHandler() 调用了函数 xTaskIncrementTick() 来处理系统时钟节拍。在调用函数 xTaskIncrementTick() 前后分别屏蔽了受 FreeRTOS 管理的中断和取消中断屏蔽,这是因为 SysTick 的中断优先级设置为最低的中断优先等级,在 SysTick 的中断中处理 FreeRTOS 的系统时钟节拍时,并不希望收到其他中断的影响。在通过函数 xTaskIncrementTick() 处理完系统时钟节拍和相关事务后,再根据函数 xTaskIncrementTick 的返回值,决定是否进行任务切换,如果进行任务切换,就触发 PendSV 异常,在本次 SysTick 中断及其他中断处理完成后,就会进入 PendSV 的中断服务函数进行任务切换。

BaseType_t xTaskIncrementTick( void )
{
    TCB_t * pxTCB;
    TickType_t xItemValue;
    BaseType_t xSwitchRequired = pdFALSE;

    #if ( configUSE_PREEMPTION == 1 ) && ( configNUMBER_OF_CORES > 1 )
    BaseType_t xYieldRequiredForCore[ configNUMBER_OF_CORES ] = { pdFALSE };
    #endif /* #if ( configUSE_PREEMPTION == 1 ) && ( configNUMBER_OF_CORES > 1 ) */

    traceENTER_xTaskIncrementTick();
    traceTASK_INCREMENT_TICK( xTickCount );

    // 判断任务调度器是否运行,只有任务调度器正在运行的情况下,才作相应处理
    if( uxSchedulerSuspended == ( UBaseType_t ) 0U )
    {
        // 使用 const 定义,表示在此次 SysTick 中断的整个过程中,系统时钟节拍的值不会再改变
        const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;

        // 此全局变量为系统使用节拍计数器
        xTickCount = xConstTickCount;

        // 如果系统时钟节拍计数器的值为0,说明系统时钟节拍计数器已经溢出了,因此需要作相应的处理
        if( xConstTickCount == ( TickType_t ) 0U )
        {
            // 切换阻塞态任务列表,FreeRTOS定义了两个阻塞态任务列表,就是为了解决系统节拍计数器溢出的问题
            taskSWITCH_DELAYED_LISTS();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

        // 检查是否有阻塞任务阻塞超时,阻塞态任务列表中的阻塞任务时按照阻塞超时时间排序的,
        // 因此,只要按顺序检查出一个未超时的阻塞任务,就没必要再往下检查了
        if( xConstTickCount >= xNextTaskUnblockTime )
        {
            for( ; ; )
            {
                // 判断阻塞态任务列表中是否有阻塞任务
                if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
                {
                    // 此全局变量用于记录下一个阻塞任务的阻塞超时系统节拍,因此阻塞态任务列表中没有阻塞任务,因此将此全局变量设置为最大值
                    xNextTaskUnblockTime = portMAX_DELAY;
                    break;
                }
                else
                {
                    // 获取阻塞态任务列表中第一个阻塞任务的任务状态列表项值, 这个值记录了,该阻塞任务的阻塞超时系统时钟节拍
                    pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
                    xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );

                    // 判断阻塞任务的阻塞时间是否还未超时
                    if( xConstTickCount < xItemValue )
                    {
                        // 如果阻塞任务的阻塞时间还未超时,将记录下一个阻塞任务阻塞超时时间的全局变量设置为最大值,
                        // 退出循环,因为阻塞态任务列表中没有超时的阻塞任务
                        xNextTaskUnblockTime = xItemValue;
                        break;
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }

                    // 将阻塞时间超时的阻塞任务,从阻塞态任务列表中移除
                    listREMOVE_ITEM( &( pxTCB->xStateListItem ) );

                    // 判断阻塞任务是否正在等待事件发生,如果是,那么设置的等待时间(阻塞时间)已经超时,
                    // 任务阻塞可能是等待事件引起的,可能是调用 FreeRTOS 的延时函数引起的
                    if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
                    {
                        // 等待事件的时间已经超时,将任务的事件列表项,从等待的事件列表中移除
                        listREMOVE_ITEM( &( pxTCB->xEventListItem ) );
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }

                    prvAddTaskToReadyList( pxTCB );                             // 将阻塞超时的任务添加到就绪态任务列表中

                    /* A task being unblocked cannot cause an immediate
                     * context switch if preemption is turned off. */
                    #if ( configUSE_PREEMPTION == 1 )                           // 此宏用于启用抢占式调度
                    {
                        #if ( configNUMBER_OF_CORES == 1 )
                        {
                            // 在抢占式调度中,如果阻塞超时的任务优先级高于当前系统正在运行的任务,
                            // 那么阻塞超时的任务将抢占 CPU 的使用权,因此需要进行任务切换
                            if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
                            {
                                xSwitchRequired = pdTRUE;                       // 标记需要进行任务切换
                            }
                            else
                            {
                                mtCOVERAGE_TEST_MARKER();
                            }
                        }
                        #else /* #if( configNUMBER_OF_CORES == 1 ) */
                        {
                            prvYieldForTask( pxTCB );
                        }
                        #endif /* #if( configNUMBER_OF_CORES == 1 ) */
                    }
                    #endif /* #if ( configUSE_PREEMPTION == 1 ) */
                }
            }
        }

        // 宏 configUSE_TIME_SLICING 用于启用时间片调度
        #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
        {
            #if ( configNUMBER_OF_CORES == 1 )
            {
                // 判断当前系统正在运行的任务所在优先级就绪态任务列表中,是否还有其他优先级相同的任务,
                // 如果有,由于使能了时间片调度,因此需要进行任务切换,切换到下一个相同优先级的任务
                if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > 1U )
                {
                    xSwitchRequired = pdTRUE;                                   // 标记需要进行任务切换
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            #else /* #if ( configNUMBER_OF_CORES == 1 ) */
            {
                BaseType_t xCoreID;

                for( xCoreID = 0; xCoreID < ( ( BaseType_t ) configNUMBER_OF_CORES ); xCoreID++ )
                {
                    if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCBs[ xCoreID ]->uxPriority ] ) ) > 1U )
                    {
                        xYieldRequiredForCore[ xCoreID ] = pdTRUE;
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
            }
            #endif /* #if ( configNUMBER_OF_CORES == 1 ) */
        }
        #endif /* #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */

        // 此宏用于使能系统时钟节拍钩子函数,使用此宏能,需要用户定义相关的钩子函数
        #if ( configUSE_TICK_HOOK == 1 )
        {
            // 防止在任务调度器恢复后补偿处理完成之前,调用钩子函数
            if( xPendedTicks == ( TickType_t ) 0 )
            {
                vApplicationTickHook();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        #endif /* configUSE_TICK_HOOK */

        #if ( configUSE_PREEMPTION == 1 )                                       // 此宏用于启用抢占式调度
        {
            #if ( configNUMBER_OF_CORES == 1 )
            {
                // 全局变量用于在系统运行的任意时刻标记需要进行任务切换,此全局变量在此时统一处理
                if( xYieldPendings[ 0 ] != pdFALSE )
                {
                    xSwitchRequired = pdTRUE;                                   // 标记需要进行任务调度
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            #else /* #if ( configNUMBER_OF_CORES == 1 ) */
            {
                BaseType_t xCoreID, xCurrentCoreID;
                xCurrentCoreID = ( BaseType_t ) portGET_CORE_ID();

                for( xCoreID = 0; xCoreID < ( BaseType_t ) configNUMBER_OF_CORES; xCoreID++ )
                {
                    #if ( configUSE_TASK_PREEMPTION_DISABLE == 1 )
                        if( pxCurrentTCBs[ xCoreID ]->xPreemptionDisable == pdFALSE )
                    #endif
                    {
                        if( ( xYieldRequiredForCore[ xCoreID ] != pdFALSE ) || ( xYieldPendings[ xCoreID ] != pdFALSE ) )
                        {
                            if( xCoreID == xCurrentCoreID )
                            {
                                xSwitchRequired = pdTRUE;
                            }
                            else
                            {
                                prvYieldCore( xCoreID );
                            }
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                }
            }
            #endif /* #if ( configNUMBER_OF_CORES == 1 ) */
        }
        #endif /* #if ( configUSE_PREEMPTION == 1 ) */
    }
    else
    {
        // 此全局变量用于记录在任务调度器挂起的情况下,SysTick 中断了多少次,以在任务调度器恢复后,作相应的补偿处理
        xPendedTicks += 1U;

        // 此宏用于使能系统时钟节拍钩子函数,使用此宏能,需要用户定义相关的钩子函数
        #if ( configUSE_TICK_HOOK == 1 )
        {
            vApplicationTickHook();
        }
        #endif
    }

    traceRETURN_xTaskIncrementTick( xSwitchRequired );

    // 返回是否需要进行任务切换标志
    return xSwitchRequired;
}

  从上面的代码可以看到,函数 xTaskIncrementTick() 处理了系统时钟节拍、阻塞任务列表、时间片调度等。

  处理系统时钟节拍,就是在每次 SysTick 中断发生的时候,将全局变量 xTickCount 的值加1,也就是将系统时钟节拍计数器的值加 1。

  处理阻塞任务列表,就是判断阻塞态任务列表中是否有阻塞任务超时,如果有,就将阻塞时间超时的阻塞态任务移到就绪态任务列表中,准备执行。同时在系统时钟节拍计数器 xTickCount 的加 1 溢出后,将两个阻塞态任务列表调换,这是 FreeRTOS 处理系统时钟节拍计数器溢出的一种机制。

  处理时间片调度,就是在每次系统时钟节拍加 1 后,切换到另外一个同等优先级的任务中运行,要注意的是,此函数只是做了需要进行任务切换的标记,在函数退出后,会统一进行任务切换,因此时间片调度导致的任务切换,也可能因为有更高优先级的阻塞任务就绪导致任务切换,而出现任务切换后运行的任务比任务切换前运行任务的优先级高,而非相等优先级。

二、FreeRTOS的任务延时函数

void vTaskDelay( const TickType_t xTicksToDelay );              // 任务延时函数,延时单位:系统时钟节拍
vTaskDelayUntil( pxPreviousWakeTime, xTimeIncrement );          // 任务绝对延时函数,延时单位:系统时钟节拍
BaseType_t xTaskAbortDelay( TaskHandle_t xTask );               // 终止任务延时函数
  • 相对延时:指每次延时都是从执行函数 vTaskDelay() 开始,直到延时指定的时间结束。
  • 绝对延时:指将整个任务的运行周期看成一个整体,适用于需要按照一定频率运行的任务。

相对延迟与绝对延迟

2.1、vTaskDelay()函数

  vTaskDelay() 函数用于对任务进行延时,延时的时间单位为系统时钟节拍,如需使用子函数,需要的 FreeRTOSConfig.h 文件中将配置项 INCLUDE_vTaskDelay 配置为 1。此函数在 task.c文件中有定义,具体的代码如下所示:

void vTaskDelay( const TickType_t xTicksToDelay )
{
    BaseType_t xAlreadyYielded = pdFALSE;

    traceENTER_vTaskDelay( xTicksToDelay );

    // 只有在延时时间大于0的时候,才需要进行任务阻塞,否则相当于强制进行任务切换,而不阻塞任务
    if( xTicksToDelay > ( TickType_t ) 0U )
    {
        vTaskSuspendAll();                                                      // 挂起任务调度器
        {
            configASSERT( uxSchedulerSuspended == 1U );
            traceTASK_DELAY();                                                  // 用于调试

            prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );           // 任务添加到阻塞态任务列表中
        }
        // 恢复任务调度器运行,用此函数会返回是否需要进行任务切换
        xAlreadyYielded = xTaskResumeAll();
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    if( xAlreadyYielded == pdFALSE )                                            //  根据标志进行任务切换
    {
        taskYIELD_WITHIN_API();
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    traceRETURN_vTaskDelay();
}

  使用函数 vTaskDelay() 进行任务延时时,被延时的任务为调用该函数的任务,即调用该函数时,系统中正在运行的任务,此函数无法指定将其他任务进行任务延时。

  函数 vTaskDelay() 传入的参数 xTicksToDelay 是任务被延时的具体延时时间,时间的单位为系统时钟节拍。FreeRTOS 是以系统时钟节拍作为计量的时间单位的,而系统时钟节拍对应的物理时间长短与 FreeRTOSConfig.h 文件中的配置 configTICK_RATE_HZ 有关,配置项 configTICK_RATE_HZ 是用于配置系统时钟节拍的频率的。这里,我们将此配置项配置成了 1000,即系统时钟节拍的频率为 1000,换算过来,一个系统时钟节拍就是 1 毫秒。

  在使用此函数进行任务延时时,如果传入的参数为 0,那表明不进行任务延时,而是强制进行一次任务切换。

  在使用此函数进行任务延时时,会调用函数 prvAddCurrentTaskToDelayedList() 将被延时的任务添加到阻塞态任务列表中进行延时,系统会在每一次 SysTick 中断发生时,处理阻塞态任务列表。

  其中 prvAddCurrentTaskToDelayedList() 函数在 task.c 文件中有定义,具体的代码如下所示:

static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait,                    // 阻塞时间
                                            const BaseType_t xCanBlockIndefinitely )    // 无期限阻塞(阻塞时间为最大值)
{
    TickType_t xTimeToWake;
    const TickType_t xConstTickCount = xTickCount;
    List_t * const pxDelayedList = pxDelayedTaskList;
    List_t * const pxOverflowDelayedList = pxOverflowDelayedTaskList;

    #if ( INCLUDE_xTaskAbortDelay == 1 )                                        // 此宏用于开启任务延时中断功能
    {
        // 如果开启了任务延时中断功能,那么将任务的延时中断标志复位设置为假,当任务延时被中断时,再将其设置为真
        pxCurrentTCB->ucDelayAborted = ( uint8_t ) pdFALSE;
    }
    #endif

    // 将当前任务从所在任务列表中移除,当前任务为调用了会触发阻塞的 API 函数的任务,当任务延时被中断时,再将其设置为真
    if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
    {
        // 如果将当前任务从所在就绪态任务列表中移除后,原本所在就绪态任务列表中每有其他任务,那么将任务优先级记录中该任务的优先级清除
        portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    #if ( INCLUDE_vTaskSuspend == 1 )                                           // 此宏用于启用任务挂起功能
    {
        // 如果阻塞的时间为最大值并且允许任务被无期限阻塞,形参xCanBlockIndefinitely用于定义,
        // 在形参xTicksToWait 为最大值的情况下,是否将任务无期限阻塞,即将任务挂起
        if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )
        {
            // 将任务添加到挂起态任务列表中挂起,即将任务无期限阻塞
            listINSERT_END( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
        }
        else
        {
            // 计算任务在未来被取消阻塞时,E系统时钟节拍计数器的值,计算出来的值可能会出现溢出的情况,但系统会有相应的处理机制,即两个阻塞态任务列表
            xTimeToWake = xConstTickCount + xTicksToWait;

            // 设置任务的状态列表项的值为计算出来的值,那么在 SysTick 中断服务函数中处理阻塞态任务列表时,就可以通过这个值,判断任务时候阻塞超时
            listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );

            if( xTimeToWake < xConstTickCount )                                 // 如果计算出来的值溢出
            {
                traceMOVED_TASK_TO_OVERFLOW_DELAYED_LIST();
                // 将任务添加到阻塞超时时间溢出列表
                vListInsert( pxOverflowDelayedList, &( pxCurrentTCB->xStateListItem ) );
            }
            else
            {
                traceMOVED_TASK_TO_DELAYED_LIST();
                // 将任务添加到阻塞态任务列表
                vListInsert( pxDelayedList, &( pxCurrentTCB->xStateListItem ) );

                // 全局变量 xNextTaskUnblockTime 用于保存系统中最近要发生超时的系统节拍计数器的值
                if( xTimeToWake < xNextTaskUnblockTime )
                {
                    xNextTaskUnblockTime = xTimeToWake;                         //  有新的任务阻塞,因此要更新 xNextTaskUnblockTime
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
        }
    }
    #else /* INCLUDE_vTaskSuspend */
    {
        // 计算任务在未来被取消阻塞时,系统时钟节拍计数器的值,计算出来的值可能会出现溢出的情况,但系统会有相应的处理机制,即两个阻塞态任务列表
        xTimeToWake = xConstTickCount + xTicksToWait;

        // 设置任务的状态列表项的值为计算出来的值,那么在 SysTick 中断服务函数中处理阻塞态任务列表时, 就可以通过这个值,判断任务时候阻塞超时
        listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );

        if( xTimeToWake < xConstTickCount )                                     // 如果计算出来的值溢出
        {
            traceMOVED_TASK_TO_OVERFLOW_DELAYED_LIST();
            // 将任务添加到阻塞超时时间溢出列表
            vListInsert( pxOverflowDelayedList, &( pxCurrentTCB->xStateListItem ) );
        }
        else
        {
            traceMOVED_TASK_TO_DELAYED_LIST();
            vListInsert( pxDelayedList, &( pxCurrentTCB->xStateListItem ) );    // 将任务添加到阻塞态任务列表

            // 全局变量 xNextTaskUnblockTime 用于保存系统中最近要发生超时的系统节拍计数器的值
            if( xTimeToWake < xNextTaskUnblockTime )
            {
                xNextTaskUnblockTime = xTimeToWake;                             // 有新的任务阻塞,因此要更新 xNextTaskUnblockTime
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }

        ( void ) xCanBlockIndefinitely;                                         // 不使用任务挂起功能,就不使用这个入参
    }
    #endif /* INCLUDE_vTaskSuspend */
}

  函数 prvAddCurrentTaskToDelayedList() 是将任务当前任务添加到阻塞态任务列表中,其中入参 xTicksToWait 就是要任务被阻塞的时间,形参 xCanBlockIndefinitely 为当 xTicksToWait 为最大值时,是否运行将任务无期限阻塞,即将任务挂起,当然,能够这样做的前提是,在 FreeRTOSConfig.h 文件中开启了挂起任务功能。

  此函数在将任务添加到阻塞态任务列表中后,还会更新全局变量 xNextTaskUnblockTime,全局变量 xNextTaskUnblockTime 用于记录系统中的所有阻塞态任务中未来最近一个阻塞超时任务的阻塞超时时系统时钟节拍计数器的值,因此,在往阻塞态任务列表添加任务后,就需要更新这个全局变量,因为,新添加的阻塞态任务可能是未来系统中最早阻塞超时的阻塞任务。

2.2、vTaskDelayUntil()函数

  vTaskDelayUntil() 用于以一个绝对的时间阻塞任务,适用于需要按照一定频率运行的任务,函数 vTaskDelayUntil() 实际上是一个宏,在 task.h 文件中有定义,具体的代码如下所示:

#define vTaskDelayUntil( pxPreviousWakeTime, xTimeIncrement )                   \
    do {                                                                        \
        ( void ) xTaskDelayUntil( ( pxPreviousWakeTime ), ( xTimeIncrement ) ); \
    } while( 0 )

  从上面的代码可以看出,宏 vTaskDelayUntil() 实际上就是函数 xTaskDelayUntil() ,函数 xTaskDelayUntil() 在 task.c 文件中有定义,具体的代码如下所示:

BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,              // 上一次阻塞超时时间
                            const TickType_t xTimeIncrement )                   // 延时的时间
{
    TickType_t xTimeToWake;
    BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;

    traceENTER_xTaskDelayUntil( pxPreviousWakeTime, xTimeIncrement );

    configASSERT( pxPreviousWakeTime );
    configASSERT( ( xTimeIncrement > 0U ) );

    vTaskSuspendAll();                                                          // 挂起任务调度器
    {
        /* Minor optimisation.  The tick count cannot change in this
            * block. */
        const TickType_t xConstTickCount = xTickCount;

        configASSERT( uxSchedulerSuspended == 1U );

        // 计算任务下一次阻塞超时的时间,这个阻塞超时时间是相对于上一次阻塞超时的时间的
        xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;

        if( xConstTickCount < *pxPreviousWakeTime )                             // 如果在上一次阻塞超时后,系统时钟节拍计数器溢出过
        {
            // 只有在下一次阻塞超时时间也溢出,并且下一次阻塞超时时间大于系统时钟节拍计数器的值时,要做相应的溢出处理,否则就好像没有溢出
            if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
            {
                xShouldDelay = pdTRUE;                                          // 标记因为溢出,需要做相应的处理
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            // 系统时钟节拍计数器没有溢出, 但是下一次阻塞超时时间溢出了,并且下一次阻塞超时时间大于系统时钟节拍计数器的值时,需要做相应的溢出处理
            if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
            {
                xShouldDelay = pdTRUE;                                          // 标记因为溢出,需要做相应的溢出处理
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }

        *pxPreviousWakeTime = xTimeToWake;                                      //  更新上一次阻塞超时时间为下一次阻塞超时时间

        if( xShouldDelay != pdFALSE )                                           // 根据标记,做相应的溢出处理
        {
            traceTASK_DELAY_UNTIL( xTimeToWake );                               // 用于调试

            // 将任务添加到阻塞态任务列表中
            prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    xAlreadyYielded = xTaskResumeAll();                                         // 恢复任务调度器运行,调用此函数会返回是否需要进行任务切换

    if( xAlreadyYielded == pdFALSE )                                            // 根据标志进行任务切换
    {
        taskYIELD_WITHIN_API();
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    traceRETURN_xTaskDelayUntil( xShouldDelay );

    return xShouldDelay;
}

  从上面的代码可以看出,函数 xTaskDelayUntil() 对任务进行延时的操作,是相对于任务上一次阻塞超时的时间,而不是相对于系统当前的时钟节拍计数器的值,因此,函数能够更准确地以一定的频率进行任务延时,更加适用于需要按照一定频率运行的任务。

2.3、xTaskAbortDelay()函数

  xTaskAbortDelay() 用于终止处于阻塞态任务的阻塞,此函数在 task.c 文件中有定于,具体的代码如下所示:

BaseType_t xTaskAbortDelay( TaskHandle_t xTask )
{
    TCB_t * pxTCB = xTask;
    BaseType_t xReturn;

    traceENTER_xTaskAbortDelay( xTask );

    configASSERT( pxTCB );

    vTaskSuspendAll();                                                          // 挂起任务调度器*
    {
        if( eTaskGetState( xTask ) == eBlocked )                                // 被中断阻塞时的任务一定处于阻塞状态
        {
            xReturn = pdPASS;

            ( void ) uxListRemove( &( pxTCB->xStateListItem ) );                // 将任务从所在任务列表(阻塞态任务列表)中移除

            taskENTER_CRITICAL();                                               // 进入临界区K
            {
                // 阻塞任务因为等待时间而被阻塞,因为要中断任务阻塞,因此将任务从所在事件列表中移除
                if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
                {
                    ( void ) uxListRemove( &( pxTCB->xEventListItem ) );        // 将任务从所在事件列表中移除
                    pxTCB->ucDelayAborted = ( uint8_t ) pdTRUE;                 // 标记任务阻塞被中断
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            taskEXIT_CRITICAL();                                                // 退出临界区

            prvAddTaskToReadyList( pxTCB );                                     // 将任务添加到就绪态任务列表中

            #if ( configUSE_PREEMPTION == 1 )                                   // 此宏用于启用抢占式调度
            {
                #if ( configNUMBER_OF_CORES == 1 )
                {
                    // 如果启用了抢占式调度,就需要判断刚添加到就绪态任务列表中的任务是否为系统中优先级最高的就绪态任务,如果是,就绪要进行任务切换
                    if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
                    {
                        xYieldPendings[ 0 ] = pdTRUE;                           // 标记需要进行任务切换
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                #else /* #if ( configNUMBER_OF_CORES == 1 ) */
                {
                    taskENTER_CRITICAL();
                    {
                        prvYieldForTask( pxTCB );
                    }
                    taskEXIT_CRITICAL();
                }
                #endif /* #if ( configNUMBER_OF_CORES == 1 ) */
            }
            #endif /* #if ( configUSE_PREEMPTION == 1 ) */
        }
        else
        {
            xReturn = pdFAIL;                                                   // 待取消阻塞的任务不处于阻塞态, 返回错误
        }
    }
    ( void ) xTaskResumeAll();                                                  // 恢复任务调度器

    traceRETURN_xTaskAbortDelay( xReturn );

    return xReturn;
}

  xTaskAbortDelay() 函数会将阻塞任务从阻塞态任务列表中移除,并将任务添加到就绪态任务列表中。

  因为有任务添加到就绪态任务列表中,因此需要的启用抢占式调度的情况下,判断刚添加就绪态任务列表中的任务是否为系统中优先级最高的任务,如果是的话,就需要进行任务切换,这就是抢占式调度的抢占机制。

  任务被阻塞可能不仅仅因为是被延时,还有可能是在等待某个事件的发生,如果任务是因为等待事件而被阻塞,那么中断阻塞的时候,需要将任务从所在事件列表中移除。

三、实验例程

  main() 函数:

int main(void)
{
    HAL_Init();
    System_Clock_Init(8, 336, 2, 7);
    Delay_Init(168);

    UART_Init(&g_usart1_handle, USART1, 115200);
    LED_Init();

    freertos_demo();
  
    return 0;
}

  FreeRTOS 的入口函数:

/**
 * @brief FreeRTOS的入口函数
 * 
 */
void freertos_demo(void)
{
    xTaskCreate((TaskFunction_t        ) start_task,                            // 任务函数
                (char *                ) "start_task",                          // 任务名
                (configSTACK_DEPTH_TYPE) START_TASK_STACK_SIZE,                 // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) START_TASK_PRIORITY,                   // 任务优先级
                (TaskHandle_t *        ) start_task_handle);                    // 任务句柄

    vTaskStartScheduler();                                                      // 开启任务调度器
}

  start_task 任务配置:

/**
 * START_TASK 任务配置
 * 包括: 任务优先级 任务栈大小 任务句柄 任务函数
 */
#define START_TASK_PRIORITY     1
#define START_TASK_STACK_SIZE   128

TaskHandle_t start_task_handle;

void start_task(void *pvParameters );

/**
 * @brief 开始任务的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();                                                       // 进入临界区,关闭中断

    xTaskCreate((TaskFunction_t        ) task1,                                 // 任务函数
                (char *                ) "task1",                               // 任务名
                (configSTACK_DEPTH_TYPE) TASK1_STACK_SIZE,                      // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) TASK1_PRIORITY,                        // 任务优先级
                (TaskHandle_t *        ) &task1_handle);                        // 任务句柄

    xTaskCreate((TaskFunction_t        ) task2,                                 // 任务函数
                (char *                ) "task2",                               // 任务名
                (configSTACK_DEPTH_TYPE) TASK2_STACK_SIZE,                      // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) TASK2_PRIORITY,                        // 任务优先级
                (TaskHandle_t *        ) &task2_handle);                        // 任务句柄

    vTaskDelete(NULL);                                                          // 删除任务自身

    taskEXIT_CRITICAL();                                                        // 退出临界区,重新开启中断
}

  task1 任务配置:

/**
 * TASK1 任务配置
 * 包括: 任务优先级 任务栈大小 任务句柄 任务函数
 */
#define TASK1_PRIORITY     2
#define TASK1_STACK_SIZE   128

TaskHandle_t task1_handle;

void task1(void *pvParameters);

/**
 * @brief 任务1的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void task1(void *pvParameters)
{
    while (1)
    {
        // task1的任务时长大于等于600ms
        HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_9);
        printf("task1 开始运行\r\n");
        Delay_ms(100);
        vTaskDelay(500);
    }
}

  task2 任务配置:

/**
 * TASK2 任务配置
 * 包括: 任务优先级 任务栈大小 任务句柄 任务控制块  任务栈 任务函数
 */
#define TASK2_PRIORITY     2
#define TASK2_STACK_SIZE   128

TaskHandle_t task2_handle;

void task2(void *pvParameters);

/**
 * @brief 任务2的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void task2(void *pvParameters )
{
    TickType_t xLastWakeTime = xTaskGetTickCount();                             // 获取任务开始时间

    while (1)
    {
        // task1的任务时长等于500ms
        HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
        printf("task2 开始运行\r\n");
        Delay_ms(100);
        vTaskDelayUntil(&xLastWakeTime, 500);
    }
}
posted @ 2024-03-11 19:22  星光映梦  阅读(1094)  评论(0)    收藏  举报