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

软件定时器是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;
			}
		}
	}
}

4 软件定时器的使用

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接口,在回调函数中也绝对不允许出现死循环。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值