freertos内核原理Day6(实现任务延时列表)

目录

6.实现任务延时列表

6.1实现时延列表

6.1.1定义时延列表(task.c中定义)

6.1.2任务延时列表初始化

6.1.3定义任务下一时刻解锁时间变量xNextTaskUnblockTime(task.c中定义)

6.2修改阻塞延时函数vTaskDelay

6.3修改xTaskIncrementTick函数

6.3.1假设现在时基变量没有溢出,有任务到期的情况

6.3.2 假设此时时基变量溢出,且有溢出的阻塞任务到期的情况

6.4 仿真


学习这节请完全读懂第4节的内容;(这节非常非常的绕,请对照程序一步步看)

第4节实现阻塞延时用的方法是通过延时量xTicksToDelay来实现的,当任务调用阻塞延时时任务就会被挂起(这里只是将uxTopReadyPriority相应bit位置0,且在每个systick中断内都会扫描任务列表中的每个任务的变量xTicksToDelay,之后每个任务中的变量xTicksToDelay-1,当减到0时,则表明该任务就绪(置uxTopReadyPriority相应bit位1),最后进行任务切换。(上面实现阻塞延时的方式容易理解(主要是为了这节用任务延时列表的方式实现做铺垫),但要是任务多了,每次扫描所有任务就会花费大量时间,而且这个操作是在临界保护断中进行的,临界保护断要求代码精简(不然容易错过其它重要任务中断,从而丧失原有的实时性),显然这样做有点儿得不尝失了),故下面大致介绍新实现的原理:

现在以时基变量xTickCount为基准,创建一个全局变量xNextTaskUnblockTime( 下次任务解锁时刻,其值=xTickCount+任务需要延时时间xTicksToDelay )以及两个任务延时列表( 两个列表都是用于挂载被阻塞延时任务的,不过考虑到时基变量xTickCount会溢出,这才使用两个延时列表,一个挂载xTickCount没溢出时的延时任务,一个挂载xTickCount溢出时的延时任务 ),当任务调用阻塞延时时,任务就会从就绪列表上被删除,然后被挂载到任务延时列表中去,在这之后每次systick中断中xTickCount每次都要和xNextTaskUnblockTime的值做比较,如果xTickCount>=xNextTaskUnblockTime了,则表示有任务到期了,需要将那个任务就绪。

6.实现任务延时列表

6.1实现时延列表

6.1.1定义时延列表(task.c中定义)

static List_t xDelayedTaskList1;
static List_t xDelayedTaskList2;
static List_t * volatile pxDelayedTaskList;
static List_t * volatile pxOverflowDelayedTaskList;

1、可以看到我们定义了任务延时列表根节点1和2(1是挂载系统时基变量溢出前的延时任务的,2是用来挂载系统时基变量溢出后的延时任务的);

2、定义了一个任务延时根节点指针pxDelayedTaskList(用来指向时基没有溢出的那条列表);

3、定义了一个任务延时根节点指针pxOverflowDelayedTaskList(用来指向时基溢出的那条列表);

6.1.2任务延时列表初始化

既然任务延时列表是属于延时列表的一种,则在任务列表初始化时,应该一并初始化任务延时列表;

/* 初始化任务相关的列表 */
void prvInitialiseTaskLists( void )
{
    UBaseType_t uxPriority;
    /* 初始化就绪列表 */
    for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
	{
		vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
	}
    vListInitialise( &xDelayedTaskList1 );
	vListInitialise( &xDelayedTaskList2 );

    pxDelayedTaskList = &xDelayedTaskList1;
	pxOverflowDelayedTaskList = &xDelayedTaskList2;
}

初始化任务相关的列表函数prvInitialiseTaskLists具体做了以下操作:

1、就绪列表中的所有根节点都初始化一遍(将上一下一指针指向自己);

2、用初始化普通根节点的方法,初始化已经创建了的两个任务延时列表根节点xDelayedTaskList1和xDelayedTaskList2;

3、初始化两个任务延时根节点指针的指向,pxDelayedTaskList->没溢出的那个延时列表的根节点,pxOverflowDelayedTaskList ->已溢出的那个延时列表的根节点;

6.1.3定义任务下一时刻解锁时间变量xNextTaskUnblockTime(task.c中定义)

xNextTaskUnblockTime是下一时刻解锁时间变量,其存储着下一个阻塞任务解锁阻塞态的时间,xNextTaskUnblockTime = xTickCount(系统当前时基) + xTicksToDelay(要阻塞的时间);它是一个不能被优化的静态变量,在task.c中定义:(这个变量存的时最近那个唤醒时刻的时间)

static volatile BaseType_t xNumOfOverflows 			= ( BaseType_t ) 0;

这个变量的初始化是在启动调度器时完成的,具体如下所示:

void vTaskStartScheduler( void )
{
/*======================================创建空闲任务start==============================================*/     
    TCB_t *pxIdleTaskTCBBuffer = NULL;
    StackType_t *pxIdleTaskStackBuffer = NULL;
    uint32_t ulIdleTaskStackSize;
    
    /* 获取空闲任务的内存:任务栈和任务TCB */
    vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, 
                                   &pxIdleTaskStackBuffer, 
                                   &ulIdleTaskStackSize );    
    
    xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask,              /* 任务入口 */
					                     (char *)"IDLE",                           /* 任务名称,字符串形式 */
					                     (uint32_t)ulIdleTaskStackSize ,           /* 任务栈大小,单位为字 */
					                     (void *) NULL,                            /* 任务形参 */
                                         (UBaseType_t) tskIDLE_PRIORITY,           /* 任务优先级,数值越大,优先级越高 */
					                     (StackType_t *)pxIdleTaskStackBuffer,     /* 任务栈起始地址 */
					                     (TCB_t *)pxIdleTaskTCBBuffer );           /* 任务控制块 */
