FreeRTOS高级篇4---FreeRTOS任务切换分析

FreeRTOS任务相关的代码大约占总代码的一半左右,这些代码都在为一件事情而努力,即找到优先级最高的就绪任务,并使之获得CPU运行权。任务切换是这一过程的直接实施者,为了更快的找到优先级最高的就绪任务,任务切换的代码通常都是精心设计的,甚至会用到汇编指令或者与硬件相关的特性,比如Cortex-M3的CLZ指令。因此任务切换的大部分代码是由硬件移植层提供的,不同的平台,实现发方法也可能不同,这篇文章以Cortex-M3为例,讲述FreeRTOS任务切换的过程。
FreeRTOS有两种方法触发任务切换:

  • 执行系统调用,比如普通任务可以使用taskYIELD()强制任务切换,中断服务程序中使用portYIELD_FROM_ISR()强制任务切换;
  • 系统节拍时钟中断

对于Cortex-M3平台,这两种方法的实质是一样的,都会使能一个PendSV中断,在PendSV中断服务程序中,找到最高优先级的就绪任务,然后让这个任务获得CPU运行权,从而完成任务切换。
      对于第一种任务切换方法,不管是使用taskYIELD()还是portYIELD_FROM_ISR(),最终都会执行宏portYIELD(),这个宏的定义如下:

#define portYIELD()						{									/*产生PendSV中断*/		                        	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;		}

对于第二种任务切换方法,在系统节拍时钟中断服务函数中,首先会更新tick计数器的值、查看是否有任务解除阻塞,如果有任务解除阻塞的话,则使能PandSV中断,代码如下所示:

void xPortSysTickHandler( void )
{
	/* 设置中断掩码 */
	vPortRaiseBASEPRI();
	{
		/* 增加tick计数器值,并检查是否有任务解除阻塞 */
		if( xTaskIncrementTick() != pdFALSE )
		{
			/* 需要任务切换。产生PendSV中断 */
			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
		}
	}
	vPortClearBASEPRIFromISR();
}

从上面的代码中可以看出,PendSV中断的产生是通过代码:portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT实现的,它向中断状态寄存器bit28位写入1,将PendSV中断设置为挂起状态,等到优先级高于PendSV的中断执行完成后,PendSV中断服务程序将被执行,进行任务切换工作。
      Cortex-M3架构下,PendSV中断服务程序源码如下所示,这篇文章重点分析这段代码。

__asm void xPortPendSVHandler( void )
{
	extern uxCriticalNesting;
	extern pxCurrentTCB;            /* 指向当前激活的任务 */
	extern vTaskSwitchContext;      

	PRESERVE8

	mrs r0, psp                   /* PSP内容存入R0 */
	isb                           /* 指令同步隔离,清流水线 */

	ldr	r3, =pxCurrentTCB     /* 当前激活的任务TCB指针存入R2 */
	ldr	r2, [r3]

	stmdb r0!, {r4-r11}          /* 保存剩余的寄存器,异常处理程序执行前,硬件自动将xPSR、PC、LR、R12、R0-R3入栈 */
	str r0, [r2]		     /* 将新的栈顶保存到任务TCB的第一个成员中 */

	stmdb sp!, {r3, r14}         /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护; R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护*/
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY   /* 进入临界区 */
	msr basepri, r0
	dsb                         /* 数据和指令同步隔离 */
	isb
	bl vTaskSwitchContext        /* 调用函数,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */
	mov r0, #0                   /* 退出临界区*/
	msr basepri, r0
	ldmia sp!, {r3, r14}         /* 恢复R3和R14*/

	ldr r1, [r3]
	ldr r0, [r1]		     /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
	ldmia r0!, {r4-r11}	     /* 出栈*/
	msr psp, r0
	isb
	bx r14                      /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/
	nop
}

为了便于理解上面的代码,我们先用流程图的方式将整个过程画出来,然后再逐句分析代码。因为图形可以简化程序,并且信息更容易接受。

 

图1-1:任务切换流程

先强调图1-1中的几个术语,首先是“主堆栈指针MSP”和“进程堆栈指针PSP”。对于Cortex-M3硬件,当系统复位后,默认使用MSP指针。MSP指针用于操作系统内核以及处理异常(也就是说中断服务程序中默认强制使用MSP指针,这是硬件自动设置的)。任务(进程)使用PSP指针,操作系统负责从MSP指针切换到PSP指针。这个过程在《FreeRTOS高级篇3---启动调度器》一文的最后部分中进行了讲解:在SVC中断服务程序中启动第一个任务,当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态。
      其次,“堆栈”和“任务堆栈”也值得强调一下。每个任务都有自己的“任务堆栈”,在任务创建时会创建指定大小的任务堆栈,这是任务能够独立运行的前提条件之一。在任务中定义的局部变量,会优先使用寄存器,寄存器不够时就使用任务堆栈的空间。如果在任务中调用其它函数,则调用前的保存信息也存到任务堆栈中去。根据任务代码来估算任务堆栈的大小是件十分重要的技能。前面也说了,Cortex-M3硬件有两个堆栈指针,操作系统内核以及异常处理程序中使用MSP指针,所以它们也需要一个堆栈空间,我们称之为“堆栈”,这个堆栈空间和任务堆栈空间在物理上是绝对不可以重叠的,图1-2展示了一个编译好的程序可能的RAM分配情况(堆栈向下生长)。

 

