FreeRTOS原理剖析:任务延时

1. 任务延时相关API函数

函数描述
vTaskDelay()任务相对延时
vTaskDelayUntil()任务绝对延时,相对于任务相对延时而言,即以一定的周期执行任务函数
xTaskAbortDelay()任务中止延时函数,该函数能立即解除任务的阻塞状态,将任务插入就绪列表中

任务挂起中其它重要的API函数(介绍过的函数不列出,请参考前面的文章):

函数描述
prvAddCurrentTaskToDelayedList()将当前任务添加到延时列表或者溢出延时列表中
eTaskGetState()或者任务的状态,如运行态、挂起态、阻塞态等

2. 任务延时的基本概念

在FreeRTOS中任务的延时和阻塞都是以系统节拍时钟周期为单位,在FreeRTOSConfig.h中需要设置:

#define configTICK_RATE_HZ	(1000)             //时钟节拍频率,这里设置为1000,周期就是1ms

在FreeRTOS中,系统节拍时钟周期为时钟节拍频率的导数,即T=1/f,比如设置时钟节拍频率为1000,则系统节拍时钟周期为1/1000(s),即为1ms。当任务调度器正常工作时,每1ms产生一次节拍中断,同时变量xTickCount加1,xTickCount记录系统节拍时钟中断次数。当变量xTickCount累加到0xFFFF FFFF时,再增加1,则会溢出。
为了解决xTickCount溢出问题,在FreeRTOS中使用了两个延时列表:xDelayedTaskList1和xDelayedTaskList2,并且使用延时列表指针和溢出延时列表指针分别指向上面两个延时列表。在创建第一个任务时,会调用函数prvInitialiseTaskLists()来初始化两个指针。当xTickCount溢出时,会调用宏taskSWITCH_DELAYED_LISTS(),将延时列表指针和溢出延时列表指针交换,这样,不管时钟节拍数xTickCount在任意数值时,都可通过插入到延时列表或者溢出延时列表,实现数值为0xFFFF FFFF的时钟节拍数延时。

#define taskSWITCH_DELAYED_LISTS()								\
{																\
	List_t *pxTemp;												\
	configASSERT( ( listLIST_IS_EMPTY( pxDelayedTaskList ) ) );	\
																\
	/* 将指向延时列表和溢出延时列表的指针交换 */						\
	pxTemp = pxDelayedTaskList;									\
	pxDelayedTaskList = pxOverflowDelayedTaskList;				\
	pxOverflowDelayedTaskList = pxTemp;							\
																\
	xNumOfOverflows++;											\
																\			
	/* 重新获取下一次解除阻塞的时间 */								\
	prvResetNextTaskUnblockTime();								\
}

说明:

  • 在延时列表中,会根据任务的延时时间先后插入到延时列表中,延时时间短的在前,延时时间长的在后,同时下一个要唤醒的任务时间值保存在xNextTaskUnblockTime中。
  • 系统节拍到达最近的唤醒时间时,系统会将应该唤醒的任务从延时列表中清除,并挂接到就绪列表中。
  • 当系统节拍溢出后,延时列表变成溢出延时列表,溢出延时列表变成了延时列表。而原先的延时列表里的任务在系统节拍溢出之后肯定已经全部被超时唤醒。

3. 任务的相对延时函数vTaskDelay()

3.1 相对延时vTaskDelay()分析

使用任务的相对延时函数vTaskDelay()时,一般先执行完任务函数的主要功能模块,再以用户设置的时钟节拍数xTicksToDelay 延时任务函数。如果xTicksToDelay 设置为0,相当于只执行了一次任务切换。
使用此函数,需要将INCLUDE_vTaskDelay设置为1,在FreeRTOSConfig.h中定义。

函数原型如下:

/********************************************************
参数:xTicksToDelay :需要延时的时钟节拍数(0~0xFFFF FFFF)
返回:无
*********************************************************/
void vTaskDelay( const TickType_t xTicksToDelay )

