FreeRTOS任务调度

FreeRTOS任务有运行态,就绪态,阻塞态和挂起态4种状态。在开启调度器后,任务状态会在等待事件发生或调用某些API函数进行转换。列如,调用vTaskSuspend()函数挂起任务,调用vTaskResume()函数将挂起的任务恢复。

1 开启调度器

任务创建完成后,通过vTaskStartSchedule()函数开启调度器。在调度器开启的过程中,会创建空闲任务并启动第一个任务。

1.1 调度器开启任务

调度器开启函数为vTaskStartSchedule(),该函数在tasks.c文件中定义,省略部分条件编译代码后的代码如下。

void vTaskStartScheduler( void )
{
    BaseType_t xReturn;

	/*使用动态内存分配方法创建最低优先级的空闲任务代码*/
	xReturn = xTaskCreate(	prvIdleTask,
							configIDLE_TASK_NAME,
							configMINIMAL_STACK_SIZE,
							( void * ) NULL,
							portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
							&xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */

    /*如果使能了软件定时器功能,则创建软件定时器服务任务*/
	#if ( configUSE_TIMERS == 1 )
	{
		if( xReturn == pdPASS )
		{
			xReturn = xTimerCreateTimerTask();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
	#endif /* configUSE_TIMERS */

    /*空闲任务或软件定时器服务任务创建成功*/
	if( xReturn == pdPASS )
	{
		/*关中断,在启动第一个任务代码中通过SVC调用开中断 */
		portDISABLE_INTERRUPTS();
        /*设置一些静态变量*/
		xNextTaskUnblockTime = portMAX_DELAY;
		xSchedulerRunning = pdTRUE;
		xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;

		/*宏configGENERATE_RUN_TIME_STATS为1表示使能时间统计功能,需要用户用下面这个宏
        实现一个高于心跳时钟精度的软件定时器配置代码*/
		portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();

		traceTASK_SWITCHED_IN();

		/*在硬件移植层文件 port.c中实现与硬件相关的嘀嗒定时器、FPU和Pendsv中断初始化,并
        启动第一个任务*/
		if( xPortStartScheduler() != pdFALSE )
		{
			/*若调度器开启成功,则程序不会运行到这里*/
		}
		else
		{
			/*只有调用xTaskEndScheduler()函数终止调度器程序才会运行到这里*/
		}
	}
	else
	{
		/*程序运行到这里表明调度器没有启动成功,原因是在创建空闲任务或软件定时器服务任务(如果
        了对应的宏)时没有足够的内存用于创建任务*/
		configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
	}

	/*防止编译器报错*/
	( void ) xIdleTaskHandle;
}
/*-----------------------------------------------------------*/

1.2 调度器开启过程

首先创建一个空闲任务,运行于最低优先级(由宏portPRIVILEGE_BIT定义,通常为0),如果配置了宏configUSE_TIMERS为1,则创建软件定时器服务任务。如果前面的任务创建成功,则通过调用与移植层硬件相关的xPortStartSchedule()函数来初始化SysTick 嘀嗒定时器作为FreeRTOS的心跳时钟,然后启动第一个任务,启动任务后程序将不会退出任务调度。调度器开启流程如图所示。

 1.3 启动第一个任务

在调度器开启的过程中,使用与移植层硬件密切相关的xPortStartSchedule()函数来初始化中断、嘀嗒定时器,并启动第一个任务,省略部分条件编译代码后的代码如下。

BaseType_t xPortStartScheduler( void )
{
	#if( configASSERT_DEFINED == 1 )
	{
		volatile uint32_t ulOriginalPriority;
		volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
		volatile uint8_t ucMaxPriorityValue;

		/* Determine the maximum priority from which ISR safe FreeRTOS API
		functions can be called.  ISR safe functions are those that end in
		"FromISR".  FreeRTOS maintains separate thread and ISR API functions to
		ensure interrupt entry is as fast and simple as possible.

		Save the interrupt priority value that is about to be clobbered. */
		ulOriginalPriority = *pucFirstUserPriorityRegister;

		/* Determine the number of priority bits available.  First write to all
		possible bits. */
		*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;

		/* Read the value back to see how many bits stuck. */
		ucMaxPriorityValue = *pucFirstUserPriorityRegister;

		/* The kernel interrupt priority should be set to the lowest
		priority. */
		configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );

		/* Use the same mask on the maximum system call priority. */
		ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

		/* Calculate the maximum acceptable priority group value for the number
		of bits read back. */
		ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
		while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
		{
			ulMaxPRIGROUPValue--;
			ucMaxPriorityValue <<= ( uint8_t ) 0x01;
		}

		#ifdef __NVIC_PRIO_BITS
		{
			/* Check the CMSIS configuration that defines the number of
			priority bits matches the number of priority bits actually queried
			from the hardware. */
			configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
		}
		#endif

		#ifdef configPRIO_BITS
		{
			/* Check the FreeRTOS configuration that defines the number of
			priority bits matches the number of priority bits actually queried
			from the hardware. */
			configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
		}
		#endif

		/* Shift the priority group value back to its position within the AIRCR
		register. */
		ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
		ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

		/* Restore the clobbered interrupt priority register to its original
		value. */
		*pucFirstUserPriorityRegister = ulOriginalPriority;
	}
	#endif /* conifgASSERT_DEFINED */

	/*设置 Penasv和滴答定时器的中断优先级为最低*/
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

	/*设置滴答定时器的定时时间并使能滴答定时器中断*/
	vPortSetupTimerInterrupt();

	/*初始化临界段嵌套计数器*/
	uxCriticalNesting = 0;

	/*启动第一个任务*/
	prvStartFirstTask();

	/*程序不会运行到这里*/
	return 0;
}
/*-----------------------------------------------------------*/

这部分代码与移植层便件密切相关,首先设置PcndSV 和滴答定时器的中断优先级)最低;然后初始化滴答定时器并使能嘀嗒定时器中断,最后调用prvStartFirstTas()函数开中断,并触发SVC中断来启动第一个任务,至此调度器开启完成。

2 任务的挂起和恢复

刚创建的任务处于就绪态,可以被调度器选中从而进入运行态。若某个任务很长时间才运行一次,FreeRTOS提供了任务的挂起和恢复函数供用户调用。

2.1 任务的挂起

        1. 任务的挂起函数vTaskSuspend()

        vTaskSuspend()函数用于将某个任务设置为挂起态,进入挂起态的任务不会运行。推出挂起态的唯一方法就是调用任务恢复函数。
任务挂起函数原型如下。

void vTaskSuspend(TaskHandle_t xTaskToSuspend);

         参数说明如下。

        xTaskToSuspend:要挂起任务的任务句柄,为NULL时表示挂起任务本身。

        返回值:无。

        任务挂起该函数使用了一个参数xTaskToSuspend,为要挂起任务的任务句柄,创建任务的时候会为每个任务分配一个任务句柄。如果使用xTaskCreate()函数创建任务,那么该函数的参数pxCreatedTask就是此任务的任务句柄;如果使用xTaskCreateStati()函数创建任务,那么该函数的返回值就是此任务的任务句柄。也可以通过xTaskGetHandle()函数来根据任务名获取任务句柄。如果该参数为NULL,则表示挂起任务本身。

2.2  任务的恢复

   将一个任务从挂起态恢复到就绪态要使用任务恢复函数。任务恢复函数有两个版本,vTaskResume()和xTaskResumeFromISR()。后者是中断版本,在受FreeRTOS管理的中断服务函数中使用。

        1. 任务恢复函数vTaskResume()

        只有通过vTaskSuspend()函数挂起的任务,才能用vTaskResume()函数恢复。vTaskResume()函数原型如下。

void vTaskResume(TaskHandle_t xTaskToResume);

        参数说明如下。

        xTaskToResume:要恢复任务的任务句柄。

        返回值:无。

        xTaskToResume()函数有一个参数xTaskToResume,表示要恢复任务的任务返回值。若在中断服务函数中使用任务恢复函数,则需要使用它的中断版本。

        2. 中断版本任务恢复函数 xTaskResumeFromISR()

        xTaskResumeFromISR()函数在中断服务函数中使用,该函数原型如下。

BaseType_t xTaskResumeFromISR(TaskHandle t xTaskToResume);

        参数说明如下。

        xTaskToResume:要恢复任务的任务句柄。

        返回值:pdTRUE或pdFALSE。

        参数xTaskToResume 同样表示要恢复任务的任务句柄,有一个返回值。当要恢复任务的优先级等于或高于正在运行的任务(被中断打断的任务)时,返回pdTRUE,这意味着退出中断服务函数后必须进行一次上下文切换。当要恢复任务的优先级低于当前正在运行的任务(被中断打断的任务)时,返回pdFALSE,这意味着退出中断服务函数后无须进行上下文切换。

2.3 任务挂机和恢复示例

本示例通过appStartTask()函数创建两个FreeRTOS任务。

任务1的任务函数为Led0Task(),优先级为4,其功能是使LED0每秒闪烁1次,在完成5次闪烁后挂起任务本身,并恢复任务2运行。
任务2的任务函数是Led1Task(),优先级为4,其功能是使LED1每秒闪烁2次,在创建该任务时先挂起任务本身,任务运行后,在完成5次闪烁后挂起任务本身并恢复任务1运行。
 
static TaskHandle_t Led0TaskHandle = NULL;//任务LED0任务句柄
static TaskHandle_t Led1TaskHandle = NULL;//任务LED1任务句柄
/**********************************************************************
函 数 名:Led0Task
功能说明:LED0每秒闪烁1次,闪烁5次后挂起自己,并恢复任务2
形    参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级:	4
**********************************************************************/
static void Led0Task(void *pvParameters)
{
	uint16_t cnt = 0;			//检测灯闪烁次数的局部变量
	while(1)
	{
		GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)(1 - GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_4)));
		vTaskDelay(500);
		printf("LED0  1秒为周期闪烁   \r\n");
		if(++cnt >=10)
		{
				if(eTaskGetState(Led1TaskHandle) != eDeleted) //如果任务1没有被删除
				{
					cnt = 0;
					vTaskResume(Led1TaskHandle);//恢复任务2
					
					printf("任务1已经挂起 \r\n");
					vTaskSuspend(NULL);/*参数NULL 表示挂起任务本身*/
				}
		}
	}
}
/**********************************************************************
函 数 名:Led1Task
功能说明:LED1每秒闪烁2次
形    参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级:	4
**********************************************************************/
static void Led1Task(void *pvParameters)
{
	uint16_t cnt = 0;			//检测灯闪烁次数的局部变量
	while(1)
	{
		GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)(1 - GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_5)));
		vTaskDelay(250);
		printf("LED1  以 0.5秒为周期闪烁  \r\n");
				if(++cnt >=10)
		{
				if(eTaskGetState(Led1TaskHandle) != eDeleted) //如果任务1没有被删除
				{
					cnt = 0;
					vTaskResume(Led0TaskHandle);//恢复任务1
					
					printf("任务2已经挂起 \r\n");
					vTaskSuspend(NULL);/*参数NULL 表示挂起任务本身*/
				}
		}

	}
	//如果在任务的具体实现中会跳出while死循环,则这个任务必须在函数运行完之前删除它。
	//传入NULL参数表示删除的是当前任务。
	vTaskDelete(NULL);
}
/**********************************************************************
函 数 名:appStartTask
功能说明:任务开始函数,用于创建其他函数并且开启调度器
形    参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
**********************************************************************/
void appStartTask(void)
{
		taskENTER_CRITICAL();   /*进入临界段,关中断*/
		xTaskCreate(Led0Task,"Led0Task",128,NULL,4,&Led0TaskHandle);
		xTaskCreate(Led1Task,"Led1Task",128,NULL,4,&Led1TaskHandle);
		vTaskSuspend(Led1TaskHandle);/*挂起任务2*/
		taskEXIT_CRITICAL(); 	/*退出临界段,关中断*/
		vTaskStartScheduler();/*开启调度器*/
}