/*======================================创建空闲任务end================================================*/ 
    xNextTaskUnblockTime = portMAX_DELAY;
    xTickCount = ( TickType_t ) 0U;

    /* 启动调度器 */
    if( xPortStartScheduler() != pdFALSE )
    {
        /* 调度器启动成功,则不会返回,即不会来到这里 */
    }
}

可以看到在启动调度函数中,xNextTaskUnblockTime被初始化成0xffffffffUL;

6.2修改阻塞延时函数vTaskDelay

现在改变了实现阻塞延时的方式,所以这里要修改以前阻塞延时的代码,具体如下:

void vTaskDelay( const TickType_t xTicksToDelay )
{
    TCB_t *pxTCB = NULL;
    /* 获取当前任务的TCB */
    pxTCB = pxCurrentTCB;
    /* 设置延时时间 */
    //pxTCB->xTicksToDelay = xTicksToDelay;
    /* 将任务插入到延时列表 */
    prvAddCurrentTaskToDelayedList( xTicksToDelay );
    /* 任务切换 */
    taskYIELD();
}

可以看到这里屏蔽了以前依靠xTicksToDelay变量延时的代码 ,并新增了将任务插入到延时列表的函数,prvAddCurrentTaskToDelayedList这个函数具体如下:

static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait )
{
    TickType_t xTimeToWake;
    
    /* 获取系统时基计数器xTickCount的值 */
    const TickType_t xConstTickCount = xTickCount;

    /* 将任务从就绪列表中移除 */
	if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
	{
		/* 将任务在优先级位图中对应的位清除 */
        portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
	}

    /* 计算延时到期时,系统时基计数器xTickCount的值是多少 */
    xTimeToWake = xConstTickCount + xTicksToWait;

    /* 将延时到期的值设置为节点的排序值 */
    listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );

    /* 溢出 */
    if( xTimeToWake < xConstTickCount )
    {
        vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
    }
    else /* 没有溢出 */
    {

        vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );

        /* 更新下一个任务解锁时刻变量xNextTaskUnblockTime的值 */
        if( xTimeToWake < xNextTaskUnblockTime )
        {
            xNextTaskUnblockTime = xTimeToWake;
        }
    }	
}

