FreeRTOS - 软件定时器

发布于:2024-10-18 ⋅ 阅读:(8) ⋅ 点赞:(0)

在学习FreeRTOS过程中,结合韦东山-FreeRTOS手册和视频、野火-FreeRTOS内核实现与应用开发、及网上查找的其他资源,整理了该篇文章。如有内容理解不正确之处,欢迎大家指出,共同进步。

1. 软件定时器

软件定时器也可以完成两类事情:

  • 在"未来"某个时间点,运行函数
  • 周期性地运行函数

在FreeRTOS里,我们也可以设置无数个"软件定时器",它们都是基于系统滴答中断(Tick Interrupt)。

1.1 软件定时器的特性

定时器,是指从指定的时刻开始,经过一个指定时间,然后触发一个超时事件,用户 可以自定义定时器的周期与频率。使用定时器跟使用手机闹钟是类似的:

  • 指定时间:启动定时器和运行回调函数,两者的间隔被称为定时器的周期(period)。
  • 指定类型,定时器有两种类型:
    • 一次性(One-shot timers): 这类定时器启动后,它的回调函数只会被调用一次; 可以手工再次启动它,但是不会自动启动它。
    • 自动加载定时器(Auto-reload timers ): 这类定时器启动后,时间到之后它会自动启动它; 这使得回调函数被周期性地调用。
  • 指定要做什么事,就是指定回调函数

实际的闹钟分为:有效、无效两类。软件定时器也是类似的,它由两种状态:

  • 运行(Running、Active):运行态的定时器,当指定时间到达之后,它的回调函数会被调用
  • 冬眠(Dormant):冬眠态的定时器还可以通过句柄来访问它,但是它不再运行,它的回调函数不会被调用

定时器运行情况示例如下:

  • Timer1:它是一次性的定时器,在t1启动,周期是6个Tick。经过6个tick后,在t7执行回调函数。它的回调函数只会被执行一次,然后该定时器进入休眠状态。
  • Timer2:它是自动加载的定时器,在t1启动,周期是5个Tick。每经过5个tick它的回调函数都被执行,比如在t6、t11、t16都会执行。

1.2 硬件定时器和软件定时器

定时器有硬件定时器和软件定时器之分:

  • 硬件定时器:是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断。硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式。
  • 软件定时器:软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受硬件定时器资源限制的定时器服务,它实现的功能与硬件定时器也是类似的。
  • 使用硬件定时器时,每次在定时时间到达之后就会自动触发一个中断,用户在中断中处理信息;
  • 而使用软件定时器时,需要我们在创建软件定时器时指定时间到达后要调用的函数(也称超时函数/回调函数,为了统一,下文均用回调函数描述),在回调函数中处理信息。
  • 注意:软件定时器回调函数的上下文是任务,下文所说的定时器均为软件定时器。
1.3 软件定时器的管理(运作机制)
  • 软件定时器在被创建之后,当经过设定的时钟计数值后会触发用户定义的回调函数。 定时精度与系统时钟的周期有关。
  • 一般系统利用 SysTick 作为软件定时器的基础时钟。
  • 两次触发回调函数的时间间隔 xTimerPeriodInTicks 叫定时器的定时周期。

软件定时器是可选的系统资源,在创建定时器的时候会分配一块内存空间。当用户创建并启动一个软件定时器时, FreeRTOS 会根据当前系统时间及用户设置的定时确定该定时器唤醒时间,并将该定时器控制块挂入软件定时器列表,FreeRTOS 中采用两个定时器列表维护软件定时器,pxCurrentTimerListpxOverflowTimerList是列表指针,在初始化的时候分别指向 xActiveTimerList1 与 xActiveTimerList2。

  • pxCurrentTimerList:系统新创建并激活的定时器都会以超时时间升序的方式插入到 pxCurrentTimerList 列表中。系统在定时器任务中扫描 pxCurrentTimerList 中的第一个定时器,看是否已超时,若已经超时了则调用软件定时器回调函数。否则将定时器任务挂起, 因为定时时间是升序插入软件定时器列表的,列表中第一个定时器的定时时间都还没到的话,那后面的定时器定时时间自然没到。
  • pxOverflowTimerList :是在软件定时器溢出的时候使用,作用和pxCurrentTimerList 一致。

