三、任务切换之PendSV异常

文章目录

  • PendSV异常
    • 1. 没有PendSV异常的任务切换
    • 2. 有PendSV异常的任务切换
      • 2.1 系统调用引起的任务切换
      • 2.2 systick中断引起任务切换
      • 2.3 PendSV异常处理函数
      • 2.4 寻找下一个要运行的任务
      • 2.5 时间片调度


PendSV异常

    PendSV翻译为可挂起系统中断,从名字上可以看出,这个系统中断可以被挂起,等到时机成熟的时候再去执行,一般会把它的优先级设置为最低,它对OS操作系统的任务切换时非常重要的,是OS设计的关键。可以通过中断控制和状态寄存器ICSR的bit28,也就是PendSV的挂起位置1,就会触发一次PendSV中断。与SVC异常不同,它是不精确的,因此它可以再更高优先级的异常中来被触发,触发后要等到当前的高优先级异常处理完之后再去执行。
(后面会详细讲一下中断和NVIC)

1. 没有PendSV异常的任务切换

    首先说一下能够引起上下文切换的情况有两种:(1)执行一次系统调用,例如调用taskYIELD_IF_USING_PREEMPTION()->抢占式调度;(2)系统滴答定时器(systick)中断。
    当没有PendSV时,systick引起上下文切换的方式如下:
三、任务切换之PendSV异常_第1张图片
在每一次systick中断来的时候都会进行一次上下文切换,这种情况并没有考虑其他中断的情况,当在systick来之前有其他中断来了,其他中断执行到一半的时候,systick异常来了,此时具体情况如下:
三、任务切换之PendSV异常_第2张图片
当systick打断IRQ再回到任务中的时候就会触发fault,引起系统的异常。

2. 有PendSV异常的任务切换

    当有PendSV异常的时候,具体的过程如下:
三、任务切换之PendSV异常_第3张图片
FreeRTOS系统的任务切换最终都是在PendSV中断服务函数中完成的,ucos也是在PendSV中断中完成任务切换的。因为这个原因,PendSV异常触发的条件和上面说的触发任务切换的条件一样。

2.1 系统调用引起的任务切换

    当有更高优先级任务来的时候抢占式调度会暂停当前的任务去执行新来的更高优先级的任务,即调用taskYIELD_IF_USING_PREEMPTION()宏,具体的代码如下:

// Tasks.c
#if( configUSE_PREEMPTION == 0 )
	/* If the cooperative scheduler is being used then a yield should not be
	performed just because a higher priority task has been woken. */
	#define taskYIELD_IF_USING_PREEMPTION()
#else
	#define taskYIELD_IF_USING_PREEMPTION() portYIELD_WITHIN_API()
#endif

// FreeRTOS.h
#ifndef portYIELD_WITHIN_API
	#define portYIELD_WITHIN_API portYIELD
#endif

// Portmacro.h
/* Scheduler utilities. */
/* 任务切换其实就是挂起了个PendSV异常 */
#define portYIELD()																\
{																				\
	/* Set a PendSV to request a context switch. */								\
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;								\ // 中断控制和状态寄存器的Bit28位置1,启动PendSV异常
																				\
	/* 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 );											\ // 内存屏障其实没有,为了保证代码的统一性
}

#define portEND_SWITCHING_ISR( xSwitchRequired ) if( xSwitchRequired != pdFALSE ) portYIELD()
#define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x ) // 中断中的任务切换同样调用portYIELD()

2.2 systick中断引起任务切换

    FreeRTOS中大多数的情况还是用systick中断来维持正常的任务切换,systick中断的具体函数如下(省略了使能Tickless的代码,后面会详细讲Tickless):

void xPortSysTickHandler( void )
{
	/* The SysTick runs at the lowest interrupt priority, so when this interrupt
	executes all interrupts must be unmasked.  There is therefore no need to
	save and then restore the interrupt mask value as its value is already
	known - therefore the slightly faster vPortRaiseBASEPRI() function is used
	in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
	/* SysTick以最低的中断优先级运行,因此当执行此中断时,必须屏蔽所有中断。    无需保存然后恢复中断屏蔽值,因为其值已知 因此使用稍快的vPortRaiseBASEPRI()函数代替portSET_INTERRUPT_MASK_FROM_ISR() */
	vPortRaiseBASEPRI();
	{
		/* Increment the RTOS tick. */
		if( xTaskIncrementTick() != pdFALSE ) // 增加时钟计数器xTickCount的值
		{
			/* A context switch is required.  Context switching is performed in
			the PendSV interrupt.  Pend the PendSV interrupt. */
			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; // 同样需要挂起一个PendSV异常
		}
	}
	vPortClearBASEPRIFromISR();
	#if( configUSE_TICKLESS_IDLE == 1 )
	{
	    // 省略
	}
	#endif
}

2.3 PendSV异常处理函数

    此函数被FreeRTOS重定义了,它是用汇编编写的,具体函数解析如下:
汇编语言相关:
浮点寄存器相关指令
三、任务切换之PendSV异常_第4张图片
多加载多存储指令
三、任务切换之PendSV异常_第5张图片

