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

2023-11-06

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

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

FreeRTOS源码探析之——软件定时器 的相关文章

  • 在VSCode中配置Anaconda

    自己摸索中遇到的问题和解决办法做一个小小的记录 以免日后忘记 还请路过的大佬不吝赐教 之前一直在用VSCode调试Python 对于无法联网的电脑 直接pip安装一些包并不现实 而由于Python 3 7 3的版本太低 部分pip包在PyP
  • 按照指定规则对输入的字符串进行处理。详细描述:第一步:将输入的两个字符串str1和str2进行前后合并。

    while True 牛课网HJ30 字符串合并处理 try s list input replace 第一步 将输入的两个字符串str1和str2进行前后合并把空格去掉并且变成list s 2 sorted s 2 s 1 2 sorte
  • Flutter系列-BottomNavigationBar使用

    先上效果图 正文 flutter新手 第一次开发 记录开发过程 首先写了底部tab切换 使用的是官方widget BottomNavigationBar 先上完整代码 main dart import package flutter mat
  • 为什么要三次握手?两次握手不行吗?

    三次握手的目的是为了确认客户端和服务端的接收能力和发送能力 第一次握手成功时 服务端确认客户端的发送能力和服务端的接收能力 第二次握手成功时 客户端确认服务端的发送能力和接收能力和客户端的发送能力和接收能力 但此时服务端不能确认客户端的接收
  • vue element Tooltip套用Popover

    需求 同一个图标 鼠标移入显示提示语 点击显示列表 鼠标移入 鼠标点击
  • Linux命令ifconfig报错-bash: ifconfig: command not found

    查看我们配置的网卡 vi etc sysconfig network scripts ifcfg ens33 如果想要配置静态IP http t csdn cn L3TPM 进入sbin目录 sbin 管理员可以操作的命令存放的目录 cd
  • 利用PLSQL Developer对oracle中的数据进行备份恢复

    以备份scott用户为例 目标 备份里面所有的对象 切换到scottconn scott tiger 进入 工具 gt 导出用户对象 如图所示 创建表空间及用户名 并赋予权限 创建表空间 Create tablespace adm data
  • Excel公式不能自动更新数据

    Excel公式不能自动更新数据 问题及解决办法 原因 Excel的公式计算配置为 手动 状态 问题及解决办法 原因 Excel的公式计算配置为 手动 状态 单击 公式 选项卡 然后单击 计算选项 按钮 将计算选项设置为 自动 即可
  • 【STM32Cube】学习笔记(四):LED&按键&蜂鸣器

    文章目录 摘要 一 简介 1 GPIO简介 2 LED 3 按键 4 蜂鸣器 二 硬件电路设计 1 LED电路 2 按键电路 2 蜂鸣器电路 三 软件设计 1 CubeMX配置 2 CubeIDE代码 3 结果显示 四 总结 五 附录 摘要
  • idea如何import_导入项目

    今天 炸药 问我idea如何导入项目 先close project 再到开始界面import 然后一直next就可以了 具体操作和注意事项 特别注意 gt gt gt 开始导入项目前 注意 删掉别人电脑的idea配置和class 只留src
  • Unity3D里不存在Vuforia

    起初在Unity里没能找到ARCamera 然后在搜索框里去搜索Vuforia 发现根本不存在 下载Vuforia插件包 下载Vofuria插件包 在Unity里导入Vuforia插件包 在Unity里导入插件包后 软件会提示update
  • R语言深度学习驱动的课程推荐:基于关联规则挖掘的实践

    目录 1 引言 2 数据集准备 3 数据预处理 4 关联规则挖掘 5 结果分析 6 总结 摘要 课程推荐在教育领域具有重要意义
  • 文件IO

    文章目录 简介 OPEN函数 CREATE函数 CLOSE函数 LSEEK函数 READ函数 WRITE函数 简介 文件IO 常用五大函数 open read write lseek close 称之为不带缓存的I O 不带缓存 指的是每个
  • 一些好用的 alias 命令

    一些好用的 alias 命令 Linux和MaxOSX的 alias 命令使用技巧 因为 gist 被强的比较厉害 所以只好放到 repo 中 alias 注意 等号两边不能有空格 可以放到 bash profile 文件中 SYS ALI
  • A Survey on Application of Knowledge Graph

    本文是针对 A Survey on Application of Knowledge Graph 的一个翻译 知识图谱应用综述 摘要 1 引言 2 应用 2 1 问答系统 2 1 1 基于语义分析 2 1 2 基于信息检索 2 1 3 基于
  • 分布式系统日志集中到一台服务器,将分布式中多台节点的日志信息集中到一个节点上...

    转载 http my oschina net duxuefeng blog 317570 1 准备 master 10 1 5 241 slave01 10 1 5 242 在服务器端和客户端分别安装rsyslog root master
  • QT(2):信号槽机制和源码

    信号槽 信号槽是观察者模式的一种实现 订阅 发布 一个信号就是一个能够被观察的事件 一个槽就是一个观察者 被观察者 多观察者 信号发出者调用所有注册的槽函数 信号槽本质上是两个对象的函数地址映射 单线程下 相当于函数指针调用 多线程下 发送
  • 多态 数组 继承 类与接口

    多态 1 多态概念 多态就是多种状态 表现为多种形式的能力 2 多态怎么理解 多态可分为静态多态和动态多态 静态多态 当创建一个父类时 子类可以调用父类中的所有方法 而自己没有独有的方法 具体表现方法是重载 动态多态 当创建一个父类时 子类
  • 算法竞赛进阶指南 递归实现组合型枚举

    文章目录 1 递归实现指数型枚举 2 递归实现排列型枚举 题目链接 https ac nowcoder com acm contest 998 B 1 递归实现指数型枚举 思路 在 递归实现指数型枚举 的基础上 如果已经选择了超过 m m