那么系统如何处理软件定时器列表?

  • 系统在不断运行,而 xTimeNow(xTickCount) 随着 SysTick 的触发一直在增长(每一次硬件定时器中断来临,xTimeNow 变量会加 1),
  • 在软件定时器任务运行的时候会获取下一个要唤醒的定时器,比较当前系统时间xTimeNow 是否大于或等于下一个定时器唤醒时间 xTicksToWait,若大于则表示已经超时, 定时器任务将会调用对应定时器的回调函数,否则将软件定时器任务挂起,直至下一个要唤醒的软件定时器时间到来或者接收到命令消息。

使用软件定时器时候要注意以下几点:

  • 软件定时器的回调函数类似硬件的中断服务函数,所以,回调函数也要快进快出,而且回调函数中不能使用任何可能引起软件定时器任务挂起或者阻塞的 API 接口,在回调函数中也绝对不允许出现死循环(软件定时器回调函数的上下文环境是任务)。
  • 创建单次软件定时器,该定时器超时执行完回调函数后,系统会自动删除该软件定时器,并回收资源。
  • 软件定时器使用了系统的一个队列和一个任务资源,会定义一个软件定时器队列长度configTIMER_QUEUE_LENGTH=10,软件定时器任务的优先级默认为 configTIMER_TASK_PRIORITY = (40),为了更好响应,该优先级应设置为所有任务中最高的优先级。
  • 定时器任务的堆栈大小默认为 configTIMER_TASK_STACK_DEPTH = 256个字节。

2. 软件定时器创建和启动

2.1 软件定时器控制块
typedef struct tmrTimerControl
{
	const char				*pcTimerName;	/* 软件定时器名字*/	
	ListItem_t				xTimerListItem;	/* 软件定时器列表项,用于插入定时器列表*/	
	TickType_t				xTimerPeriodInTicks;/* 软件定时器的周期*/
	UBaseType_t				uxAutoReload;	/* 软件定时器是否自动重置,为 pdFalse,是单次模式 */	
	void 					*pvTimerID;		/* 软件定时器ID,
                        当一个回调函数分配给一个或多个软件定时器时,
                        在回调函数中根据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;

2.2 软件定时器创建函数 xTimerCreate() --已删除静态部分

软件定时器创建成功后是处于休眠状态的。

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )

	TimerHandle_t xTimerCreate(	const char * const pcTimerName,			/*lint !e971 Unqualified char types are allowed for strings and single characters only. */
								const TickType_t xTimerPeriodInTicks,
								const UBaseType_t uxAutoReload,
								void * const pvTimerID,
								TimerCallbackFunction_t pxCallbackFunction )
	{
	Timer_t *pxNewTimer;
        /* 为这个软件定时器申请一块内存 */
		pxNewTimer = ( Timer_t * ) pvPortMalloc( sizeof( Timer_t ) );

		if( pxNewTimer != NULL )
		{
            /* 内存申请成功,进行初始化软件定时器 */
			prvInitialiseNewTimer( pcTimerName, 
                                  xTimerPeriodInTicks, 
                                  uxAutoReload, 
                                  pvTimerID, 
                                  pxCallbackFunction, 
                                  pxNewTimer );
		}

		return pxNewTimer;
	}

#endif /* configSUPPORT_STATIC_ALLOCATION */
2.2.1 prvInitialiseNewTimer ()函数

初始化一个新的软件定时器

static void prvInitialiseNewTimer(	const char * const pcTimerName,			/*lint !e971 Unqualified char types are allowed for strings and single characters only. */
									const TickType_t xTimerPeriodInTicks,
									const UBaseType_t uxAutoReload,
									void * const pvTimerID,
									TimerCallbackFunction_t pxCallbackFunction,
									Timer_t *pxNewTimer )
{
	/* 断言,判断定时器的周期是否大于 0 */
	configASSERT( ( xTimerPeriodInTicks > 0 ) );

	if( pxNewTimer != NULL )
	{
		/* 初始化软件定时器列表与创建软件定时器消息队列 */
		prvCheckForValidListAndQueue();

		/* 初始化软件定时信息,这些信息保存在软件定时器控制块中 */
		pxNewTimer->pcTimerName = pcTimerName;
		pxNewTimer->xTimerPeriodInTicks = xTimerPeriodInTicks;
		pxNewTimer->uxAutoReload = uxAutoReload;
		pxNewTimer->pvTimerID = pvTimerID;
		pxNewTimer->pxCallbackFunction = pxCallbackFunction;
		vListInitialiseItem( &( pxNewTimer->xTimerListItem ) );
		traceTIMER_CREATE( pxNewTimer );
	}
}