函数源代码如下:

void vTaskDelay( const TickType_t xTicksToDelay )
{
	BaseType_t xAlreadyYielded = pdFALSE;

	/* 如果延时时间大于0 */
	if( xTicksToDelay > ( TickType_t ) 0U )
	{
		configASSERT( uxSchedulerSuspended == 0 );
		
		vTaskSuspendAll();	/* 挂起任务调度器 */
		{
			traceTASK_DELAY();

			/* 将该任务从就绪列表中删除,添加到延时列表或溢出延时列表,延时时长为当前节拍到唤醒时间 */
			prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
		}
		xAlreadyYielded = xTaskResumeAll();	/* 恢复任务调度器,有可能进行任务切换 */
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}

	/* 如果在函数xTaskResumeAll()中没有进行任务切换 */
	if( xAlreadyYielded == pdFALSE )
	{
		portYIELD_WITHIN_API();	/* 进行一次任务切换 */
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
3.2 函数prvAddCurrentTaskToDelayedList()

该函数通过计算任务下一个唤醒的时间是否溢出了,从而将当前任务添加到延时列表还是溢出延时列表。

函数原型如下:

/********************************************************
参数:		  xTicksToWait:需要延时的系统节拍数
	 xCanBlockIndefinitely: pdTRUE:允许无限期阻塞
	 						pdFALSE:不允许无限期阻塞
返回:无
*********************************************************/
static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely )

函数源代码如下:

static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely )
{
	TickType_t xTimeToWake;
	const TickType_t xConstTickCount = xTickCount;

    /* 如果使能了中止延时功能 */
	#if( INCLUDE_xTaskAbortDelay == 1 )
	{
		pxCurrentTCB->ucDelayAborted = pdFALSE;
	}
	#endif

	/* 将当前任务从就绪列表中移除,如果此时该优先级列表中无其它任务 */
	if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
	{
		/* 清除相应的就绪状态位 */
		portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}

	/* 如果使能任务挂起功能 */
	#if ( INCLUDE_vTaskSuspend == 1 )
	{
		/* 延时时间为portMAX_DELAY并且允许无限期阻塞 */
		if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )
		{
			/* 将当前任务插入到挂起列表中 */
			vListInsertEnd( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
		}
		else
		{
			/* 
			 * 计算该任务的下一次唤醒时间 
			 * xConstTickCount = xTickCount,为当前系统的时钟节拍数
			 * xTicksToWait为任务需要延时的时钟节拍数
			 */
			xTimeToWake = xConstTickCount + xTicksToWait;

			/* 设置当前运行任务的状态列表项中项值(成员xItemValue)为下一次唤醒的时间值 */
			listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );

			/* 下一次唤醒时间值小于系统当前节拍,说明唤醒该任务那时的系统节拍已经溢出 */
			if( xTimeToWake < xConstTickCount )
			{
				/* 将当前正在运行的任务插入溢出延时列表中 */
				vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
			}
			else	/* 唤醒时间大于当前节拍,说明没有溢出 */
			{
				/* 将当前正在运行的任务插入延时列表中 */
				vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );

				/* 当前任务唤醒时间小于原先的下一个解除阻塞任务的唤醒时间 */
				if( xTimeToWake < xNextTaskUnblockTime )
				{
					/* 当前任务唤醒时间为下一个解除阻塞任务的唤醒时间 */
					xNextTaskUnblockTime = xTimeToWake;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
	}
	#else /* INCLUDE_vTaskSuspend */
	{
		/* 下一次任务唤醒的时间 */
		xTimeToWake = xConstTickCount + xTicksToWait;

		/* 设置当前运行任务状态列表项中成员xItemValue为下一次唤醒的时间值 */
		listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );

		/* 下一次唤醒时间值小于系统当前节拍,说明系统节拍已经溢出 */
		if( xTimeToWake < xConstTickCount )
		{
			/* 将当前正在运行的任务插入溢出延时列表中 */
			vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
		}
		else
		{
			/* 将当前正在运行的任务插入延时列表中 */
			vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );

			/* 当前任务唤醒时间小于原先的下一个解除阻塞任务的唤醒时间 */
			if( xTimeToWake < xNextTaskUnblockTime )
			{
				/* 当前任务唤醒时间为下一个解除阻塞任务的唤醒时间 */
				xNextTaskUnblockTime = xTimeToWake;
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}

		( void ) xCanBlockIndefinitely;
	}
	#endif /* INCLUDE_vTaskSuspend */
}

