欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

FreeRTOS任务切换源码分析

程序员文章站 2024-02-22 18:40:46
...

多任务系统可以实现多个任务并发执行,如果是单核处理器,那么CPU执行任务A一段时间,执行任务B一段时间,执行任务C一段时间,宏观上看就是多个任务同时执行。在单核处理器跑多个任务的情况下,CPU是如何从一个任务切换到另一个任务的?本文我们来探究一下FreeRTOS的任务切换。
本文硬件基于STM32F103单片机M3内核,软件基于FreeRTOS V9.0.0。
FreeRTOS的任务切换是在PendSV中断服务函数中完成的(ucos,rthread等实时操作系统也是在PendSV中断服务函数中完成任务切换的)。PendSV中断服务函数代码是汇编语言编写的,并且FreeRTOS官方已经帮用户实现了(在port.c文件中,port.c文件中代码都是和处理器密切相关的),这也使我们移植FreeRTOS变的简单,FreeRTOS官方的PendSV中断函数名字是__asm void xPortPendSVHandler( void ) 如果我们现有工程中PendSV中断是别的名字,比如STM32是void PendSV_Handler(void),那我们就宏定义一下

#define xPortPendSVHandler 	PendSV_Handler

当然这不是唯一的办法,你也可以在启动代码的中断向量表中做修改
FreeRTOS任务切换源码分析说了这么多PendSV中断,可能有大兄弟问为什么要在PendSV中断中切换任务,带着这个疑问我们来看一下PendSV的特点。
1.PendSV异常是属于CPU内核异常。
2.PendSV异常可以由软件触发,将ICSR寄存器的bit28置1可以触发PendSV异常。
3.PendSV异常优先级可以由用户设置,将PendSV的中断优先级设置为最低,等到其它中断全部执行完成后再响应PendSV异常。
我们知道FreeRTOS任务切换有任务级切换和中断级切换,其实他们最终的操作都是通过将ICSR寄存器的bit28置1来触发PendSV中断,在PendSV中断中完成任务切换。
接下来我们看一个重点,PendSV中断服务函数。PendSV中断服务函数是FreeRTOS的重中之重。
PendSV中断服务函数

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

	PRESERVE8
	//Cortex-M3内核是双堆栈机制,当多任务执行的时候使用的是PSP,PSP的值是任务栈的栈顶指针 
	mrs r0, psp//PSP寄存器的值保存到R0寄存器中(R0保存了任务栈的栈顶指针)
	isb

	
	ldr	r3, =pxCurrentTCB		//获取当前任务控制块的地址
	ldr	r2, [r3]//当前任务控制块地址所指的值保存到了R2中,任务控制块的第一个成员是栈顶指针
							//即R2寄存器保存了任务栈顶指针

	//保r4到r11寄存器的值到堆栈,stmdb指令是带回显的  r0寄存器的值会自动更新 
	stmdb r0!, {r4-r11}			/* Save the remaining registers. */
	//R2寄存器保存了任务控制块的地址
	str r0, [r2]				//更新R2寄存器的值(更新栈顶指针的值)  
											//经过压栈R0寄存器保存了最新的栈顶指针 
											//将最新的栈顶指针保存到R2寄存器中

	stmdb sp!, {r3, r14}//将R3和R14寄存器压栈
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0			//关中断
	dsb
	isb
	bl vTaskSwitchContext		//调用vTaskSwitchContext函数 获取下一个要运行的任务
	mov r0, #0
	msr basepri, r0					//开中断
	ldmia sp!, {r3, r14}		//恢复R3和R14寄存器中的值

	//获取下一个要运行任务的栈顶,并将栈顶指针保存到R0寄存器中
	ldr r1, [r3]
	ldr r0, [r1]				/* The first item in pxCurrentTCB is the task top of stack. */
	//从任务堆栈中恢复R4~R11寄存器的值到CPU寄存器,
	ldmia r0!, {r4-r11}			/* Pop the registers and the critical nesting count. */
	msr psp, r0		//将任务的栈顶指针赋值到PSP寄存器中
	isb
	bx r14			//跳转到下一个任务
	nop
}

