FreeRTOS-延时函数与系统时钟节拍

FreeRTOS-延时函数与系统时钟节拍

  • FreeRTOS中的延时函数是一个很重要的部分,几乎每个任务中都会调用延时函数,在任务中调用延时函数相当于该函数进入阻塞状态,从而会产生任务调度,让CPU处理其他任务。将FreeRTOS提供了两种延时函数可以使用,一种是相对延时函数,另一种是绝对延时函数。本章深入分析这两种函数的延时源码以及如何产生任务调度的。

系统时钟节拍

  • FreeRTOS中有一个系统时钟节拍SysTicks,这个系统时钟犹如心脏一样维持系统的运行,通过xTickCount时刻反应当前是什么时间段。滴答定时器会定期产生中断,每产生一次中断,xTickCount就会自动+1。上一章节中就曾有提到过系统时钟节拍,但是没有深入分析,本章节就深入分析系统时钟节拍。其函数定义如下:

BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;


	traceTASK_INCREMENT_TICK( xTickCount );
	if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )---(1)
	{
		const TickType_t xConstTickCount = xTickCount + 1;---(2)

		xTickCount = xConstTickCount;

		if( xConstTickCount == ( TickType_t ) 0U )---(3)
		{
			taskSWITCH_DELAYED_LISTS();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		if( xConstTickCount >= xNextTaskUnblockTime )---(4)
		{
			for( ;; )---(5)
			{
				if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )---(6)
				{
					xNextTaskUnblockTime = portMAX_DELAY; ---(7)
					break;
				}
				else
				{
					pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );---(8)
					xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );---(9)

					if( xConstTickCount < xItemValue )---(10)
					{

						xNextTaskUnblockTime = xItemValue;
						break;
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}

					( void ) uxListRemove( &( pxTCB->xStateListItem ) );---(11)
					if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )---(12)
					{
						( void ) uxListRemove( &( pxTCB->xEventListItem ) );
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}

					prvAddTaskToReadyList( pxTCB );---(13)

					#if (  configUSE_PREEMPTION == 1 )---(14)
					{
						if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
						{
							xSwitchRequired = pdTRUE;
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					#endif /* configUSE_PREEMPTION */
				}
			}
		}

		#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )---(15)
		{
			if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
			{
				xSwitchRequired = pdTRUE;
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		#endif 

		#if ( configUSE_TICK_HOOK == 1 )---(16)
		{
			if( uxPendedTicks == ( UBaseType_t ) 0U )
			{
				vApplicationTickHook();
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		#endif /* configUSE_TICK_HOOK */
	}
	else---(17)
	{
		++uxPendedTicks;

		/* The tick hook gets called at regular intervals, even if the
		scheduler is locked. */
		#if ( configUSE_TICK_HOOK == 1 )---(18)
		{
			vApplicationTickHook();
		}
		#endif
	}

	#if ( configUSE_PREEMPTION == 1 )---(19)
	{
		if( xYieldPending != pdFALSE )
		{
			xSwitchRequired = pdTRUE;
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
	#endif /* configUSE_PREEMPTION */

	return xSwitchRequired;
}
  1. 首先判断任务调度是否被挂起
  2. 该句和下面一句主要是对xTickCount自加1。
  3. 因为xTickCount是一个32位的变量,所以随着系统一直运行,一定会有溢出的时候。当发生变量溢出时,FreeRTOS提供了一种解决方法,就是再定义一个溢出列表,如果溢出了,则使用溢出列表来计数,然后再重新交换为原来的列表使用。这里就是判断该变量是否溢出了如果溢出了则会使两个列表发生交换。
  4. 判断是否有任务要被解除阻塞
  5. 循环将已经达到相应的延时时间,需要解除阻塞状态全部任务添加到就绪列表中。
  6. 判断延时列表是否为空,否则就解除任务阻塞,并获取最近的任务解除阻塞时间点。
  7. 如果延时列表为空,而现在又到解除任务阻塞的时间点了,就直接将下一次阻塞时间设置为最大并跳出循环不执行后面的程序。
  8. 延时列表不为空,就 获取处于时间点距现在最近的任务的任务控制块
  9. 获取下一个最近的阻塞时间点
  10. 阻塞时间点有效性判断,如果改时间点还没有到达,则该时间点就是下一个要解除阻塞的时间点,否则可能有多个任务需要解除阻塞状态。
  11. 将任务状态列表从任务控制块中移除,以便添加新的任务状态
  12. 该任务可能同时在等待信号量或者事件,所以也需要将其从事件列表中移除。
  13. 将该任务添加到就绪列表中,解除阻塞状态
  14. 因为使用的是抢占式内核,所以还需要判断刚解除阻塞的任务 任务优先级是否大于当前任务,大于则要进行任务切换
  15. 如果使能了时间片,则还需要同优先级之间的调度
  16. 如果使能了时钟节拍钩子函数,就调用钩子函数
  17. 如果任务调度器被挂起,则由uxPendedTicks计数,并且uxPendedTicks自加1。
  18. 如果使能了时钟节拍钩子函数,就调用钩子函数
  19. 使用抢占式内核,判断是否要进行任务调度

vTaskDelay()

  • 前面我们已经分析了系统时钟节拍计数的原理,即当滴答定时器产生中断的时候,系统时钟节拍计数器自加1,并判断是否要有解除阻塞的任务,如果有则解除阻塞。任务阻塞正是由vTaskDelay()函数产生的,函数源码如下:
void vTaskDelay( const TickType_t xTicksToDelay )
	{
	BaseType_t xAlreadyYielded = pdFALSE;

		/* A delay time of zero just forces a reschedule. */
		if( xTicksToDelay > ( TickType_t ) 0U )---(1)
		{
			configASSERT( uxSchedulerSuspended == 0 );
			vTaskSuspendAll();---(2)
			{
				traceTASK_DELAY();
				prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );---(3)
			}
			xAlreadyYielded = xTaskResumeAll();---(4)
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		if( xAlreadyYielded == pdFALSE )---(5)
		{
			portYIELD_WITHIN_API();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
  1. 进行参数有效性判断,即延时时间必须大于1ms
  2. 挂起任务调度器,以免其他任务产生影响
  3. 将任务添加到延时列表中
  4. 任务调度器解挂,并将返回值保留,调用xTaskResumeAll()函数返回pdTRUE时表示需要进行任务调度,这一期间有任务被添加到请求列表中。
  5. 判断是否要进行任务调度
  • 那么调用函数prvAddCurrentTaskToDelayedList()将任务添加到延时列表中会执行什么操作呢? 下面是prvAddCurrentTaskToDelayedList()函数定义。
static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely )
{
TickType_t xTimeToWake;
const TickType_t xConstTickCount = xTickCount;---1

	#if( INCLUDE_xTaskAbortDelay == 1 )
	{
		pxCurrentTCB->ucDelayAborted = pdFALSE;
	}
	#endif

	if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )---2
	{
		portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );---3
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}

	#if ( INCLUDE_vTaskSuspend == 1 )---4
	{
		if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )---5
		{
			vListInsertEnd( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );---6
		}
		else
		{
			xTimeToWake = xConstTickCount + xTicksToWait;---7
			
			listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );---8

			if( xTimeToWake < xConstTickCount )---9
			{
				vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
			}
			else
			{
				vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );---10

				if( xTimeToWake < xNextTaskUnblockTime )---11
				{
					xNextTaskUnblockTime = xTimeToWake;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
	}
	#else /* INCLUDE_vTaskSuspend */
	{
	···
	}
	#endif /* INCLUDE_vTaskSuspend */
}

  1. 记录当前时钟系统节拍的值
  2. 因为要将该任务添加到延时列表中,所以要将其从任务就绪列表中移除
  3. 取消任务在最高就绪态的标志位
  4. 如果使能了任务挂起
  5. 判断延时时间是否为最大并且确定任务状态为阻塞态,这里传入的形参xCanBlockIndefinitely 为pdFALSE,
  6. 如果满足5的条件则将该任务添加到延时列表的末尾,因为延时时间为最大。
  7. 不满足5的条件,则计算唤醒时间
  8. 将唤醒时间赋值给该任务列表的列表值
  9. 判断该任务唤醒时间是否小于当前时钟节拍计数值,即判断变量的有效性
  10. 将该任务按照列表值即唤醒时间从小到大插入到延时列表中
  11. 如果该任务为所有延时列表唤醒时间最小的,则更新下一次解除阻塞时间为该任务唤醒时间
  • 将整个过程串起来就是,调用函数vTaskDelay()时,设定一个延时时间,然后计算唤醒时间,并将该任务按照唤醒时间从小到大的顺序插入到延时列表中,于是便进入到了阻塞状态,并且始终保证下一个任务唤醒时间点为最小的那个任务唤醒时间,然后系统滴答定时器定期产生中断,来判断是否要接触某些任务的阻塞状态,并判断是否要进行任务切换。这就是延时函数和任务切换的基本原理。但是这个

vTaskDelayUntil()

  • vTaskDelay()函数是一个相对延时函数,对于一些要求固定周期运行的任务是不适合的,因此FreeRTOS提供了一个绝对延时函数。函数定义如下。
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
	{
	TickType_t xTimeToWake;
	BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;

		vTaskSuspendAll();
		{
			const TickType_t xConstTickCount = xTickCount
			xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;---1

			if( xConstTickCount < *pxPreviousWakeTime )---2
			{
				if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )---3
				{
					xShouldDelay = pdTRUE;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else---4
			{
				if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )---5
				{
					xShouldDelay = pdTRUE;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			
			*pxPreviousWakeTime = xTimeToWake;---6

			if( xShouldDelay != pdFALSE )---7
			{
				traceTASK_DELAY_UNTIL( xTimeToWake );

				prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );---8
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		xAlreadyYielded = xTaskResumeAll();---9
		if( xAlreadyYielded == pdFALSE )---10
		{
			portYIELD_WITHIN_API();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
  1. 计算唤醒时间,上一次唤醒时间+所要延时的时间

  2. 我们在前面已经说过,时钟节拍计数器是会溢出的,所以考虑计数器溢出,则会出现xConstTickCount <pxPreviousWakeTime的情况,那么既然xConstTickCount 都要小于pxPreviousWakeTime,xTimeToWake也会小于*pxPreviousWakeTime,并且肯定大于xConstTickCount 。关系图如下所示。
    FreeRTOS-延时函数与系统时钟节拍_第1张图片

  3. 判断情况的有效性,排除不可能的情况,如果成立,则该任务应当延时

  4. xConstTickCount 没有溢出

  5. 这种情况下也可能会有溢出,一种情况是只有xTimeToWake溢出,另外两个不溢出,则这时候三者关系为:xTimeToWake FreeRTOS-延时函数与系统时钟节拍_第2张图片
    另一种情况是,三者都不溢出,也就是pxPreviousWakeTime FreeRTOS-延时函数与系统时钟节拍_第3张图片
    所以该语句就是用于判断是否是以上两种情况,如果是,则标记为应当延时。

  6. 更新任务唤醒时间点

  7. 判断该任务是否满足延时条件,满足则添加到延时列表中

  8. 注意这里的延时时间不再是xTimeIncrement 而是 xTimeToWake - xConstTickCount,这样相当于减去了函数本体运行时间,所以能够确保较为精确地延时,因为相对延时函数是没有计算函数本体的时间的,所以其延时时间如果是1000ms,则其执行周期要大于1000ms

  9. 解除调度器挂起

  10. 判断是否要进行任务调度

  • 接下来是绝对延时函数的用法示例:
void led_task(void *pvParameters)
{
	long i;
	TickType_t PreviousWakeTime;
	const TickType_t TimeIncrement = pdMS_TO_TICKS(1000);//将ms转化为对应的时钟节拍数
	PreviousWakeTime = xTaskGetTickCount();//获取当前时钟节拍
	while(1)
	{
		LED0= ~LED0;
		for(i=0; i<0x6ffff;i++);
		//vTaskDelay(1000);
		vTaskDelayUntil(&PreviousWakeTime,TimeIncrement);//执行绝对延时函数,周期为1000ms
	}
}



到这里我们就将时钟节拍以及延时函数产生任务调度的原理讲述完了。

你可能感兴趣的:(FreeRTOS,freertos,操作系统,嵌入式)