4. 任务的绝对延时vTaskDelayUntil()

任务绝对延时函数vTaskDelayUntil()是相对于任务相对延时函数而言的,它能使任务以一定的周期(或频率)运行,该周期也不是绝对精准,因为有更高优先级或者中断程序运行时才轮到你。使用此函数需将INCLUDE_vTaskDelayUntil设置为1,在FreeRTOSConfig.h 中定义。

函数原型如下:

void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )

参数说明:

  • pxPreviousWakeTime: 上一次任务延时被唤醒的时间节点,第一次时,需要设置该值,若不设置值或默认为0,将造成延时函数第一次不准,常用xTaskGetTickCount()得到系统时钟节拍赋值给pxPreviousWakeTime。
  • xTimeIncrement: 任务延时的时钟节拍数,可用宏定义pdMS_TO_TICKS()转换成系统时钟节拍数。

返回:

用法如下:

void TaskFunc(void *pvParameters) 
{ 	
	TickType_t	PreviousWakeTime; 

	/* 延时时间需要转换为系统节拍数,这里延时100ms */ 	
	const TickType_t TimerIncrement = pdMS_TO_TICKS(100);
   
   	/* 获取系统当前系统节拍数用于vTaskDelayUntil形参 */ 	
   	PreviousWakeTime =  xTaskGetTickCount(); 	
   	
   	while(1)    
   	{
   		/* 任务主体 */
       LED0=!LED0;
   
   		/* 进行延时 */
       vTaskDelayUntil(&PreviousWakeTime, TimerIncrement);	/* LED灯每隔500ms亮灭一次 */    
    } 
}

函数源代码如下:

