一文带你详细了解FreeRTOS调度器开启过程

上一篇文章是记录我学习FreeRTOS列表和列表项的相关知识。但是在了解完列表和列表项之后,其实还有一个十分关键的问题摆在我们面前——一个操作系统最核心的内容就是 多任务管理。所以关于FreeRTOS的任务调度器的开启过程,同样值得我们去深入了解。

目录

​编辑 一、任务调度器开启的相关函数

二、内核相关硬件初始化函数分析 

三、使能FPU的相关函数分析

四、启动第一个任务 

五、SVC中断服务函数 

六、空闲任务 



 一、任务调度器开启的相关函数

之前我们都是在main()函数中先创建一个开始任务start_task,后面紧接着调用函数vTaskStartScheduler()。这个函数的功能就是开启任务调度器的,这个函数在文件tasks.c中有定义,该函数代码如下:

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

接下来我们来对该函数进行分析一下。 

创建空闲任务,如果使用静态内存的话使用函数 xTaskCreateStatic()来创建空闲任务,优先级为tskIDLE_PRIORITY,宏tskIDLE_PRIORITY为0,也就是说空闲任务的优先级为最低。

如果使用软件定时器的话还需要通过函数xTimerCreateTimerTask()来创建定时器服务任务。定时器服务任务的具体创建过程是在函数xTimerCreateTimerTask()中完成的。

关闭中断,在SVC中断服务函数vPortSVCHandler()中会打开中断。

变量xSchedulerRunning设置为pdTRUE,表示调度器开始运行。

当宏configGENERATE_RUN_TIME_STATS为1的时候说明使能时间统计功能,此时需要用户实现宏portCONFIGURE_TIMER_FOR_RUN_TIME_STATS,此宏用来配置一个定时器/计数器。

调用函数xPortStartScheduler()来初始化跟调度器启动有关的硬件,比如滴答定时器、FPU单元和 PendSV中断等等。

二、内核相关硬件初始化函数分析 

FreeRTOS系统时钟是由滴答定时器来提供的,而且任务切换也会用到PendSV中断,这些硬件的初始化由函数xPortStartScheduler()来完成,缩减后的函数代码如下。

(1)、设置PendSV的中断优先级,为最低优先级。

(2)、设置滴答定时器的中断优先级,为最低优先级。
(3)、调用函数vPortSetupTimerInterrupt()来设置滴答定时器的定时周期,并且使能滴答定时器的中断,函数比较简单,大家自行查阅分析。
(4)、初始化临界区嵌套计数器。
(5)、调用函数prvEnableVFP()使能FPU。
(6)、设置寄存器FPCCR 的bit31和 bit30都为1,这样SO~S15和FPSCR寄存器在异常入口和退出时的状态自动保存和恢复。并且异常流程使用惰性压栈的特性以保证中断等待。关于FPCCR寄存器和惰性压栈的知识大家可以结合浮点运算的相关知识进行了解。

三、使能FPU的相关函数分析

在函数xPortStartScheduler()中会通过调用prvEnableVFP()来使能FPU,这个函数是汇编形式的,在文件 port.c中有定义,函数如下:

(1)、利用寄存器CPACR可以使能或禁止FPU,此寄存器的地址为0XEO00ED88,此寄存器的CP10(bit20和 bit21)和 CP11(bit22和 bit23)用于控制FPU。通常将这4个bit都设置为1来开启FPU,表示全访问。此行代码将地址0XEO00ED88保存在寄存器R0中。
(2)、读取R0中保存的存储地址处的数据,也就是CPACR寄存器的值,并将结果保存在R1寄存器中。
(3)、R1中的值与(0xf<<20)进行按位或运算,也就是Rl=R1|0X00F00000。此时R1所保存的值的bit20~bit23就都为1了,将这个值写入寄存器CPACR中就可开启FPU。
(4)、将R1中的值写入R0中保存的地址处,也就是寄存器CPACR中。
(5)、函数返回。bx为间接跳转指令,一般为BX <Rm>,也就是跳转到存放在Rm 中的地
址处,此处是跳转到R14存放的地址处。R14寄存器页叫做链接寄存器(LR),也可以用LR表示。这个寄存器用于函数或子程序调用时返回地址的保存。