2.4 下载测试 

将程序下载到开发板,可以看到LED0以每秒1次的闪烁频率闪烁,闪烁5次后状态保持,同时LED1开始以每秒2次的频率闪烁,闪烁5次后状态保持。

3 任务的调度

FreeRTOS支持3种任务调度方式:抢占式调度、时间片调度和合作式调度。实际中主要应用的是抢占式调度和时间片调度,合作式调度主要是为早期那些资源很少的MCU准备的,特点是开销很小,现在的微控制器功能已经非常强大了,基本不会用到合作式调度,FreeRTOS也已经不再打算对这部分进行更新。
抢占式调度用于任务有不同优先级的场合。每个任务都有不同的优先级,任务会一直运行,直到被高优先级任务抢占,或者遇到阻塞式的API函数调用,如最简单的vTaskDelay()函数调用,才让出CPU使用权。抢占式调度总是选择就绪列表中优先级最高的任务来运行。
时间片调度用于多个任务具有相同优先级的场合。当多个任务优先级相同时,每个任务在运行一个时间片(一个系统时钟节拍的长度,在STM32微控制器中就是嘀嗒定时器的中断周期)后就让出CPU使用权,让其他优先级相同的任务有被运行的机会。

3.1 FreeRTOS任务切换场合

无论是抢占式调度还是时间片调度,都涉及任务的切换。FreeRTOS会在下面两种情况下执行任务切换(也称上下文切换)。

