FreeRTOS启动任务调度器源码分析

在RTOS系统中,启动了任务调度器以后,各个任务的执行由系统统一管理。各种RTOS都有开启任务调度器的函数。
在实际应用中,我们创建完任务以后就可以开启任务调度器了。FreeRTOS中使用vTaskStartScheduler()函数开启任务调度器。该函数是FreeRTOS内核的重点。我们接下来分析一下FreeRTOS启动任务调度器的函数,探究一下调度器是如何启动的。
vTaskStartScheduler()函数如下:

void vTaskStartScheduler( void )
{
BaseType_t xReturn;

	/* Add the idle task at the lowest priority. */
	#if( configSUPPORT_STATIC_ALLOCATION == 1 )//使用静态内存方式
	{
		StaticTask_t *pxIdleTaskTCBBuffer = NULL;
		StackType_t *pxIdleTaskStackBuffer = NULL;
		uint32_t ulIdleTaskStackSize;

		/* The Idle task is created using user provided RAM - obtain the
		address of the RAM then create the idle task. */
		vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
		xIdleTaskHandle = xTaskCreateStatic(	prvIdleTask,
												"IDLE",
												ulIdleTaskStackSize,
												( void * ) NULL,
												( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
												pxIdleTaskStackBuffer,
												pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */

		if( xIdleTaskHandle != NULL )
		{
			xReturn = pdPASS;
		}
		else
		{
			xReturn = pdFAIL;
		}
	}
	#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、创建空闲任务,空闲任务的优先级必须设置为最低优先级,在FreeRTOS中空闲任务是必不可少的,空闲任务的作用是系统中没有用户任务需要处理的时候让CPU去指针空闲任务函数,让CPU有事可做,空闲任务还有其他作用(比如回收内存、让CPU进入睡眠模式等等),我们后续讨论。
2.创建定时服务任务,定时服务任务是一个可选项,当宏configUSE_TIMERS为1的时候开启定时服务任务。定时服务任务在需要软件定时器的时候开启。为了尽量的保证软件定时器的定时精度,FreeRTOS官方强烈建议将定时服务任务的优先级设置为最高优先级。
3.关闭中断,初始化一些全局变量。

xNextTaskUnblockTime = portMAX_DELAY;//下一个任务解锁时间
xSchedulerRunning = pdTRUE;			//将任务调度器开启标志设置为pdTRUE
xTickCount = ( TickType_t ) 0U;		//系统时钟节拍设置为0

4、调用xPortStartScheduler()函数初始化处理器,该函数在port.c文件中,是与处理器密切相关的函数。

BaseType_t xPortStartScheduler( void )
{
	#if( configASSERT_DEFINED == 1 )//与断言相关的宏
	{
		volatile uint32_t ulOriginalPriority;
		volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
		volatile uint8_t ucMaxPriorityValue;

		/* Determine the maximum priority from which ISR safe FreeRTOS API
		functions can be called.  ISR safe functions are those that end in
		"FromISR".  FreeRTOS maintains separate thread and ISR API functions to
		ensure interrupt entry is as fast and simple as possible.

		Save the interrupt priority value that is about to be clobbered. */
		ulOriginalPriority = *pucFirstUserPriorityRegister;

		/* Determine the number of priority bits available.  First write to all
		possible bits. */
		*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;

		/* Read the value back to see how many bits stuck. */
		ucMaxPriorityValue = *pucFirstUserPriorityRegister;

		/* Use the same mask on the maximum system call priority. */
		ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

		/* Calculate the maximum acceptable priority group value for the number
		of bits read back. */
		ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
		while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
		{
			ulMaxPRIGROUPValue--;
			ucMaxPriorityValue <<= ( uint8_t ) 0x01;
		}

		/* Shift the priority group value back to its position within the AIRCR
		register. */
		ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
		ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

		/* Restore the clobbered interrupt priority register to its original
		value. */
		*pucFirstUserPriorityRegister = ulOriginalPriority;
	}
	#endif /* conifgASSERT_DEFINED */

	/* Make PendSV and SysTick the lowest priority interrupts. */
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;//设置PENDSV的中断优先级
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;//设置滴答定时器的中断优先级

	/* Start the timer that generates the tick ISR.  Interrupts are disabled
	here already. */
	vPortSetupTimerInterrupt();//配置SYSTICK的CTRL和LOAD寄存器

	/* Initialise the critical nesting count ready for the first task. */
	uxCriticalNesting = 0;//临界区嵌套次数变量清0

	/* Start the first task. */
	prvStartFirstTask();//开始运行第一个任务

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

(1)设置PendSV和滴答定时器的中断优先级,此处将PendSV中断和滴答定时器中断优先级设置为最低优先级,其目的是让CPU优先处理用户的中断服务函数,而不是优先处理任务切换。
优先级是如何设置的呢?我们看一下PendSV中断优先级是如何设置的,

portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;//设置PENDSV的中断优先级

portNVIC_SYSPRI2_REG 是宏定义

#define portNVIC_SYSPRI2_REG				( * ( ( volatile uint32_t * ) 0xe000ed20 ) )

该宏定义的含义是:0xe000ed20先强制转换为一个uint32_t类型的指针,* ( ( volatile uint32_t * ) 0xe000ed20 )就是0xe000ed20 地址处的值了,* ( ( volatile uint32_t * ) 0xe000ed20 ) | = portNVIC_PENDSV_PRI 相当于将portNVIC_PENDSV_PRI 写入到了0xe000ed20 地址处(不知道我这里是否表达明白了)。这是C语言指针的知识了。
在这里插入图片描述上图是Cortex-M3权威指南中截取出来的,我们可以看到保存PendSV中断的优先级地址是0xE000ED22,而我们操作的却是0xE000ED20。为什么呢?Cortex-M3权威指南有这样一段话:每个外部中断都有一个对应的优先级寄存器,每个寄存器占用 8 位,但是允许最少只使
用最高 3 位。 4 个相临的优先级寄存器拼成一个 32 位寄存器。

#define portNVIC_PENDSV_PRI					( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL )

操作的是0xE000ED20的地址(uint32_t *类型指针),而configKERNEL_INTERRUPT_PRIORITY 左移16位赋值到0xE000ED20地址处,这样恰好将configKERNEL_INTERRUPT_PRIORITY 写入到了0xE000ED22地址处(PendSV中断优先级地址处)。
滴答定时器中断优先级同理,此处不再赘述。
(2)vPortSetupTimerInterrupt()函数配置滴答定时器

	void vPortSetupTimerInterrupt( void )
	{
		/* Calculate the constants required to configure the tick interrupt. */
		#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 /* configUSE_TICKLESS_IDLE */

		/* Configure SysTick to interrupt at the requested rate. */
		//配置SYSTICK的LOAD寄存器
		portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
		//配置SYSTICK的CTRL寄存器
		portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
	}

如何将重载寄存器的值赋值到滴答定时器重载寄存器以及如何配置滴答定时器,这里就不赘述了。参考PendSV中断优先级寄存器理解。此处我们来分析一下如何根据CPU的频率和FreeRTOS时钟节拍频率计算出滴答定时器的重载值。
下面我们来推导一下(默认滴答定时器时钟与CPU时钟一致),
configTICK_RATE_HZ :FreeRTOS时钟节拍频率。那么中断的周期就是(1/configTICK_RATE_HZ )s
configSYSTICK_CLOCK_HZ :CPU运行频率。由于滴答定时器的时钟频率等于CPU的时钟频率,滴答定时器计一个数是(1/configSYSTICK_CLOCK_HZ)s
中断周期除以计一个数的周期得出了需要计多少个数。将该数据写如滴答定时器重载寄存器即可。
((1/configTICK_RATE_HZ )s)/((1/configSYSTICK_CLOCK_HZ)s) = configSYSTICK_CLOCK_HZ/configTICK_RATE_HZ

portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );是滴答定时器的配置,翻一下M3权威指南就可以了。
3、uxCriticalNesting = 0;//临界区嵌套次数变量清0
4、调用函数prvStartFirstTask();该函数名字是启动第一个任务,但是第一个任务并不是在这个函数启动的,FreeRTOS的第一个任务是在SVC中断服务函数中启动的,该函数触发了SVC中断。

__asm void prvStartFirstTask( void )
{
	PRESERVE8
	ldr r0, =0xE000ED08		//将0xE000ED08保存到R0寄存器(0xE000ED08是VTOR寄存器的地址,将VTOR寄存器的地址保存到R0寄存器)
	ldr r0, [r0]		//将R0地址处的值赋值给R0(将VTOR寄存器的值赋值给R0,也就是将向量表的起始地址赋值给R0),如果是STM32结果是0x08000000  
	ldr r0, [r0]		//将R0地址处的值赋值给R0(将向量表起始地址处的值赋值给R0),向量表起始地址处存放的是MSP,结果是R0寄存器保存的是MSP的初始值

	//MSP的初始值赋值给MSP
	/* Set the msp back to the start of the stack. */
	msr msp, r0//将R0寄存器的值赋值给MSP寄存器(将MSP的值保存到MSP寄存器)
	/* Globally enable interrupts. */
	//开启全局中断
	cpsie i			//清除RPIMASK的值,使能全局中断	
	cpsie f			//清除FAULTMASK的值,使能全局中断
	dsb					//数据同步屏障
	isb					//指令同步屏障
	/* Call SVC to start the first task. */
	svc 0				//触发SVC中断  CPU接下来就响应SVC中断
	nop
	nop
}

注释很详细了,此处不再另做解释了。可以看到最后是触发了SVC中断,而一个任务是在SVC中断服务函数启动的,我们就来看看SVC中断服务函数。

__asm void vPortSVCHandler( void )
{
	PRESERVE8

	ldr	r3, =pxCurrentTCB	//pxCurrentTCB自身的地址存储到R3寄存器中   R3 = &pxCurrentTCB 
	//把R3寄存器的值赋值到R1寄存器中
	ldr r1, [r3]			//把R3寄存器中地址所指向的值赋值到R1(也就是pxCurrentTCB指向的地址赋值给R1,即将当前任务控制块的地址赋值给R1)
	ldr r0, [r1]			//把R1寄存器中地址所指向的值赋值到R0(也就是将任务任务控制块中第一个成员变量的值赋值R0,也就是将栈顶指针赋值给R0)此时R0中保存了栈顶指针所指向的地址
	//至此已经获取到了栈顶指针 R0中保存了任务的栈顶指针
	
	//从任务堆栈中把对应寄存器的值(任务现场)恢复到内核寄存器中,其它内核寄存器是自动恢复的
	ldmia r0!, {r4-r11}		//出栈,将堆栈中的值恢复到CPU寄存器中(现场恢复),创建任务时,任务的堆栈中已经初始化了任务的现场,此处把CPU堆栈中的值把还原到CPU寄存器中
												//至此 需用手动恢复的寄存器已经恢复完毕,R0中保存了栈顶指针,此时栈顶指针指向的是自动恢复的寄存器(并不是真正的指向寄存器,而是指向寄存器对应的在堆栈中的地址)
	msr psp, r0				//将R0寄存器的值赋值给PSP,初始化PSP,运行任务的时候要使用PSP 
	isb								//指令同步屏障
	//开启中断 操作BASEPRI寄存器
	mov r0, #0			//将R0寄存器赋值为0
	msr	basepri, r0  //basepri寄存器中写入R0寄存器的值,即basepri寄存器中写入0. basepri寄存器写0开启中断  
	orr r14, #0xd		//执行此指令以后自动出栈的寄存器就会从堆栈中自动恢复,到此所有的内核寄存器都已恢复完毕。
	bx r14		//跳转到第一个任务 进入第一个任务运行。这样第一个任务就启动了。
}

启动任务调度器中涉及到许多CPU内核的知识和ARM汇编指令。这些我都是查找网络资料学习的,对这些知识不是很了解。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值