博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
FreeRTOS源码分析与应用开发03:时间管理
阅读量:249 次
发布时间:2019-03-01

本文共 10536 字,大约阅读时间需要 35 分钟。

目录


1. FreeRTOS延时函数

1.1 延时列表组织

FreeRTOS中的延时列表由如下组件构成,

static List_t xDelayedTaskList1;static List_t xDelayedTaskList2;static List_t * volatile pxDelayedTaskList; // 任务延时列表指针static List_t * volatile pxOverflowDelayedTaskList; // 任务延时溢出列表指针static volatile TickType_t xNextTaskUnblockTime // 下一任务唤醒时间

① 由于记录系统时间的全局变量xTickCount为uint32_t类型,所以一定会发生计数溢出,因此FreeRTOS中定义了2个延时列表,分别用于处理溢出 & 非溢出时的任务延时

当xTickCount计数溢出时,会交换pxDelayedTaskList & pxOverflowDelayedTaskList指针的指向

② 全局变量xNextTaskUnblockTime记录的是pxDelayTaskList指向的延时列表中下一个要被唤醒任务的绝对系统时间

1.2 vTaskDelay函数

// xTicksToDelay为要延时的tick数void vTaskDelay( const TickType_t xTicksToDelay ){    BaseType_t xAlreadyYielded = pdFALSE;    // 如果延时tick数为0,将会触发一次任务切换    if( xTicksToDelay > ( TickType_t ) 0U )    {        configASSERT( uxSchedulerSuspended == 0 );        // 此处通过关闭调度器保护全局资源        vTaskSuspendAll();        {            prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );        }        xAlreadyYielded = xTaskResumeAll();    }        // 如果xTaskResumeAll中没有触发任务调度,此处触发一次    // 因为要将调用vTaskDelay函数的任务阻塞,肯定要进行一次任务调度    if( xAlreadyYielded == pdFALSE )    {        portYIELD_WITHIN_API();    }}

说明1:相对延时

vTaskDelay实现的是相对性延时,指定的延时时间从vTaskDelay调用结束之后开始计算

② 正是由于相对延时的特性,vTaskDelay并不适用于周期性执行任务的场合

注意区分如下2种表述,

a. 每10ms运行一次

b. 每次运行后延时10ms

 

说明2:prvAddCurrentTaskToDelayedList函数

该函数是实现延时的核心函数

