FreeRTOS源码分析-5 系统延时详解

文章详细解析了FreeRTOS中的系统延时API,包括vTaskDelay和vTaskDelayUntil的实现原理和区别,以及相对延时和绝对延时的应用。vTaskDelay用于相对延时,任务在指定周期后变为就绪状态,而vTaskDelayUntil则允许任务在特定时间点后唤醒。此外,还介绍了任务挂起和恢复的机制,涉及调度器的挂起和恢复操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1 系统延时API详解

2 相对延时与绝对延时的区别

3 相对延时与绝对延时的应用 

 4 系统延时函数实现原理

4.1 vTaskDelay业务流程

4.2 vTaskDelayUntil业务流程

5 任务挂起/任务恢复详解


1 系统延时API详解

 

 

 TickType_t 实际上是uint32_t类型

2 相对延时与绝对延时的区别

3 相对延时与绝对延时的应用 

  • 1、创建一个任务使用vTaskDelayUntil()
  • 2、分别在两个任务里定时5s打印一次任务运行状态
  • 3、vTaskDelayUntil()任务   vTaskDelay()任务。添加HAL_Delay()观察两种延时接口的区别

 HAl库会用到systick,在操作系统也会用到systick,所以我们会把HAL库换成tim1,这样不冲突。

cubemx配置

使能vTaskDelayUntil 
串口通信、FreeRTOS任务新建2个,优先级相同、时钟配置、SW配置

 

 4 系统延时函数实现原理

搞清楚4个函数

vTaskDelay

vTaskDelayUntil

vTaskSuspendAll/xTaskResumeAll(调度锁,用来触发整个调度器挂起和恢复)

4.1 vTaskDelay业务流程

1.挂起调度器
挂起调度器的原因:
挂起3个步骤
1切换任务状态
2计算系统节拍值
3上下文的切换。处理这些要占用cpu的时间
如果这个时候有个优先级高的任务产生调度cpu的抢占,等任务恢复就会遥遥无期
调度锁的作用:挂起和恢复,是一种资源的保护

2.添加任务到延时列表
延时列表会去遍历所有延时列表任务,
然后再去控制块里读取延时时间,
如果延时时间到达就会恢复任务到就绪态

3.恢复调度器进行上下文切换

vTaskDelay源码分析

void vTaskDelay( const TickType_t xTicksToDelay )
{
    //xAlreadyYielded :已经调度的状态,初始赋值为0
    BaseType_t xAlreadyYielded = pdFALSE;

	/* 延时周期是否大于0,不大于0,就不应该调度 */
	if( xTicksToDelay > ( TickType_t ) 0U )
	{
		configASSERT( uxSchedulerSuspended == 0 );
        
        //挂起调度器
		vTaskSuspendAll();
		{
			traceTASK_DELAY();

			/*
            1、添加到延时列表中
            2、需要传入两个参数
                2.1、xTicksToDelay延时:周期 
                2.2、pdFALSE 延时:状态值为0 
            */
			prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
		}
        //恢复调度器,这个调度器是有返回值的,这个返回值,表示在恢复调度器的时候,是否已经进行了任务切换
		xAlreadyYielded = xTaskResumeAll();
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}

	/* xAlreadyYielded 等于代表在恢复调度器的时候,没有进行任务切换 */
	if( xAlreadyYielded == pdFALSE )
	{
        //调用了任务切换:内部就是触发PendSV
		portYIELD_WITHIN_API();
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
}

prvAddCurrentTaskToDelayedList源码分析

//添加任务到延时列表中
//传入两个参数
//xTicksToWait:延迟周期
//xCanBlockIndefinitely :延时的确定状态,阻塞或不阻塞
static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely )
{
    //延时周期----下次唤醒的时间
    TickType_t xTimeToWake;
    //系统节拍值,是个全局变量,会在Systick中++
    const TickType_t xConstTickCount = xTickCount;

	/*把当前任务从就绪列表中移除 */
	if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
	{
		/* The current task must be in a ready list, so there is no need to
		check, and the port reset macro can be called directly. */
		portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
    //是否使用任务挂起功能
	#if ( INCLUDE_vTaskSuspend == 1 )
	{
        //portMAX_DELAY  = 0xFFFFFFFF,表示一直阻塞
        //xCanBlockIndefinitely =True,表示可以无限阻塞
		if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )
		{
			/* 把任务添加到,挂起列表中去 */
			vListInsertEnd( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
		}
		else
		{
			/* 先去计算,下次唤醒的tick值 */
			xTimeToWake = xConstTickCount + xTicksToWait;

			/* 每个任务控制块里,状态列表都有一个延时值:value 
                这个value就是任务延时周期,在systick里面进行比较,是否达到
            */
			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; 
        
        //赋值到任务控制块里
		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();
			}
		}

		/* Avoid compiler warning when INCLUDE_vTaskSuspend is not 1. */
		( void ) xCanBlockIndefinitely;
	}
	#endif /* INCLUDE_vTaskSuspend */
}

