FreeRTOS专题七:任务延时列表

在之前,为了实现任务的阻塞延时,在任务控制块中内置了一个延时变量xTicksToDelay。

每当任务需要延时的时候,就初始化 xTicksToDelay 需要延时的时间,然后将任务挂起,这里的挂起只是将任务在优先级位图表 uxTopReadyPriority 中对应的位清零,并不会将任务从就绪列表中删除。当每次时基中断(SysTick 中断)来临时,就扫描就绪列表中的每个任务的 xTicksToDelay,如果 xTicksToDelay 大于 0 则递减一次,然后判断xTicksToDelay 是否为 0,如果为 0 则表示延时时间到,将该任务就绪(即将任务在优先级位图表 uxTopReadyPriority 中对应的位置位),然后进行任务切换。这种延时的缺点是,在每个时基中断中需要对所有任务都扫描一遍,费时,优点是容易理解。

任务延时列表的工作原理:

在 FreeRTOS 中,有一个任务延时列表,当任务需要延时的时候,则先将任务挂起,即先将任务从就绪列表删除,然后插入到任务延时列表,同时更新下一个任务的解锁时刻变量:xNextTaskUnblockTime 的值。xNextTaskUnblockTime 的值等于系统时基计数器的值 xTickCount 加上任务需要延时的值 xTicksToDelay。当系统时基计数器 xTickCount 的值与 xNextTaskUnblockTime 相等时,就表示有任务延时到期了,需要将该任务就绪。与 RT-Thread 和 μC/OS 在解锁延时任务时要扫描定时器列表这种时间不确定性的方法相比,FreeRTOS 这个 xNextTaskUnblockTime全局变量设计的非常巧妙。任务延时列表表维护着一条双向链表,每个节点代表了正在延时的任务,节点按照延时时间大小做升序排列。当每次时基中断(SysTick 中断)来临时,就拿系统时基计数器的值 xTickCount 与下一个任务的解锁时刻变量 xNextTaskUnblockTime 的值相比较,如果相等,则表示有任务延时到期,需要将该任务就绪,否则只是单纯地更新系统时基计数器xTickCount的值,然后进行任务切换。

1 实现任务延时列表:

定义两条任务延时列表(一条没有溢出,一条溢出),然后定义两个列表指针,分别指向这两条列表:

// 定义两个任务延时列表
static List_t xDelayedTaskList1;
static List_t xDelayedTaskList2;
// 在定义两个列表指针,指向上面两个列表
static List_t * volatile pxDelayedTaskList;
static List_t * volatile pxOverflowDelayedTaskList;

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;
}

3 定义下一个任务时刻和溢出变量:

       定义xNextTaskUnblockTime变量,表示下一个任务要解锁的时刻。xNextTaskUnblockTime 的值等于系统时基计数器的值 xTickCount 加上任务需要延时值 xTicksToDelay。当系统时基计数器 xTickCount 的值与 xNextTaskUnblockTime 相等时,就表示有任务延时到期了,需要将该任务就绪。

static volatile TickType_t xNextTaskUnblockTime		= ( TickType_t ) 0U;
static volatile BaseType_t xNumOfOverflows 			= ( BaseType_t ) 0;

xNextTaskUnblockTime变量在启动调度器时,被初始化为系统最大延时:

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================================================*/ 
    // #define portMAX_DELAY ( TickType_t ) 0xffffffffUL 
		// 启动调度器时,xNextTaskUnblockTime直接被初始化成系统最大延时
		xNextTaskUnblockTime = portMAX_DELAY;
    xTickCount = ( TickType_t ) 0U;

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

4 修改 vTaskDelay()函数,不再像之前,把延时时间添加到任务控制块,而是直接把要延时的任务,插入到延时列表:

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

那么此时,由于添加了任务的延时列表,延时的时候不用再依赖任务控制块TCB 中内置的延时变量 xTicksToDelay了。

5 上一步调用了将任务插入延时列表的函数,那现在去实现这个函数(功能很明确,因为要堵塞演示,所以先将任务从就绪列表内移除,并清除优先级位,然后设置延时到期的tick数,判断是否溢出,然后以节点辅助值升序插入到相应列表,再更新下一个时刻xNextTaskUnblockTime的值),实现过程如下:

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;
        }
    }	
}

注意:调用函数 uxListRemove()将任务从就绪列表移除,uxListRemove()会返回当前链表下节点的个数,如果为 0,则表示当前链表下没有任务就绪,则调用函数portRESET_READY_PRIORITY()将任务在优先级位图表 uxTopReadyPriority 中对应的位清
除(这样查找最高优先级时,就查不到这个列表了)
。因为 FreeRTOS 支持同一个优先级下可以有多个任务,所以在清除优先级位图表uxTopReadyPriority 中对应的位时要判断下该优先级下的就绪列表是否还有其它的任务。