随机推荐

  • k8s集群中部署微服务Vue

    k8s集群中部署微服务后台管理项目 admin 一 项目创建 修改依据 npm版本为12 2 0 可以使用 nvm进行安装及切换使用 npm install node sass 4 14 npm install 二 项目容器镜像准备 roo
  • 设计链表00

    题目链接 设计链表 题目描述 注意 addAtIndex index val 方法中 如果 index 等于链表的长度 则该节点将附加到链表的末尾 如果 index 大于链表长度 则不会插入节点 如果index小于0 则在头部插入节点 解答
  • python中类及其实例详解

    http wiki woodpecker org cn moin PyNewStyleClass 1 python中的 new style class 及其实例详解 原文见 Python In a Nutshell 2003 5 2节 1
  • Windows11 0x80190001错误解决

    一 Windows11 0x80190001错误 笔者当前使用的系统版本为 win11 21H2 windows11出现这样的错误 初步判断为windows的网络连接问题引起的 或者是系统当前网络设置的问题 当然也不排除win11自身系统的
  • 图形学变换——平移、旋转和缩放

    图形学变换 一 概述 二 平移 二 旋转 三 缩放 一 概述 在齐次坐标中 所有的仿射变换都可以使用如下形式的 4 x 4 矩阵来表示 点 p x y z
  • Java知识点回顾系列(集合框架)

    记录一下Java中集合的使用与区别 主要讲List Set Map的原理 使用方法 在Java世界里 集合框架的核心接口为Collection List 列表 Set 集合 和Map 映射 集合关系图 根据上面的关系图可以得到以下的结论 C
  • 游戏业务被攻击了应该如何防护?

    游戏作为最容易遭到黑客攻击行业 总是避免不了被攻击 这个问题始终是绕不过去的一个点 那么为什么黑客这么喜欢攻击游戏行业呢 主要原因有那么几点 1 勒索行为 这个是绝大部分黑客攻击平台的原因 是为了勒索钱财 收取保护费 2 恶意竞争 同行竞争
  • 区块链技术基本概念(上)

    区块链本质是一个对等网络的分布式账本数据库 数据区块 挖矿与分叉问题 一 区块是在挖矿的过程中产生的 二 挖矿实际上就是穷举随机数算法 把上个区块的哈希值加上10分钟内的全部交易单打包 再加上一个随机数 算出一个256位的字符串哈希值 输入
  • 01-物联网(环保管家)(一)温湿度与plc通信具体实现步骤

    先解释几个名词 DTU Data Transfer unit 是专门用于将串口数据转换为IP数据或将IP数据转换为串口数据通过无线通信网络进行传送的无线终端设备 DTU广泛应用于气象 水文水利 地质等行业 MQTT MQTT 是 IBM 为
  • 数据结构(栈和队列)

    Queue常用子类 PriorityQueue Deque常用子类 LinkedList以及ArrayDeque Queue有一个直接子类PriorityQueue 而Deque中直接子类有两个 LinkedList以及ArrayDeque
  • 时间序列分析 - 移动平均SMA, EMA(EWMA) 之python

    pandas pandas DataFrame rolling pandas DataFrame ewm pandas DataFrame mean 其中rolling可以指定窗口类型win type 比如boxcar boxcar tri
  • jsp为基础,设计并实现了一个酒店客房管理系统

    摘要 本毕业设计以jsp为基础 设计并实现了一个酒店客房管理系统 该系统包括客房信息管理 客房预订管理和客房入住管理等模块 通过使用jsp技术 能够实现酒店客房的在线管理 提高客房管理的效率和准确性 关键词 jsp 酒店客房管理系统 客房信
  • gauge自动化框架踩坑(五):关于表格

    官方文档使用表格的方式有两种 1 表格参数 2 动态参数 现针对不同的情况做一分析 一 表格参数 官方文档的解释是 表格参数被用来作为一个step 执行多条数据 以官网的模板项目为例 这里给了一个表格 作为一个step Almost all
  • 将时间序列转成图像——马尔可夫转移场方法 Matlab实现

    目录 1 方法 2 Matlab代码实现 3 结果 若觉文章质量良好且有用 请别忘了点赞收藏加关注 这将是我继续分享的动力 万分感谢 其他 1 时间序列转二维图像方法及其应用研究综述 vm 1215的博客 CSDN博客 2 将时间序列转成图
  • BOOST升压电路PCB布局布线

    一 正确找出BOOST的高频电流环路 尽可能让di dt大的路径小 在boost中为开关管 二极管 与输出电容 二 输入环路 先经过Cin再到芯片输入脚 三 输出环路 重要 SW覆盖面积要小 四 反馈环路 重要 与FB相连的两个电阻越靠近F
  • 零拷贝内存 & 固定内存

    一 总览 虚拟内存是一种计算机内存管理的技术 它让程序认为程序自身有一段完整的连续可用的内存 一个地址空间 当程序运行时所占的内存空间大于物理空间容量 操作系统可以将暂时不用的数据放入到磁盘 用的时候再拿出来 这样磁盘有一部分空间就是用来存
  • log4cplus 分级显示控制

    前不久已经整理过一篇关于log4cplus介绍和使用基础的文章 这次来看看如何更好的使用log4cplus这个 功能强大的日志系统吧 层次结构 在log4cplus中 所有logger都通过一个层次化的结构 其实内部是hash表 来组织的
  • Python中@property和@setter的用法

    一 property 用法 可以使用 property装饰器来创建只读属性 property装饰器会将方法转换为相同名称的只读属性 这样可以防止属性被修改 实例 class DataSet object property def metho
  • mysql grouping sets_GROUPING SETS与GROUP_ID

    SELECT E DEPARTMENT ID DID E JOB ID JOB E MANAGER ID MID SUM E SALARY SUM SAL COUNT E EMPLOYEE ID CNT GROUP ID GG FROM E
  • FreeRTOS源码探析之——软件定时器

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