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指令就能够跳转到用户任务函数?内部各个寄存器保存的的数值有什么作用?这些问题我会在下一篇文章中进行介绍。

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