最精髓的一步:

更新下一个任务解锁时刻变量 xNextTaskUnblockTime 的值。这一步很重要,在 xTaskIncrementTick()函数中,我们只需要让系统时基计数器 xTickCount 与xNextTaskUnblockTime 的值先比较就知道延时最快结束的任务是否到期

6 修改 xTaskIncrementTick()函数,这个函数相当麻烦,要仔细看注释。实现的功能就是,不断tick,然后到了任务延时时间,就取出任务加入到就绪列表,再切换任务。

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

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

	/* 如果xConstTickCount溢出,则切换延时列表 */
	// 当系统时基计数器溢出的时候,延时列表pxDelayedTaskList 和pxOverflowDelayedTaskList要互相切换
	if( xConstTickCount == ( TickType_t ) 0U )
	{
		taskSWITCH_DELAYED_LISTS();
	}

	/* 最近的延时任务延时到期 */
	// tick不断增加,当正好增加到当前最近要执行的任务时
	if( xConstTickCount >= xNextTaskUnblockTime )
	{
		for( ;; )
		{
			if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
			{
				/* 延时列表为空,设置xNextTaskUnblockTime为可能的最大值 */
				xNextTaskUnblockTime = portMAX_DELAY;
				break;
			}
			else /* 延时列表不为空 */
			{
				/* 从任务延时列表中(pxDelayedTaskList指针指向任务延时列表,而不是溢出列表),
				获得第一个节点的任务控制块,这也是最近要执行的任务 */
				pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
				// 获得节点的辅助排序值,然后更新到xNextTaskUnblockTime变量中,这里因为是死循环
				// 所以会不断地取出前面的节点,那么节点排序值就会越来越大
				xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );

				/* 直到将延时列表中所有延时到期的任务移除才跳出for循环 */
				// 这样设置,是为了取出延时列表内,同一时刻到期的所有任务,然后跳出
				// 一旦当前tick值 < 节点排序值,说明任务没有到期,直接跳出
        if( xConstTickCount < xItemValue )
				{
					xNextTaskUnblockTime = xItemValue;
					break;
				}
				// 下面的函数,执行对象都是时间到期的任务,因为一旦没到期,函数就跳出了
				/* 将任务从延时列表移除,消除等待状态 */
				( void ) uxListRemove( &( pxTCB->xStateListItem ) );

				/* 将解除等待的任务添加到就绪列表 */
				prvAddTaskToReadyList( pxTCB );
			}
		}
	}/* xConstTickCount >= xNextTaskUnblockTime */
    
    /* 任务切换 */
		// 此时,所有在当前tick到期的任务,都已经从任务延时列表中移除,并添加到就绪列表
		// 一旦任务切换,这些任务就会按照优先级来执行
    portYIELD();
}

7 修改充值就绪优先级的宏定义 taskRESET_READY_PRIORITY()

#define taskRESET_READY_PRIORITY( uxPriority )														\
	{																									\
		if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 )	\
		{																								\
			portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) );							\
		}																								\
	}

在没有添加任务延时列表之前,与任务相关的列表只有一个,就是就绪列表,无论任务在延时还是就绪都只能通过扫描就绪列表来找到任务的 TCB,从而实现系统调度。所以在之前,实现 taskRESET_READY_PRIORITY()函数的时候,不用先判断当前优先级下就绪列表中的链表的节点是否为 0,而是直接把任务在优先级位图表uxTopReadyPriority 中对应的位清零。因为当前优先级下就绪列表中的链表的节点不可能为0,目前我们还没有添加其它列表来存放任务的 TCB,只有一个就绪列表。


但是从现在开始,我们额外添加了延时列表,当任务要延时的时候,将任务从就绪列表移除,然后添加到延时列表,同时将任务在优先级位图表 uxTopReadyPriority 中对应的位清除。在清除任务在优先级位图表 uxTopReadyPriority 中对应的位的时候,与上一章同的是需要判断就绪列表 pxReadyTasksLists[]在当前优先级下对应的链表的节点是否为 0(因为同一优先级下面,可能会有多个任务),只有当该链表下没有任务时才真正地将任务在优先级位图表 uxTopReadyPriority 中对应的位清零(防止影响到该优先级下面其他任务的执行)

测试结果:

FreeRTOS专题七:任务延时列表_第1张图片

 

你可能感兴趣的:(FreeRTOS专题七:任务延时列表)