FreeRTOS专题五:空闲任务与阻塞延时

在上一章节中,任务体内的延时使用的是软件延时,即还是让 CPU 空等来达到延时的效果。使用 RTOS 的很大优势就是榨干 CPU 的性能,永远不能让它闲着,任务如果需要延时也就不能再让 CPU 空等来实现延时的效果。

RTOS 中的延时叫阻塞延时,即任务需要延时的时候,任务会放弃 CPU的使用权,CPU可以去干其它的事情,当任务延时时间到,重新获取 CPU使用权,任务继续运行,这样就充分地利用了 CPU的资源,而不是干等着。

当任务需要延时,进入阻塞状态,那 CPU 又去干什么事情了?如果没有其它任务可以运行,RTOS 都会为 CPU 创建一个空闲任务,这个时候 CPU 就运行空闲任务。

空闲任务的优先级是最低的,主要是做一些系统内存的清理工作。空闲任务是系统在【启动调度器】的时候创建的,而不是在主函数一开始,和任务函数一起创建,这点要比较注意。

接下来,实现一个空闲任务:

1 定义空闲任务的栈

StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];

2 定义空闲任务的任务控制块

TCB_t IdleTaskTCB;

3 定义空闲任务函数主体

static portTASK_FUNCTION( prvIdleTask, pvParameters )
{
	/* 防止编译器的警告 */
	( void ) pvParameters;
    
    for(;;)
    {
        /* 空闲任务暂时什么都不做 */
    }
}

4 创建空闲任务(在启动调度器时创建的)

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,                            /* 任务形参 */
		(StackType_t *)pxIdleTaskStackBuffer,     /* 任务栈起始地址 */
		(TCB_t *)pxIdleTaskTCBBuffer );           /* 任务控制块 */
    /* 将任务添加到就绪列表 */                                 
    vListInsertEnd( &( pxReadyTasksLists[0] ), &( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem ) );
/*======================================创建空闲任务end================================================*/
                                         
    /* 手动指定第一个运行的任务 */
    pxCurrentTCB = &Task1TCB;
                                         
    /* 初始化系统时基计数器 */
    xTickCount = ( TickType_t ) 0U;
    
    /* 启动调度器 */
    if( xPortStartScheduler() != pdFALSE )
    {
        /* 调度器启动成功,则不会返回,即不会来到这里 */
    }
}

在调用创建函数前,先通过了一个函数来获取空闲任务的内存。这个实在是太简单,跟之前创建任务函数没有什么区别,我感觉就是多此一举。但归根结底,就是赋值吧,把前面3个过程创建的任务栈、任务控制块和空闲任务函数主体传入任务创建函数。然后把空闲任务加载到就绪列表,空闲任务在就绪列表内是最低优先级(数组下标为0)。

实现阻塞延时

阻塞延时的阻塞是指任务调用该延时函数后,任务会被剥离 CPU 使用权,然后进入阻塞状态,直到延时结束,任务重新获取 CPU 使用权才可以继续运行。在任务阻塞的这段时间,CPU可以去执行其它的任务,如果其它的任务也在延时状态,那么 CPU就将运行空闲任务。

当任务1在阻塞延时,调度器会尝试运行任务2;此时如果任务2也在阻塞延时,调度器就会去运行空闲任务。

延时阻塞函数vTaskDelay(),先设置延时时间,那么它都进入延时了,肯定要切换任务啊:

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

为了记录任务需要延时的时间,我们需要在任务控制块中添加一个成员xTicksToDelay,单位是SysTick的中断周期。在野火的书籍中,SysTick的中断周期设置为10ms,那么调用vTaskDelay(2)就会延时2 * 10ms。此时,任务控制块的完整定义为:

typedef struct tskTaskControlBlock
{
	volatile StackType_t    *pxTopOfStack;    /* 栈顶 */

	ListItem_t			    xStateListItem;   /* 任务节点 */
    
    StackType_t             *pxStack;         /* 任务栈起始地址 */
	                                          /* 任务名称,字符串形式 */
	char                    pcTaskName[ configMAX_TASK_NAME_LEN ];

    TickType_t xTicksToDelay; /* 用于延时 */    
} tskTCB;
typedef tskTCB TCB_t;