PendSV中断服务函数做了3件事
1.保存正在运行任务的现场。将CPU寄存器的值保存到当前任务的任务堆栈中。
2.查找下一个正在运行的任务。
3.恢复下一个要运行任务的现场。将下一个要运行任务的CPU寄存器值从堆栈恢复到CPU寄存器中。
在PendSV中断服务函数中调用了vTaskSwitchContext函数,vTaskSwitchContext函数是查找下一个要运行任务的函数。
vTaskSwitchContext函数如下:

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();

		#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

				/* Add the amount of time the task has been running to the
				accumulated time so far.  The time the task started running was
				stored in ulTaskSwitchedInTime.  Note that there is no overflow
				protection here so count values are only valid until the timer
				overflows.  The guard against negative values is to protect
				against suspect run time stat counter implementations - which
				are provided by the application, not the kernel. */
				if( ulTotalRunTime > ulTaskSwitchedInTime )
				{
					pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
				ulTaskSwitchedInTime = ulTotalRunTime;
		}
		#endif /* configGENERATE_RUN_TIME_STATS */

		/* 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();//选择一个最高优先级的任务
		traceTASK_SWITCHED_IN();

		#if ( configUSE_NEWLIB_REENTRANT == 1 )
		{
			/* Switch Newlib's _impure_ptr variable to point to the _reent
			structure specific to this task. */
			_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
		}
		#endif /* configUSE_NEWLIB_REENTRANT */
	}
}

该函数中先判断了任务调度器是否是挂起的,如果调度器没有挂起就调用了taskSELECT_HIGHEST_PRIORITY_TASK()函数,选择一个最高优先级的任务。

	#define taskSELECT_HIGHEST_PRIORITY_TASK()														\
	{																								\
	UBaseType_t uxTopPriority;																		\
																									\
		/* Find the highest priority list that contains ready tasks. */								\
		portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );								\
		configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 );		\
		listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );		\
	} /* taskSELECT_HIGHEST_PRIORITY_TASK() */

使用宏portGET_HIGHEST_PRIORITY查找最高优先级

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

这里我们默认硬件方式查找。
__clz是计算前导0指令。__clz可以计算出从最高位开始前边有多少个0
比如
uint32_t a = 0x00000001;
__clz(a) 计算出的结果是31
uint32_t a = 0x40000000;
__clz(a) 计算出的结果是1 。
当使用硬件方式查找最高优先级的方法,uxReadyPriorities 变量的每一个bit都表示一个优先级,bit0表示优先级0 ,bit31表示优先级31。因为硬件方式查找时每一个bit都表示一个优先级,所以使用硬件方式的时候FreeRTOS最多支持32个优先级(0~31优先级)。

uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

举个例子:
uxReadyPriorities 等于 0x80000000时 表示第31优先级下有任务就绪, __clz(uxReadyPriorities ) 等于0 ,31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) )等于31,恰好算出31。
uxReadyPriorities 等于 0x00000002时 表示第1优先级下有任务就绪 __clz(uxReadyPriorities ) 等于30 ,31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) )等于1,恰好算出1。
最终uxTopPriority保存了就绪任务中最高的优先级。
接下来又调用了listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );
根据对应优先级的列表,更新pxCurrentTCB指针的值。
注意pxReadyTasksLists是一个列表类型的数组,FreeRTOS每个优先级下都有一个就绪列表。
listGET_OWNER_OF_NEXT_ENTRY()函数我们在列表与列表项章节已经分析过了。
listGET_OWNER_OF_NEXT_ENTRY()函数就是每次调用的时候pxIndex向后偏移一个节点,但是会跳过根节点。
由此可以看出,当某一优先级下就绪列表中不只一个任务时,他们是轮询执行的。
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; 更新pxTCB,也就是更新pxCurrentTCB。

相关标签: FreeRTOS