FreeRTOS - 软件定时器

在学习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_STARTxMessage.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 listalso 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 timercommands. */if( xMessage.xMessageID < ( BaseType_t ) 0 ){const CallbackParameters_t * const pxCallback = &( xMessage.u.xCallbackParameters );/* The timer uses the xCallbackParameters member to request acallback 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 pendedfunction calls. *//* 判断定时器命令是否有效 */if( xMessage.xMessageID >= ( BaseType_t ) 0 ){/* The messages uses the xTimerParameters member to work on asoftware 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, butit must be present in the function call.  prvSampleTimeNow() must becalled after the message is received from xTimerQueue so there is nopossibility of a higher priority task adding a message to the messagequeue with a time that is ahead of the timer daemon task (because itpre-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 activetimer 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 canbe longer or shorter than the old one.  The command time istherefore set to the current time, and as the period cannotbe zero the next expiry time can only be in the future,meaning (unlike for the xTimerStart() case above) there isno 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 dynamicallyallocated. *//* 删除定时器*//* 判断定时器内存是否需要释放(动态释放)*/#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 ordynamically, so check before attempting to free thememory. */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, 100ms453   buzzer_buzz(2500, 100);// 2500HZ, 200ms
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/450615.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring AI Alibaba 接入国产大模型通义千问

整体介绍 本文是一个详细的例子&#xff0c;讲解了如何基于spring ai 来调用通义千问国产大模型&#xff0c;有详细的代码和配置&#xff0c;并且免费。 Spring AI&#xff1a;简化Java开发者构建AI应用的统一框架 在过去&#xff0c;Java 开发者在构建 AI 应用时面临的一大…

【ios】解决xcode版本过低无法真机调式的问题

最低要求和支持的 SDK&#xff1a;Xcode - 支持 - Apple Developer 我的Xcode版本是14.2 手机系统版本是iOS15.8.3 步骤一 在终端中运行 open /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport 步骤二 先去https://github.com/fi…

AI 设计工具合集

&#x1f423;个人主页 可惜已不在 &#x1f424;这篇在这个专栏AI_可惜已不在的博客-CSDN博客 &#x1f425;有用的话就留下一个三连吧&#x1f63c; ​ 前言: AI 视频&#xff0c;科技与艺术的精彩融合。它借助先进的人工智能技术&#xff0c;为影像创作带来全新可能。本书…

星海智算:【萤火遛AI-Stable-Diffusion】无需部署一键启动

部署流程 1、注册算力云平台&#xff1a;星海智算 https://gpu.spacehpc.com/ 2、创建实例&#xff0c;镜像请依次点击&#xff1a;“镜像市场”->“更换”->“AI绘画”->“萤火遛AI-Stable Diffusion”。 程序首次启动可能需要几分钟&#xff0c;待实例显示“运行…

2009年国赛高教杯数学建模A题制动器试验台的控制方法分析解题全过程文档及程序

2009年国赛高教杯数学建模 A题 制动器试验台的控制方法分析 汽车的行车制动器&#xff08;以下简称制动器&#xff09;联接在车轮上&#xff0c;它的作用是在行驶时使车辆减速或者停止。制动器的设计是车辆设计中最重要的环节之一&#xff0c;直接影响着人身和车辆的安全。为了…

MOE论文详解(4)-GLaM

2022年google在GShard之后发表另一篇跟MoE相关的paper, 论文名为GLaM (Generalist Language Model), 最大的GLaM模型有1.2 trillion参数, 比GPT-3大7倍, 但成本只有GPT-3的1/3, 同时效果也超过GPT-3. 以下是两者的对比: 跟之前模型对比如下, 跟GShard和Switch-C相比, GLaM是第一…

[WPF初学到大神] 1. 什么是WPF, MVVM框架, XAML?

什么是WPF? WPF(Windows Presentation Foundation) 包含XAML标记语言和后端代码来开发桌面应用程序的. 用VS新建项目有WPF(.Net Framework和.Net应用程序), 该怎么选? 首选 .NET 应用程序(.NET Core 或 .NET 5/6/7/8新版本)拥有更好的性能、跨平台Windows, Linux, Mac支…

电气自动化13:PLC控制硬件组成与工作扫描原理

1.PLC硬件组成&#xff1a; CPU&#xff08;中央处理器&#xff09; 存储器 系统程序存储器用户程序存储器分为&#xff1a;用户程序存储器&#xff08;程序区&#xff09;、功能存储器&#xff08;数据区&#xff09; 输入/输出&#xff08;I/O&#xff09;接口电路 电源 …

SpringBoot优雅下线

一&#xff0c;什么是优雅下线 当我们需要部署新版本代码的时候&#xff0c;需要重启服务&#xff0c;这个时候可能会出现一些问题&#xff0c;比如之前服务正在处理的请求还在处理&#xff0c;这个时候如果强制的停止服务&#xff0c;会造成数据丢失或者请求失败的情况。那么…

后端Web开发

一、Maven &#xff08;一&#xff09;、概述 视频中要用的是jdk11 &#xff08;二&#xff09;、 idea集成Maven 1.配置Maven环境 2.创建Maven项目 3.导入Maven项目 法一&#xff1a; 法二&#xff1a; &#xff08;三&#xff09;、依赖管理 1.依赖配置 2.依赖传递 3.依…

数控机械制造工厂ERP适用范围有哪些

在当今制造业高速发展的背景下&#xff0c;企业资源计划(ERP)系统已成为提升工厂管理效率、实现生产自动化与信息化的关键工具。特别是对于数控机械制造工厂而言&#xff0c;一个合适的ERP系统能够帮助其优化生产流程、提高产品质量、降低生产成本并增强市场竞争力。 1. 生产计…

06 算法基础:算法的定义、表现形式(自然语言、伪代码、流程图)、五个特性(有穷性、确定性、可行性、输入、输出)、好算法的设计目标

目录 1 算法的定义 2 算法的三种表现形式 2.1 自然语言 2.2 伪代码 2.3 流程图 3 算法的五个特性 3.1 有穷性 3.2 确定性 3.3 可行性 3.4 输入 3.5 输出 4 好算法的设计目标 4.1 正确性 4.2 可读性 4.3 健壮性 4.4 通用性 4.5 高效率与低存储量 1 算法的定义 …

Java笔记-static关键字

1.static关键字内存说明 2.访问特点 package com.test.Statics2;import com.test.statics.Student;public class Test {public static void main(String[] args) {// 静态成员中访问非静态成员// method3() // 错误-不能直接调用&#xff0c;需要new对象调用Test test01 new T…

英伟达开源超强模型Nemotron-70B;OpenAI推出Windows版ChatGPT桌面客户端

&#x1f989; AI新闻 &#x1f680; 英伟达开源超强模型Nemotron-70B 摘要&#xff1a;英伟达近日开源了新型AI模型Nemotron-70B&#xff0c;迅速超越GPT-4o和Claude 3.5 Sonnet&#xff0c;成为AI社区的新宠。该模型在多项基准测试中表现优异&#xff0c;采用混合训练方法和…

STM32CUBEIDE新建工程

新建工作区 可以在下面的目录创建工作区&#xff0c;来管理不同的工程&#xff0c;其中有一个是第一次打开软件的时候创建的。 新建一个工程 使用stm32cubeMX生成程序 生成之后直接打开&#xff0c;由于有一些.c.h文件是我们自己建立的&#xff0c;所以需要手动添加进工程…

Linux的开发工具gcc Makefile gdb的学习

一&#xff1a;gcc/g 1. 1 背景知识 1. 预处理&#xff08;进行宏替换) 预处理 ( 进行宏替换 ) 预处理功能主要包括宏定义,文件包含,条件编译,去注释等。 预处理指令是以#号开头的代码行。 实例: gcc –E hello.c –o hello.i 选项“-E”,该选项的作用是让 gcc 在预处理结…

Unicode编码检查, 字符计算, Utf8与Utf16互转, GBK字符计算

CUnicodeUtils #pragma once #include <stdint.h> #include <string>class CUnicodeUtils { public:// // brief: 获取UTF16字符个数// param: pData 数据(UTF16编码, 大端字节序或小端字节序, 可包含BOM)// param: size 数据长度(字节)//…

MySQL日期类型选择建议

我们平时开发中不可避免的就是要存储时间&#xff0c;比如我们要记录操作表中这条记录的时间、记录转账的交易时间、记录出发时间、用户下单时间等等。你会发现时间这个东西与我们开发的联系还是非常紧密的&#xff0c;用的好与不好会给我们的业务甚至功能带来很大的影响。所以…

对话型AI:Auto Possess Player Auto Possess AI

Auto Possess Player “Auto Possess Player” 是一个常见于游戏开发&#xff0c;尤其是在 Unreal Engine 中的术语。它指的是一个功能或设置&#xff0c;使得一个特定的角色或对象在游戏开始时自动接管玩家的控制权。以下是一些关键点&#xff1a; 含义 自动控制&#xff…

STM32外设之ADC应用--寄存器开发

1.ADC简介 模数转换器&#xff08;Analog-to-Digital Converter&#xff0c;简称ADC&#xff09;是一种重要的电子设备&#xff0c;它能够将模拟信号转换为数字信号。是一种将连续变化的模拟信号转换为离散的数字信号的电子设备。这种转换使得模拟信号可以在数字系统中进行处理…