FreeRTOS源码探析之——软件定时器

2020-12-02 14:36:05 浏览数 (1)

软件定时器是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守护任务),并且可以看到是通过宏定义的方式选择编译:

代码语言:javascript复制
/* 启动调度器 */ 
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函数创建两个列表和一个消息队列

代码语言:javascript复制
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,利用“定时器命令队列”向软件定时器任务发送一些命令,任务在接收到命令就会去处理命令对应的程序,比如启动定时器,停止定时器,复位、删除、改变周期等。

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

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

代码语言:javascript复制
/* 检查是否有可用的列表和队列 */
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函数,如启动、停止、删除、复位、改变周期等,实际是通过宏定义的方式提供:

代码语言:javascript复制
/*启动定时器*/
#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消息队列,由软件定时器任务(守护任务来)接收并进行处理。

代码语言:javascript复制
/* 软件定时器打包命令与发送 */
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任务。

代码语言:javascript复制
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 获取下一个定时超时时间

代码语言:javascript复制
/* 获取下一次的定时器超时时间 */
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
  • 若大于则表示已经超时,定时器任务将会调用对应定时器的回调函数
  • 否则将软件定时器任务挂起,直至下一个要唤醒的软件定时器时间到来或者接收到命令消息
代码语言:javascript复制
/* 处理或阻塞软件定时器任务 */
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 获取当前时间并决定是否切换列表

代码语言:javascript复制
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), 下一次调用,通过比较相邻两次调用的值判断节拍计数器是否溢出。当系统节拍计数器溢出, 必须切换计时器列表。如果当前计时器列表中仍然引用任何计时器,那么它们一定已经过期,应该在切换列表之前进行处理。

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

代码语言:javascript复制
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 处理超时的定时器

代码语言:javascript复制
/* 处理超时的定时器 */
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()函数,定时器定时时间还没到,将当前任务挂起,直到定时器到期才唤醒或者收到命令的时候唤醒:

代码语言:javascript复制
/* 让队列按照给定的时间进行阻塞 */
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 任务每次执行期间回去读取并执行,下面看看该函数的具体内容:

代码语言:javascript复制
/*处理命令队列中接收的消息*/
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;
            }
        }
    }
}

4

4软件定时器的使用

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

代码语言:javascript复制
/* 软件定时器结构体 */
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 创建一个软件定时器

代码语言:javascript复制
#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接口,在回调函数中也绝对不允许出现死循环。
  • 创建单次软件定时器,该定时器超时执行完回调函数后,系统会自动删除该软件定时器,并回收资源。

0 人点赞