__asm void xPortPendSVHandler( void )
{
	extern uxCriticalNesting;       // 中断嵌套层数
	extern pxCurrentTCB;            // 当前TCB的指针
	extern vTaskSwitchContext;      // c语言的任务切换函数

	PRESERVE8

	mrs r0, psp                    // 把进程栈的指针给到r0
	isb
	/* Get the location of the current TCB. */
	ldr	r3, =pxCurrentTCB
	ldr	r2, [r3]                   // 上面两个语句是获取当前任务控制块的指针

	/* Is the task using the FPU context?  If so, push high vfp registers. */
	tst r14, #0x10
	it eq                          // 上面两句判断是否使能了FPU,如果使能了,任务切换的时候就要将s16~s31手动保存到任务栈中,其中s0~s15和FPSCR是自然保存的
	vstmdbeq r0!, {s16-s31}        // 手动将s16~s31收到保存到任务栈中

	/* Save the core registers. */
	stmdb r0!, {r4-r11, r14}       // 保存r4-r11,r14寄存器

	/* Save the new top of stack into the first member of the TCB. */
	str r0, [r2]                   // 将r0的值写入寄存器r2所保存的地址中,即将新的栈顶指针保存到TCB的第一个字段中,上面的操作使r2里面存了TCB的指针

	stmdb sp!, {r0, r3}           // 将寄存器r3暂时压栈,寄存器r3中保存了当前任务的任务控制块,而接下来要调用函数vTaskSwitchContext(),为了防止r3的值被改写,多以这里临时将R3的值先压栈
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	cpsid i                      
	msr basepri, r0              // 关闭中断
	dsb
	isb
	cpsie i
	bl vTaskSwitchContext       // 跳转到任务切换函数,将pxCurrentTCB更新为马上要切换的任务
	mov r0, #0                  // 把r0寄存器清0
	msr basepri, r0             // 打开中断
	
	ldmia sp!, {r0, r3}         // 恢复临时压栈的r0和r3寄存器

	/* The first item in pxCurrentTCB is the task top of stack. */
	ldr r1, [r3]                // r3里面原来存的是TCB指针的指针,即pxCurrentTCB这个变量的指针,出栈之后pxCurrentTCB的值已经改变了,多以此时r3所保存的地址处的数据也就发生了变化。(但是r3寄存器里面的内容没有变化)
	ldr r0, [r1]                // 获取新的要运行的任务的任务栈顶,将其保存到r0中

	/* Pop the core registers. */
	ldmia r0!, {r4-r11, r14}    // 与SVC一样的,将r4-r11和r14赋值任务栈中的值

	/* Is the task using the FPU context?  If so, pop the high vfp registers
	too. */
	tst r14, #0x10
	it eq
	vldmiaeq r0!, {s16-s31}    // 如果使用了FPU同样要手动恢复s16-s31浮点寄存器

	msr psp, r0                // 更新任务栈指针psp
	isb
	#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
		#if WORKAROUND_PMU_CM001 == 1
			push { r14 }
			pop { pc }
			nop
		#endif
	#endif

    /* r14里面的值为EXC_RETURN值,这个值用于退出SVV或PendSV中断处理器应该处于什么状态。处理器进入异常或
    中断服务程序(ISR)时,链接寄存器R14的值会被更新为EXC_RETURN,之后该数值会在异常处理结束时触发异常
    返回,这里强制将EXC_RETURN设置为0xfffffffd,表示退出异常以后CPU进入线程模式并且使用进程栈。详情请参
    照《Cortex-M4权威指南》中的第八章,深入了解异常处理 */
	bx r14                      
	/* 跳转到r14链接寄存器,执行此代码之后硬件自动恢复寄存器r0~r3、r12、LR、PC和xPSR的值,确定异常返回以后
	应该进入处理器模式还是进程模式,使用主栈指针MSP还是进程栈PSP。由于是任务切换,肯定还会进入进程模式
	,并且使用进程栈PSP,寄存器PC值会被恢复为即将运行的任务的任务函数,这样新的任务就开始运行,任务切换
	成功。*/
}

2.4 寻找下一个要运行的任务

    上面PendSV汇编中用到了一个函数vTaskSwitchContext()来获取下一个任务,这个函数的根本意义就是改变全局变量pxCurrentTCB,改变的依据就是就绪态任务的优先级。寻找下一个任务可以有两种方式,一个是软件方式适用于所有的CPU架构,另一个是硬件的方式,计算前导0,并用31-前导0的个数,这种方法速度更快。