图1-2:RAM中的变量和堆栈分布示意图

有了上面的基础,接下来我们来分析PendSV中断服务程序。

mrs r0, psp 

是将任务堆栈指针PSP的值保存到寄存器R0中,因为接下来我们会将寄存器R4~R11也保存到任务堆栈中,但是我们没有哪个汇编指令能直接操作PSP完成入栈,所以只能借助R0。

ldr	r3, =pxCurrentTCB		    /* 当前激活的任务TCB指针存入R2 */
ldr	r2, [r3]

这两句代码是获取当前激活的任务TCP指针,指针pxCurrentTCB前面文章已经提到过很多次了,它是位于tasks.c文件中定义的唯一一个全局指针型变量,指向当前激活的任务TCB。

stmdb r0!, {r4-r11}

这句代码用于将寄存器R4~R11保存到当前激活的程序任务堆栈中,并且同步更新寄存器R0的值。

str r0, [r2]

寄存器R2中保存当前激活的任务TCB指针,在《FreeRTOS高级篇2---FreeRTOS任务创建分析》中讲任务TCB数据结构时我们知道,任务TCB数据结构第一个成员一定是指向任务当前堆栈栈顶的指针变量pxTopOfStack。这句代码将R0的内容保存到任务TCB数据结构的第一个成员pxTopOfStack中,也就是将最新的任务堆栈指针保存到任务TCB的pxTopOfStack字段中。当任务被激活时,就是从这个字段中获取任务堆栈指针,然后完成数据出栈操作的。

stmdb sp!, {r3, r14}

将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext。调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护。R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护。

mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0

这两句代码用来进入临界区,中断优先级大于等于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断都会被屏蔽。

bl vTaskSwitchContext

调用函数,选择下一个要执行的任务,也就是寻找处于就绪态的最高优先级任务。变量pxCurrentTCB指向找到的任务TCB。这个函数是核心中的核心,所有的其它代码都是为了保证这个函数能正确运行。
      某些运行FreeRTOS的硬件有两种方法:通用方法和特定于硬件的方法(以下简称“特殊方法”)。
      1.对于通用方法:

  • configUSE_PORT_OPTIMISED_TASK_SELECTION设置为0或者硬件不支持这种特殊方法。
  • 可以用于所有FreeRTOS支持的硬件。
  • 完全用C实现,效率略低于特殊方法。
  • 不强制要求限制最大可用优先级数目

2.对于特殊方法:

  • 并非所有硬件都支持。
  • 必须将configUSE_PORT_OPTIMISED_TASK_SELECTION设置为1。
  • 依赖一个或多个特定架构的汇编指令(一般是类似计算前导零[CLZ]指令)。
  • 比通用方法更高效。
  • 一般强制限定最大可用优先级数目为32(0~31)。

Cortex-M3即支持通用方法也支持特殊方法,默认的移植层使用特殊方法。我们先来看一下通用方法如何找到下一个要执行的任务。
      在函数vTaskSwitchContext中使用宏taskSELECT_HIGHEST_PRIORITY_TASK()完成任务寻址工作,使用通用方法时,这个宏的代码如下所示。pxReadyTasksLists是定义在tasks.c中的静态列表数组,表示就绪任务列表数组。在《FreeRTOS高级篇2---FreeRTOS任务创建分析》中讲过这个变量:新创建任务的过程中,任务TCB中的状态列表项xStateListItem会挂接到就绪任务列表数组中。uxTopReadyPriority也是定义在tasks.c中的静态变量,在此之前,它已经代表处于就绪态任务的最高优先级值,在FreeRTOS任务创建与分析一文中,我们也讲到了这个变量:每次任务创建,都会判断新任务的优先级是否大于这个变量,如果大于,还会更新这个变量的值。
      while()循环从优先级uxTopReadyPriority开始,从就绪列表数组pxReadyTasksLists中找出优先级最高的任务,然后调用宏listGET_OWNER_OF_NEXT_ENTRY获取最高优先级列表中的下一个列表项,并从该列表项中获取任务TCB指针赋给变量pxCurrentTCB。

	#define taskSELECT_HIGHEST_PRIORITY_TASK()									{																			/* 从就绪列表数组中找出最高优先级列表*/						while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopReadyPriority ] ) ) )				{																			configASSERT( uxTopReadyPriority );											--uxTopReadyPriority;													}																																				/* 相同优先级的任务使用时间片共享处理器就是通过这个宏实现*/  			listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &( pxReadyTasksLists[ uxTopReadyPriority ] ) );   	} /* taskSELECT_HIGHEST_PRIORITY_TASK */

