1. 程式人生 > 實用技巧 >4.FreeRTOS排程器的啟動簡易分析

4.FreeRTOS排程器的啟動簡易分析

FreeRTOS排程器的啟動簡易分析

  • 前言:上一篇我分析了關於一個任務的建立過程,既然建立了任務,自然是要用。那麼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進入使用者模式和跳到某個任務的執行程式碼中。