prvCheckForValidListAndQueue() 函数中系统将初始化软件定时器列表与创建软件定时器消息队列,也叫“定时器命令队列”,因为在使用软件定时器的 时候,用户是无法直接控制软件定时器的,必须通过“定时器命令队列”向软件定时器发 送一个命令,软件定时器任务被唤醒就去执行对应的命令操作。

2.2.2 prvCheckForValidListAndQueue() 函数–已删除静态部分和其他代码
static void prvCheckForValidListAndQueue( void )
{
	taskENTER_CRITICAL();
	{
		if( xTimerQueue == NULL )
		{
            /* 初始化软件定时器链表*/
			vListInitialise( &xActiveTimerList1 );
			vListInitialise( &xActiveTimerList2 );
			pxCurrentTimerList = &xActiveTimerList1;
			pxOverflowTimerList = &xActiveTimerList2;

			/* 创建定时器命令队列*/
			xTimerQueue = xQueueCreate( 
                ( UBaseType_t ) configTIMER_QUEUE_LENGTH, 
                sizeof( DaemonTaskMessage_t ) );
		}else{
			mtCOVERAGE_TEST_MARKER();
		}
	}
	taskEXIT_CRITICAL();
}

定时器任务优先级和队列长度:

typedef struct tmrTimerParameters
{
	TickType_t			xMessageValue;		/*<< An optional value used by a subset of commands, for example, when changing the period of a timer. */
	Timer_t *			pxTimer;			/*<< The timer to which the command will be applied. */
} TimerParameter_t;

/* 回调函数*/
typedef struct tmrCallbackParameters
{
	PendedFunction_t	pxCallbackFunction;	/* << The callback function to execute. */
	void *pvParameter1;						/* << The value that will be used as the callback functions first parameter. */
	uint32_t ulParameter2;					/* << The value that will be used as the callback functions second parameter. */
} CallbackParameters_t;

/* 守护任务队列消息*/
typedef struct tmrTimerQueueMessage
{
	BaseType_t			xMessageID;			/*<< The command being sent to the timer service task. */
	union
	{
		TimerParameter_t xTimerParameters;
		#if ( INCLUDE_xTimerPendFunctionCall == 1 )
			CallbackParameters_t xCallbackParameters;
		#endif /* INCLUDE_xTimerPendFunctionCall */
	} u;
} DaemonTaskMessage_t;
2.3 软件定时器启动函数
2.3.1 xTimerStart()

在系统开始运行的时候,系统会帮我们自动创建一个软件定时器任务 (prvTimerTask),在这个任务中,如果暂时没有运行中的定时器,任务会进入阻塞态等待命令,而我们的启动函数xTimerStart()就是通过“定时器命令队列”向定时器任务发送一个启动命令, 定时器任务获得命令就解除阻塞,然后执行启动软件定时器命令。

#define xTimerStart( xTimer, xTicksToWait )\
xTimerGenericCommand( ( xTimer ),/*软件定时器句柄*/\
                     tmrCOMMAND_START,/*软件定时器启动命令*/\
                     ( xTaskGetTickCount() ),/*获取当前系统时间*/\
                     NULL,/*pxHigherPriorityTaskWoken 为 NULL*/\
                     ( xTicksToWait ) )/*超时阻塞时间*/\

2.3.2 xTimerGenericCommand函数

软件定时器支持的命令:

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;  // tmrCOMMAND_START
		xMessage.u.xTimerParameters.xMessageValue = xOptionalValue;
		xMessage.u.xTimerParameters.pxTimer = ( Timer_t * ) 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.3.3 xTimerStartFromISR()

3. 软件定时器任务(守护任务)

软件定时器的功能是在定时器任务(定时器守护任务)prvTimerTask中实现的,软件定时器的很多 API 函数通过一个 “定时器命令队列”的队列来给定时器守护任务发送命令。任务在接收到命令就会去处理命令对应的程序,比如启动定时器,停止定时器等。