对于Cortex-M3硬件,还支持特殊方法选择下一个要执行的任务,那就是利用硬件提供的计算前导零指令CLZ。特殊方法时,宏taskSELECT_HIGHEST_PRIORITY_TASK()的代码如下所示。

	#define taskSELECT_HIGHEST_PRIORITY_TASK()									{																		UBaseType_t uxTopPriority;																																/* 从就绪列表数组中找出最高优先级列表*/          						portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );					listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); 	} /* taskSELECT_HIGHEST_PRIORITY_TASK() */

与通用方法相比,可以发现从就绪列表数组中找出最高优先级列表代码不同了,特殊方法使用宏portGET_HIGHEST_PRIORITY来实现,将宏定义替换后,代码为:

uxTopPriority = ( 31UL - ( uint32_t ) __clz( (uxTopReadyPriority) ) )

在此之前,静态变量uxTopReadyPriority同样已经包含处于就绪态任务的最高优先级的信息。与通用方法中使用任务优先级数值不同,在特殊方法中,uxTopReadyPriority使用每一位来表示任务,比如变量uxTopReadyPriority的bit0为1,则表示存在优先级为0的就绪任务,bit10为1则表示存在优先级为10的就绪任务。由于32位整形数最多只有32位,因此使用这种特殊方法限定最大可用优先级数目为32,即优先级0~31。
      我们这来看看__clz( (uxTopReadyPriority)是什么意思,__clz()会被汇编指令CLZ替换掉,这个指令用来计算一个变量从最高位开始的连续零的个数。举个例子,假如变量uxTopReadyPriority为0x09(二进制为:0000 0000 0000 0000 0000 0000 0000 1001),即bit3和bit0为1,表示存在优先级为0和3的就绪任务。则__clz( (uxTopReadyPriority)的值为28,uxTopPriority =31-28=3,即优先级为3的任务是就绪态最高优先级任务。下面的代码跟通用方法一样,调用宏listGET_OWNER_OF_NEXT_ENTRY获取最高优先级列表中的下一个列表项,并从该列表项中获取任务TCB指针赋给变量pxCurrentTCB。

mov r0, #0                   /* 退出临界区*/
msr basepri, r0

这两句代码用来退出临界区,通过向寄存器BASEPRI写入数值0来实现。

ldmia sp!, {r3, r14}

这句代码将寄存器R3和R14从堆栈中恢复,现在R3保存变量pxCurrentTCB的地址,需要注意的是,变量pxCurrentTCB在函数vTaskSwitchContext中可能已被修改,指向新的最高优先级就绪任务;R14保存退出异常需要的信息。

ldr r1, [r3]
ldr r0, [r1]	

这两句代码获取变量pxCurrentTCB指向的任务TCB指针,并将TCB的第一个成员——当前堆栈栈顶的指针变量pxTopOfStack的值保存到寄存器R0中,也就是将即将运行的任务堆栈栈顶值存入R0。

ldmia r0!, {r4-r11}

将寄存器R4~R11出栈,并同时更新R0的值。

msr psp, r0

将最新的任务堆栈栈顶赋值给线程堆栈指针PSP。

bx r14

从异常中断服务程序退出。异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、使用PSP堆栈指针还是MSP堆栈指针。当调用 bx r14指令后,硬件会知道要从异常返回,然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。
      至此,任务切换完成。

时间: 2024-08-02 04:05:27

FreeRTOS高级篇4---FreeRTOS任务切换分析的相关文章

FreeRTOS高级篇8---FreeRTOS任务通知分析

在FreeRTOS版本V8.2.0中推出了全新的功能:任务通知.在大多数情况下,任务通知可以替代二进制信号量.计数信号量.事件组,可以替代数长度为1的队列(可以保存一个32位整数或指针值),并且任务通知速度更快.使用的RAM更少!我在< FreeRTOS系列第14篇---FreeRTOS任务通知>一文中介绍了任务通知如何使用以及局限性,今天我们将分析任务通知的实现源码,看一下任务通知是如何做到效率与RAM消耗双赢的.        在<FreeRTOS高级篇6---FreeRTOS信号量

FreeRTOS高级篇5---FreeRTOS队列分析

FreeRTOS提供了多种任务间通讯方式,包括: 任务通知(版本V8.2以及以上版本) 队列 二进制信号量 计数信号量 互斥量 递归互斥量 其中,二进制信号量.计数信号量.互斥量和递归互斥量都是使用队列来实现的,因此掌握队列的运行机制,是很有必要的.      队列是FreeRTOS主要的任务间通讯方式.可以在任务与任务间.中断和任务间传送信息.发送到队列的消息是通过拷贝实现的,这意味着队列存储的数据是原数据,而不是原数据的引用.先看一下队列的数据结构: typedef struct Queue

FreeRTOS高级篇2---FreeRTOS任务创建分析

在FreeRTOS基础系列<FreeRTOS系列第10篇---FreeRTOS任务创建和删除>中介绍了任务创建API函数xTaskCreate(),我们这里先回顾一下这个函数的声明: BaseType_t xTaskCreate( TaskFunction_tp vTaskCode, const char * constpcName, unsigned short usStackDepth, void *pvParameters, UBaseType_t uxPriority, TaskHan

FreeRTOS高级篇6---FreeRTOS信号量分析

FreeRTOS的信号量包括二进制信号量.计数信号量.互斥信号量(以后简称互斥量)和递归互斥信号量(以后简称递归互斥量).关于它们的区别可以参考< FreeRTOS系列第19篇---FreeRTOS信号量>一文. 信号量API函数实际上都是宏,它使用现有的队列机制.这些宏定义在semphr.h文件中.如果使用信号量或者互斥量,需要包含semphr.h头文件. 二进制信号量.计数信号量和互斥量信号量的创建API函数是独立的,但是获取和释放API函数都是相同的:递归互斥信号量的创建.获取和释放AP

FreeRTOS高级篇3---启动调度器

使用FreeRTOS,一个最基本的程序架构如下所示: int main(void) { 必要的初始化工作; 创建任务1; 创建任务2; ... vTaskStartScheduler(); /*启动调度器*/ while(1); } 任务创建完成后,静态变量指针pxCurrentTCB(见<FreeRTOS高级篇2---FreeRTOS任务创建分析>第7节内容)指向优先级最高的就绪任务.但此时任务并不能运行,因为接下来还有关键的一步:启动FreeRTOS调度器. 调度器是FreeRTOS操作系

FreeRTOS高级篇7---FreeRTOS内存管理分析

内存管理对应用程序和操作系统来说都非常重要.现在很多的程序漏洞和运行崩溃都和内存分配使用错误有关.        FreeRTOS操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些内存管理函数是如何实现的.这样做大有好处,可以增加系统的灵活性:不同的应用场合可以使用不同的内存分配实现,选择对自己更有利的内存管理策略.比如对于安全型的嵌入式系统,通常不允许动态内存分配,那么可以采用非常简单的内存管理策略,一经申请的内存,甚至不允许被释放.在满足设计要求的前提

FreeRTOS高级篇9---FreeRTOS系统延时

FreeRTOS提供了两个系统延时函数:相对延时函数vTaskDelay()和绝对延时函数vTaskDelayUntil().相对延时是指每次延时都是从任务执行函数vTaskDelay()开始,延时指定的时间结束:绝对延时是指每隔指定的时间,执行一次调用vTaskDelayUntil()函数的任务.换句话说:任务以固定的频率执行.在<FreeRTOS系列第11篇---FreeRTOS任务控制>一文中,已经介绍了这两个API函数的原型和用法,本文将分析这两个函数的实现原理. 1. 相对延时函数v

FreeRTOS高级篇11---空闲任务分析

当RTOS调度器开始工作后,为了保证至少有一个任务在运行,空闲任务被自动创建,占用最低优先级(0优先级). xReturn = xTaskCreate( prvIdleTask, "IDLE",configMINIMAL_STACK_SIZE, (void * ) NULL, (tskIDLE_PRIORITY | portPRIVILEGE_BIT ), &xIdleTaskHandle); 空闲任务是FreeRTOS不可缺少的任务,因为FreeRTOS设计要求必须至少有一个

FreeRTOS高级篇1---FreeRTOS列表和列表项

FreeRTOS内核调度大量使用了列表(list)这一数据结构.我们如果想一探FreeRTOS背后的运行机制,首先遇到的拦路虎就是列表.对于FreeRTOS内核来说,列表就是它最基础的部分.我们在这一章集中讲解列表和列表项的结构以及操作函数,在下一章讲解任务创建时,会用到本章的知识点. 列表被FreeRTOS调度器使用,用于跟踪任务,处于就绪.挂起.延时的任务,都会被挂接到各自的列表中.用户程序如果有需要,也可以使用列表. FreeRTOS列表使用指针指向列表项.一个列表(list)下面可能有很