当前位置:首页 > 公众号精选 > 码农爱学习

软件定时器是FreeRTOS中的一个重要模块,使用软件定时器可以方便的实现一些与超时或周期性相关的功能,本篇从FreeRTOS的源码入手,来分析FreeRTOS软件定时器的运行机理。

1 基础知识

1.1 软件定时器与硬件定时器的区别

硬件定时器

  • 每次在定时时间到达之后就会自动触发一个中断,用户在中断服务函数中处理信息

  • 硬件定时器的精度一般很高,可以达到纳秒级别

  • 硬件定时器是芯片本身提供的定时功能

软件定时器

  • 指定时间到达后要调用回调函数(也称超时函数),用户在回调函数中处理信息

  • 硬件定时器的定时精度与系统时钟的周期有关,一般系统利用SysTick作为软件定时器的基础时钟,系统节拍配置为FreeRTOSConfig.h中的configTICK_RATE_HZ,默认是1000,那么系统的时钟节拍周期就为1ms

  • 软件定时器是由操作系统提供的一类系统接口

注意:软件定时器回调函数的上下文是任务,回调函数要快进快出,且回调函数中不能有任何阻塞任务运行的情况,如vTaskDelay()以及其它能阻塞任务运行的函数。

1.2 软件定时器的两种工作模式

FreeRTOS提供的软件定时器支持单次模式和周期模式

  • 单次模式:当用户创建了定时器并启动了定时器后,定时时间到了,只执行一次回调函数之后就将该定时器删除,不再重新执行。

  • 周期模式:这个定时器会按照设置的定时时间循环执行回调函数,直到用户将定时器删除

2软件定时器工作原理

通过查看FreeRTOS的源码,可以发现,软件定时器的运行原理实际是FreeRTOS 通过一个 prvTimerTask任务(也叫守护任务Daemon)管理软定时器,它是在启动调度器时自动创建的。另外,软件定时器在FreeRTOS中是可选功能,如果需要使用软件定时器,需要设置 FreeRTOSConfig.h 中的宏定义configUSE_TIMERS为1 。

先用一个图来表示整个创建过程:

下面来看一下启动调度器时是怎么创建Daemon任务的。

2.1 任务调度器函数创建Daemon任务

main函数的最后会启动FreeRTOS的任务调度函数,在该函数中会创建软件定时器任务(即Daemon守护任务),并且可以看到是通过宏定义的方式选择编译:

/* 启动调度器 */ void vTaskStartScheduler( void ){    ...略去部分代码    #if ( configUSE_TIMERS == 1 )    {        if( xReturn == pdPASS )        {            /* 创建软件定时器任务(守护任务) */            xReturn = xTimerCreateTimerTask();        }        else        {            mtCOVERAGE_TEST_MARKER();        }    }    #endif /* configUSE_TIMERS */    ...略去部分代码 }

xTimerCreateTimerTask()只是一个函数名,它内部的函数内容如下。

2.2 创建Daemon任务

软件定时器任务(Daemon任务)的创建是通过xTaskCreate方法来创建,在创建守护任务之前,还要先通过prvCheckForValidListAndQueue函数创建两个列表和一个消息队列:

BaseType_t xTimerCreateTimerTask( void ){    BaseType_t xReturn = pdFAIL;    /* 创建列表与消息队列 */    prvCheckForValidListAndQueue();    if( xTimerQueue != NULL ){        #if( configSUPPORT_STATIC_ALLOCATION == 1 )        ...略去部分代码        #else        {            /* 创建软件定时器任务(守护任务) */            xReturn = xTaskCreate(  prvTimerTask,                                    "Tmr Svc",                                    configTIMER_TASK_STACK_DEPTH,                                    NULL,                                    ( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,                                    &xTimerTaskHandle );        }        #endif /* configSUPPORT_STATIC_ALLOCATION */}    else{        mtCOVERAGE_TEST_MARKER();}    configASSERT( xReturn );    return xReturn;}

创建列表与消息队列的具体函数内容如下:

2.3 创建列表与消息队列

由于系统节拍采用32位变量进行计数,总有一天会溢出,所以软件定时器使用了两个列表:

  • 当前定时器列表pxCurrentTimerList :系统新创建并激活的定时器都会以超时时间升序的方式插入到pxCurrentTimerList列表中。系统在定时器任务中扫描pxCurrentTimerList中的第一个定时器,看是否已超时,若已经超时了则调用软件定时器回调函数,否则将定时器任务挂起。

  • 溢出定时器列表pxOverflowTimerList:在软件定时器溢出的时候使用,作用与pxCurrentTimerList一致。

定时器列表会按照唤醒时间从早到晚挂接在当前定时器列表中,唤醒时间如果溢出了就挂接在溢出定时器列表中。当系统节拍溢出之后,两个列表的功能会进行交换,即当前列表变为溢出列表,溢出列表变为当前列表。

此外,FreeRTOS的软件定时器还使用了一个消息队列xTimerQueue,利用“定时器命令队列”向软件定时器任务发送一些命令,任务在接收到命令就会去处理命令对应的程序,比如启动定时器,停止定时器,复位、删除、改变周期等。

假如定时器任务处于阻塞状态,我们又需要马上再添加一个软件定时器的话,就是采用这种消息队列命令的方式进行添加,才能唤醒处于等待状态的定时器任务,并且在任务中将新添加的软件定时器添加到软件定时器列表中

(注:事件标志组在中断中设置事件标志,实际也是通过队列发送消息给软件定时器任务来执行)

/* 检查是否有可用的列表和队列 */static void prvCheckForValidListAndQueue( void ){    /* 进入临界区 */    taskENTER_CRITICAL();{        /* 还没有创建队列 */        if( xTimerQueue == NULL )        {                       /* 初始化定时器列表1 */            vListInitialise( &xActiveTimerList1 );            /* 初始化定时器列表2 */            vListInitialise( &xActiveTimerList2 );            /* 当前定时器列表 */            pxCurrentTimerList = &xActiveTimerList1;            /* 溢出定时器列表 */            pxOverflowTimerList = &xActiveTimerList2;            #if( configSUPPORT_STATIC_ALLOCATION == 1 )            ...略去部分代码            #else            {                /* 创建定时器消息队列 */                xTimerQueue = xQueueCreate( ( UBaseType_t ) configTIMER_QUEUE_LENGTH, sizeof( DaemonTaskMessage_t ) );            }            #endif            #if ( configQUEUE_REGISTRY_SIZE > 0 )            {                if( xTimerQueue != NULL )                {                    vQueueAddToRegistry( xTimerQueue, "TmrQ" );                }                else                {                    mtCOVERAGE_TEST_MARKER();                }            }            #endif /* configQUEUE_REGISTRY_SIZE */        }        else        {            mtCOVERAGE_TEST_MARKER();        }}    taskEXIT_CRITICAL();}

既然消息队列是用来处理软件定时器的一些操作指令的,那这些在哪里呢?其实就是软件定时器的一些API函数,如下。

2.4 软件定时器API函数实际原理

软件定时器的多种API函数,如启动、停止、删除、复位、改变周期等,实际是通过宏定义的方式提供:

/*启动定时器*/#define xTimerStart(xTimer, xTicksToWait) xTimerGenericCommand((xTimer), tmrCOMMAND_START, (xTaskGetTickCount()), NULL, (xTicksToWait))/*停止定时器*/#define xTimerStop(xTimer, xTicksToWait) xTimerGenericCommand((xTimer), tmrCOMMAND_STOP, 0U, NULL, (xTicksToWait))/*改变定时器周期*/#define xTimerChangePeriod(xTimer, xNewPeriod, xTicksToWait) xTimerGenericCommand((xTimer), tmrCOMMAND_CHANGE_PERIOD, (xNewPeriod), NULL, (xTicksToWait))/*删除定时器*/#define xTimerDelete(xTimer, xTicksToWait) xTimerGenericCommand((xTimer), tmrCOMMAND_DELETE, 0U, NULL, (xTicksToWait))/*复位定时器*/#define xTimerReset(xTimer, xTicksToWait) xTimerGenericCommand((xTimer), tmrCOMMAND_RESET, (xTaskGetTickCount()), NULL, (xTicksToWait))/*从中断中启动定时器*/#define xTimerStartFromISR(xTimer, pxHigherPriorityTaskWoken) xTimerGenericCommand((xTimer), tmrCOMMAND_START_FROM_ISR, (xTaskGetTickCountFromISR()), (pxHigherPriorityTaskWoken), 0U)/*从中断中停止定时器*/#define xTimerStopFromISR(xTimer, pxHigherPriorityTaskWoken) xTimerGenericCommand((xTimer), tmrCOMMAND_STOP_FROM_ISR, 0, (pxHigherPriorityTaskWoken), 0U)/*从中断中改变定时器周期*/#define xTimerChangePeriodFromISR(xTimer, xNewPeriod, pxHigherPriorityTaskWoken) xTimerGenericCommand((xTimer), tmrCOMMAND_CHANGE_PERIOD_FROM_ISR, (xNewPeriod), (pxHigherPriorityTaskWoken), 0U)/*从中断中复位定时器*/#define xTimerResetFromISR(xTimer, pxHigherPriorityTaskWoken) xTimerGenericCommand((xTimer), tmrCOMMAND_RESET_FROM_ISR, (xTaskGetTickCountFromISR()), (pxHigherPriorityTaskWoken), 0U)

这些API函数对应的宏定义,本质上又都是调用了xTimerGenericCommand函数来实现对消息的打包和发送。

2.5 软件定时器打包命令与发送

该函数将命令打包成队列项发送给xTimerQueue消息队列,由软件定时器任务(守护任务来)接收并进行处理。

/* 软件定时器打包命令与发送 */BaseType_t xTimerGenericCommand( TimerHandle_t xTimer, const BaseType_t xCommandID, const TickType_t xOptionalValue, BaseType_t * const pxHigherPriorityTaskWoken, const TickType_t xTicksToWait ){    BaseType_t xReturn = pdFAIL;    DaemonTaskMessage_t xMessage;    configASSERT( xTimer );    if( xTimerQueue != NULL ){        /* 命令码 */        xMessage.xMessageID = xCommandID;        /* 命令有效值 */        xMessage.u.xTimerParameters.xMessageValue = xOptionalValue;        /* 定时器句柄 */        xMessage.u.xTimerParameters.pxTimer = xTimer;        /* 不带中断命令 */        if(xCommandID < tmrFIRST_FROM_ISR_COMMAND)        {            /* 调度器正在运行 */            if(xTaskGetSchedulerState() == taskSCHEDULER_RUNNING)            {                /* 将命令消息发送到队列,可以阻塞一定时间 */                xReturn = xQueueSendToBack(xTimerQueue, &xMessage, xTicksToWait);            }            /* 调度器不在运行 */            else            {                /* 将命令消息发送到队列 ,不带阻塞时间*/                xReturn = xQueueSendToBack(xTimerQueue, &xMessage, tmrNO_DELAY);            }        }        /* 带中断命令 */        else        {            /* 将命令消息发送到队列 */            xReturn = xQueueSendToBackFromISR(xTimerQueue, &xMessage, pxHigherPriorityTaskWoken);        }        traceTIMER_COMMAND_SEND( xTimer, xCommandID, xOptionalValue, xReturn );}    else{        mtCOVERAGE_TEST_MARKER();}    return xReturn;}

上面分析的差不多了,现在回到重点,回顾2.2的xTimerCreateTimerTask()函数,在创建列表与消息队列后,会使用xTaskCreate方法来创建软件定时器任务prvTimerTask(),该任务实体的具体内容如下:

2.6 软件定时器任务基本功能(三部分)

软件定时器任务的具体内容可分为三部分:

  • 获取最近一次定时器超时时间

  • 处理超时的定时器或者让队列阻塞

  • 处理队列接收到的命令

三部分不断循环处理实现Daemon任务。

static void prvTimerTask( void *pvParameters ){TickType_t xNextExpireTime;BaseType_t xListWasEmpty;    /* Just to avoid compiler warnings. */    ( void ) pvParameters;    #if( configUSE_DAEMON_TASK_STARTUP_HOOK == 1 )    ...略去部分代码    #endif /* configUSE_DAEMON_TASK_STARTUP_HOOK */    for( ;; ){        /* 获取最近一次定时器超时时间 */        xNextExpireTime = prvGetNextExpireTime(&xListWasEmpty);        /* 处理超时的定时器或者让队列阻塞 */        prvProcessTimerOrBlockTask(xNextExpireTime, xListWasEmpty);        /* 处理队列接收到的命令 */        prvProcessReceivedCommands();}}

以上介绍了从启动调度器到实现Daemon任务的具体过程,下面来详细分析Daemon任务中的三部分功能的细节。

3 软件定时器任务功能分析

先来一张整体结构图:

首先是从定时器列表中获取下一次的溢出时间,因为各定时器的溢出时间是按照升序排列的,因此只需获取下一次的溢出时间。

3.1 获取下一个定时超时时间

/* 获取下一次的定时器超时时间 */static TickType_t prvGetNextExpireTime( BaseType_t * const pxListWasEmpty ){TickType_t xNextExpireTime;    /* 判断当前定时器列表是否为空 */    *pxListWasEmpty = listLIST_IS_EMPTY( pxCurrentTimerList );    /* 当前列表非空 */    if( *pxListWasEmpty == pdFALSE ){        /* 获取最近超时时间 */        xNextExpireTime = listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxCurrentTimerList );}    else /* 当前列表为空 */{        /*超时时间设为0,使任务非阻塞 */        xNextExpireTime = ( TickType_t ) 0U;}    return xNextExpireTime;}

3.2 处理或阻塞软件定时器任务

那系统如何处理软件定时器列表?系统在不断运行,而xTimeNow(xTickCount)随着SysTick的触发一直在增长,在软件定时器任务运行的时候会获取下一个要唤醒的定时器:

  • 比较当前系统时间xTimeNow是否大于或等于下一个定时器唤醒时间xTicksToWait

  • 若大于则表示已经超时,定时器任务将会调用对应定时器的回调函数

  • 否则将软件定时器任务挂起,直至下一个要唤醒的软件定时器时间到来或者接收到命令消息

/* 处理或阻塞软件定时器任务 */static void prvProcessTimerOrBlockTask( const TickType_t xNextExpireTime, BaseType_t xListWasEmpty ){    TickType_t xTimeNow;    BaseType_t xTimerListsWereSwitched;    /* 挂起调度器 */    vTaskSuspendAll();    {        /* 获取当前时间,并判断是否需要切换定时器列表,如果需要则切换 */        xTimeNow = prvSampleTimeNow( &xTimerListsWereSwitched );        /* 定时器列表没有切换 */        if( xTimerListsWereSwitched == pdFALSE )        {            /* 当前列表中有定时器,且下次唤醒时间小于当前时间,即超时了 */            if( ( xListWasEmpty == pdFALSE ) && ( xNextExpireTime <= xTimeNow ) )            {                /* 解除调度器挂起 */                ( void )xTaskResumeAll();                /* 处理超时的定时器 */                prvProcessExpiredTimer( xNextExpireTime, xTimeNow );            }            else/* 定时器列表为空,或者没有超时 */            {                /* 定时器列表为空 */                if( xListWasEmpty != pdFALSE )                {                    /* 判断溢出列表是否为空,如果两个列表都为空,则无限期阻塞 */                    xListWasEmpty = listLIST_IS_EMPTY( pxOverflowTimerList );                }                /* 定时器定时时间还没到,将当前任务挂起,让队列按照给定的时间进行阻塞 */                vQueueWaitForMessageRestricted( xTimerQueue, ( xNextExpireTime - xTimeNow ), xListWasEmpty );                /* 解除调度器挂起 */                if( xTaskResumeAll() == pdFALSE )                {                    /* 申请切换任务 */                    portYIELD_WITHIN_API();                }                else                {                    mtCOVERAGE_TEST_MARKER();                }            }        }        else        {            /* 解除调度器挂起 */            ( void ) xTaskResumeAll();        }    }}

3.2.1 获取当前时间并决定是否切换列表

static TickType_t prvSampleTimeNow( BaseType_t * const pxTimerListsWereSwitched ){    TickType_t xTimeNow;    /*静态变量 记录上一次调用时系统节拍值*/    PRIVILEGED_DATA static TickType_t xLastTime = ( TickType_t ) 0U;     /*获取本次调用节拍结束器值*/    xTimeNow = xTaskGetTickCount();    /*判断节拍计数器是否溢出过*/    if( xTimeNow < xLastTime )    {        /*发生溢出,处理当前链表上所有定时器并切换管理链表*/        prvSwitchTimerLists();        *pxTimerListsWereSwitched = pdTRUE;    }    else    {        *pxTimerListsWereSwitched = pdFALSE;    }    /*更新时间记录*/    xLastTime = xTimeNow;    return xTimeNow;}

可以看到, 该函数每次调用都会记录当前系统节拍时间(TickCount), 下一次调用,通过比较相邻两次调用的值判断节拍计数器是否溢出。当系统节拍计数器溢出, 必须切换计时器列表。如果当前计时器列表中仍然引用任何计时器,那么它们一定已经过期,应该在切换列表之前进行处理。

切换列表的具体内容如下:

static void prvSwitchTimerLists( void ){    TickType_t xNextExpireTime, xReloadTime;    List_t *pxTemp;    Timer_t *pxTimer;    BaseType_t xResult;    /* 列表非空,循环处理,直至将该列表处理完 */    while( listLIST_IS_EMPTY( pxCurrentTimerList ) == pdFALSE )    {        xNextExpireTime = listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxCurrentTimerList );        /* 从列表中移除软件定时器 */        pxTimer = ( Timer_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxCurrentTimerList );        ( void ) uxListRemove( &( pxTimer->xTimerListItem ) );        traceTIMER_EXPIRED( pxTimer );        /*执行回调函数*/        pxTimer->pxCallbackFunction( ( TimerHandle_t ) pxTimer );        /*对于周期定时器*/        if( pxTimer->uxAutoReload == ( UBaseType_t ) pdTRUE )        {            /*计算重新加载值:下个溢出时间 + 定时周期*/            xReloadTime = ( xNextExpireTime + pxTimer->xTimerPeriodInTicks );            /*如果重新加载值>下个溢出时间,应该将计时器重新插入当前列表,以便在此循环中再次处理它*/            if( xReloadTime > xNextExpireTime )            {                listSET_LIST_ITEM_VALUE( &( pxTimer->xTimerListItem ), xReloadTime );                listSET_LIST_ITEM_OWNER( &( pxTimer->xTimerListItem ), pxTimer );                vListInsert( pxCurrentTimerList, &( pxTimer->xTimerListItem ) );            }            else/*否则,应该发送一个命令来重新启动计时器,以确保它只插入到列表之后列表已被交换*/            {                xResult = xTimerGenericCommand( pxTimer, tmrCOMMAND_START_DONT_TRACE, xNextExpireTime, NULL, tmrNO_DELAY );                configASSERT( xResult );                ( void ) xResult;            }        }        else        {            mtCOVERAGE_TEST_MARKER();        }    }    pxTemp = pxCurrentTimerList;    pxCurrentTimerList = pxOverflowTimerList;    pxOverflowTimerList = pxTemp;}

(切换列表这里还没完全弄明白)

下面来看一下如何处理到时(或超时)的定时器:

3.2.2 处理超时的定时器

/* 处理超时的定时器 */static void prvProcessExpiredTimer( const TickType_t xNextExpireTime, const TickType_t xTimeNow ){    BaseType_t xResult;    /* 获取最近的超时定时器 */    Timer_t *const pxTimer = ( Timer_t * )listGET_OWNER_OF_HEAD_ENTRY( pxCurrentTimerList );    /* 将最近的超时定时器从活跃列表中移除 */    (void)uxListRemove( &( pxTimer->xTimerListItem ) );    traceTIMER_EXPIRED(pxTimer);    /* 周期定时 */    if( pxTimer->uxAutoReload == ( UBaseType_t )pdTRUE )    {        /* 重新计算超时时间并加入活跃列表,如果下一次超时时间都已经过了 */        if( prvInsertTimerInActiveList( pxTimer, ( xNextExpireTime + pxTimer->xTimerPeriodInTicks ), xTimeNow, xNextExpireTime ) != pdFALSE )        {            /* 通知守护任务来处理(将定时器插入活跃列表) */            xResult = xTimerGenericCommand( pxTimer, tmrCOMMAND_START_DONT_TRACE, xNextExpireTime, NULL, tmrNO_DELAY );            configASSERT( xResult );            ( void )xResult;        }        else        {            mtCOVERAGE_TEST_MARKER();        }    }    else    {        mtCOVERAGE_TEST_MARKER();    }    /* 调用回调函数 */    pxTimer->pxCallbackFunction( ( TimerHandle_t )pxTimer );}

3.2.3 让队列按照给定的时间进行阻塞

回顾prvProcessTimerOrBlockTask()函数,定时器定时时间还没到,将当前任务挂起,直到定时器到期才唤醒或者收到命令的时候唤醒:

/* 让队列按照给定的时间进行阻塞 */void vQueueWaitForMessageRestricted( QueueHandle_t xQueue, TickType_t xTicksToWait, const BaseType_t xWaitIndefinitely ){    Queue_t *const pxQueue = xQueue;    /* 锁定队列 */    prvLockQueue( pxQueue );    /* 队列为空 */    if( pxQueue->uxMessagesWaiting == ( UBaseType_t )0U )    {        /* 将任务插入等待接收队列项而阻塞的事件列表,并加入延时列表进行阻塞延时 */        vTaskPlaceOnEventListRestricted( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait, xWaitIndefinitely );    }    /* 队列不为空 */    else    {        mtCOVERAGE_TEST_MARKER();    }    /* 解锁队列 */    prvUnlockQueue(pxQueue);}

3.3 处理命令队列中接收的消息

用户将需要处理的定时器命令发送到定时器的消息队列, Daemon 任务每次执行期间回去读取并执行,下面看看该函数的具体内容:

/*处理命令队列中接收的消息*/static void prvProcessReceivedCommands( void ){    DaemonTaskMessage_t xMessage;    Timer_t *pxTimer;    BaseType_t xTimerListsWereSwitched, xResult;    TickType_t xTimeNow;    /*消息队列接收*/    while( xQueueReceive( xTimerQueue, &xMessage, tmrNO_DELAY ) != pdFAIL )    {        #if ( INCLUDE_xTimerPendFunctionCall == 1 )        {            /* 命令码小于等于0 (事件标志组中断中置位的命令)*/            if( xMessage.xMessageID < ( BaseType_t ) 0 )            {                const CallbackParameters_t * const pxCallback = &( xMessage.u.xCallbackParameters );                configASSERT( pxCallback );                /* 执行回调函数 */                pxCallback->pxCallbackFunction( pxCallback->pvParameter1, pxCallback->ulParameter2 );            }            else            {                mtCOVERAGE_TEST_MARKER();            }        }        #endif /* INCLUDE_xTimerPendFunctionCall */        /* 命令码大于等于0 (软件定时器命令)*/        if( xMessage.xMessageID >= ( BaseType_t ) 0 )        {            /* 定时器句柄 */            pxTimer = xMessage.u.xTimerParameters.pxTimer;            /* 定时器队列项包含该定时器 */            if( listIS_CONTAINED_WITHIN( NULL, &( pxTimer->xTimerListItem ) ) == pdFALSE )            {                /* 移除该定时器 */                ( void ) uxListRemove( &( pxTimer->xTimerListItem ) );            }            else            {                mtCOVERAGE_TEST_MARKER();            }            traceTIMER_COMMAND_RECEIVED( pxTimer, xMessage.xMessageID, xMessage.u.xTimerParameters.xMessageValue );            /* 获取当前时间,并判断是否需要切换定时器列表,如果需要则切换 */            xTimeNow = prvSampleTimeNow( &xTimerListsWereSwitched );            /* 消息类型 */            switch( xMessage.xMessageID )            {                /* 定时器启动或者复位 */                case tmrCOMMAND_START :                case tmrCOMMAND_START_FROM_ISR :                case tmrCOMMAND_RESET :                case tmrCOMMAND_RESET_FROM_ISR :                case tmrCOMMAND_START_DONT_TRACE :                    /* 计算超时时间,超时时间没过加入活跃列表,超时时间已过返回pdTrue */                    if( prvInsertTimerInActiveList( pxTimer,  xMessage.u.xTimerParameters.xMessageValue + pxTimer->xTimerPeriodInTicks, xTimeNow, xMessage.u.xTimerParameters.xMessageValue ) != pdFALSE )                    {                        /* 在加入列表前已经超时,执行对应的回调函数 */                        pxTimer->pxCallbackFunction( ( TimerHandle_t ) pxTimer );                        traceTIMER_EXPIRED( pxTimer );                        /*如果是周期定时器*/                        if( pxTimer->uxAutoReload == ( UBaseType_t ) pdTRUE )                        {                            /* 发送消息,通知守护任务将定时器插入当前列表 */                            xResult = xTimerGenericCommand( pxTimer, tmrCOMMAND_START_DONT_TRACE, xMessage.u.xTimerParameters.xMessageValue + pxTimer->xTimerPeriodInTicks, NULL, tmrNO_DELAY );                            configASSERT( xResult );                            ( void ) xResult;                        }                        else                        {                            mtCOVERAGE_TEST_MARKER();                        }                    }                    else                    {                        mtCOVERAGE_TEST_MARKER();                    }                    break;                /* 停止定时器 */                case tmrCOMMAND_STOP :                case tmrCOMMAND_STOP_FROM_ISR :                    /* 定时器已经从活跃列表中移除,所以什么都不做 */                    break;                /* 改变定时器周期 */                case tmrCOMMAND_CHANGE_PERIOD :                case tmrCOMMAND_CHANGE_PERIOD_FROM_ISR :                    /* 取出新的频率 */                    pxTimer->xTimerPeriodInTicks = xMessage.u.xTimerParameters.xMessageValue;                    configASSERT( ( pxTimer->xTimerPeriodInTicks > 0 ) );                    /* 计算超时时间,超时时间没过则加入活跃列表 */                    ( void ) prvInsertTimerInActiveList( pxTimer, ( xTimeNow + pxTimer->xTimerPeriodInTicks ), xTimeNow, xTimeNow );                    break;                /* 删除定时器 */                case tmrCOMMAND_DELETE :                    #if( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 0 ) )                    {                        /* 释放软件定时器内存 */                        vPortFree( pxTimer );                    }                    #elif( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 1 ) )                    {                        if( pxTimer->ucStaticallyAllocated == ( uint8_t ) pdFALSE )                        {                            /* 释放软件定时器内存 */                            vPortFree( pxTimer );                        }                        else                        {                            mtCOVERAGE_TEST_MARKER();                        }                    }                    #endif /* configSUPPORT_DYNAMIC_ALLOCATION */                    break;                default :                    /* Don't expect to get here. */                    break;            }        }    }}

44软件定时器的使用

4.1 软件定时器控制块(结构体)

/* 软件定时器结构体 */typedef struct tmrTimerControl{    const char *pcTimerName;            /* 定时器名字 */    ListItem_t xTimerListItem;          /* 定时器列表项 */    TickType_t xTimerPeriodInTicks;     /* 定时器定时时间 */    UBaseType_t uxAutoReload;           /* 定时器周期模式 */    void *pvTimerID;                    /* 定时器ID */    TimerCallbackFunction_t pxCallbackFunction; /* 定时器回调函数 */    #if (configUSE_TRACE_FACILITY == 1)        UBaseType_t uxTimerNumber;    #endif    #if ((configSUPPORT_STATIC_ALLOCATION == 1) && (configSUPPORT_DYNAMIC_ALLOCATION == 1))        uint8_t ucStaticallyAllocated; /*标记定时器使用的内存, 删除时判断是否需要释放内存*/    #endif}xTIMER;typedef xTIMER Timer_t;

4.2 创建一个软件定时器

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )TimerHandle_t xTimerCreate( const char * const pcTimerName,        /* 定时器名字 */                           const TickType_t xTimerPeriodInTicks,   /* 定时器定时时间 */                           const UBaseType_t uxAutoReload,         /* 定时器周期模式 */                           void * const pvTimerID,                 /* 定时器ID */                           TimerCallbackFunction_t pxCallbackFunction ) /* 定时器回调函数 */{    Timer_t *pxNewTimer;    /*为软件定时器申请内存*/    pxNewTimer = ( Timer_t * ) pvPortMalloc( sizeof( Timer_t ) );    if( pxNewTimer != NULL )    {        prvInitialiseNewTimer( pcTimerName, xTimerPeriodInTicks, uxAutoReload, pvTimerID, pxCallbackFunction, pxNewTimer );        #if( configSUPPORT_STATIC_ALLOCATION == 1 )        {            /* 定时器可以静态创建,也可以动态创建,注意这个计时器是动态创建的,以防稍后删除计时器 */            pxNewTimer->ucStaticallyAllocated = pdFALSE;        }        #endif /* configSUPPORT_STATIC_ALLOCATION */    }    return pxNewTimer;}#endif /* configSUPPORT_STATIC_ALLOCATION */

成功申请定时器后, 定时器并没有开始工作, 需要调用启动或复位等API函数将该定时器中的 xTimerListItem 插入到定时器管理链表中, Daemon 任务才能在该定时器设定的溢出时刻调用其回调函数。

4.3 启动定时器

当用户创建并启动一个软件定时器时, FreeRTOS会根据当前系统时间及用户设置的定时确定该定时器唤醒时间,并将该定时器控制块挂入软件定时器列表

下面来看一下当启动多个软件定时器时,软件定时器列表是如何来管理这些定时器的:

例如:系统当前时间xTimeNow值为0,注意:xTimeNow其实是一个局部变量,是根据xTaskGetTickCount()函数获取的,实际它的值就是全局变量xTickCount的值,表示当前系统时间。

4.3.1 例子1

  • 在当前系统中已经创建并启动了1个定时时间为200定时器Timer1

  • 当系统时间xTimeNow为20的时候,用户创建并且启动一个定时时间为100的定时器Timer2,此时Timer2的溢出时间xTicksToWait就为定时时间+系统当前时间(100+20=120),然后将Timer2按xTicksToWait升序插入软件定时器列表中

  • 当系统时间xTimeNow为40的时候,用户创建并且启动了一个定时时间为50的定时器Timer3,那么此时Timer3的溢出时间xTicksToWait就为40+50=90,同样安装xTicksToWait的数值升序插入软件定时器列表中

4.3.2 例子2

创建并且启动在已有的两个定时器中间的定时器也是一样的:

  • 创建定Timer1并且启动后,假如系统经过了50个tick, xTimeNow从0增长到50,与Timer1的xTicksToWait值相等, 这时会触发与Timer1对应的回调函数,从而转到回调函数中执行用户代码,同时将Timer1从软件定时器列表删除,如果软件定时器是周期性的,那么系统会根据Timer1下一次唤醒时间重新将Timer1添加到软件定时器列表中,按照xTicksToWait的升序进行排列。

  • 同理,在xTimeNow=40的时候创建的Timer3,在经过130个tick后(此时系统时间xTimeNow是40,130个tick就是系统时间xTimeNow为170的时候),与Timer3定时器对应的回调函数会被触发,接着将Timer3从软件定时器列表中删除,如果是周期性的定时器,还会按照xTicksToWait升序重新添加到软件定时器列表中。

5 总结与注意事项

  • 编译定时器相关代码, 如需要使用定时器,需要先在 FreeRTOSConfig.h 中正确配置宏 configUSE_TIMERS为 1

  • 软件定时器使用了系统的一个队列和一个任务资源,软件定时器任务的优先级默认为configTIMER_TASK_PRIORITY, 如果优先级太低, 可能导致定时器无法及时执行,所以为了更好响应,该优先级应设置为所有任务中最高的优先级。

  • 定时器任务的消息队列深度为configTIMER_QUEUE_LENGTH, 设置定时器都是通过发送消息到该队列实现的

  • 定时器任务的堆栈大小默认为configTIMER_TASK_STACK_DEPTH个字节。

  • 软件定时器的回调函数中应快进快出,绝对不允许使用任何可能引软件定时器起任务挂起或者阻塞的API接口,在回调函数中也绝对不允许出现死循环。

  • 创建单次软件定时器,该定时器超时执行完回调函数后,系统会自动删除该软件定时器,并回收资源。

  • FreeRTOS介绍与源码结构分析

  • FreeRTOS移植-基于STM32F407

  • FreeRTOS例程1-基础任务创建

  • FreeRTOS例程2-任务挂起恢复与使用中断遇到的坑!

  • FreeRTOS例程3-串口中断接收不定长的数据与二值信号量的使用

  • FreeRTOS例程4-串口DMA收发不定长数据

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