(1)执行系统调用。
(2)嘀嗒定时器中断。

执行系统调用就是执行FreeRTOS 提供的API函数,这类函数有很多个,但实际操作任务切换的是taskYIELD()宏,该宏与硬件平台无关,在task.h文件中定义。

#define taskYIELD()     portYIELD()

可以看出,taskYIELD()宏又由 portYIELD()宏所定义。不同硬件平台,任务切换的方法也有差异,所以portYIELD()宏是与硬件相关的代码,在硬件移植层的portmacro.h文件中定义。对STM32微控制器,代码如下。

#define portYIELD()																\
{																				\
	/*置Pendsv中断挂起位以切换上下文*/				\
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;								\
																				\
	/* Barriers are normally not required but do ensure the code is completely	\
	within the specified behaviour for the architecture. */						\
	__dsb( portSY_FULL_READ_WRITE );											\
	__isb( portSY_FULL_READ_WRITE );											\
}

portYIELD()宏的主要工作就是将中断控制和状态寄存器(ICSR)的bit28置1,触发一个PendSV 中断,FreeRTOS在PendSV中断里进行任务切换。
除执行系统调用会触发任务切换以外,嘀嗒定时器中断也会触发任务切换。FreeRTOS嘀嗒定时器的中断服务函数如下。