总结:添加任务到延时列表的处理,主要是去计算tick值,通过tick值判断是添加到延时溢出列表、还是延时列表、还是挂起,三种状态进行处理。

4.2 vTaskDelayUntil业务流程

  • 挂起调度器
  • 判断记录的系统节拍值是否溢出,如果溢出,并且大于当前滴答值,把当前任务添加到延时列表(uint32_t 0xFFFFFFFF 溢出就是0了,溢出并且大于当前滴答值,说明任务还没到达)
  • 判断记录的系统节拍,值是否溢出,没有溢出,当前定时间隔小于记录值,或者大于系统节拍值,把当前任务添加到延时列表(认为任务可以触发,添加到延时列表)
  • 更新记录值,恢复调度器,进行上下文切换

源码分析

void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
{
    //下次任务要唤醒的系统节拍值
    TickType_t xTimeToWake;
    //xAlreadyYielded:表示是否已经进行了任务切换
    //xShouldDelay :表示是否需要进行延时处理
    BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;

    //挂起调度器
	vTaskSuspendAll();
	{
		/* 获取全局变量系统节拍值,在systick中会++ */
		const TickType_t xConstTickCount = xTickCount;

		/* 获取任务下次唤醒的系统节拍值 */
		xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;

        //pxPreviousWakeTime指向上一次保存的任务的唤醒节拍值
        //这个时候如果大于当前系统节拍值,无非两种可能
        //1、延时周期达到了
        //2、整个tick计数值,已经溢出了
		if( xConstTickCount < *pxPreviousWakeTime )
		{
			/*1、下次要唤醒的系统节拍值要小于上次要唤醒的节拍值---表示系统节拍值计数溢出
              2、下次要唤醒的系统节拍值大于当前的系统节拍值---表示需要延时
            */
			if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
			{
                //标记需要延时
				xShouldDelay = pdTRUE;
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else //系统节拍值没有溢出
		{
			/*1、下次要唤醒的系统节拍值,小于上次唤醒的系统节拍值---证明系统节拍值溢出,延时进行
              2、下次要唤醒的系统节拍值大于当前系统节拍值---证明延时周期,在当前的时间之后
             */
			if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
			{
                //标记延时状态
				xShouldDelay = pdTRUE;
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}

		/* 保存下次唤醒的节拍值 */
		*pxPreviousWakeTime = xTimeToWake;

        //判断是否需要延时
		if( xShouldDelay != pdFALSE )
		{
			traceTASK_DELAY_UNTIL( xTimeToWake );

			/*添加任务到延时列表中 */
			prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
    //调度器恢复,如果调度器内部已经进行了任务切换,那么返回一个True
	xAlreadyYielded = xTaskResumeAll();

	/* 如果调度器没有进行任务切换,那么要进行任务切换 */
	if( xAlreadyYielded == pdFALSE )
	{
        //进行PendSV异常
		portYIELD_WITHIN_API();
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
}

5 任务挂起/任务恢复详解

vTaskSuspendAll业务流程

  • ++uxSchedulerSuspended(在分析systick调度的时候也用到过)
  • 上下文切换中断判断,uxSchedulerSuspended>0不进行任务切换

 

void vTaskSuspendAll( void )
{
	/* 调取记录值++
          这个值是去让Systick中断产生的时候,不去遍历阻塞列表,进行任务恢复

     */
	++uxSchedulerSuspended;
}

vTaskResumeAll业务流程

  • 进入临界区,挂起记录减1
  • 判断是挂起就绪列表(全称是挂起就绪列表)是否为空,不为空,添加任务到就绪列表中,如果优先级高于当前任务,则进行上下文切换。(任务可以在中断中ISR恢复,我们是没法进行任务切换的,所以要放入挂起就绪列表中)
  • 判断调度器挂起后的SysTick值,重新遍历阻塞列表,进行上下文切换(每次在systick中,都会判断是否挂起,挂起的话不做处理,但是systick会累加,如果不重新遍历阻塞列表,找出之前错过的任务列表,进行调度)
BaseType_t xTaskResumeAll( void )
{
    TCB_t *pxTCB = NULL;
    //是否已经任务切换
    BaseType_t xAlreadyYielded = pdFALSE;

	/* 进入临界段,不想中断打扰 */
	configASSERT( uxSchedulerSuspended );

	taskENTER_CRITICAL();
	{
        //调度器记录值减一
		--uxSchedulerSuspended;
        //如果调度器恢复了
		if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
		{
            //判断当前任务数量是否大于0
			if( uxCurrentNumberOfTasks > ( UBaseType_t ) 0U )
			{
        
				/* 从挂起的就绪列表中遍历*/
				while( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE )
				{
                    //获取任务控制块
					pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &xPendingReadyList ) );
                    //移除 挂起就绪列表
                    //移除 事件列表
					( void ) uxListRemove( &( pxTCB->xEventListItem ) );
					( void ) uxListRemove( &( pxTCB->xStateListItem ) );
				    //添加到就绪列表
                	prvAddTaskToReadyList( pxTCB );

					/* 如果优先级大于当前任务优先级,则进行任务切换 */
					if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
					{
						xYieldPending = pdTRUE;
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}

                //获取到的任务控制块不为空
				if( pxTCB != NULL )
				{
					//需要更新系统的时间片
					prvResetNextTaskUnblockTime();
				}

				/* 获取 在调度器挂起时,systick挂起记录值 */
				{
					UBaseType_t uxPendedCounts = uxPendedTicks; /* Non-volatile copy. */
                    //如果记录值大于0
					if( uxPendedCounts > ( UBaseType_t ) 0U )
					{
						do
						{
                            //进行systick调度处理,其实就是遍历阻塞列表,如果需要任务切换,返回Ture
							if( xTaskIncrementTick() != pdFALSE )
							{
                                //标记任务需要切换
								xYieldPending = pdTRUE;
							}
							else
							{
								mtCOVERAGE_TEST_MARKER();
							}
							--uxPendedCounts;
                            //一直遍历,直到 uxPendedCounts= 0
						} while( uxPendedCounts > ( UBaseType_t ) 0U );
                        //赋值为0
						uxPendedTicks = 0;
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}

                //如果需要任务切换
				if( xYieldPending != pdFALSE )
				{
                    //判断是否内核是抢占式
					#if( configUSE_PREEMPTION != 0 )
					{
                        //标记已经进行调度的状态
						xAlreadyYielded = pdTRUE;
					}
					#endif
                    //进行调度
					taskYIELD_IF_USING_PREEMPTION();
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
    //退出临界段
	taskEXIT_CRITICAL();
    //返回调度值
	return xAlreadyYielded;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

4IOT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值