FreeRTOS內核源碼解讀之-------系統啓動(二)

FreeRTOS系統啓動過程主要分爲三部分:彙編部分、main函數初始化部分、開啓任務調度部分。
對於彙編部分主要是設置一些中斷向量表、設置堆和棧等一些C語言運行需要的條件,當這些部分設置完成時候,就會跳轉到main函數運行。對於main函數初始化部分,主要是做一些必要的硬件外設初始化、板級初始化、還有就是任務的創建。任務創建完成之後,就會開啓調度器,FreeRTOS開始運行。
下面就講一下FreeRTOS是怎麼開始運行的:
由於之前講過一篇關於apollo2 MCU的彙編啓動,關於Cortex-M4的彙編啓動部分基本一致。大家可以看之前博客。直接從main函數開始,上代碼:

int main(void)
{ 

	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//設置系統中斷優先級分組4
	delay_init(168);					//初始化延時函數
	uart_init(115200);     				//初始化串口
	LED_Init();		        			//初始化LED端口
	KEY_Init();							//初始化按鍵
	LCD_Init();							//初始化LCD
	my_mem_init(SRAMIN);            	//初始化內部內存池
	
	POINT_COLOR = RED;
	LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");	
	LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 20-1");
	LCD_ShowString(30,50,200,16,16,"Mem Manage");
	LCD_ShowString(30,70,200,16,16,"KEY_UP:Malloc,KEY1:Free");
	LCD_ShowString(30,90,200,16,16,"KEY0:Use Mem");
	LCD_ShowString(30,110,200,16,16,"ATOM@ALIENTEK");
	LCD_ShowString(30,130,200,16,16,"2016/11/14");
	
	LCD_ShowString(30,170,200,16,16,"Total Mem:      Bytes");
	LCD_ShowString(30,190,200,16,16,"Free  Mem:      Bytes");
	LCD_ShowString(30,210,200,16,16,"Message:    ");
	POINT_COLOR = BLUE;
	
	//創建開始任務
    xTaskCreate((TaskFunction_t )start_task,            //任務函數
                (const char*    )"start_task",          //任務名稱
                (uint16_t       )START_STK_SIZE,        //任務堆棧大小
                (void*          )NULL,                  //傳遞給任務函數的參數
                (UBaseType_t    )START_TASK_PRIO,       //任務優先級
                (TaskHandle_t*  )&StartTask_Handler);   //任務句柄              
    vTaskStartScheduler();          //開啓任務調度
}

main函數第一部分會進行一些硬件初始化(串口、定時器、LED、LCD顯示器),之後會調用任務創建函數xTaskCreate,此函數會創建一個start任務。關於start_task函數代碼如下:

//開始任務任務函數
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();           //進入臨界區
    //創建TASK1任務
    xTaskCreate((TaskFunction_t )malloc_task,             
                (const char*    )"malloc_task",           
                (uint16_t       )MALLOC_STK_SIZE,        
                (void*          )NULL,                  
                (UBaseType_t    )MALLOC_TASK_PRIO,        
                (TaskHandle_t*  )&MallocTask_Handler);   
    vTaskDelete(StartTask_Handler); //刪除開始任務
    taskEXIT_CRITICAL();            //退出臨界區
}

關於start任務會進行一系列的任務創建,當這些任務創建完成之後,start會將自己刪除。從上面代碼可以看出,在start任務中會創建一個malloc任務。具體的malloc任務的代碼如下:

//MALLOC任務函數 
void malloc_task(void *pvParameters)
{
	u8 *buffer;		
	u8 times,i,key=0;
	u32 freemem;

	LCD_ShowxNum(110,170,configTOTAL_HEAP_SIZE,5,16,0);//顯示內存總容量	
    while(1)
    {
		key=KEY_Scan(0);
		switch(key)
		{
			case WKUP_PRES:				
				buffer=pvPortMalloc(30);			//申請內存,30個字節
				printf("申請到的內存地址爲:%#x\r\n",(int)buffer);
				break;
			case KEY1_PRES:				
				if(buffer!=NULL)vPortFree(buffer);	//釋放內存
				buffer=NULL;
				break;
			case KEY0_PRES:
				if(buffer!=NULL)					//buffer可用,使用buffer
				{
					times++;
					sprintf((char*)buffer,"User %d Times",times);//向buffer中填寫一些數據
					LCD_ShowString(94,210,200,16,16,buffer);
				}
				break;
		}
		freemem=xPortGetFreeHeapSize();		//獲取剩餘內存大小
		LCD_ShowxNum(110,190,freemem,5,16,0);//顯示內存總容量	
		i++;
		if(i==50)
		{
			i=0;
			LED0=~LED0;
		}
        vTaskDelay(10);
    }
}