3.1 守护任务

要理解软件定时器API函数的参数,特别是里面的xTicksToWait,需要知道定时器执行的过程。

FreeRTOS中有一个Tick中断,软件定时器基于Tick来运行。在哪里执行定时器函数?第一印象就是在Tick中断里执行:

  • 在Tick中断中判断定时器是否超时
  • 如果超时了,调用它的回调函数

FreeRTOS是RTOS,它不允许在内核、在中断中执行不确定的代码:如果定时器函数很耗时,会影响整个系统。

所以,FreeRTOS中,不在Tick中断中执行定时器函数。

在哪里执行?在某个任务里执行,这个任务就是: prvTimerTask 任务 ,RTOS守护任务

当FreeRTOS的配置项configUSE_TIMERS被设置为1(使用软件定时器)时,在启动调度器时,会自动创建RTOS 守护任务。

prvTimerTask 任务会在其执行期间检查用户启动的时间周期溢出的定时器,并调用其回调函数

3.2 守护任务的调度

守护任务的调度,跟普通的任务并无差别。当守护任务是当前优先级最高的就绪态任务时,它就可以运行。它的工作有两类:

  • 处理命令:从命令队列里取出命令、处理
  • 执行定时器的回调函数

能否及时处理定时器的命令、能否及时执行定时器的回调函数,严重依赖于守护任务的优先级。下面使用2个例子来演示。

例子1:守护任务的优先性级较低

  • t1:Task1处于运行态,守护任务处于阻塞态。 守护任务在这两种情况下会退出阻塞态切换为就绪态:命令队列中有数据、某个定时器超时了。 至于守护任务能否马上执行,取决于它的优先级。
  • t2:Task1调用 xTimerStart() 要注意的是,xTimerStart() 只是把"start timer"的命令发给"定时器命令队列",使得守护任务退出阻塞态。 在本例中,Task1的优先级高于守护任务,所以守护任务无法抢占Task1。
  • t3:Task1执行完 xTimerStart() ,但是定时器的启动工作由守护任务来实现,所以xTimerStart()返回并不表示定时器已经被启动了。
  • t4:Task1由于某些原因进入阻塞态,现在轮到守护任务运行。 守护任务从队列中取出"start timer"命令,启动定时器。
  • t5:守护任务处理完队列中所有的命令,再次进入阻塞态。Idel任务时优先级最高的就绪态任务,它执行。

例子2:守护任务的优先性级较高

  • t1:Task1处于运行态,守护任务处于阻塞态。 守护任务在这两种情况下会退出阻塞态切换为就绪态:命令队列中有数据、某个定时器超时了。 至于守护任务能否马上执行,取决于它的优先级。
  • t2:Task1调用xTimerStart() 要注意的是,xTimerStart()只是把"start timer"的命令发给"定时器命令队列",使得守护任务退出阻塞态。 在本例中,守护任务的优先级高于Task1,所以守护任务抢占Task1,守护任务开始处理命令队列。 Task1在执行xTimerStart()的过程中被抢占,这时它无法完成此函数。
  • t3:守护任务处理完命令队列中所有的命令,再次进入阻塞态。 此时Task1是优先级最高的就绪态任务,它开始执行。
  • t4:Task1之前被守护任务抢占,对xTimerStart()的调用尚未返回。现在开始继续运行次函数、返回。
  • t5:Task1由于某些原因进入阻塞态,进入阻塞态。Idel任务时优先级最高的就绪态任务,它执行。

  • 注意:假设定时器在后续某个时刻tX超时了,超时时间是"tX-t2",而非"tX-t4",从 xTimerStart() 函数被调用时算起。
3.3 回调函数

定时器的回调函数的原型如下:

void ATimerCallback( TimerHandle_t xTimer );

定时器的回调函数是在守护任务中被调用的,守护任务不是专为某个定时器服务的,它还要处理其他定时器。

所以,定时器的回调函数不要影响其他人:

  • 回调函数要尽快实行,不能进入阻塞状态
  • 不要调用会导致阻塞的API函数,比如 vTaskDelay()
  • 可以调用 xQueueReceive() 之类的函数,但是超时时间要设为0:即刻返回,不可阻塞
