duduru

freertos内核--任务调度剖析

前言

在使用freertos的时候,我们都知道在创建了一系列任务之后,启用调度器,系统就可以帮我们管理任务,分配资源。本文主要对调度器的原理进行剖析,从vTaskStartScheduler()函数开始,一探究竟。

freertos版本:9.0.0

启动调度器

vTaskStartScheduler()

vTaskStartScheduler()用于开启调度器,具体代码如下:

void vTaskStartScheduler( void ){
   
   
	BaseType_t xReturn;
	xReturn = xTaskCreate( prvIdleTask,
						   "IDLE",configMINIMAL_STACK_SIZE,
						   ( void * ) NULL,
						   ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
						   &xIdleTaskHandle );
	if( xReturn == pdPASS )
	{
   
   	
		portDISABLE_INTERRUPTS();
		if( xPortStartScheduler() != pdFALSE )
		{
   
   
			/* Should not reach here as if the scheduler is running the
			function will not return. */
		}	
	}			   
}

代码已做删减(只显示了核心部分,下同)。可以看到在开启调度器的时候,vTaskStartScheduler()主要做了三件事:创建空闲任务;关闭中断,确保后续工作不会被Systick打断;同时调用xPortStartScheduler()(此处以ARM_CM3内核为例)

xPortStartScheduler()

xPortStartScheduler()函数代码如下:

BaseType_t xPortStartScheduler( void )
{
   
   
extern void vPortStartFirstTask( 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. */
	prvPortStartFirstTask();

	/* Should never get here as the tasks will now be executing!  Call the task
	exit error function to prevent compiler warnings about a static function
	not being called in the case that the application writer overrides this
	functionality by defining configTASK_RETURN_ADDRESS. */
	prvTaskExitError();

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

第一步是对系统Systick中断和PendSV中断进行优先级设置,均设置为最低( 为什么要设置Systick中断和PendSV中断最低优先级),第二步使能Systick中断(但是中断不会发生,因为之前关闭了中断),最后调用vPortStartFirstTask()。

执行第一个任务

vPortStartFirstTask()

vPortStartFirstTask()用于执行第一个任务。

static void prvPortStartFirstTask( void )
{
   
   
	__asm volatile(
					" ldr r0, =0xE000ED08 	\n" /* Use the NVIC offset register to locate the stack. */
					" ldr r0, [r0] 			\n"
					" ldr r0, [r0] 			\n"
					" msr msp, r0			\n" /* Set the msp back to the start of the stack. */
					" cpsie i				\n" /* Globally enable interrupts. */
					" cpsie f				\n"
					" dsb					\n"
					" isb					\n"
					" svc 0					\n" /* System call to start first task. */
					" nop					\n"
				);
}

代码分析:

ldr r0, =0xE000ED08 	
ldr r0, [r0] 			
ldr r0, [r0] 			
msr msp, r0

这4步的目的是给主堆栈的栈顶指针msp赋初值,具体操作步骤如图:
在这里插入图片描述

主堆栈指针就指向了栈顶0x200008DB。

cpsie i
cpsie f

之后开启中断和异常,让下面的SVC中断能够响应

svc 0

产生系统调用服务号为0的SVC中断

SVC中断服务函数

vPortSVCHandler()函数代码如下:

void vPortSVCHandler( void )
{
   
   
	__asm volatile (
					"	ldr	r3, pxCurrentTCBConst2		\n" /* Restore the context. */
					"	ldr r1, [r3]					\n" /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
					"	ldr r0, [r1]					\n" /* The first item in pxCurrentTCB is the task top of stack. */
					"	ldmia r0!, {r4-r11}				\n" /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
					"	msr psp, r0						\n" /* Restore the task stack pointer. */
					"	isb								\n"
					"	mov r0, #0 						\n"
					"	msr	basepri, r0					\n"
					"	orr r14, #0xd					\n"
					"	bx r14							\n"
					"									\n"
					"	.align 4						\n"
					"pxCurrentTCBConst2: .word pxCurrentTCB				\n"
				);
}

代码分析:

ldr	r3, pxCurrentTCBConst2	
ldr r1, [r3]				
ldr r0, [r1]

此部分代码是将pxCurrentTCBConst2这个任务控制块指向的第一个成员的值赋值给r0。从下图任务控制块的结构体可以看到,第一个成员是栈顶指针。

在这里插入图片描述

ldmia r0!, {
   
   r4-r11}	
msr psp, r0

以r0 为基地址,将栈中向上增长的8个字的内容加载到CPU寄存
器r4~r11,同时r0 也会跟着自增。并将自增后的r0赋值给psp,如下图所示:
在这里插入图片描述

mov r0, #0
msr	basepri, r0	

basepri寄存器置0,打开所有中断

orr r14, #0xd
bx r14	

当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、

posted on 2022-10-27 16:21  duduru  阅读(0)  评论(0)    收藏  举报  来源

导航