void xPortSysTickHandler( void )
{
	
	vPortRaiseBASEPRI();
	{
		/*系统时钟节拍加1并判断有无需要切换的任务*/
		if( xTaskIncrementTick() != pdFALSE )
		{
            /*置Penasv中断挂起位以切换上下文*/
			PortNVIC_INT_CTRL_REG = POrtNVIC_PENDSVSET_BIT;
		}
	}
	vPortClearBASEPRIFromISR();
}

可以看出,无论在哪种任务切换场合,FreeRTOS都是通过置PendsV中断挂起位触发一次PendsV中断来进行任务切换的。

3.2 PendSV中断

在STM32 微控制器中,有SVC和PendSV两个系统中断,前者叫作系统服务调用,简称系统调用;后者叫作可挂起的系统调用。触发PendSV 中断的方法是将ICSR的bit28置1。PendSV中断的优先级可以通过编程进行设置,bit28置1后,若其中断优先级不够,则将等待更高优先级的中断执行完毕后才会执行,这种特性使PendsV 中断非常适合在操作系统中用于任务切换。
在实时操作系统中,绝不希望在中断发生时进行任务切换,因为这会造成中断处理被延迟,而且延迟时间往往还不可预知,这与实时操作系统的实时性要求相悖。FreeRTOS通过将PendSV 中断设置成最低优先级,使得在PendSV 中断中进行的任务切换延迟到所有其他中断服务函数都已处理完成之后。

任务切换流程中有两个任务:任务A和任务B(在STM32微控制器中,FreeRTOS任务运行于线程模式,所以有时任务也被称为线程)。任务A通过系统调用切换到任务B,此过程中没有其他中断发生。任务 B 通过嘀嗒定时器中断切换到任务A,此过程中有其他中断发生,该中断被喃嗒定时器中断打断,FreeRTOS 在嘀嗒定时器中断服务函数中将PendSV中断挂起位置1,处理完其他事务后退出嘀嗒定时器中断,让被打断的中断得以继续执行。在所有中断执行完成后,在PendSV 中执行上下文切换,切换到任务A,从而保证了实时性,具体流程如下。

(1)任务 A 呼叫 SVC请求任务切换。例如,等待某些工作完成,最简单的是调用任
务阻塞函数vTaskDelay()。
(2)FreeRTOS 接收到请求,做好执行上下文切换的准备,并且置一个PendsV异常。
(3)   当CPU退出SVC后,立即进入PendSV,从而执行上下文切换。
(4)当PendsV执行完毕后,返回到任务B,同时进入线程模式。(5)在任务B执行过程中,发生了一个中断,并且ISR开始执行。

(6)在ISR执行过程中,发生SysTick中断,并且抢占了该ISR。
(7)在SysTick中断服务程序里,FreeRTOS执行必要的操作,然后置PendsV异常以
做好执行上下文切换的准备。
(8)  SysTick中断退出后,回到先前被抢占的ISR中,ISR继续执行。
(9)  ISR 执行完毕并退出后,PendSV 中断服务程序开始执行,并且在PendSV中执行
上下文切换。
(10)  PendSV执行完毕后,回到任务A,同时系统再次进入线程模式。

3.3 PendSV中断服务函数

 针对STM32微控制器硬件架构,FreeRTOS 任务切换通过 PendSV 中断服务函数实现。PendSV中断服务函数的名称是PendSV_Handler,为了方便不同硬件平台之间的移植,FreeRTOS 对 PendSV 中断服务函数重新进行了定义,定义为xPortPendSVHandler,在FreeRTOSConfig.h 头文件中将xPortPendSVHandler与PendSV_Handler 通过宏定义对应起来。

#define xPortPendSVHandler PendSV_Handler

xPortPendSVHandler 在硬件移植层的port.c文件中实现,代码采用汇编语言编写。

__asm void xPortPendSVHandler( void )
{
	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;

	PRESERVE8

	mrs r0, psp
	isb
//将当前激活的任务TCB指针存入r2
	ldr	r3, =pxCurrentTCB		/* Get the location of the current TCB. */
	ldr	r2, [r3]

	stmdb r0!, {r4-r11}			/* Save the remaining registers. */
	str r0, [r2]				/* Save the new top of stack into the first member of the TCB. */

	stmdb sp!, {r3, r14}
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0
	dsb
	isb
	bl vTaskSwitchContext
	mov r0, #0
	msr basepri, r0
	ldmia sp!, {r3, r14}

	ldr r1, [r3]
	ldr r0, [r1]				/* The first item in pxCurrentTCB is the task top of stack. */
	ldmia r0!, {r4-r11}			/* Pop the registers and the critical nesting count. */
	msr psp, r0
	isb
	bx r14
	nop
}

这部分汇编代码比较复杂,主要执行了任务的切换相关的一些操作。首先,获取当前任务的TCB,与其他的需要入栈的寄存器一起存入任务堆栈。其次,调用vTaskSwitchContext()函数查找下一个要运行的任务,并将全局pxCurrentTCB指向这个要运行的任务。最后,获取新任务的堆栈地址,相关寄存器出栈,PC寄存器恢复为新任务的任务函数,完成任务切换。