这个函数做了以下操作:

1、首先创建临时变量xTimeToWake(用于存放当前任务下一次将在系统时基为多少时被唤醒的时间数据),又创建了变量xConstTickCount(用于获取当前系统时基

2、将当前任务从就绪列表中删除(uxListRemove这个函数在第一节链表操作中有介绍,其作用是将传入节点从其所属链表移除,且返回当前优先级就绪根节点下有多少个任务),这里判断当其返回值等于0时(就是将当前任务从就绪列表中移除了后,当前任务所处优先级根节点下已经没有任务了),此时就可以调用portRESET_READY_PRIORITY函数将这个优先级的任务置为“阻塞态”;

3、将计算得到的当前任务要被唤醒时刻赋予给xTimeToWake;(这里注意一点,若此时计算出的xTimeToWake溢出了,也不用担心,因为系统时基也会跟着溢出

4、 将当前任务的排序值设置为当前任务要被唤醒的那个时间;(这样做的好处就是,将其按照升序插入延时链表中时,若有任务的延时到了,此时就不用将各个任务都扫描一遍了,因为肯定时第一个任务的延时到了,直接将根节点后的第一个任务从延时列表中删除,再将这个任务插入到就绪列表中即可

5、判断下一次唤起任务时,系统时基是否溢出,若溢出则将延时任务插入到挂载系统溢出时的那个延时列表上,若没溢出就将任务插入到挂载系统没溢出时的那个延时列表上,最后将任务被唤起的下一刻时间赋予给xNextTaskUnblockTime(这个变量存的时最近那个唤醒时刻的时间

6.3修改xTaskIncrementTick函数

 由于阻塞延时的实现主要是在xTaskIncrementTick函数中,所以新的阻塞延时的实现在要对其进行大的改动,具体如下所示:

void xTaskIncrementTick( void )
{
	TCB_t * pxTCB;
	TickType_t xItemValue;

	const TickType_t xConstTickCount = xTickCount + 1;
	xTickCount = xConstTickCount;

	/* 如果xConstTickCount溢出,则切换延时列表 */
	if( xConstTickCount == ( TickType_t ) 0U )
	{
		taskSWITCH_DELAYED_LISTS();
	}
	/* 最近的延时任务延时到期 */
	if( xConstTickCount >= xNextTaskUnblockTime )
	{
		for( ;; )
		{
			if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
			{
				/* 延时列表为空,设置xNextTaskUnblockTime为可能的最大值 */
				xNextTaskUnblockTime = portMAX_DELAY;
				break;
			}
			else /* 延时列表不为空 */
			{
				pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
				xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
				/* 直到将延时列表中所有延时到期的任务移除才跳出for循环 */
                if( xConstTickCount < xItemValue )
				{
					xNextTaskUnblockTime = xItemValue;
					break;
				}
				/* 将任务从延时列表移除,消除等待状态 */
				( void ) uxListRemove( &( pxTCB->xStateListItem ) );
				/* 将解除等待的任务添加到就绪列表 */
				prvAddTaskToReadyList( pxTCB );
			}
		}
	}/* xConstTickCount >= xNextTaskUnblockTime */
    /* 任务切换 */
    portYIELD();
}

xTaskIncrementTick函数的新实现主要做了以下操作:

1、首先定义了一个任务控制块指针pxTCB(其是用来指向任务延时列表中的第一个任务,也就是会最先退出阻塞延时的那个任务),定义了一个xItemValue变量存储排序值(就是任务的延时到期值),下面又做了一个将系统时基+1的操作;

2、做判断若当前时基在做了+1操作后(也就是+1ms之后),系统时基变量变为了0,则此时只有一种情况,那就是系统时基溢出,此时调用交换指向函数,这个函数具体如下:

#define taskSWITCH_DELAYED_LISTS()\
{\
	List_t *pxTemp;\
	pxTemp = pxDelayedTaskList;\
	pxDelayedTaskList = pxOverflowDelayedTaskList;\
	pxOverflowDelayedTaskList = pxTemp;\
	xNumOfOverflows++;\
	prvResetNextTaskUnblockTime();\
}

可以看到这个函数主要就是将pxDelayedTaskList 指向pxOverflowDelayedTaskList指向的那个延时列表,pxOverflowDelayedTaskList 指向pxDelayedTaskList以前指向的那个延时列表,之后静态变量xNumOfOverflows+1(这个变量是用来存储系统时基变量溢出次数的)最后执行prvResetNextTaskUnblockTime函数,这个函数的实现具体如下:

static void prvResetNextTaskUnblockTime( void )
{
  TCB_t *pxTCB;

	if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
	{
		/* The new current delayed list is empty.  Set xNextTaskUnblockTime to
		the maximum possible value so it is	extremely unlikely that the
		if( xTickCount >= xNextTaskUnblockTime ) test will pass until
		there is an item in the delayed list. */
		xNextTaskUnblockTime = portMAX_DELAY;
	}
	else
	{
		/* The new current delayed list is not empty, get the value of
		the item at the head of the delayed list.  This is the time at
		which the task at the head of the delayed list should be removed
		from the Blocked state. */
		( pxTCB ) = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
		xNextTaskUnblockTime = listGET_LIST_ITEM_VALUE( &( ( pxTCB )->xStateListItem ) );
	}
}

 其做了这些操作:

  • 若pxDelayedTaskList 当前指向的延时列表为空(也就是交换两个延时列表指针指向后pxDelayedTaskList 指向那个链表为空),那么此时初始化xNextTaskUnblockTime 记录下一个任务阻塞到期变量为0xffffffffUL;
  •  若交换指向后其指向延时列表不为空,则获取当前pxDelayedTaskList指向的延时列表下的第一个任务的排序值;

以上就是taskSWITCH_DELAYED_LISTS函数执行的具体操作;

6.3.1假设现在时基变量没有溢出,有任务到期的情况

这一段配合着xTaskIncrementTick函数和prvAddCurrentTaskToDelayedList函数一起看才容易理清;

3、在vTaskDelay函数种调用prvAddCurrentTaskToDelayedList函数时,会将当前延时任务插入到延时列表中,并判断当前任务被唤醒的时刻是否时最快的那个,若是则更新下一刻解锁时刻变量xNextTaskUnblockTime为当前延时任务的解锁时刻(能保证xNextTaskUnblockTime始终存的是解锁最快的那个延时任务的时刻值),下一刻当systick中断且有任务到期时会进入到xTaskIncrementTick函数中,由于第一个任务到期时候肯定没有溢出(因为我们假设刚刚插入的那个任务延时结束时时基是没溢出的状态,则只有这个任务到期后,时间解锁值在其后面的任务才有可能溢出),故现在没有溢出发生所以两个指向延时列表的指针是没有发生指针指向上的切换的,所以现在着两个指针的指向如下所示:

之后程序会在xTaskIncrementTick中执行for死循环内的程序,首先看pxDelayedTaskList指向的列表是否空载:

  •  若为空(这里不可能为空,因为刚刚在vTaskDelay函数中已经将阻塞延时任务挂载到了pxDelayedTaskList指向的列表1 ),则初始化xNextTaskUnblockTime为最大值(因为现在延时列表里已经没有需要解除的阻塞任务了,此时下一刻不可能会有任务被解除阻塞,故被初始化)后退出for死循环;
  •  若不为空,则将延时列表中将要最先退出阻塞的那个任务退出时的时间赋予给临时变量xItemValue,而后判断这个任务的解锁时间是否大于当前时基了,大于当前时基则证明当前任务是到期了的,此时将此任务从阻塞任务列表中删除,并将此任务重新添加到就绪列表中,由于这个程序是在for死循环,所以还要再次执行,这次循环同样先检查延时列表(上次循环已经删除过第一个阻塞到期的任务了)里能最先解除延时的任务它此时能否退出阻塞(这是为了用for迭代的方式将当前时刻所有到期的任务从列表里删除),若这个任务在此刻还没到期,则初始化xNextTaskUnblockTime为当前延时任务的到期值,最后退出for迭代,之后进入任务切换;

6.3.2 假设此时时基变量溢出,且有溢出的阻塞任务到期的情况

这一段配合着xTaskIncrementTick函数和prvAddCurrentTaskToDelayedList函数一起看才容易理清;

3、有溢出的阻塞任务时,其在prvAddCurrentTaskToDelayedList中会被直接按升序插入到pxOverflowDelayedTaskList所指向的延时列表(此时它的排序值为溢出后的值),此时并不会更新xNextTaskUnblockTime变量因为溢出后的唤醒值是一定小于当前没有溢出的唤醒时间值的,这之后直到时基走到允许的最大值0xfffffff ms后(这时pxDelayedTaskList所指向的列表中的延时任务将会被全部就绪,因为此时没溢出的延时任务的解除时间肯定都到了),下一次进入systick中断再次调用xTaskIncrementTick函数时时基就会发生溢出(此时在prvResetNextTaskUnblockTime函数内,xNextTaskUnblockTime的值将会被初始化),此时在xTaskIncrementTick函数中会调用taskSWITCH_DELAYED_LISTS延时指针指向列表调换函数(这时pxOverflowDelayedTaskList就会指向延时列表1,pxDelayedTaskList指向延时列表2,而pxOverflowDelayedTaskList指向列表1在时基溢出之前其挂载的延时任务就被清空了,pxDelayedTaskList指向的延时列表2上挂载了在prvAddCurrentTaskToDelayedList函数中按升序插入pxOverflowDelayedTaskList指向那个列表(也就是列表2),在taskSWITCH_DELAYED_LISTS函数中xNextTaskUnblockTime 被更新为了溢出后第一个任务退出阻塞态的时间值,这之后系统就会又像6.3.1所诉的情况运行;

 至此用列表的方式实现阻塞延时就完成了,这节的内容要多理一下,很绕;

6.4 仿真

这节内容仿真出的结果和第4节的一样,只不过换了一种阻塞延时实现的方式;

程序是由野火提供的例程:

链接:https://pan.baidu.com/s/1SLwcWTflYudnkAUjwM-_yA?pwd=1234 
提取码:1234

本人为初学菜鸟,文章如有错误地方,感谢指正!!

参考:野火freertos内核实现与应用

  • 31
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
FreeRTOS(Real-time Operating System)是一个开源的实时操作系统内核,适用于嵌入式系统的开发。它提供了基础的多任务调度、定时器、事件控制等功能,使得开发者能够轻松地开发和管理嵌入式设备。本文主要介绍了FreeRTOS内核实现和应用开发实战指南。 首先,文章讲解了FreeRTOS内核实现FreeRTOS内核是一个精简的、高效的实时操作系统,其核心思想是基于优先级的协作式多任务调度。文章详细介绍了FreeRTOS任务管理、调度器、中断处理以及同步机制等内核组件的实现原理。通过深入理解这些内核组件的实现细节,开发者可以更好地理解FreeRTOS内核的工作原理,并进行定制化的开发。 其次,文章提供了FreeRTOS的应用开发实战指南。通过一个简单的示例,详细演示了如何使用FreeRTOS进行应用开发。文章从创建任务任务同步、任务间通信等方面进行了讲解,并给出了具体的代码示例。开发者可以根据这个实战指南,快速上手使用FreeRTOS进行应用开发,并在嵌入式系统实现复杂的功能。 此外,文章还介绍了FreeRTOS的一些应用案例。通过这些案例,开发者可以了解如何将FreeRTOS应用于不同领域的嵌入式系统中。例如,文章介绍了使用FreeRTOS开发物联网设备、嵌入式系统监控等应用案例,展示了FreeRTOS在实际项目中的应用场景和优势。 总的来说,这篇文章通过介绍FreeRTOS内核实现和提供应用开发实战指南,帮助开发者更好地理解和使用FreeRTOS,从而在嵌入式系统开发中实现高效、稳定的应用程序。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值