四、启动第一个任务 

经过上面的操作以后我们就可以启动第一个任务了,函数 prvStartFirstTask()用于启动第一个任务,这是一个汇编函数,函数源码如下:

(1)将0XE000ED08保存在寄存器R0中。一般来说向量表应该是从起始地址(0X00000000)开始存储的,不过,有些应用可能需要在运行时修改或重定义向量表,Cortex-M处理器为此提供了一个叫做向量表重定位的特性。向量表重定位特性提供了一个名为向量表偏移寄存器(VTOR)的可编程寄存器。VTOR 寄存器的地址就是0XE000EDO8,通过这个寄存器可以重新定义向量表,比如在STM32F767的ST官方库中会通过函数SystemInit()来设置VTOR寄存器,代码如下:
SCB->VTOR=FLASH_BASE| VECT_TAB_OFFSET; //VTOR=0x08000000+0X00
通过上面一行代码就将向量表开始地址重新定义到了0X08000000,向量表的起始地址存储的就是 MSP初始值。

(2)、读取R0中存储的地址处的数据开将具保仔在K0寄存器,也就读取寄存器VTOR中的值,并将其保存在R0寄存器中。这一行代码执行完就以后R0的值应该为0X0800000。
(3)、
读取RO中存储的地址处的数据并将其保存在RO寄存器,也就是读取地址0X08000000
处存储的数据,并将其保存在RO寄存器中。
我们知道向量表的起始地址保存的就是主栈指针MSP的初始值,这一行代码执行完以后寄存器RO就存储MSP的初始值。现在来看(1)、(2)、
(3)这三步其实就是
为了获取MSP的初始值而已!
(4)、
复位MSP,R0中保存了MSP的初始值,将其赋值给MSP就相当于复位MSP。

(5)和(6)、使能中断

(7)和(8)、数据同步和指令同步屏障。
(9),调用SVC指令触发SVC中断,SVC也叫做请求管理调用,SVC和 PendSV异常对于OS的设计来说非常重要。SVC异常由SVC指令触发。在FreeRTOS中仅仅使用SVC异常来启动第一个任务,后面的程序中就再也用不到SVC了。

补充:

在 FreeRTOS 中,SVC 异常指的是 Supervisor Call(SVC)指令引发的异常。这是一种在 ARM Cortex-M 处理器上常见的异常类型。

SVC 指令用于向处理器请求特权操作,例如执行操作系统的系统调用或切换任务。当用户程序执行 SVC 指令时,处理器会触发 SVC 异常,并将控制权转移到异常处理程序。

在 FreeRTOS 中,SVC 异常通常用于实现任务切换和系统服务调用。当一个任务需要主动让出 CPU 的控制权,或者需要请求系统服务时,它可以通过执行 SVC 指令来触发 SVC 异常,并由 FreeRTOS 的 SVC 异常处理程序进行相应的处理。

通过 SVC 异常,FreeRTOS 能够实现任务调度、任务管理和系统服务等功能,以提供多任务操作系统的支持。

五、SVC中断服务函数 

在函数 prvStartFirstTask()中通过调用SVC 指令触发了SVC中断,而第一个任务的启动就是在SVC中断服务函数中完成的,SVC中断服务函数应该为SVC_Handler(),但是FreeRTOSConfig.h中通过#define的方式重新定义为了xPortPendSVHandler(),如下:

函数 vPortSVCHandler()在文件 port.c中定义,这个函数也是用汇编写的,函数源码如下:

(1)、获取pxCurrentTCB指针的存储地址pxCurrentTCB是一个指向TCB_t的指针,这个指针永远指向正在运行的任务。这里先获取这个指针存储的地址,比如我现在的代码测试出来这个指针是存放在0X20020034,如图所示。