3.4 prvTimerTask()函数
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 )
	{
		extern void vApplicationDaemonTaskStartupHook( void );
		vApplicationDaemonTaskStartupHook();
	}
	#endif /* configUSE_DAEMON_TASK_STARTUP_HOOK */

	for( ;; )
	{
		/* 获取下一个要到期的软件定时器的时间 */
	(1)	xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );

		/* 处理定时器或者将任务阻塞到下一个到期的软件定时器时间  */
	(2)	prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );

		/* 读取“定时器命令队列”,处理相应命令。*/
	(3)	prvProcessReceivedCommands();
	}
}

软件定时器任务的处理很简单,如果当前有软件定时器在运行,那么它大部分的时间都在等待定时器到期时间的到来,或者在等待对软件定时器操作的命令,而如果没有软件定时器在运行,那定时器任务的绝大部分时间都在阻塞中等待定时器的操作命令。

3.4.1 prvGetNextExpireTime

获取下一个要到期的软件定时器的时间,因为软件定时器是由定时器列表维护的,并且按照到期的时间进行升序排列,只需获取软件定时器列表中的第一个定时器到期时间就是下一个要到期的时间。

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
	{
		/* Ensure the task unblocks when the tick count rolls over. */
		xNextExpireTime = ( TickType_t ) 0U;
	}

	return xNextExpireTime;
}

#define listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxList )\
        ( ( ( pxList )->xListEnd ).pxNext->xItemValue )
3.4.2 prvProcessTimerOrBlockTask

处理定时器或者将任务阻塞到下一个到期的软件定时器时间。因为系统时间节拍随着系统的运行可能会溢出,那么就需要处理溢出的情况;如果没有溢出, 那么就等待下一个定时器到期时间的到来。该函数每次调用都会记录节拍值, 下一次调用, 通过比较相邻两次调用的值判断节拍计数器是否溢出过。当节拍计数器溢出,需要处理掉 当前定时器列表上的定时器(因为这条定时器列表上的定时器都已经溢出了),然后切换定时器列表。

软件定时器是一个任务,在下一个定时器到了之前的这段时间,系统要把任务状态转移为阻塞态,让其他的任务能正常运行,这样子就使得系统的资源能充分利用。

软件定时器任务大多数时间都处于阻塞状态的,而且一般在 FreeRTOS 中,软件定时器任务一般设置为所有任务中最高优先级,这样一来,定时器的时间一到,就会马上到定时器任务中执行对应的回调函数。