/** xTicksToWait:要延时的tick数* xCanBlockIndefinitely:是否支持挂起式睡眠*/static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait,const BaseType_t xCanBlockIndefinitely ){TickType_t xTimeToWake;const TickType_t xConstTickCount = xTickCount; // 取出当前系统时间    // 将任务从就绪列表删除,如果该优先级已无任务,则注销该优先级    // 调用vTaskDelay函数的任务一定处于就绪列表    if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) ==    ( UBaseType_t ) 0 )	{        portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority,        uxTopReadyPriority );	}	#if ( INCLUDE_vTaskSuspend == 1 )	{        if( ( xTicksToWait == portMAX_DELAY ) &&        ( xCanBlockIndefinitely != pdFALSE ) )        {            // 如果延时tick数为portMAX_DELAY且支持挂起式睡眠            // 则直接将任务加入挂起列表            vListInsertEnd( &xSuspendedTaskList,            &( pxCurrentTCB->xStateListItem ) );        }        else        {            // xTimeToWake为唤醒当前任务的绝对系统时间            xTimeToWake = xConstTickCount + xTicksToWait;            // 延时列表中的任务,按唤醒绝对系统时间升序排列            listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ),            xTimeToWake );            if( xTimeToWake < xConstTickCount )            {                // 如果延时溢出,则加入任务延时溢出列表                vListInsert( pxOverflowDelayedTaskList,                &( pxCurrentTCB->xStateListItem ) );            }            else            {                // 如果延时未溢出,则加入任务延时列表                vListInsert( pxDelayedTaskList,                &( pxCurrentTCB->xStateListItem ) );                // 如果该任务唤醒时间早于当前系统下一任务唤醒时间,                // 则更新系统下一任务唤醒时间                if( xTimeToWake < xNextTaskUnblockTime )                {                    xNextTaskUnblockTime = xTimeToWake;                }            }        }	}	#else /* INCLUDE_vTaskSuspend */	// 不支持vTaskSuspend函数的情况,除了不使用挂起列表,其余流程相同	#endif /* INCLUDE_vTaskSuspend */}

 

说明3:延时tick数计算宏

FreeRTOS中的pdMS_TO_TICKS宏,提供了将毫秒数转换为ticks数的功能,可用于计算延时tick数

#define pdMS_TO_TICKS( xTimeInMs ) ( ( TickType_t ) \( ( ( TickType_t ) ( xTimeInMs ) * ( TickType_t ) configTICK_RATE_HZ )\/ ( TickType_t ) 1000 ) )

注意:此处一定要先计算乘法后计算除法,因为如果configTICK_RATE_HZ小于1000,先计算除法的结果就是始终为0

1.3 vTaskDelayUntil函数

/** pxPreviousWakeTime:指向保存上次任务唤醒时间的变量* xTimeIncrement:在上次任务唤醒时间的基础上要延时的tick数*/void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime,const TickType_t xTimeIncrement ){	TickType_t xTimeToWake;	BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;    configASSERT( pxPreviousWakeTime ); // 指针有效性    configASSERT( ( xTimeIncrement > 0U ) ); // 延时时间有效性    configASSERT( uxSchedulerSuspended == 0 ); // 不能关闭调度器    // 关闭调度器,实现任务间的资源保护,中断仍可响应    vTaskSuspendAll();    {        // 获取当前系统时间        const TickType_t xConstTickCount = xTickCount;        // 基于上次任务唤醒时间,计算本次任务唤醒时间        xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;        // 判断任务是否需要进入延时        if( xConstTickCount < *pxPreviousWakeTime )        {            // xConstTickCount发生溢出            // 那么只要xTimeToWake也溢出,并且超过任务主体时间,则仍需要延时            if( ( xTimeToWake < *pxPreviousWakeTime ) &&            ( xTimeToWake > xConstTickCount ) )            {                xShouldDelay = pdTRUE;            }        }        else        {            // xConstTickCount没有发生溢出            // 那么只要xTimeToWake超过任务主体时间,则仍需要延时            if( ( xTimeToWake < *pxPreviousWakeTime ) ||            ( xTimeToWake > xConstTickCount ) )            {                xShouldDelay = pdTRUE;            }        }        // 更新上次任务唤醒时间,用于下次调用vTaskDelayUntil        *pxPreviousWakeTime = xTimeToWake;        // 如果任务仍需要延时,则将其加入延时列表        if( xShouldDelay != pdFALSE )        {            prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount,            pdFALSE );        }    }    xAlreadyYielded = xTaskResumeAll();    // 如果恢复调度器时没有触发任务调度,此处强制触发一次    // 因为如果任务需要延时,需要调度其他任务运行    if( xAlreadyYielded == pdFALSE )    {        portYIELD_WITHIN_API();    }}

说明1:绝对延时

① 从vTaskDelayUntil函数的实现可知,任务的唤醒时间是基于上次唤醒时间计算的,因此是一种绝对延时,也就是在固定周期内会唤醒任务

② 调用vTaskDelayUntil函数时传递的参数xTimeIncrement可以理解为任务运行的周期

注意:这里的周期性运行并不是绝对的,会受到任务优先级 & 中断的影响

 

说明2:判断是否要延时是vTaskDelayUntil函数的关键

结合下面3张示意图,很容易理解何种情况下任务仍然需要延时

① xConstTickCount & xTimeToWake均没有发生溢出

② xConstTickCount没有发生溢出,但xTimeToWake发生溢出

③ xConstTickCount & xTimeToWake均发生溢出

注意:从上述示意图中可见,任务执行的时间必须小于任务周期时间xTimeIncrement,否则周期性的延时是没有意义的

 

说明3:延时tick数的计算

在调用prvAddCurrentTaskToDelayedList时需要传递的是任务需要延时的tick数,使用的计算方式如下,

xTimeToWake - xConstTickCount

 这里需要特别注意,当处于上文中第2种示意图的情况时,xTimeToWake - xConstTickCount得到的是一个负数,但是因为参与计算的变量均为4B,截断后得到的正好是正确的值

作为说明,我们以1B的变量为例,

0x00(xTimeToWake) - 0xFF(xConstTickCount) = -255D = 0b1 0000 0001,

1B变量截断后的值为0x1,这正是要延时的正确tick数

 

说明4:vTaskDelayUntil函数使用示例

void TestTask(void *pvParameters){    TickType_t PreviousWakeTime;    const TickType_t TimeIncrement = pdMS_TO_TICKS(50);        PreviousWakeTime = xTaskGetTickCount();    for (;;)    {        /* 任务主体 */        vTaskDelayUntil(&PreviousWakeTime, TimeIncrement);    }}

任务中第1次调用vTaskDelayUntil函数时,需要将PreviousWakeTime初始化为进入任务的for循环的时间点,也就是第1次进入任务主体的时间点。在以后的运行中,vTaskDelayUntil函数中会自动更新PreviousWakeTime

2. FreeRTOS系统时钟节拍

2.1 SysTick定时器

2.1.1 概述

SysTick属于Cortex-M内核,而不是属于SoC

② SysTick本质上是一个24位的向下计数器

③ 设置好定时周期,使能SysTick中断之后,SysTick会周期性触发中断

FreeRTOS使用SysTick作为系统时钟

2.1.2 重要寄存器

SysTick共有4个寄存器,

① CTRL寄存器

② LOAD寄存器

2.1.3 设置示例

vPortSetupTimerInterrupt函数中,对SysTick进行了设置,该函数调用关系如下,

vTaskStartScheduler--> xPortStartScheduler    --> vPortSetupTimerInterrupt

① SysTick重装载值根据硬件系统时钟频率和操作系统时钟频率计算得到

② 在控制寄存器中,设置使用系统时钟,并使能中断与SysTick

至此,SysTick即可按照configTICK_RATE_HZ的设置产生中断

 

注意:由于SysTick的计数值是有24位上限的,所以定时的周期也有上限,也就是2^24 / configSYSTICK_CLOCK_HZ

configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ = 2^24--> 1 / configTICK_RATE_HZ = 2^24 / configSYSTICK_CLOCK_HZ--> configTICK_RATE_HZ = configSYSTICK_CLOCK_HZ / 2^24这里被限制的其实是configTICK_RATE_HZ可设置的值的下限

2.2 SysTick中断处理

2.2.1 xPortSysTickHandler函数

 

SysTick中断处理函数的核心是调用xTaskIncrementTick函数,并根据该函数的返回值,确定是否需要触发任务调度

 

说明:此处开关中断为何没有使用portSET_INTERRUPT_MASK_FROM_ISR

正如代码中注释,SysTick中断优先级最低,所以当该中断运行时,不会抢占其他中断,所以在确知不会发生中断嵌套的情况下,没有保存 & 恢复当前BASEPRI寄存器的值

2.2.2 xTaskIncrementTick函数

BaseType_t xTaskIncrementTick( void ){TCB_t * pxTCB;TickType_t xItemValue;BaseType_t xSwitchRequired = pdFALSE;	if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )	{        // 任务调度器没有关闭        // 增加系统tick计数        const TickType_t xConstTickCount = xTickCount + 1;        xTickCount = xConstTickCount;        // 如果系统tick计数溢出,则交换延时列表        if( xConstTickCount == ( TickType_t ) 0U )        {            taskSWITCH_DELAYED_LISTS();        }        // 如果系统当前时间 >= 下一任务唤醒时间,说明有任务延时到期,需要唤醒        if( xConstTickCount >= xNextTaskUnblockTime )        {            for( ;; )            {                if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )                {                    // 延时列表为空,设置最大下一任务解锁时间,并退出循环                    xNextTaskUnblockTime = portMAX_DELAY;                    break;                }                else                {                    // 取出延时列表第1个任务的唤醒时间                    // 延时列表中的任务按唤醒时间升序排列                    pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY(                    pxDelayedTaskList );                    xItemValue = listGET_LIST_ITEM_VALUE(                    &( pxTCB->xStateListItem ) );                    // 如果延时列表队首任务已经无需唤醒                    // 则更新下一任务唤醒时间并退出循环                    if( xConstTickCount < xItemValue )                    {                        xNextTaskUnblockTime = xItemValue;                        break;                    }                                        // 将任务从延时队列删除                    ( void ) uxListRemove( &( pxTCB->xStateListItem ) );                    // 如果延时的任务还在等待事件,则将其从相应的事件列表中删除                    // 此时对应任务带延时等待事件已超时                    if( listLIST_ITEM_CONTAINER(                    &( pxTCB->xEventListItem ) ) != NULL )                    {                        ( void ) uxListRemove( &( pxTCB->xEventListItem ));                    }                    // 将唤醒的任务加入就绪列表                    prvAddTaskToReadyList( pxTCB );                    // 如果使能抢占,则当唤醒任务优先级高于当前任务时,                    // 标识需要触发任务调度                    #if (  configUSE_PREEMPTION == 1 )                    {                        if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )                        {                            xSwitchRequired = pdTRUE;                        }                    }                    #endif /* configUSE_PREEMPTION */                }            }        }        // 如果使能抢占与同优先级共享时间片,且当前任务优先级下有其他任务,        // 标识需要触发任务调度        #if ( ( configUSE_PREEMPTION == 1 ) &&        ( configUSE_TIME_SLICING == 1 ) )        {            if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[            pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )            {                xSwitchRequired = pdTRUE;            }        }        #endif	}	else	{        // 如果任务调度器被关闭,记录pending的时钟tick        // 被pending的时钟tick将在xTaskResumeAll函数中被处理        ++uxPendedTicks;	}    // 如果使能抢占,检查是否有被pending的调度请求    #if ( configUSE_PREEMPTION == 1 )    {        if( xYieldPending != pdFALSE )        {            xSwitchRequired = pdTRUE;        }    }    #endif /* configUSE_PREEMPTION */    return xSwitchRequired;}

说明:交换延时列表

当系统tick计数溢出时,会调用taskSWITCH_DELAYED_LISTS宏交换延时列表

注意:在交换延时列表后,需要更新下一任务唤醒时间,即用交换后的延时列表队首任务的唤醒时间更新下一任务唤醒时间(如果没有任务,则设置为portMAX_DELAY) 

转载地址:http://kqwx.baihongyu.com/

你可能感兴趣的文章