void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
{
	TickType_t xTimeToWake;
	BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;

	configASSERT( pxPreviousWakeTime );
	configASSERT( ( xTimeIncrement > 0U ) );
	configASSERT( uxSchedulerSuspended == 0 );

	vTaskSuspendAll();	/* 挂起任务调度器 */
	{

		const TickType_t xConstTickCount = xTickCount;	/* 获取系统时钟节拍数 */

		/* 计算下一次任务唤醒的时间 */
		xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;

		/* 前一次唤醒时间大于系统当前节拍,即说明此时系统节拍已经溢出 */
		if( xConstTickCount < *pxPreviousWakeTime )
		{
			/* 
			 * 在下面分析,若此时系统节拍数已经溢出,则只有一种情况,也就是下面的第三种情况。
			 * 如果前一次唤醒时间大于下一次任务唤醒时间,且下一次唤醒时间大于系统节拍 
			 */
			if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
			{
				xShouldDelay = pdTRUE;	/* 需要延时 */
			}
			else	/* 系统节拍没有溢出 */
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			/* 
			 * 如果此时系统节拍数没有溢出,则有两种情况,分别为下面分析的第二种和第三种情况
			 * 如果前一次唤醒时间大于下一次任务唤醒时间,或下一次唤醒时间大于系统节拍 
			 */
			if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
			{
				xShouldDelay = pdTRUE;	/* 需要延时 */
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}

		*pxPreviousWakeTime = xTimeToWake;	/* 将下一次唤醒时间赋值给pxPreviousWakeTime */

		/* 如果需要延时 */
		if( xShouldDelay != pdFALSE )
		{
			traceTASK_DELAY_UNTIL( xTimeToWake );

			/* 将当前任务添加到延时列表中,延时时间为当前节拍到唤醒时间 */
			prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
	xAlreadyYielded = xTaskResumeAll();	/* 解除调度器挂起,解除时可能切换任务 */

	/* 如果解除任务调度器时没有执行任务切换 */
	if( xAlreadyYielded == pdFALSE )
	{
		portYIELD_WITHIN_API();		/* 请求任务切换 */
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
}

任务绝对延时函数中主要包含四个变量:

  • pxPreviousWakeTime:保存上一次任务延时结束唤醒的时间点,每次延时都会参考该时间点。
  • xTimeIncrement:需要显示的时间节拍点,由用户提供。
  • xConstTickCount:记录系统时间节拍点。
  • xTimeToWake:记录任务唤醒时间点,该值等于pxPreviousWakeTime加上xTimeIncrement。

由于xConstTickCount和xTimeToWake变量会有溢出的情况,则有三种情况,以下三种情况都将插入到延时列表中:

第一种情况: 系统节拍数xConstTickCount此时无溢出,计算下一次任务唤醒时间点xTimeToWake无溢出。
在这里插入图片描述
第二种情况: 系统节拍数xConstTickCount此时无溢出,计算下一次任务唤醒时间点xTimeToWake溢出。
在这里插入图片描述
第三种情况: 系统节拍数已经xConstTickCount溢出,计算下一次任务唤醒时间点xTimeToWake也溢出。
在这里插入图片描述
说明:

  • 图中“任务主体”是任务正在执行的主体代码,“vTaskDelayUntil部分”是对任务的延时,“(3)处为其它执行的任务”,任务的总延时时间为xTimeIncrement。
  • 绝对延时函数是相对于相对延时函数而言的,在相对延时函数中,直接将需要函数参数xTicksToDelay作为下一次唤醒的延时时间,而绝对延时函数将函数参数xTimeIncrement加上前一次的延时时间点(*pxPreviousWakeTime) ,然后再减去系统时间节拍数xConstTickCount,最终的数值作为延时时间。
  • 对于第二种情况,xTimeToWake 小于 xConstTickCount,由于xTimeToWake 和 xConstTickCount 都是 uint32_t,则当xTimeToWake - xConstTickCount 作为延时时间时,相当于xTimeToWake 加上 xConstTickCount 的补数,也就是图中xConstTickCount 后面部分时间加上xTimeToWake 前面部分时间。

5. 任务中止延时函数xTaskAbortDelay()

5.1 任务中止延时函数xTaskAbortDelay()

该函数解除任务的阻塞状态,将任务插入就绪列表中。使用此函数需要INCLUDE_xTaskAbortDelay设置为1,在FreeRTOSConfig.h中定义。

函数原型如下:

/********************************************************
参数:xTask:函数的句柄
返回:BaseType_t:
*********************************************************/
BaseType_t xTaskAbortDelay( TaskHandle_t xTask )
BaseType_t xTaskAbortDelay( TaskHandle_t xTask )
{
	TCB_t *pxTCB = ( TCB_t * ) xTask;
	BaseType_t xReturn = pdFALSE;

	configASSERT( pxTCB );

	vTaskSuspendAll();	/* 挂起任务调度器 */
	{
		/* 获取任务的状态(如延时、挂起、就绪等),如果任务阻塞了,满足条件进入 */
		if( eTaskGetState( xTask ) == eBlocked )
		{
			/* 解除任务所有的状态 */
			( void ) uxListRemove( &( pxTCB->xStateListItem ) );
			
			taskENTER_CRITICAL();	/* 进入临界区 */
			{
				/* 如果任务有任务事件等待 */
				if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
				{
					/* 移除任务的事件 */
					( void ) uxListRemove( &( pxTCB->xEventListItem ) );
					
					/* 设置任务解除延时中断标志为pdTRUE */
					pxTCB->ucDelayAborted = pdTRUE;		
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			taskEXIT_CRITICAL();	/* 退出临界区 */

			prvAddTaskToReadyList( pxTCB );	/* 将任务添加到就绪表中 */

			/* 如果使用抢占式内核 */
			#if (  configUSE_PREEMPTION == 1 )
			{
				/* 如果恢复的任务优先级比当前运行的任务优先级高 */
				if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
				{

					xYieldPending = pdTRUE;	/* 设置需要任务切换标志 */
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			#endif /* configUSE_PREEMPTION */
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
	xTaskResumeAll();	/* 解除调度器挂起,解除时可能切换任务 */

	return xReturn;
}
5.2 得到任务状态函数eTaskGetState()

通过带入函数的句柄,将获取任务的状态,使用此函数需要设置INCLUDE_eTaskGetState为1,或者设置configUSE_TRACE_FACILITY为1,在FreeRTOSConfig.h中定义。

函数原型如下:

/********************************************************
参数:xTask:函数的句柄
返回:eTaskState :eRunning:任务处于运行态
				  eBlocked:任务处于阻塞态
				eSuspended:任务处于挂起态
  				  eDeleted:任务已经需要删除或任务无挂接在任何状态下,即无用的任务
				    eReady:任务处于就绪态
*********************************************************/
eTaskState eTaskGetState( TaskHandle_t xTask )

函数源代码如下:

eTaskState eTaskGetState( TaskHandle_t xTask )
{
	eTaskState eReturn;
	List_t *pxStateList;
	const TCB_t * const pxTCB = ( TCB_t * ) xTask;

	configASSERT( pxTCB );

	/* 如果任务等于当前正在运行的任务 */
	if( pxTCB == pxCurrentTCB )
	{
		/* 标记eReturn 为运行态 */
		eReturn = eRunning;
	}
	else
	{
		taskENTER_CRITICAL();	/* 进入临界区 */
		{
			/* 得到TCB状态列表项挂接在那个列表中,如延时列表,挂起列表等 */
			pxStateList = ( List_t * ) listLIST_ITEM_CONTAINER( &( pxTCB->xStateListItem ) );
		}
		taskEXIT_CRITICAL();	/* 退出临界区 */

		/* 如果任务挂接在延时列表或者溢出延时列表中 */
		if( ( pxStateList == pxDelayedTaskList ) || ( pxStateList == pxOverflowDelayedTaskList ) )
		{
			/* 标记eReturn为阻塞态,eReturn为返回值 */
			eReturn = eBlocked;
		}

		/* 如果使能了挂起功能 */
		#if ( INCLUDE_vTaskSuspend == 1 )
			else if( pxStateList == &xSuspendedTaskList )	/* 如果任务挂接在挂起列表中 */
			{
				/* 如果列表无任何等待事件 */
				if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) == NULL )
				{
					eReturn = eSuspended;	/* 标记eReturn为挂起态,eReturn为返回值 */
				}
				else	/* 任务有等待事件 */
				{
					eReturn = eBlocked;	/* 标记eReturn为阻塞,eReturn为返回值 */
				}
			}
		#endif

		/* 如果使能任务删除功能 */
		#if ( INCLUDE_vTaskDelete == 1 )
		
			/* 如果任务挂接在等待删除列表或者任务没有挂接在任何一个列表中 */
			else if( ( pxStateList == &xTasksWaitingTermination ) || ( pxStateList == NULL ) )
			{
				eReturn = eDeleted;	/* 标记eReturn删除,eReturn为返回值 */
			}
		#endif
		else
		{
			eReturn = eReady;	/* 标记eReturn为就绪态,eReturn为返回值 */
		}
	}

	return eReturn;
} 

参考资料:

【1】: 正点原子:《STM32F407 FreeRTOS开发手册V1.1》
【2】: 野火:《FreeRTOS 内核实现与应用开发实战指南》

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值