3.4  查找下一个要运行的任务

 在PendSV中断服务函数中,通过调用vTaskSwitchContext()函数来查找下一个要运行的、已经就绪且优先级最高的任务,该函数在tasks.c文件中实现,省略部分条件编译代码后的代码如下。

void vTaskSwitchContext( void )
{
	if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
	{
		/*若调度器挂起,则不能进行任务切换*/
		xYieldPending = pdTRUE;
	}
	else
	{
		xYieldPending = pdFALSE;
		traceTASK_SWITCHED_OUT();
        /*堆栈溢出检测,如果使能了的话*/
		

		taskCHECK_FOR_STACK_OVERFLOW();

	

		/*使用通用方法或硬件方法查找下一个要运行的任务 */
		taskSELECT_HIGHEST_PRIORITY_TASK(); 
		traceTASK_SWITCHED_IN();

		
	}
}

进入函数后,先判断调度器有没有挂起,若挂起了则不能进行任务切换,若没有挂起则通过 taskSELECT_HIGHEST_PRIORITY_TASK()函数来选择已经就绪且优先级最高的任务。选择已经就绪且优先级最高的任务有两种方法:通用方法和硬件方法。通过宏configUSE_PORT_OPTIMISED_TASK_SELECTION进行选择,该宏为0时使用通用方法,为1时使用硬件方法。
通用方法对所有硬件平台都适用,在tasks.c文件中给出了实现这个通用方法的宏。

	#define taskSELECT_HIGHEST_PRIORITY_TASK()															\
	{																									\
	UBaseType_t uxTopPriority = uxTopReadyPriority;														\
																										\
		/*从就绪任务列表数组中找出最高优先级列表*/			
		while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )							\
		{																								\
			configASSERT( uxTopPriority );																\
			--uxTopPriority;																			\
		}																								\
																										\
		/*通过这个宏实现相同优先级的任务使用时间片共享处理器 */									\
		listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );			\
		uxTopReadyPriority = uxTopPriority;																\
	} 

pxReadyTasksLista[]是定义在tasks.c文件中的就绪任务列表数组,一个优先级对应个列表。在新建在务的过程中,TCB中的状态列表项xStateListftem会挂接到就绪任务,表数组中。uxTopReadyPriority保存处于就绪态任务的最高优先级值,每次创建任务,者会判断新任务的优先级是否大于这个变量,如果大于,就更新这个变量的值。
从记录的最高优先级uxTopReadyPriority 开始,在就绪任务列表数维pxReadyTasksLists[中找出优先级最高的任务,然后调用宏 listGET_OWNER_OF_NEXT_ENTRYO获取最高优先级列表中的下一个列表项,并从该列表项中获取TCB指针赋给变fpxCurrentTCB。
采用通用方法查找下一个运行要的任务,没有使用与硬件相关的指令。通用方法适合不同硬件平台使用,它对任务的数量也没有限制,但运行效率比硬件方法要低很多。
查我下一个要运行的任务还可使用特定硬件的特定指令,即硬件方法。对于STM32微控制器,可使用计算前导零指令CLZ来实现,硬件方法宏代码如下。

   #define taskSELECT_HIGHEST_PRIORITY_TASK()															\
	{																									\
	    UBaseType_t uxTopPriority = uxTopReadyPriority;														\
																										\
		/*通过这个宏找出就绪任务列表中优先级最高的任务*/					\
		while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )							\
		{																								\
			configASSERT( uxTopPriority );																\
			--uxTopPriority;																			\
		}																								\
																										\
		
		listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );			\
		uxTopReadyPriority = uxTopPriority;																\
	} 

通过宏portGET_HIGHEST_PRIORITY()来实现硬件方法中的计算前导零功能,该求被替换后变成如下形式。

uxTopPriority=(31UL-( uint32_t)_clz(( uxTopReadyPriority)))


在硬件方法中,uxTopPriority 同样表示就绪任务的最高优先级,它通过31减去前导
零的个数得出。在计算前导零时,uxTopReadyPriority 使用每一位来表示任务优先级。

变量uxTopReadyPriority 的 bit0为1表示存在优先级为0的就绪任务,bit16为1表示存在优先级为16的就绪任务,以此类推。由于32位整型数据最多只有32位,因此使用硬件方法查找下一个要运行的任务,任务的可用优先级值最大为32,即从优先级0到优先级31,这是使用硬件方法的局限。


