FreeRTOS原理剖析:任务切换过程

1. 任务切换相关API函数

函数 描述
xPortPendSVHandler() PendSV中断服务函数,其实函数原型为PendSV_Handler()
vTaskSwitchContext() 检查任务堆栈使用是否溢出,和查找下一个优先级高的任务,如果使能运行时间统计功能,会计算任务运行时间

2. 任务切换的基本知识

在FreeRTOS任务管理中,最主要的目的就是找到就绪态优先级最高的任务,然后执行任务切换,从而能保持优先级最高的任务一直占用CPU资源。为了达到最优性能,任务切换部分程序使用汇编代码编写。

FreeRTOS有两种方法触发任务切换:

  • 系统节拍时钟中断(SysTick定时器)。切换过程参考《FreeRTOS原理剖析:系统节拍时钟分析》
  • 执行系统调用代码。普通任务使用taskYIELD()强制任务切换;中断服务程序使用portYIELD_FROM_ISR()强制任务切换;在应用程序里也可以通过设置xYieldPending的值来通知调度器进行任务切换。

其中:

// 对于普通任务时
#define taskYIELD()				portYIELD()
#define portYIELD_WITHIN_API 	portYIELD
// 对于中断服务程序时
#define portEND_SWITCHING_ISR( xSwitchRequired ) if( xSwitchRequired != pdFALSE ) portYIELD()
#define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x )

可以看出,最终执行的代码段为:

#define portYIELD()											\
{															\
	/* 														\
	 * 通过向中断控制及状态寄存器ICSR的第28位写入1,触发PendSV中断	\
	 * 地址为0xE000 ED04 									\
	 */														\
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;			\
															\
	/* dsb和isb 完成数据同步隔离和指令同步隔离					\
	 * 保证之前存储器访问操作和指令都执行完						\
	 */														\
	__dsb( portSY_FULL_READ_WRITE );						\
	__isb( portSY_FULL_READ_WRITE );						\
}

通过向中断控制及状态寄存器ICSR(地址:0xE000 ED04)的第28位写入1,触发PendSV中断,从而执行任务切换。

3. 任务切换过程

3.1 PendSV中断服务函数分析

在FreeRTOS中,有:

#define xPortPendSVHandler 	PendSV_Handler

源代码如下:

__asm void xPortPendSVHandler( void )
{
	extern uxCriticalNesting;
	extern pxCurrentTCB;		/* 永远会指向当前激活的任务 */
	extern vTaskSwitchContext;

	PRESERVE8

	mrs r0, psp					/* 读取进程栈指针PSP,保存在R0中,此时SP的值为MSP */
	isb							/* 指令同步隔离 */

	/* 这两句使R2中保存当前激活的任务TCB首地址 */
	ldr	r3, =pxCurrentTCB		/* 将pxCurrentTCB储存的地址保存到R3,注意的是pxCurrentTCB的储存地址是固定不变的,但指向是可变的 */
	ldr	r2, [r3]				/* 将R3地址所处数据保存在R2,即TCB的首地址 */

	/* 
	 * 前两句判断是否使能了FPU,如果使能了,则手动将s16~s31压入栈中
	 * 其中s0~s15和FPSCR硬件自动完成 
	 */
	tst r14, #0x10
	it eq
	vstmdbeq r0!, {s16-s31}

	/* 将当前激活任务的寄存器值入栈,并更新R0,另外硬件自动将xPSR、PC、LR、R12、R0~R3入栈 */
	stmdb r0!, {r4-r11, r14}	

	/* R0为PSP地址,R2为激活任务的TCB地址,R0的值写入R2所保存的地址去,即TCB第一个成员指向线程堆栈指针,在每次任务切换最后都会更新PSP */
	str r0, [r2]				

	stmdb sp!, {r3}				/* 将R3临时压入堆栈,R3保存了pxCurrentTCB地址,函数调用后会用到,因此要入栈保护 */

	/* 关中断,中断优先级号大于等于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断都会被屏蔽 */
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY	
	msr basepri, r0				
	
	dsb							/* 数据同步隔离 */
	isb							/* 指令同步隔离 */
	
	bl vTaskSwitchContext		/* 切换到vTaskSwitchContext,查找下一个任务 */

	/* 开中断 */	
	mov r0, #0	
	msr basepri, r0				
	
	ldmia sp!, {r3}				/* 恢复R3,R3保存了pxCurrentTCB的地址,这里pxCurrentTCB的地址固定,但指向改变了 */

	/* 当前激活的TCB栈顶值存入R0 */
	ldr r1, [r3]				/* 将pxCurrentTCB指向的地址赋值给R1,即将TCB的首地址赋值给R1 */
	ldr r0, [r1]				/* 将R1地址所处的数据赋值给R0,即当前激活任务TCB的第一项的值赋给R0 */

	ldmia r0!, {r4-r11, r14}	/* 将寄存器R4~R11出栈,并同时更新R0的值 */


	/* 
	 * 前两句判断是否使能了FPU,如果使能了,手动恢复s16-s31浮点寄存器
	 * 其中s0~s15和FPSCR硬件自动完成 
	 */
	tst r14, #0x10
	it eq
	vldmiaeq r0!, {s16-s31}

	msr psp, r0					/* 将最新的任务堆栈栈顶赋值给线程堆栈指针PSP */
	isb							/* 指令同步隔离,清流水线 */
	
	#ifdef WORKAROUND_PMU_CM001
		#if WORKAROUND_PMU_CM001 == 1
			push { r14 }
			pop { pc }
			nop
		#endif
	#endif
	
	bx r14	/* 当调用 bx r14指令退出中断,堆栈指针PSP指向了新任务堆栈的正确位置 */

}

