FreeRTOS的调度原理

前言

以个人理解,FreeRTOS内核调度的本质是利用了从异常中断返回时,切换任务栈的机制,使得进入新的任务下进行执行任务,实现内核调度功能。

内核进入第一个空闲任务分析

  • 内核版本:FreeRTOS V9.0.0
  • 硬件平台:STM32F103ZE
  • 仿真平台:MDK5.23

启动代码分析

// startup_stm32f10x_hd.s
; Reset handler
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit
                BLX     R0               
                LDR     R0, =__main
                BX      R0
                ENDP

内核初始化时首先执行startup_stm32f10x_hd.s这个文件内部的内容:

  • 首先设置初始化堆栈
  • 然后设置PC指针为Reset_Handler标签
    这样在进入复位向量后,先初始化时钟系统,然后进入main函数执行应用程序代码。
// src code
int main(void)
{
	BaseType_t xReturn = pdPASS;//定义一个创建信息返回值,默认为 pdPASS 
	BspInit();
	printf("FreeRTOSTask\r\n");
	printf("Please send queue message by press KEY2 or KEY_UP\n");
	printf("ReceiveTask receive message echo in USART\n\n");
	if(pdPASS == xReturn)
	{
		vTaskStartScheduler();//启动任务,开始调度
	}		
	else
	{
		return -1;
	}
	while(1);//正常不会执行到这里	
}

上面进入初始化bsp相关外设后,会直接进入系统调度vTaskStartScheduler:

void vTaskStartScheduler( void )
{
BaseType_t xReturn;

	/* Add the idle task at the lowest priority. */
	#if( configSUPPORT_STATIC_ALLOCATION == 1 )
	{
		StaticTask_t *pxIdleTaskTCBBuffer = NULL;
		StackType_t *pxIdleTaskStackBuffer = NULL;
		uint32_t ulIdleTaskStackSize;

		/* The Idle task is created using user provided RAM - obtain the
		address of the RAM then create the idle task. */
		vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
		xIdleTaskHandle = xTaskCreateStatic(	prvIdleTask,
												"IDLE",
												ulIdleTaskStackSize,
												( void * ) NULL,
												( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
												pxIdleTaskStackBuffer,
												pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */

		if( xIdleTaskHandle != NULL )
		{
			xReturn = pdPASS;
		}
		else
		{
			xReturn = pdFAIL;
		}
	}
	#else
	{
	  "本例使用的是动态申请内存的方式进行分析调度原理,所以可以从此处开始:"
	  "这一步是申请空闲任务,优先级为最低"
		/* The Idle task is being created using dynamically allocated RAM. */
		xReturn = xTaskCreate(	prvIdleTask,
								"IDLE", configMINIMAL_STACK_SIZE,
								( void * ) NULL,
								( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
								&xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
	}
	#endif /* configSUPPORT_STATIC_ALLOCATION */

	#if ( configUSE_TIMERS == 1 )
	{
		if( xReturn == pdPASS )
		{
			xReturn = xTimerCreateTimerTask();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
	#endif /* configUSE_TIMERS */

	if( xReturn == pdPASS )
	{
		/* Interrupts are turned off here, to ensure a tick does not occur
		before or during the call to xPortStartScheduler().  The stacks of
		the created tasks contain a status word with interrupts switched on
		so interrupts will automatically get re-enabled when the first task
		starts to run. */
		portDISABLE_INTERRUPTS();

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

		xNextTaskUnblockTime = portMAX_DELAY;
		xSchedulerRunning = pdTRUE;
		xTickCount = ( TickType_t ) 0U;

		/* If configGENERATE_RUN_TIME_STATS is defined then the following
		macro must be defined to configure the timer/counter used to generate
		the run time counter time base. */
		portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
		
		"上面申请完空闲任务后,会开始进入系统调度,按照已有的概念,我们知道最终会进入空闲"
		"任务中执行,如何进入这个空闲任务,是接下来我们分析xPortStartScheduler的重点:"
		/* Setting up the timer tick is hardware specific and thus in the
		portable interface. */
		if( xPortStartScheduler() != pdFALSE )
		{
			/* Should not reach here as if the scheduler is running the
			function will not return. */
		}
		else
		{
			/* Should only reach here if a task calls xTaskEndScheduler(). */
		}
	}
	else
	{
		/* This line will only be reached if the kernel could not be started,
		because there was not enough FreeRTOS heap to create the idle task
		or the timer task. */
		configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
	}

	/* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,
	meaning xIdleTaskHandle is not used anywhere else. */
	( void ) xIdleTaskHandle;
}

上面申请完空闲任务后,会开始进入系统调度,按照已有的概念,我们知道最终会进入空闲任务中执行,如何进入这个空闲任务,是接下来我们分析xPortStartScheduler的重点:

// ARM_CM3/port.c
BaseType_t xPortStartScheduler( void )
{
...
	/* Make PendSV and SysTick the lowest priority interrupts. */
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

	/* Start the timer that generates the tick ISR.  Interrupts are disabled
	here already. */
	vPortSetupTimerInterrupt();
	
	/* Initialise the critical nesting count ready for the first task. */
	uxCriticalNesting = 0;
	
	"重点会进入这个函数,开启第一个任务:"
	/* Start the first task. */
	prvStartFirstTask();

	/* Should not get here! */
	return 0;
}


__asm void prvStartFirstTask( void )
{
	PRESERVE8    "8字节对齐"

	/* Use the NVIC offset register to locate the stack. */
	ldr r0, =0xE000ED08    "这个地址是NVIC偏移寄存器的地址,用来定位当前的栈的"
	ldr r0, [r0]
	ldr r0, [r0] "执行完这句后,r0寄存器内部存放的就是当前的栈地址"

	/* Set the msp back to the start of the stack. */
	msr msp, r0  "由于CM3的双堆栈特性,初始化时,会进入主堆栈。因此将r0的值传输msp"
	/* Globally enable interrupts. */
	cpsie i   "强制使能所有的中断"
	cpsie f
	dsb "同步数据,主要是担心某些存储器写入存在缓冲机制"
	isb
	/* Call SVC to start the first task. */
	svc 0   
	nop
	nop
}

svc 0 可以触发一个系统调用,使其接下来的pc指针强制进入svc下的异常处理回调函数:

// ARM_CM3/port.c
__asm void vPortSVCHandler( void )
{
	PRESERVE8
	
	"下面这一句,是将当前存储TCB变量的地址放入r3寄存器中"
	ldr	r3, =pxCurrentTCB	/* Restore the context. */
	"下面这一句,是将当前的TCB指向的地址取出放入r1寄存器中"
	ldr r1, [r3]			/* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
	"下面这一句,是将TCB结构体中的第一个变量取出来放入r0寄存器中;"
	"TCB结构体中的第一个变量也就是当前的栈顶地址"
	ldr r0, [r1]			/* The first item in pxCurrentTCB is the task top of stack. */
	"下面这一句,是将r0处读取多个数据,依次放入r4-r11寄存器中,每读取一次,r0自增一次"
	"这里相当于从当前的栈顶指针出栈8个数据到r4-r11寄存器中"
	ldmia r0!, {r4-r11}		/* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
	"这个时候将r0处的栈顶指针放入psp中,psp属于进程堆栈,专门用来保存正在执行的任务堆栈"
	"此时r0寄存器存放的是空闲任务携带参数的地址"
	msr psp, r0				/* Restore the task stack pointer. */
	"强制指令同步,数据同步"
	isb
	"下面这一句,是将r0寄存器清0"
	mov r0, #0
	"下面这一句,是将特殊寄存器basepri置0,表示开启所有的中断使能"
	msr	basepri, r0
	"下面这一句,逻辑或的意思,将r14寄存器内部的值与0x0d或后,再存入r14"
	"这一步很关键,为什么要或0x0d?下面详细说明"
	orr r14, #0xd
	"异常返回指令,这个返回指令有两种意思:在普通函数返回时,即将使用的PC指针就是目前r14"
	"寄存器中的值;在异常函数中返回时,r14表示一个exc_return数值,硬件系统会根据这个"
	"值来判断下一步的堆栈、pc值"
	bx r14
}

关于exc_return的解释:
在这里插入图片描述

从上图看出,使用0x0d或操作,是为了返回线程模式,并使用线程堆栈。
那么是如何使用线程堆栈的呢?此处的使用的硬件平台是stm32f103系列,所以对应的内核为cortex-m3内核,使用的指令架构为armv7-m指令集,因此可以查阅该指令集下异常返回伪代码:

Exception return operation
The ExceptionReturn() pseudocode function describes the exception return operation:
// ExceptionReturn()
// =================
ExceptionReturn(bits(28) EXC_RETURN)
assert CurrentMode == Mode_Handler;
if HaveFPExt() then
if !IsOnes(EXC_RETURN<27:5>) then UNPREDICTABLE;
else
if !IsOnes(EXC_RETURN<27:4>) then UNPREDICTABLE;
integer ReturningExceptionNumber = UInt(IPSR<8:0>);
integer NestedActivation; // used for Handler => Thread check when value == 1
NestedActivation = ExceptionActiveBitCount(); // Number of active exceptions
if ExceptionActive[ReturningExceptionNumber] ==0’ then
DeActivate(ReturningExceptionNumber);
UFSR.INVPC =1;
LR = 0xF0000000 + EXC_RETURN;
ExceptionTaken(UsageFault); // returning from an inactive handler
return;
else
case EXC_RETURN<3:0> of
when ‘0001// return to Handler
frameptr = SP_main;
CurrentMode = Mode_Handler;
CONTROL.SPSEL =0;
when ‘1001// returning to Thread using Main stack
if NestedActivation != 1 && CCR.NONBASETHRDENA ==0’ then
DeActivate(ReturningExceptionNumber);
UFSR.INVPC =1;
LR = 0xF0000000 + EXC_RETURN;
ExceptionTaken(UsageFault); // return to Thread exception mismatch
return;
else
frameptr = SP_main;
CurrentMode = Mode_Thread;
CONTROL.SPSEL =0;
when ‘1101// returning to Thread using Process stack
if NestedActivation != 1 && CCR.NONBASETHRDENA ==0’ then
DeActivate(ReturningExceptionNumber);
UFSR.INVPC =1;
LR = 0xF0000000 + EXC_RETURN;
ExceptionTaken(UsageFault); // return to Thread exception mismatch
return;
else
frameptr = SP_process;
CurrentMode = Mode_Thread;
CONTROL.SPSEL =1;
otherwise
DeActivate(ReturningExceptionNumber);
UFSR.INVPC =1;
LR = 0xF0000000 + EXC_RETURN;
ExceptionTaken(UsageFault); // illegal EXC_RETURN
return;
DeActivate(ReturningExceptionNumber);
PopStack(frameptr);
if CurrentMode==Mode_Handler AND IPSR<8:0> ==000000000’ then
UFSR.INVPC =1;
PushStack(); // to negate PopStack()
LR = 0xF0000000 + EXC_RETURN;
ExceptionTaken(UsageFault); // return IPSR is inconsistent
return;
if CurrentMode==Mode_Thread AND IPSR<8:0> !=000000000’ then
UFSR.INVPC =1;
"出栈的关键部分"
PushStack(); // to negate PopStack()
LR = 0xF0000000 + EXC_RETURN;
ExceptionTaken(UsageFault); // return IPSR is inconsistent
return;
ClearExclusiveLocal();
SetEventRegister() // see WFE instruction for more details
InstructionSynchronizationBarrier();
if CurrentMode==Mode_Thread AND NestedActivation == 0 AND SCR.SLEEPONEXIT ==1’ then
SleepOnExit(); // IMPLEMENTATION DEFINED

上述伪代码中PushStack为出栈部分,当异常返回时,正常的话会执行出栈操作,这里我们可以找到pc寄存器即将放置哪个值:

// PopStack()
// ==========
PopStack(bits(32) frameptr) /* only stack locations, not the load order, are architected */
if HaveFPExt() && EXC_RETURN<4> ==0’ then
framesize = 0x68;
forcealign =1;
else
framesize = 0x20;
forcealign = CCR.STKALIGN;
"此处的frameptr就是对应psp的值"
R[0] = MemA[frameptr,4];
R[1] = MemA[frameptr+0x4,4];
R[2] = MemA[frameptr+0x8,4];
R[3] = MemA[frameptr+0xC,4];
R[12] = MemA[frameptr+0x10,4];
LR = MemA[frameptr+0x14,4];
"下一句,就是取(psp+向右偏移24位bits)地址内的32位值,将此值赋值给pc"
"此时新的pc的值必须半字对齐"
PC = MemA[frameptr+0x18,4]; // UNPREDICTABLE if the new PC not halfword aligned
psr = MemA[frameptr+0x1C,4];
if HaveFPExt() then
if EXC_RETURN<4> ==0’ then
if FPCCR.LSPACT ==1’ then
FPCCR.LSPACT =0; // state in FP is still valid
else
CheckVFPEnabled();
for i = 0 to 15
S[i] = MemA[frameptr+0x20+(4*i),4];
FPSCR = MemA[frameptr+0x60,4];
CONTROL.FPCA = NOT(EXC_RETURN<4>);
spmask = Zeros(29)((psr<9> AND forcealign):00;
case EXC_RETURN<3:0> of
when ‘0001// returning to Handler
SP_main = (SP_main + framesize) OR spmask;
when ‘1001// returning to Thread using Main stack
SP_main = (SP_main + framesize) OR spmask;
when ‘1101// returning to Thread using Process stack
SP_process = (SP_process + framesize) OR spmask;
APSR<31:27> = psr<31:27>; // valid APSR bits loaded from memory
if HaveDSPExt() then
APSR<19:16> = psr<19:16>;
IPSR<8:0> = psr<8:0>; // valid IPSR bits loaded from memory
EPSR<26:24,15:10> = psr<26:24,15:10>; // valid EPSR bits loaded from memory
return;

从上面的伪代码可以发现异常返回时,先判断r14的值,再根据相应的值进行出栈,然后进入新的任务栈中执行任务。所以内核就是这样进入第一个空闲任务的:

  • 建立空闲任务堆栈,并初始化,填充任务回调函数等参数;
  • 触发svc中断,进入svc中断处理函数;
  • 在svc中断处理函数中,手动调整psp堆栈为空闲任务堆栈值;
  • 返回时,借助异常返回指令,最终正确跳转到空闲任务的回调函数中。

仿真演示

打开仿真软件,配置如下:
在这里插入图片描述

打开工程,启动断点调试:
在这里插入图片描述

此处是建立空闲任务后,初始化空闲任务的堆栈。
在这里插入图片描述

这里注意寄存器的值,还有此处堆栈处的值,在右下角内存地址处可以看到。

在这里插入图片描述

上面这张图可以发现r14寄存器的低4位是0x0d,说明异常返回时即将进入的线程模式下的进程堆栈。
在这里插入图片描述

上面这个图,注意看到psp的地址位0x20000578,r15的地址位0x08000fe8,r14的地址为0x080011dd,是不是与右下角的内存地址全部对应上了。

仿真的结果与理论一致,所以内核就是这样进入第一个空闲任务的。那么后面是如何调度的呢?这就需要借助挂起中断了。

内核如何调度

上面介绍了内核如何进入第一个任务。那么在进入第一个任务之后,在运行过程中,内核是如何调度到第二个任务?其实内核切换任务是利用了PendSV(可悬起中断/系统调用)机制。在多任务环境下,内核每次切换任务时,都会进入PendSV中断服务函数里,进行切换任务栈操作。

内核调度分析

// ARM_CM3/port.c
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(). */
	vPortRaiseBASEPRI();
	{
		/* Increment the RTOS tick. */
		if( xTaskIncrementTick() != pdFALSE )
		{
			/* A context switch is required.  Context switching is performed in
			the PendSV interrupt.  Pend the PendSV interrupt. */
			"下面这一句,是将pendsv中断寄存器位置1;执行完此句话后,会触发"
			"pendsv中断,进入pendsv中断服务程序"
			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
		}
	}
	vPortClearBASEPRIFromISR();
}

上面是一个系统定时器中断服务程序,当定时到时,会进入该中断,当满足特定条件时,会将pendsv中断打开触发,然后进入pendsv中断服务程序。

// ARM_CM3/port.c
__asm void xPortPendSVHandler( void )
{
	"这是一个内嵌汇编pendsv中断服务程序,下面3句是引用3个全局变量"
	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;

	PRESERVE8  "8字节对齐"
	
	"下面这一句是将当前psp堆栈值寄存在r0中,因为psp等下会变"
	mrs r0, psp 
	isb  "强制指令清空,我的理解是将三级流水线内的指令清空"
    "下面这一句,是将pxCurrentTCB变量的地址寄存在r3中"
	ldr	r3, =pxCurrentTCB		/* Get the location of the current TCB. */
	"下面这一句,是将pxCurrentTCB指向内存区域的第一个元素地址寄存在r2中"
	"而pxCurrentTCB指向内存区域的第一个元素值,"
	"其实就是记录当前TCB的栈顶地址变量."
	"执行完下一句后,r2中的值就是指向当前TCB栈顶地址的变量"
	ldr	r2, [r3]
	"下面这一句是将寄存器r4-r11的值依次压入当前栈中,r0中的值会减少32(4x8)"
	stmdb r0!, {r4-r11}			/* Save the remaining registers. */
	"此时的r0中的值就是当前任务压栈后的栈顶地址,执行完下一句后,"
	"就是将当前TCB栈顶地址的变量重新指向更新后的栈顶地址"
	str r0, [r2]				/* Save the new top of stack into the first member of the TCB. */
	"因为该函数是中断服务程序,由于CM3的双堆栈特性,所以,"
	"此处的sp表示的是msp(主堆栈)。下面这一句,是依次将r14、r3存入msp中"
	"存放r14的原因是,后面需要使用到这个lr值;存放r3的原因是,后面要"
	"使用r3获取pxCurrentTCB,其实使用pxCurrentTCB重新加载到寄存器中也可以"
	stmdb sp!, {r3, r14}
	"下面这一句,是将系统调用的最大中断优先级值寄存到r0中,为了屏蔽一部分的中断"
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0
	"强制同步数据"
	dsb
	"强制清除指令,清除3级流水线中的指令(个人理解)"
	isb
	"跳转到vTaskSwitchContext函数,此函数的目的是为了更新pxCurrentTCB"
	"指向的地址。同时也可以看出为什么上面要压栈r14、r13了,因为存在子函数"
	bl vTaskSwitchContext
	"在更新pxCurrentTCB之后,下面这2句,是将所有的中断打开"
	mov r0, #0
	msr basepri, r0
	"下面这一句,是将当初压栈的r3、r14出栈"
	ldmia sp!, {r3, r14}
	"下面这两句,是将r0存入更新后TCB的栈顶变量"
	ldr r1, [r3]
	ldr r0, [r1]				/* The first item in pxCurrentTCB is the task top of stack. */
	"下面这一句,是将栈中的前8个值依次出栈到r4-r11"
	ldmia r0!, {r4-r11}			/* Pop the registers and the critical nesting count. */
	"下面这一句,是将r0中的值赋值到psp上,其实在初始化栈的时候,"
	"对应位置,也是r0那个位置顺序"
	msr psp, r0
	"清除指令"
	isb
	"跳转指令,这个会对r14的值进行判断,如果后4位是0x0d,"
	"会根据psp的值,出栈到pc、r1等寄存器"
	bx r14
	nop
}

上面的bx r14条转指令是神奇的指令,在这一步,才决定了pc到底指向何方。上面的汇编程序就说明了内核如何调度了,每次进入pendsv中断函数内:

  • 压栈所需要的寄存器值,比如r14、r3
  • 进入子程序,更新当前任务指针pxCurrentTCB
  • 退出子程序,出栈,结束跳转
  • 在跳转指令中,指令系统会计算出pc应该指向的值

内核调度演示

在这里插入图片描述

在这里插入图片描述

上面图片显示在执行到bx r14时,lr的值的低4位是0x04,表明即将要进入用户模式的进程堆栈中,所以指令系统会将psp出栈,更新pc,进入新的任务栈,完成一次系统调度。

内核如何更新pxCurrentTCB

按照优先级大小,轮询调度

  • 优先级相同时,每次先调度先加入链表中的任务,然后再调用后加入链表中的任务
  • 优先级不同时,先调度优先高的,后依然调度优先级高的,所以称为可抢占调度内核
  • 没有处于就绪状态的任务,不参与每次调度
  • 13
    点赞
  • 89
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值