宏_clz((uxTopReadyPriority))会被计算前导零指令CLZ替换,该指令的功能是计算一个变量从最高位开始的连续零的个数。假如变量uxTopReadyPriority为0xA9(二进制形式为0000 0000 0000 0000 0000 0000 1010 1001),即bit0、bit3、bit5和bit7为1,表示存在优先级为0、3、5和7的就绪任务,那么_clz((uxTopReadyPriority)的值为24,uxTopPriority=31-24=7,即优先级为7的任务是就绪态优先级最高的任务。其余代码跟通用方法一样,调用宏 listGET_OWNER_OF_NEXT_ENTRY获取最高优先级列表中的下一个列表项,并从该列表项中获取TCB指针赋给变量pxCurrentTCB,从而完成任务切换。 

3.5 FreeRTOS时间片调度

 当多个任务具有相同的优先级时,每个任务只运行一个系统时钟节拍,然后让出CPU使用权,让另一个任务运行,从而实现同优先级任务的调度,称为时间片调度。
要使用时间片调度方法,需要将宏 configUSE_PREEMPTION 和configUSE_TIME_SLICING设置为1,时间片的长度由宏configTICK_的调度RATE_HZ定义的系统时钟节拍决定,即嘀嗒定时器的溢出周期,当该宏为1000时,时间片长度就是1ms。时间片在嘀嗒定时器中断服务函数中完成。

void xPortSysTickHandler( void )
{

	vPortRaiseBASEPRI();
	{
		/*系统时钟节拍加1并判断有无需要切换的任务*/
		if( xTaskIncrementTick() != pdFALSE )
		{
			/*置Pendsv中断挂起位以切换上下文*/
			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
		}
	}
	vPortClearBASEPRIFromISR();
}

 在嘀嗒定时器中断服务函数中,通过xTaskIncrementTick()函数来使系统时钟节拍加1并判断有无需要切换的任务,当这个函数返回pdTRUE时表示要进行任务切换。去掉与时间片调度无关的代码之后的xTaskIncrementTick()函数如下。

BaseType_t xPortStartScheduler( void )
{
//去掉与时间片调度无关的代码
		//判断当前就绪任务列表中是否还有相同优先级的任务
		#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
		{
			if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
			{
				xSwitchRequired = pdTRUE;
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		#endif

		
}

3.6 时间片调度示例

创建2个任务,2个任务具有一样的优先级,任务的内容都是通过串口打印一句话,采用时间片调度方法,则每个任务将轮流获得CPU一个时间片的运行时间,为了方便观察,将时间片的长度取500ms。

static TaskHandle_t Led0TaskHandle = NULL;//任务LED0任务句柄
static TaskHandle_t Led1TaskHandle = NULL;//任务LED1任务句柄
/**********************************************************************
函 数 名:Led0Task
形    参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级:	4
**********************************************************************/
static void Led0Task(void *pvParameters)
{
	uint16_t cnt = 0;			//检测灯闪烁次数的局部变量
	while(1)
	{
		GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)(1 - GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_4)));	
		cnt++; 
		taskENTER_CRITICAL();//进入临界段,开中断		
		printf("任务1已经运行 %3d \r\n",cnt);
		taskEXIT_CRITICAL();//退出临界段,关中断
		
					
	}
}
/**********************************************************************
函 数 名:Led1Task

形    参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级:	4
**********************************************************************/
static void Led1Task(void *pvParameters)
{
	uint16_t cnt = 0;			//检测灯闪烁次数的局部变量
	while(1)
	{
		GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)(1 - GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_5)));
		cnt++; 
		taskENTER_CRITICAL();//进入临界段,开中断	
		printf("任务2已经运行 %3d \r\n",cnt);
		taskEXIT_CRITICAL();		//退出临界段,关中断
		
					
	}
}
/**********************************************************************
函 数 名:appStartTask
功能说明:任务开始函数,用于创建其他函数并且开启调度器
形    参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
**********************************************************************/
void appStartTask(void)
{
		taskENTER_CRITICAL();   /*进入临界段,关中断*/
		xTaskCreate(Led0Task,"Led0Task",128,NULL,4,&Led0TaskHandle);
		xTaskCreate(Led1Task,"Led1Task",128,NULL,4,&Led1TaskHandle);
		taskEXIT_CRITICAL(); 	/*退出临界段,关中断*/
		vTaskStartScheduler();/*开启调度器*/
}

实现现象:

2个任务各自运行一个时间片的长度。

3.7 空闲任务

FreeRTOS 调度器开启后,至少要有一个任务处于运行态。为了保证这一点,FreeRTO在调用vTaskStartScheduler()函数开启调度器时,会自动创建一个空闲任务。空闲任务是个非常小的循环,且拥有最低优先级(优先级0),以保证其不会妨碍具有更高优先级的务进入运行态。当然,也可以把任务创建在与空闲任务相同的优先级上,与空闲任务共优先级,但一般不建议这样做。空闲任务运行在最低优先级,可以保证一旦有更高优先的任务进入就绪态,空闲任务就会立即切出运行态。

