4.FreeRTOS调度器的启动简易分析

FreeRTOS调度器的启动简易分析

  • 架构:Cortex-M3
  • 版本:FreeRTOS V9.0.0
  • 前言:上一篇我分析了关于一个任务的创建过程,既然创建了任务,自然是要用。那么FreeRTOS中对于任务的切换,调度器发挥着巨大的作用,这是一个核心。

1.从函数vTaskStartScheduler入手

便于分析我简化了代码:

void vTaskStartScheduler( void )
{
BaseType_t xReturn;

	/* Add the idle task at the lowest priority. */
	#if( configSUPPORT_STATIC_ALLOCATION == 1 )
	{
			....
            ....
            ...
	}
	#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();

		/* 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;
}
  1. 用动态创建任务的方式,创建了一个空闲任务
  2. 创建以及初始化定时器任务
  3. 最后调用了xPortStartScheduler

我简化了一些代码xPortStartScheduler

/*
 * See header file for description.
 */
BaseType_t xPortStartScheduler( void )
{
	#if( configASSERT_DEFINED == 1 )
	{
		...
        ...
        ...
	}
	#endif /* conifgASSERT_DEFINED */

	/* 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;
}

​ 首先是我在分析List的时候讲过关于SVC和PendSV优先级的赋值情况,实际上这两个优先级是最低的。然后是启动软件定时器vPortSetupTimerInterrupt(),设置临界区嵌套深度uxCriticalNesting为0,调用prvStartFirstTask()开启第一个任务。

函数如下:

__asm void prvStartFirstTask( void )
{
	PRESERVE8

	/* Use the NVIC offset register to locate the stack. */
	ldr r0, =0xE000ED08
	ldr r0, [r0]
	ldr r0, [r0]

	/* Set the msp back to the start of the stack. */
	msr msp, r0
	/* Globally enable interrupts. */
	cpsie i
	cpsie f
	dsb
	isb
	/* Call SVC to start the first task. */
	svc 0
	nop
	nop
}

​ 首先,从中断向量表偏移寄存器0xE000ED08中取出向量表的偏移地址,向量表的前四个字节就是主堆栈地址,把地址传给MSP,开启总中断,调用svc 0,触发ISR。当进入SVC异常服务例程时,CPU处于特权模式了。在特权模式下就允许操作只有特权模式下能操作的硬件。

​ 接下来看SVC异常服务例程:

__asm void vPortSVCHandler( void )
{
	PRESERVE8

	ldr	r3, =pxCurrentTCB	/* Restore the context. */
	ldr r1, [r3]			/* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
	ldr r0, [r1]			/* The first item in pxCurrentTCB is the task top of stack. */
	ldmia r0!, {r4-r11}		/* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
	msr psp, r0				/* Restore the task stack pointer. */
	isb
	mov r0, #0
	msr	basepri, r0
	orr r14, #0xd
	bx r14
}

​ pxCurrentTCB的前四个字节就是pxTopOfStack,我前面分析了很久的pxTopOfStack并没有浪费,因为我知道,这个指针指向的位置,就是存放任务现场的地址。取出pxTopOfStack后,按照Cortex-M3的出栈顺序,取出栈中各个寄存器的值。最开始,R0存放的是pxTopOfStack的值,执行手动出栈R4~R11的之后,R0的值比pxTopOfStack多0x20,然后把R0赋值到PSP,此时执行了bx r14之后,PSP会再次出栈xPSR、PC、LR、R12、R3~R0,PSP又会再增加0x20,也就是到了任务执行代码的时候,PSP就会比pxTopOfStack大0x40。如图:

最后调用:

orr r14, #0xd
bx r14

​ 根据Cortex-M3权威手册上的说明,R14的bit0为1表示返回thumb状态,bit1和bit2分别表示返回后sp用msp还是psp、以及返回到特权模式还是用户模式。R14存储的是返回的地址,这里的返回地址一定就是某个任务的函数指针。返回时,但并不是简简单单返回,当或上0xd时,就是要让CPU进入到线程模式、Thumb状态,当线程模式时,才会使用PSP。

​ 这么看来一切都通了:创建任务的时候,把任务当成正在运行的任务,然后把任务现场统统入栈到pxTopOfStack,在启动调度器时,又把pxTopOfStack保存的现场统统出栈,最后设置R14的低三位,使得CPU进入用户模式和跳到某个任务的执行代码中。

posted @ 2020-12-13 12:01  R1chie  阅读(585)  评论(0编辑  收藏  举报