static void prvProcessTimerOrBlockTask( const TickType_t xNextExpireTime, 
                                       BaseType_t xListWasEmpty )
{
TickType_t xTimeNow;
BaseType_t xTimerListsWereSwitched;

    /* 接下来的操作会对定时器列表进行操作,
       系统不希望别的任务来操作定时器列表,
       所以暂时让定时器任务独享CPU使用权,
       在此期间不进行任务切换。*/
	vTaskSuspendAll();
	{
		/* 获取当前系统时间节拍,并判断系统节拍计数是否溢出
		如果是,那么就处理当前列表上的定时器,并切换定时器列表
		*/
		xTimeNow = prvSampleTimeNow( &xTimerListsWereSwitched );

		/*系统节拍计数器没有溢出*/
		if( xTimerListsWereSwitched == pdFALSE )
		{
			/* The tick count has not overflowed, has the timer expired? */
			
			/* 判断是否有定时器是否到期,可以触发回调函数
			   定时器列表非空并且定时器的时间已比当前时间小,说明定时器到期了
			*/
			if( ( xListWasEmpty == pdFALSE ) && ( xNextExpireTime <= xTimeNow ) )
			{
				( void ) xTaskResumeAll();// 恢复调度器
				/* 执行相应定时器的回调函数
				   对于需要自动重载的定时器,更新下一次溢出时间,插回链表*/
				prvProcessExpiredTimer( xNextExpireTime, xTimeNow );// 处理定时器
			}
			else /* 定时器没到期 */
			{
				/* 当前定时器链表中没有定时器*/
				if( xListWasEmpty != pdFALSE )
				{
					/* The current timer list is empty - is the overflow list
					also empty? */
					/* 可能是系统节拍计数器溢出了,
					定时器被添加到溢出列表中,所以判断定时器溢出列表上是否有定时器*/
					xListWasEmpty = listLIST_IS_EMPTY( pxOverflowTimerList );
				}
				 
				/*定时器定时时间还没到,将当前任务挂起,
				直到定时器到期才唤醒或者收到命令的时候唤醒
				*/
				vQueueWaitForMessageRestricted( xTimerQueue, ( xNextExpireTime - xTimeNow ), xListWasEmpty );
				/* 恢复调度器 */
				if( xTaskResumeAll() == pdFALSE )
				{
					/* 进行任务切换 */
					portYIELD_WITHIN_API();
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
		else
		{
			( void ) xTaskResumeAll();
		}
	}
}
3.5 prvProcessReceivedCommands

(3): 读取“定时器命令队列”,处理相应命令。

static void	prvProcessReceivedCommands( void )
{
DaemonTaskMessage_t xMessage;
Timer_t *pxTimer;
BaseType_t xTimerListsWereSwitched, xResult;
TickType_t xTimeNow;

	while( xQueueReceive( xTimerQueue, &xMessage, tmrNO_DELAY ) != pdFAIL ) /*lint !e603 xMessage does not have to be initialised as it is passed out, not in, and it is not used unless xQueueReceive() returns pdTRUE. */
	{
		#if ( INCLUDE_xTimerPendFunctionCall == 1 )
		{
			/* Negative commands are pended function calls rather than timer
			commands. */
			
			if( xMessage.xMessageID < ( BaseType_t ) 0 )
			{
				const CallbackParameters_t * const pxCallback = &( xMessage.u.xCallbackParameters );

				/* The timer uses the xCallbackParameters member to request a
				callback be executed.  Check the callback is not NULL. */
				configASSERT( pxCallback );

				/* Call the function. */
				pxCallback->pxCallbackFunction( pxCallback->pvParameter1, pxCallback->ulParameter2 );
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		#endif /* INCLUDE_xTimerPendFunctionCall */

		/* Commands that are positive are timer commands rather than pended
		function calls. */
		/* 判断定时器命令是否有效 */
		if( xMessage.xMessageID >= ( BaseType_t ) 0 )
		{
			/* The messages uses the xTimerParameters member to work on a
			software timer. */
			/* 获取命令指定处理的定时器 */
			pxTimer = xMessage.u.xTimerParameters.pxTimer;

			if( listIS_CONTAINED_WITHIN( NULL, &( pxTimer->xTimerListItem ) ) == pdFALSE ) /*lint !e961. The cast is only redundant when NULL is passed into the macro. */
			{
				/* The timer is in a list, remove it. */
				/* 如果定时器在链表中,将其移除 */
				( void ) uxListRemove( &( pxTimer->xTimerListItem ) );
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}

			traceTIMER_COMMAND_RECEIVED( pxTimer, xMessage.xMessageID, xMessage.u.xTimerParameters.xMessageValue );

			/* In this case the xTimerListsWereSwitched parameter is not used, but
			it must be present in the function call.  prvSampleTimeNow() must be
			called after the message is received from xTimerQueue so there is no
			possibility of a higher priority task adding a message to the message
			queue with a time that is ahead of the timer daemon task (because it
			pre-empted the timer daemon task after the xTimeNow value was set). */
			/* 判断节拍计数器是否溢出过,如果有就处理并切换定时器链表
			因为下面的操作可能有新定时器项插入确保定时器链表对应 */

			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 :
					/* Start or restart a timer. */
					/* 以上命令都是让定时器启动
				       求出定时器到期时间并插入到定时器链表中*/
					if( prvInsertTimerInActiveList( pxTimer,  xMessage.u.xTimerParameters.xMessageValue + pxTimer->xTimerPeriodInTicks, xTimeNow, xMessage.u.xTimerParameters.xMessageValue ) != pdFALSE )
					{
						/* The timer expired before it was added to the active
						timer list.  Process it now. */
						/* 定时器已经溢出赶紧执行其回调函数 */
						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 :
					/* The timer has already been removed from the active list.
					There is nothing to do here. */
					break;

				case tmrCOMMAND_CHANGE_PERIOD :
				case tmrCOMMAND_CHANGE_PERIOD_FROM_ISR :
					/* 更新定时器配置*/
					pxTimer->xTimerPeriodInTicks = xMessage.u.xTimerParameters.xMessageValue;
					configASSERT( ( pxTimer->xTimerPeriodInTicks > 0 ) );

					/* The new period does not really have a reference, and can
					be longer or shorter than the old one.  The command time is
					therefore set to the current time, and as the period cannot
					be zero the next expiry time can only be in the future,
					meaning (unlike for the xTimerStart() case above) there is
					no fail case that needs to be handled here. */
					/* 插入到定时器链表,也重新启动了定时器 */
					( void ) prvInsertTimerInActiveList( pxTimer, ( xTimeNow + pxTimer->xTimerPeriodInTicks ), xTimeNow, xTimeNow );
					break;

				case tmrCOMMAND_DELETE :
					/* The timer has already been removed from the active list,
					just free up the memory if the memory was dynamically
					allocated. */
					/* 删除定时器*/
					/* 判断定时器内存是否需要释放(动态释放)*/
					#if( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 0 ) )
					{
						/* The timer can only have been allocated dynamically -
						free it again. */
						vPortFree( pxTimer );
					}
					#elif( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 1 ) )
					{
						/* The timer could have been allocated statically or
						dynamically, so check before attempting to free the
						memory. */
						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 软件定时器停止函数 xTimerStop()

xTimerStop() 用于停止一个已经启动的软件定时器,该函数的实现也是通过“定时器命令队列”发送一个停止命令给软件定时器任务,从而唤醒软件定时器任务去将定时器停止。在使用该函数前请确认定时器已经开启 。

#define xTimerStop( xTimer, xTicksToWait )\
xTimerGenericCommand( ( xTimer ), \
                       tmrCOMMAND_STOP, \
                       0U, \
                       NULL, \
                       ( xTicksToWait ) )\

4.2 软件定时器删除函数 xTimerDelete()

xTimerDelete()用于删除一个已经被创建成功的软件定时器,删除之后就无法使用该定 时器,并且定时器相应的资源也会被系统回收释放。

删除一个软件定时器也是在软件定时器任务中删除,调用 xTimerDelete()将删除软件定时器的命令发送给软件定时器任务,软件定时器任务在接收到删除的命令之后就进行删除操作。

5. 软件定时器实验

5.1 代码–实现游戏音效

本节代码为:28_timer_game_sound,主要看nwatch\beep.c。

对于无源蜂鸣器,只要设置PWM输出方波,它就会发出声音。在game1游戏中,什么时候发出声音?球与挡球板、转块碰撞时发出声音。什么时候停止声音?发出声音后,过一阵子就应该停止声音。这使用软件定时器来实现。

在初始化蜂鸣器时,创建定时器,代码如下:

static TimerHandle_t g_TimesSound;

void buzzer_init(void)
{
	/* 初始化蜂鸣器 */
	  PassiveBuzzer_Init();
	
	/* 创建定时器,名字:GameSound,周期:200,一次性,
     * 回调函数:无参数,GameSoundTimer_CallbackFunc*/
	  g_TimesSound =  xTimerCreate( "GameSound", 
									200,
									pdFALSE,
									NULL,
									GameSoundTimer_CallbackFunc);
}

想发出声音时,调用buzzer_buzz函数,代码如下:

蜂鸣器频率为50;

void buzzer_buzz(int freq, int time_ms)
{
	/* 调用该函数时,就会持续不断的发出声音 */
	PassiveBuzzer_Set_Freq_Duty(freq, 50);  
	
	/* 想让该音乐持续若干秒后停止*/
	/* 启动定时器 */
	xTimerChangePeriod(g_TimesSound, time_ms, 0);

}

当定时器超时后,GameSoundTimer_Func函数被调用,它会停止蜂鸣器,代码如下:

static void GameSoundTimer_CallbackFunc( TimerHandle_t xTimer )
{
	/* 停止蜂鸣器 */
	PassiveBuzzer_Control(0);
}

game1里如何使用音效?先初始化,代码如下:

void game1_task(void *params)
{
    buzzer_init();
    
}
void game1_draw()
{
  /* 当球触板、墙时,发出不同频率的声音*/
  405   buzzer_buzz(2000, 100);// 2000HZ, 100ms
  453   buzzer_buzz(2500, 100);// 2500HZ, 200ms
}