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.mzph.cn/bicheng/56678.shtml

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

相关文章

JDK-23与JavaFX的安装

一、JDK-23的安装 1.下载 JDK-23 官网直接下载&#xff0c;页面下如图&#xff1a; 2.安装 JDK-23 2.1、解压下载的文件 找到下载的 ZIP 文件&#xff0c;右键点击并选择“解压到指定文件夹”&#xff0c;将其解压缩到您希望的目录&#xff0c;例如 C:\Program Files\Java\…

多进程思维导图

1> 思维导图 2> 使用父子进程完成两个文件的拷贝&#xff0c;父进程拷贝前一半&#xff0c;子进程拷贝后一半&#xff0c;两个进程同时进行&#xff08;君子作业&#xff09; #include <myhead.h> typedef struct sockaddr_in addr_in_t; typedef struct sockaddr…

毕业设计选题:基于django+vue的个人博客系统设计与开发

开发语言&#xff1a;Python框架&#xff1a;djangoPython版本&#xff1a;python3.7.7数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat11开发软件&#xff1a;PyCharm 系统展示 管理员登录 管理员功能界面 博主管理 博客文章管理 博文排行管理 博文打赏管理 博文…

Spring 的依赖注入的最常见方式

在 Spring 中&#xff0c;依赖注入的方式有多种选择。下面我们来逐一分析它们的特点、适用场景和注意事项&#xff1a; 1. 构造函数注入 构造函数注入要求在对象创建时提供所有依赖。这种方式确保依赖在对象创建后不可变&#xff0c;特别适合必须强制存在的依赖。所有依赖在对…

JavaWeb 22.Node.js_简介和安装

有时候&#xff0c;后退原来是向前 —— 24.10.7 一、什么是Node.js Node.js 是一个于 Chrome V8 的 JavaScript 运行时环境&#xff0c;可以使 JavaScript 运行在服务器端。使用 Node.js&#xff0c;可以方便地开发服务器端应用程序&#xff0c;如 Web 应用、API、后端服务&a…

Python 工具库每日推荐 【FastAPI】

文章目录 引言Web 框架的重要性今日推荐:FastAPI Web 框架主要功能:使用场景:安装与配置快速上手示例代码代码解释实际应用案例案例:构建一个简单的博客 API案例分析高级特性依赖注入系统后台任务扩展阅读与资源优缺点分析优点:缺点:总结【 已更新完 TypeScript 设计模式…

Transformer图解以及相关的概念

前言 transformer是目前NLP甚至是整个深度学习领域不能不提到的框架&#xff0c;同时大部分LLM也是使用其进行训练生成模型&#xff0c;所以transformer几乎是目前每一个机器人开发者或者人工智能开发者不能越过的一个框架。接下来本文将从顶层往下去一步步掀开transformer的面…

asp.net Core 自定义中间件

内联中间件 中间件转移到类中 推荐中间件通过IApplicationBuilder 公开中间件 使用扩展方法 调用中间件 含有依赖项的 》》》中间件 参考资料

中企通信赋能中信戴卡入选工信部颁发的2023年工业互联网试点示范名单

2024年10月17日&#xff0c;北京-随着工业互联网的迅猛发展&#xff0c;网络安全已成为国家关注的重点议题之一。日前&#xff0c;工业和信息化部&#xff08;工信部&#xff09;公布了2023年工业互联网试点示范名单&#xff0c;中企网络通信技术有限公司&#xff08;简称“中企…

读数据工程之道:设计和构建健壮的数据系统12开源软件

1. 开源软件 1.1. 开源软件(Open Source Software&#xff0c;OSS)是一种软件发行模式&#xff0c;在这种模式下&#xff0c;软件和底层代码库通常在特定的许可条款下可供普遍开发者使用 1.2. 社区管理的开源软件 1.2.1. 大部分开源软件项目…

【Qt】信号和槽——信号和槽的概念、信号和槽的使用、信号和槽的优缺点、自定义信号和槽、信号和槽的断开

文章目录 Qt1. 信号和槽的概念2. 信号和槽的使用3. 信号和槽的优缺点4. 自定义信号和槽5. 信号和槽的断开 Qt 1. 信号和槽的概念 信号是什么&#xff1a; 在Linux中&#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是第一…

opcode从零开始

opcode从零开始 参考资料: Intel 64 and IA-32 ArchitecturesSoftware Developer’s Manual Combined Volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, and 4() Intel64和IA-32架构软件开发人员手册组合卷&#xff1a;1,2a, 2B, 2C, 2D, 3A, 3B, 3C, 3D&#xff0c;和4 罗聪–c…

wordart.top - 轻松创建令人惊叹的文字云!

轻松创建令人惊叹的文字云/词云图&#xff01;适合营销人员、教育工作者、数据爱好者、创意人员、商务人士、活动策划者和社交媒体专家等多种用户群体。支持海量模版库、自定义模板、文本快速提取、精准关键字调整、词云Gif动图制作、词云视频制作、图片一键分享及数据轻松导入…

网盘如何拉新方法教程

网盘拉新是指通过各种推广方式吸引新用户注册和使用网盘服务的过程。以下是对网盘拉新的详细解释&#xff1a; 一、网盘拉新的背景与意义 背景&#xff1a;随着互联网的发展&#xff0c;网盘作为一种基于云计算技术的存储服务&#xff0c;因其便捷性和高效性而备受欢迎。为了…

Linux debian 系桌面系统安装软件方式介绍

debian 系软件的安装方式多种多样&#xff0c;但对于不想花太多时间配置各种环境和依赖的用户来说&#xff0c;建议直接使用“新立得包管理器”安装&#xff0c;既方便又直观。 本文将以常用软件为例&#xff0c;介绍常用的几种软件安装方式。 1. 使用新立得包管理器安装&…

数据字典是什么?和数据库、数据仓库有什么关系?

一、数据字典的定义及作用 数据字典是一种对数据的定义和描述的集合&#xff0c;它包含了数据的名称、类型、长度、取值范围、业务含义、数据来源等详细信息。 数据字典的主要作用如下&#xff1a; 1. 对于数据开发者来说&#xff0c;数据字典包含了关于数据结构和内容的清晰…

15分钟学Go 第4天:Go的基本语法

第4天&#xff1a;基本语法 在这一部分&#xff0c;将讨论Go语言的基本语法&#xff0c;了解其程序结构和基础语句。这将为我们后续的学习打下坚实的基础。 1. Go语言程序结构 Go语言程序的结构相对简单&#xff0c;主要包括&#xff1a; 包声明导入语句函数语句 1.1 包声…

react+video:限制快进、倍速、画中画

实现代码&#xff1a; <video ref{videoRef} src{videoUrl} className{style.video} controls onRateChange{rateChange} onPlay{playVideo} onPause{pauseVideo} onTimeUpdate{timeUpdate} disablePictureInPicture playsInline poster{poster} controlsList"nodownl…

卸载Python

1、查看安装框架位置并删除 Sudo rm -rf /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.8 2、查看应用并删除 在 /Applications/Python 3.x 看是否存在&#xff0c;如果存在并删除。 3、删除软连接 ls -l /usr/bin/py* 或 ls -…