说明:

  • 在Cortex-M处理器中有两个栈指针,一个是主栈指针(Main Stack Pointer,即MSP),它可用于线程模式,在中断模式下只能用MSP;另一个是进程堆栈指针(Processor Stack Pointer,即PSP),PSP总是用于线程模式。在任何时刻只能使用到其中一个。
  • 复位后处于线程模式特权级,默认使用MSP。在FreeRTOS中,MSP用于OS内核和异常处理,PSP用于应用任务。
  • 通过设置CONTROL寄存器的bit[1]选择使用哪个堆栈指针。CONTROL[1]=0选择主堆栈指针;CONTROL[1]=1选择进程堆栈指针。

3.2 函数vTaskSwitchContext()

该函数会更新当前任务运行时间,检查任务堆栈使用是否溢出,然后调用宏 taskSELECT_HIGHEST_PRIORITY_TASK()获取更高优先级的任务。

函数源代码如下:

void vTaskSwitchContext( void )
{
	/* 如果任务调度器已经挂起 */
	if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
	{
		xYieldPending = pdTRUE;		/* 标记任务调度器挂起,不允许任务切换 */
	}
	else
	{
		xYieldPending = pdFALSE;	/* 标记任务调度器没有挂起 */
		
		traceTASK_SWITCHED_OUT();

		/* 
		 * 如果启用运行时间统计功能,设置configGENERATE_RUN_TIME_STATS为1
		 * 如果使用了该功能,要提供以下两个宏:
		 * portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()
		 * portGET_RUN_TIME_COUNTER_VALUE()
		 */
		#if ( configGENERATE_RUN_TIME_STATS == 1 )
		{
				#ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
					portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );
				#else
					ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
				#endif

				/*  
				 * ulTotalRunTime记录系统的总运行时间,ulTaskSwitchedInTime记录任务切换的时间
				 * 如果系统节拍周期为1ms,则ulTotalRunTime要497天后才会溢出
				 * ulTotalRunTime < ulTaskSwitchedInTime表示可能溢出
				 */
				if( ulTotalRunTime > ulTaskSwitchedInTime )
				{
					/* 记录当前任务的运行时间 */
					pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}

				/* 更新ulTaskSwitchedInTime,下个任务时间从这个值开始 */
				ulTaskSwitchedInTime = ulTotalRunTime;	
		}
		#endif /* configGENERATE_RUN_TIME_STATS */

		/* 核查堆栈是否溢出 */
		taskCHECK_FOR_STACK_OVERFLOW();

		/* 寻找更高优先级的任务 */
		taskSELECT_HIGHEST_PRIORITY_TASK();
		
		traceTASK_SWITCHED_IN();

		/* 如果使用Newlib运行库,你的操作系统资源不够,而不得不选择newlib,就必须打开该宏 */
		#if ( configUSE_NEWLIB_REENTRANT == 1 )
		{
			_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
		}
		#endif /* configUSE_NEWLIB_REENTRANT */
	}
}

3.2 寻找下一个任务的方式

PendSV中会调用vTaskSwitchContext(),最后调用函数taskSELECT_HIGHEST_PRIORITY_TASK()寻找优先级最高的任务。

对于FreeRTOS的调度器,它有两种方式寻找下一个最高优先级的任务,分别为特殊方式和常用方式,在FreeRTOSConfig.h中可通过宏定义设置,如下:

/* 0:使用常用方式来选择下一个要运行的任务;1:使用特殊方法来选择下一个要运行的任务 */
#define configUSE_PORT_OPTIMISED_TASK_SELECTION	1

在FreeRTOS中,通用方法不依赖某些硬件等限制,适用于多种MCU中。特殊方式是使用了某些硬件的特性,只针对部分MCU而使用。

3.2.1 常用方法

uxTopReadyPriority 记录就绪态中最高优先级值,创建任务时会更新值,有任务添加到就绪表时也会更新值。这种方法对任务的数量无限制。

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

3.2.2 特殊方式

使用此方法,uxTopReadyPriority 每个bit位表示一个优先级,bit0表示优先级0,bit31表示优先级31,使用此方式优先级最大只能是32个。

#define taskSELECT_HIGHEST_PRIORITY_TASK()														\
{																								\
	UBaseType_t uxTopPriority;																		\
																								\
	/* 获取优先级最高的任务 */								\
	portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );								\
	configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 );		\
	
	/* 获取优先级最高任务的任务控制块 */
	listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );		\
} 

其中:

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

__clz( ( uxReadyPriorities ) 是计算uxReadyPriorities 的前导零个数,如:
二进制0001 1010 0101 1111的前导零个数为3,可以知道,最高优先级uxTopPriority 等于 31减去前导零个数。
知道最高优先级的优先级,则通过listGET_OWNER_OF_NEXT_ENTRY()对应最高优先级的列表项,将pxCurrentTCB指向对应的控制块。


参考资料:

【1】: 正点原子:《STM32F407 FreeRTOS开发手册V1.1》
【2】: 野火:《FreeRTOS 内核实现与应用开发实战指南》
【3】: 《Cortex M3权威指南(中文)》

你可能感兴趣的:(FreeRTOS原理剖析)