// tasks.c
void vTaskSwitchContext( void )
{
	if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ) // 任务调度被挂起,此时不允许任务切换
	{
		/* The scheduler is currently suspended - do not allow a context switch. */
		xYieldPending = pdTRUE;
	}
	else
	{
		xYieldPending = pdFALSE;
		traceTASK_SWITCHED_OUT(); 

		/* Check for stack overflow, if configured. */
		taskCHECK_FOR_STACK_OVERFLOW(); // 检查栈是不是溢出了

		/* Select a new task to run using either the generic C or port optimised asm code. */
		taskSELECT_HIGHEST_PRIORITY_TASK(); // 任务切换,根据配置是使用软件方式还是硬件方式 
		
		/*lint !e9079 void * is used as this macro is used with timers and co-routines too.  Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
		traceTASK_SWITCHED_IN();
	}
}

// 宏定义 根据优先级选择任务
#if ( configUSE_PORT_OPTIMISED_TASK_SELECTION == 0 ) // 使用软件方式 方法通用

	/* If configUSE_PORT_OPTIMISED_TASK_SELECTION is 0 then task selection is
	performed in a generic way that is not optimised to any particular
	microcontroller architecture. */

	/* uxTopReadyPriority holds the priority of the highest priority ready
	state task. */
	#define taskRECORD_READY_PRIORITY( uxPriority )														\
	{																									\
		if( ( uxPriority ) > uxTopReadyPriority )														\
		{																								\
			uxTopReadyPriority = ( uxPriority );														\
		}																								\
	} /* taskRECORD_READY_PRIORITY */

	/*-----------------------------------------------------------*/

	#define taskSELECT_HIGHEST_PRIORITY_TASK()															\
	{																									\
	UBaseType_t uxTopPriority = uxTopReadyPriority;														\
																										\
		/* Find the highest priority queue that contains ready tasks. */								\
		while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )							\
		{																								\
			configASSERT( uxTopPriority );																\
			--uxTopPriority;																			\
		}																								\
																										\
		/* listGET_OWNER_OF_NEXT_ENTRY indexes through the list, so the tasks of						\
		the	same priority get an equal share of the processor time. */									\
		listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ]));\
		
		uxTopReadyPriority = uxTopPriority;																\
	} /* taskSELECT_HIGHEST_PRIORITY_TASK */

	/*-----------------------------------------------------------*/

	/* Define away taskRESET_READY_PRIORITY() and portRESET_READY_PRIORITY() as
	they are only required when a port optimised method of task selection is
	being used. */
	#define taskRESET_READY_PRIORITY( uxPriority )
	#define portRESET_READY_PRIORITY( uxPriority, uxTopReadyPriority )

#else /* configUSE_PORT_OPTIMISED_TASK_SELECTION */  // 使用硬件方式 计算前导0来判断优先级

	/* If configUSE_PORT_OPTIMISED_TASK_SELECTION is 1 then task selection is
	performed in a way that is tailored to the particular microcontroller
	architecture being used. */

	/* A port optimised version is provided.  Call the port defined macros. */
	#define taskRECORD_READY_PRIORITY( uxPriority )	portRECORD_READY_PRIORITY( uxPriority, uxTopReadyPriority )

	/*-----------------------------------------------------------*/

	#define taskSELECT_HIGHEST_PRIORITY_TASK()														\
	{																								\
	UBaseType_t uxTopPriority;																		\
																									\
		/* Find the highest priority list that contains ready tasks. */								\
		portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );\ // 计算前导0的方式 前导0越少优先级越高
		configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 );		\
		listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );		\
	} /* taskSELECT_HIGHEST_PRIORITY_TASK() */

	/*-----------------------------------------------------------*/

	/* A port optimised version is provided, call it only if the TCB being reset
	is being referenced from a ready list.  If it is referenced from a delayed
	or suspended list then it won't be in a ready list. */
	#define taskRESET_READY_PRIORITY( uxPriority )														\
	{																									\
		if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 )	\
		{																								\
			portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) );							\
		}																								\
	}

#endif /* configUSE_PORT_OPTIMISED_TASK_SELECTION */

// portmacro.h
#if configUSE_PORT_OPTIMISED_TASK_SELECTION == 1

	/* Check the configuration. */
	#if( configMAX_PRIORITIES > 32 )
		#error configUSE_PORT_OPTIMISED_TASK_SELECTION can only be set to 1 when configMAX_PRIORITIES is less than or equal to 32.  It is very rare that a system requires more than 10 to 15 difference priorities as tasks that share a priority will time slice.
	#endif

	/* Store/clear the ready priorities in a bit map. */
	#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )
	#define portRESET_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) &= ~( 1UL << ( uxPriority ) )

	/*-----------------------------------------------------------*/
    // 计算前导0来计算优先级
	#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

#endif /* taskRECORD_READY_PRIORITY */

2.5 时间片调度

    FreeRTOS支持多个任务同时拥有一个优先级,它允许一个任务运行一个时间片(一个时钟节拍的长度)后让出CPU的使用权,让拥有同优先级的下一个任务运行,同优先级的任务做完了就去搞更低优先级的任务,要使用时间片调度的话宏configUSE_PREEMPTION和宏configUSE_TIME_SLICING必须为1。时间片的长度由configTICK_RATE_HZ来确定,一个时间片的长度就是滴答定时器的中断周期,这里配置configTICK_RATE_HZ为1000,即1ms产生一次systick中断。
    在systick中断服务函数Systick_Handler()中会调用FreeRTOS的API函数xPortSysTickHandler(),xPortSysTickHandler()又会调用xTaskIncrementTick函数,会根据它的返回值来确定是不是要进行调度。
三、任务切换之PendSV异常_第6张图片

你可能感兴趣的:(RTOS操作系统,PendSV,Systick)