空闲任务的主要功能如下。
(1)释放内存。如果有任务删除了自身,被删除任务的TCB 和堆栈资源会在空闲任务中释放,但用户自己分配的资源需要手动回收。
(2)处理空闲优先级任务。当采用抢占式调度方式时,如果有用户任务与空闲任务共享一个优先级,空闲任务可以不必等到时间片耗尽就进行任务切换。当没有采用抢占式调度方式时,空闲任务总是调用taskYIELD()函数试图切换用户任务,以确保能最快响应用户任务。
(3)执行空闲任务钩子函数。这个函数由用户实现,但FreeRTOS规定了函数的名称和参数,同时需要将宏configUSE_IDLE_HOOK设置为1。
(4)实现低功耗 tickless 模式。FreeRTOS的tickless模式会在空闲周期停止嘀嗒定时器,从而让微控制器长时间处于低功耗模式。移植层需要配置外部唤醒中断,当唤醒事体到来时,将微控制器从低功耗模式唤醒。微控制器被唤醒后,要重新使能嘀嗒定时器。

4 FreeRTOS内核函数

前面介绍过的调度器开启函数vTaskStartScheduler()和任务切换函数taskYIELD()都是FreeRTOS内核函数,除此之外,FreeRTOS还有一些内核函数。

4.1 临界段操作函数

在程序运行过程中,一些关键代码的执行不能被打断,这一段不能被打断的程序被称为临界段或临界区。能打断程序执行的往往是中断,所以进入、退出临界段都要进行开、关中断操作。在STM32微控制器上,FreeRTOS进入和退出临界段主要是通过操作寄存器basepri 实现的。在进入临界段时,操作寄存器basepri 关闭所有低于宏configMAX_SYSCALL_INTERRUPT_PRIORITY所设定的中断优先级的中断,也就是FreeRTOS能管理的所有中断,不由FreeRTOS 管理的中断不会被关闭。在退出临界段时,操作寄存器basepri打开所有中断。

进入和退出临界段函数有4个:

taskENTER_CRITICAL()、taskEXIT_CRITICAL()、taskENTER_CRITICAL_FROM_ISR()和 taskEXIT_CRITICAL_FROM_ISR()。

4.1.1 taskENTER_CRITICAL()和taskEXIT_CRITICAL()

进入和退出临界段函数其实是两个宏,最终实现进入临界段功能的是vPortEnterCritical()函数。进入该函数后首先用portDISABLE_INTERRUPTS()操作寄存器basepri 关闭FreeRTOS 管理的所有中断,全局变量 uxCriticalNesting加1,用来记录临界段的嵌套次数。

#define taskEXIT_CRITICAL()			portEXIT_CRITICAL()
#define portEXIT_CRITICAL()		    vPortExitCritical()
void vPortEnterCritical( void )
{
	portDISABLE_INTERRUPTS();
	uxCriticalNesting++;

	
	if( uxCriticalNesting == 1 )
	{
		configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
	}
}

与进入临界段类似,真正实现退出临界段功能的是vPortExitCritical()函数。该函数操作全局变量 uxCriticalNesting 减1,只有当uxCriticalNesting为0时,也就是所有临界段代码都退出后,才打开中断。这样保证了在有多个临界段时,不会因某个临界段代码执行完成退出而打乱其他临界段的保护。

void vPortExitCritical( void )
{
	configASSERT( uxCriticalNesting );
	uxCriticalNesting--;
	if( uxCriticalNesting == 0 )
	{
		portENABLE_INTERRUPTS();
	}
}

 4.1.2 taskENTER_CRITICAL_FROM_ISR()和 taskEXIT_CRITICAL_FROM_ISR()

taskENTER_CRITICAL_FROM_ISR()和 taskEXIT_CRITICAL_FROM_ISR()是进入和退出临界段函数的中断版本,在中断服务函数中使用。这个中断必须是FreeRTOS能管理的中断,即中断优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY所设置的值的中断。

4.2 挂起和恢复调度器函数

vTaskSuspendAll()函数用于挂起调度器,xTaskResumeAll()函数用于恢复调度器。调度器挂起后,介于vTaskSuspendAlI()和xTaskResumeAll()之间的代码不会被更高优先级的任务抢占,即任务调度被禁止。挂起调度器不用关闭中断,这一点与进入临界段不一样。挂起调度器的代码如下。

void vTaskSuspendAll( void )
{


	portSOFTWARE_BARRIER();

	++uxSchedulerSuspended;

	portMEMORY_BARRIER();
}

调度器挂起支持嵌套。在调度器挂起函数中,将挂起嵌套计数器 uxSchedulerSuspended加1,这是一个静态全局变量,在使用调度器恢复函数时,此计数器会减1,当这个值减到0时真正恢复调度器。调用几次vTaskSuspendAll()函数,就要调用几次xTaskResumeAll()函数。

4.3任务切换函数

 taskYIELD()是任务切换函数,该函数其实是一个宏,最终由宏portYIELD()实现。通过向ICSR的bit28 位写入1,启动一个PendSV中断,在PendSV中

断中完成任务切换。

#define taskYIELD()					portYIELD()
#define portYIELD()																\
{																				\
	/* 设置PendSV挂起位以切换任务 */								                \
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;								\
																				\
					                                                            \
	__dsb( portSY_FULL_READ_WRITE );											\
	__isb( portSY_FULL_READ_WRITE );											\
}