回到main函數,當創建完成任務之後,FreeRTOS會調用vTaskStartScheduler(); 開啓任務調度器。
整體步驟:main函數硬件初始化->start任務創建->start創建各種任務->start任務自殺->開啓任務調度器。
下面就開始講解調度器的實現
函數void vTaskStartScheduler( void )代碼如下:

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////                                                                                                                              //////
//////                                                  調度器                                                                      //////
//////                                                                                                                              //////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
	#if( configSUPPORT_STATIC_ALLOCATION == 1 )//若是靜態方法創建任務,由於採用動態方法,不會執行
	{
		StaticTask_t *pxIdleTaskTCBBuffer = NULL;
		StackType_t *pxIdleTaskStackBuffer = NULL;
		uint32_t ulIdleTaskStackSize;
		vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
		xIdleTaskHandle = xTaskCreateStatic(	prvIdleTask,
												"IDLE",
												ulIdleTaskStackSize,
												( void * ) NULL,
												( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
												pxIdleTaskStackBuffer,
												pxIdleTaskTCBBuffer ); 
		if( xIdleTaskHandle != NULL )
		{
			xReturn = pdPASS;
		}
		else
		{
			xReturn = pdFAIL;
		}
	}
	#else
	{//動態方法創建任務
		xReturn = xTaskCreate(	prvIdleTask,//創建空閒任務,空閒任務是必須要有的,當沒有任務需要運行時,給FreeRTOS找點事幹
								"IDLE", configMINIMAL_STACK_SIZE,//空閒任務的堆棧大小
								( void * ) NULL,//空閒任務的參數
								( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),//設置空閒任務的優先級,爲系統的最低優先級
								&xIdleTaskHandle ); //空閒任務的任務句柄
	}
	#endif
	#if ( configUSE_TIMERS == 1 )//啓用軟件定時器
	{
		if( xReturn == pdPASS )
		{//空閒任務創建成功
			xReturn = xTimerCreateTimerTask();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
	#endif 
	if( xReturn == pdPASS )
	{
		portDISABLE_INTERRUPTS();//屏蔽中斷,屏蔽掉FreeRTOS能夠管理的所有中斷
		#if ( configUSE_NEWLIB_REENTRANT == 1 )//沒有使用嵌入式C語言專用庫
		{
			_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
		}
		#endif 
		xNextTaskUnblockTime = portMAX_DELAY;//下一個需要解除阻塞的任務的時間
		xSchedulerRunning = pdTRUE;//標識任務調度器開始工作
		xTickCount = ( TickType_t ) 0U;//用於記錄系統運行時間,記錄的是運行的節拍數
		portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();//關於代碼運行時間和任務狀態收集相關功能函數。此時必須藉助MCU的系統定時器之外的一個定時器
		if( xPortStartScheduler() != pdFALSE )
		{
			
		}
		else
		{
			
		}
	}
	else
	{
		configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
	}
	( void ) xIdleTaskHandle;
}

該函數主要是首先創建空閒任務,在FreeRTOS中空閒任務是必須要有的。之後根據配置決定是否開啓軟件定時器。再之後屏蔽FreeRTOS的能夠管理的中斷,設置相應的全局變量。這裏要注意一點,空閒任務是FreeRTOS必須要創建的,而且任務的優先級是最低的。全局變量xNextTaskUnblockTime表示下一個解除阻塞的任務;全局變量xSchedulerRunning標識任務調度器開啓;xTickCount表示記錄FreeRTOS運行時間。該函數最終會調用xPortStartScheduler()函數,具體代碼如下:

BaseType_t xPortStartScheduler( void )
{
	configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );
	configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
	configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );

	#if( configASSERT_DEFINED == 1 )
	{
		volatile uint32_t ulOriginalPriority;
		volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );//0xE000E400是外部中斷0#優先級設置寄存器
		ulOriginalPriority = *pucFirstUserPriorityRegister;//保存外部中斷0#原始優先級
		*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
		ucMaxPriorityValue = *pucFirstUserPriorityRegister;//得到MCU支持的可編程優先級數量
		configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );
		ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
		ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
		while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )//ucMaxSysCallPriority此時存儲的系統可以支持的最高優先級,如果我們設置最高優先級爲5,那麼此處變量ucMaxSysCallPriority數值爲0b01010000
		{
			ulMaxPRIGROUPValue--;
			ucMaxPriorityValue <<= ( uint8_t ) 0x01;
		}
		ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
		ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
		*pucFirstUserPriorityRegister = ulOriginalPriority;
	}
	#endif
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;//設置PENDSV中斷優先級,一般會設置MCU所支持的最低優先級
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;//設置系統滴答時鐘中斷優先級,一般設置MCU所支持的最低優先級
	vPortSetupTimerInterrupt();//開啓滴答定時器
	uxCriticalNesting = 0;//用於臨界區保護嵌套作用
	prvEnableVFP();//使能FPU
	*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;//設置浮點上下文控制寄存器
	prvStartFirstTask();//啓動第一個任務
	return 0;
}