在之前,任务选择函数很简单,就是在两个任务之间来回切换。而现在,需要增加一个空闲任务,让pxCurrentTCB在这三个任务中切换,需要修改算法:

vTaskSwitchContext()任务切换函数:

void vTaskSwitchContext( void )
{
	/* 如果当前线程是空闲线程,那么就去尝试执行线程1或者线程2,
       看看他们的延时时间是否结束,如果线程的延时时间均没有到期,
       那就返回继续执行空闲线程 */
	if( pxCurrentTCB == &IdleTaskTCB )
	{
		if(Task1TCB.xTicksToDelay == 0)
		{            
            pxCurrentTCB =&Task1TCB;
		}
		else if(Task2TCB.xTicksToDelay == 0)
		{
            pxCurrentTCB =&Task2TCB;
		}
		else
		{
			return;		/* 线程延时均没有到期则返回,继续执行空闲线程 */
		} 
	}
	else
	{
		/*如果当前线程是线程1或者线程2的话,检查下另外一个线程,如果另外的线程不在延时中,就切换到该线程
        否则,判断下当前线程是否应该进入延时状态,如果是的话,就切换到空闲线程。否则就不进行任何切换 */
		if(pxCurrentTCB == &Task1TCB)
		{
			if(Task2TCB.xTicksToDelay == 0)
			{
                pxCurrentTCB =&Task2TCB;
			}
			else if(pxCurrentTCB->xTicksToDelay != 0)
			{
                pxCurrentTCB = &IdleTaskTCB;
			}
			else 
			{
				return;		/* 返回,不进行切换,因为两个线程都处于延时中 */
			}
		}
		else if(pxCurrentTCB == &Task2TCB)
		{
			if(Task1TCB.xTicksToDelay == 0)
			{
                pxCurrentTCB =&Task1TCB;
			}
			else if(pxCurrentTCB->xTicksToDelay != 0)
			{
                pxCurrentTCB = &IdleTaskTCB;
			}
			else 
			{
				return;		/* 返回,不进行切换,因为两个线程都处于延时中 */
			}
		}
	}
}

这个函数实现的功能就是:如果当前任务是空闲任务,那么就去尝试执行任务 1 或者任务 2,看看他们的延时时间是否结束,如果任务的延时时间均没有到期,那就返回继续执行空闲任务。

如果当前任务是任务 1 或者任务 2 的话,检查下另外一个任务,如果另外的任务不在延时中,就切换到该任务。否则,判断下当前任务是否应该进入延时状态,如果是的话,就切换到空闲任务,否则就不进行任何切换 。

这里有个地方值得注意(所有任务控制块的xTicksToDelay在初始化时都为0,这样才能保证一开始,所有任务同时进行):当开启调度器时,首先会选择要执行的第一个任务,如下:

/* 任务1 */
void Task1_Entry( void *p_arg )
{
	for( ;; )
	{
		flag1 = 1;
        vTaskDelay( 2 );		
		flag1 = 0;
        vTaskDelay( 2 );      
	}
}

该任务的执行流程如下:

1 设置flag = 1;

2 调用延时堵塞函数,该延时函数首先获取当前的任务控制块(任务1),然后把任务控制块内的延时时间(从初始值的0)设置为2个systick周期,再开启任务切换;

3 进入到任务切换,当前任务是任务1,那个调度器回去查询任务块2的xTicksToDelay(初始化时就为0),满足要求,就会切换到任务2去执行;

4 任务2 先设置flag2 = 1,也会把延时时间从(初始值0)设置为2个systick周期,然后开启任务切换;

5 此时,两个任务都在延时(xTicksToDelay都为2),自动切换到空闲函数;

那么,如果我们把任务控制块2的xTicksToDelay初始值从0改为1呢?可以想到的是,任务函数2不再和任务函数1同时执行,而是会延迟一个systick周期。