4.4 系统时钟节拍追加

​​​​​​vTaskStepTick()函数用于设置系统时钟节拍的追加值,在低功耗 tickless模式下使用。当宏configUSE_TICKLESS_IDLE为1时,使能低功耗tickless模式。一般情况下,会在空闲任务中让系统时钟节拍停止运行,恢复系统时钟节拍后,系统时钟节拍停止运行的节拍数就可用vTaskStepTick()函数补上。

	void vTaskStepTick( const TickType_t xTicksToJump )
	{
		/* Correct the tick count value after a period during which the tick
		was suppressed.  Note this does *not* call the tick hook function for
		each stepped tick. */
		configASSERT( ( xTickCount + xTicksToJump ) <= xNextTaskUnblockTime );
		xTickCount += xTicksToJump;
		traceINCREASE_TICK_COUNT( xTicksToJump );
	}

 vTaskStepTick()函数有一个参数xTicksToJump,为要追加的系统时钟节拍值。

5 内核函数使用示例

串口打印函数printf()在任一时刻只允许一个任务访问,当多个任务同时向串口输出字符时,将造成输出的混乱。在时间片调度示例中,是通过临界段代码保护函数taskENTER_CRITICAL()和taskEXIT_CRITICAL()来避免多个任务同时向串口输出字符的。除此之外,也可用挂起调度器的方式达到同样的目的。
本示例通过appStartTask()函数创建两个FreeRTOS任务。

 任务1的任务函数为Led0Task(),优先级为4,功能是LED0每秒闪烁1次,并将任务运行次数通过串口发送。

 任务1的任务函数为Led1Task(),优先级为3,功能是LED1每秒闪烁2次,并将任务运行次数通过串口发送。

5.1 任务函数

 任务1通过taskENTER_CRITICAL()和taskEXIT_CRITICAL()进入和退出临界段实现代码保护,任务2通过vTaskSuspendAll()和xTaskResumeAll()挂起调度器实现代码保护。

/**********************************************************************
函 数 名:Led0Task
形    参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级:	4
**********************************************************************/
static void Led0Task(void *pvParameters)
{
	uint16_t cnt = 0;			//检测灯闪烁次数的局部变量
	while(1)
	{
		GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)(1 - GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_4)));	
		cnt++; 
		taskENTER_CRITICAL();			//进入临界段,开中断		
		printf("任务1:LED0 闪烁,已经运行 %3d \r\n",cnt);
		taskEXIT_CRITICAL();//退出临界段,关中断		
			vTaskDelay(pdMS_TO_TICKS(500));
	}
}
/**********************************************************************
函 数 名:Led1Task

形    参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级:	3
**********************************************************************/
static void Led1Task(void *pvParameters)
{
	uint16_t cnt = 0;			//检测灯闪烁次数的局部变量
	while(1)
	{
		GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)(1 - GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_5)));
		cnt++; 
		vTaskSuspendAll();				//挂起调度器
		printf("任务2: LED1 闪烁,已经运行 %3d \r\n",cnt);
		if(xTaskResumeAll() == pdTRUE) //如果有任务需要切换,则函数返回pdTRUE
		{
			taskYIELD(); //进行任务切换
		
		}
		vTaskDelay(pdMS_TO_TICKS(250));
	}	
}

5.2 任务创建

static TaskHandle_t Led0TaskHandle = NULL;//任务LED0任务句柄
static TaskHandle_t Led1TaskHandle = NULL;//任务LED1任务句柄
/**********************************************************************
函 数 名:appStartTask
功能说明:任务开始函数,用于创建其他函数并且开启调度器
形    参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
**********************************************************************/
void appStartTask(void)
{
		taskENTER_CRITICAL();   /*进入临界段,关中断*/
		xTaskCreate(Led0Task,"Led0Task",128,NULL,4,&Led0TaskHandle);
		xTaskCreate(Led1Task,"Led1Task",128,NULL,3,&Led1TaskHandle);
		taskEXIT_CRITICAL(); 	/*退出临界段,关中断*/
		vTaskStartScheduler();/*开启调度器*/
}

5.3 下载测试

编译无误后将程序下载到开发板上,打开串口助手,发现无论是使用进入和退出临界段的方式进行代码保护任务1,还是使用挂起调度器的方式进行代码保护的任务2,都能保证不会有多任务同时向串口输出字符,串口调试助手显示的信息正确。

6 总结

调度器开启后,程序就不会从调度器开启函数中返回。在开启调度器时会自动创建个空闲任务,用于回收资源、进入低功耗tickless 模式。空闲任务能够获得的执行时间径往用于衡量一个系统设计是否有足够裕度。FreeRTOS 任务切换通过PendSV中断实现无论是系统调用还是嘀嗒定时器中断,都是通过将ICSR的bit28置1来触发PendSV中断,从而实现任务切换的。对于一些需要保护的代码,可以采用进入和退出临界段或挂起调度器的方式进行保护。

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值