(2)、取R3所保存的地址处的值赋给R1。通过这一步就获取到了当前任务的任务控制块的存储地址。比如当前我的程序中这个地址就为0X20021040,如图所示:

(3)、取R3所保存的地址处的值赋给R0,我们知道任务控制块的第一个字段就是任务堆栈的栈顶指针 pxTopOfStack 所指向的位置,所以读取任务控制块所在的首地址(0X20021040)得到的就是栈顶指针所指向的地址,当前我的程序中这个栈顶指针(pxTopOfStack)所指向的地址为0X20020FEC,如图所示:

可以看出(1)、(2)和(3)的目的就是获取要切换到的这个任务的任务栈顶指针,因为任务所对应的寄存器值,也就是现场都保存在任务的任务堆栈中,所以需要获取栈顶指针来恢复这些寄存器值!

(4)、R4-R11,R14这些寄存器出栈。这里使用了指令LDMIA,LDMIA指令是多加载/存储指令,不过这里使用的是具有回写的多加载/存储访问指令,用法如下:
LDMIA Rn! , {reg list}
表示从Rn指定的存储器位置读取多个字,地址在每次读取后增加(IA),Rn在传输完成以后写回。对于STM32来说地址一次增加4字节,比如如下代码:
LDR RO,=OX800
LDMIA RO!,{R2~R4}
上面两行代码就是将0X800地址的数据赋值给寄存器R2,0X804地址的数据赋值给寄存器R3,0X8008地址的数据赋值给R4寄存器,然后,重点来了!此时R0为800A!

通过这一步我们就从任务堆栈中将R4~R11,R14这几个寄存器的值给恢复了,注意R14的值为0XFFFFFFFD,这个值就是我们在初始化任务堆栈的时候保存的EXC_RETURN 的值!如图所示:

这里有朋友就要问了,RO~R3,R12,PC,xPSR这些寄存器怎么没有恢复?

这是因为这些寄存器会在退出中断的时候MCU自动出栈(恢复)的,而R4~R11需要由用户手动出栈。如果使用FPU的话还要考虑到FPU寄存器,到这步以后我们来看一下堆栈的栈顶指针指到哪里了?如图所示:


从图中可以看出恢复R4~R11和R14以后堆栈的栈顶指针应该指向地址0X20021010,也就是保存寄存器R0值的存储地址。退出中断服务函数以后进程栈指针PSP应该从这个地址开始恢复其他的寄存器值。
(5)、设置进程栈指针PSP,PSP=R0=0X20021010,如图所示:

(6)、设置寄存器RO为0。
(7)、设置寄存器BASEPRI为RO,也就是0,打开中断!

(8)、执行此行代码以后硬件自动恢复寄存器R0~R3、R12、LR、PC和xPSR的值,堆栈使用进程栈PSP,然后执行寄存器PC中保存的任务函数。至此,FreeRTOS的任务调度器正式开始运行!

六、空闲任务 

在前面讲解函数vTaskStartScheduler()时说过,此函数会创建一个名为“IDLE”的任务,这个任务叫做空闲任务。

顾名思义,空闲任务就是空闲的时候运行的任务,也就是系统中其他的任务由于各种原因不能运行的时候空闲任务就在运行。空闲任务是FreeRTOS系统自动创建的,不需要用户手动创建。任务调度器启动以后就必须有一个任务运行!

但是空闲任务不仅仅是为了满足任务调度器启动以后至少有一个任务运行而创建的,空闲任务中还会去做一些其他的事情,如下:
1、判断系统是否有任务删除,如果有的话就在空闲任务中释放被删除任务的任务堆栈和任务控制块的内存。
2、运行用户设置的空闲任务钩子函数。
3、判断是否开启低功耗tickless模式,如果开启的话还需要做相应的处理空闲任务的任务优先级是最低的,为0,任务函数为prvIdleTask()。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小_扫地僧

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值