接下来的一个问题,每个任务的延时时间xTicksToDelay在哪里递减?我们说到,这个延时时间是多少个systick周期,那肯定就是在systick的中断服务函数中啦

/*
*************************************************************************
*                             SysTick中断服务函数
*************************************************************************
*/
void xPortSysTickHandler( void )
{
	/* 关中断 */
    vPortRaiseBASEPRI();
    
    /* 更新系统时基 */
    xTaskIncrementTick();

	/* 开中断 */
    vPortClearBASEPRIFromISR();
}

首先是关掉中断,防止其他中断抢占(如外部中断,PendSV),影响系统时钟的准确度。然后更新时基,再开中断。这里,起始就是把设置了一个临界段,用来保护更新时基的函数。

更新系统时基的函数:

void xTaskIncrementTick( void )
{
    TCB_t *pxTCB = NULL;
    BaseType_t i = 0;
    
    /* 更新系统时基计数器xTickCount,xTickCount是一个在port.c中定义的全局变量 */
		// xTickCount是一个系统计数器,用于记录当前系统跑了多久
    const TickType_t xConstTickCount = xTickCount + 1;
    xTickCount = xConstTickCount;

    
    /* 扫描就绪列表中所有线程的xTicksToDelay,如果不为0,则减1 */
	for(i=0; i<configMAX_PRIORITIES; i++)
	{	
				// 从就绪列表中找到一个任务控制块,然后判断xTicksToDelay是否 = 0
        pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &pxReadyTasksLists[i] ) );
		if(pxTCB->xTicksToDelay > 0)
		{
			pxTCB->xTicksToDelay --;
		}
	}
    
    /* 任务切换 */
    portYIELD();
}

截至目前,已经定义好了systick的中断函数,但还没有开启systick,所以,接下来要进行systick的初始化:

/*
*************************************************************************
*                             初始化SysTick
*************************************************************************
*/
void vPortSetupTimerInterrupt( void )
{
     /* 设置重装载寄存器的值 */
		// 系统时钟为25Mhz,TICK_RATE_HZ为100,那么就是1秒钟执行100次中断,定时时间为10ms
    portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
    
    /* 设置系统定时器的时钟等于内核时钟
       使能SysTick 定时器中断
       使能SysTick 定时器 */
    portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | 
                                  portNVIC_SYSTICK_INT_BIT |
                                  portNVIC_SYSTICK_ENABLE_BIT ); 
}

首先,是设置重装在寄存器的值:系统时钟为25Mhz(软件仿真),TICK_RATE_HZ为100,那么就是1秒钟执行100次中断,定时时间为10ms,然后使能。这个systick初始化函数在哪里被调用呢?事实上,是在调度器启动函数里面,在启动第一个任务前被调用,开启时钟。

/*
*************************************************************************
*                              调度器启动函数
*************************************************************************
*/


BaseType_t xPortStartScheduler( void )
{
    /* 配置PendSV 和 SysTick 的中断优先级为最低 */
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
    
    /* 初始化SysTick */
    vPortSetupTimerInterrupt();

	/* 启动第一个任务,不再返回 */
	prvStartFirstTask();

	/* 不应该运行到这里 */
	return 0;
}

最终,程序的一个运行结果如下:

分析一下,首先执行任务1,flag1 = 1,然后设置堵塞延时,并切换任务,此时,任务选择函数发现任务2的延时值为0(默认复位值就是0),就转去执行任务2,任务2会把flag2 = 1,也开始延时并切换任务,接下来就进入空闲任务。这里,由于两个任务控制块的xTicksToDelay均为默认值(0),所以任务1第一次切换就到了任务2,两个任务是同时发生的

还可以测试一下,把任务2的xTicksToDelay初始值设置为1,如下

Task2TCB.xTicksToDelay = 1;

那么此时, 任务2应该会比任务1延迟执行一个systick周期,测试一下:

果然,任务2延迟了1个systick周期才执行,程序正确。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值