關於函數xPortStartScheduler主要是這是pendsv和滴答定時器的中斷優先級,一般會設置爲整個系統所支持的最低中斷優先級。開啓滴答定時器,滴答定時器的作用是產生系統節拍,進行任務切換。最後調用prvStartFirstTask()函數啓動第一個任務。
函數vPortSetupTimerInterrupt( void )代碼如下:

void vPortSetupTimerInterrupt( void )
{
	#if configUSE_TICKLESS_IDLE == 1
	{//使能低功耗模式
		ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );//滴答定時器時間
		xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;//
		ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
	}
	#endif 
	portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;//portNVIC_SYSTICK_LOAD_REG是Systick重裝載寄存器,設置系統節拍
	portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );//portNVIC_SYSTICK_CTRL_REG是Systick控制和狀態寄存器,使用外部時鐘,定時器減到0產生中斷,定時器使能
}

上述函數主要是設置滴答定時器的裝載寄存器數值,配置滴答定時器的時鐘來源,定時器開啓等。
函數prvStartFirstTask( void )如下:

__asm void prvStartFirstTask( void )
{
	PRESERVE8
	ldr r0, =0xE000ED08  //向量表偏移寄存器地址
	ldr r0, [r0]      //取向量表地址
	ldr r0, [r0]      //取MSP初始值
	msr msp, r0     //重置MSP指針
	cpsie i     //開中斷
	cpsie f      //開異常
	dsb         //數據同步隔離
	isb         //指令同步隔離
	svc 0       //異常觸發,啓動第一個任務,SVC異常
	nop
	nop
}

該函數最主要的就是產生一個SVC中斷,以此啓動第一個任務。
函數vPortSVCHandler( void )代碼如下:

__asm void vPortSVCHandler( void )
{
	PRESERVE8
	ldr	r3, =pxCurrentTCB
	ldr r1, [r3]
	ldr r0, [r1]
	ldmia r0!, {r4-r11, r14}
	msr psp, r0
	isb
	mov r0, #0
	msr	basepri, r0
	bx r14
}

關於上面函數重點說明以下幾點:
1)pxCurrentTCB存儲的是當前任務的控制塊,任務控制塊中的第一個成員變量就是任務堆棧的指針。
2)ldmia r0!, {r4-r11, r14},通過上面代碼的處理,R0寄存器保存着當前任務的棧頂,r0!表示自增的意思,因此這步操作的內容是將棧頂的數據手動加載到寄存器r4~r11,以及r14寄存器中;msr psp, r0將PSP寄存器重新指向當前任務的新的棧頂。
3)msr basepri, r0,basepri寄存器是屏蔽優先級寄存器,當我們向該寄存器寫入某一個數值n之後,就會屏蔽優先級數值高於n的所有中斷和異常,這裏要注意數值越大優先級越低。當我們寫入0時,表示不屏蔽任何中斷和異常。
4)bx r14,跳轉到用戶寫的任務函數中運行,這時候FreeRTOS開始真正運行。
關於FreeRTOS的啓動過程大致總結如下:
首先就是創建一個start任務,在start任務中會創建一堆任務;當這些任務創建完成之後,start任務會進行自殺;
接着會啓動任務調度器,在任務調度器中會進行滴答定時器設置、開啓滴答定時器、產生SVC中斷、跳轉到用戶編寫的任務函數。
可能大家會有很多疑問,比如在《FreeRTOS內核源碼解讀之-------系統啓動(一)》這篇文章中提到的MSP和PSP之間是怎麼進行切換的?爲什麼通過bx r14指令就能夠跳轉到用戶任務函數?內部各個寄存器保存的的數值有什麼作用?這些問題我會在下一篇文章中進行介紹。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章