vTaskSwitchContext

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权威指南(中文)》

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
vTaskSwitchContext函数是FreeRTOS调度器的核心函数之一,用于实现任务的上下文切换和任务调度算法。该函数的代码比较复杂,主要包括以下几个部分: 1. 检查当前任务是否处于临界区保护状态。如果当前任务处于临界区保护状态,则暂不进行任务调度,以避免出现竞争条件和死锁等问题。 2. 检查是否存在高优先级任务需要抢占当前任务。如果存在高优先级任务需要抢占当前任务,则立即进行任务切换。任务切换的流程包括保存当前任务的上下文到堆栈中,并将新任务的上下文从堆栈中恢复。任务切换的过程是通过汇编语言来实现的。 3. 检查是否存在时间片轮询定时器。如果存在时间片轮询定时器,则检查定时器是否超时。如果定时器超时,则立即进行任务切换,将当前任务的上下文保存到堆栈中,并将下一个任务的上下文从堆栈中恢复。 4. 如果没有高优先级任务需要抢占当前任务,并且没有时间片轮询定时器或者定时器没有超时,则继续执行当前任务,直到任务阻塞或者时间片轮询定时器超时。 vTaskSwitchContext函数的代码比较复杂,主要是因为它要考虑任务的优先级和时间片轮询定时器等多个因素,以实现最优的任务调度算法。在使用FreeRTOS时,需要仔细设计任务的优先级和调度算法,并合理使用阻塞和延时函数,以避免出现优先级反转、死锁等问题,并保证系统的实时性能。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值