精华内容
下载资源
问答
  • freertos任务切换原理
    千次阅读
    2019-10-04 16:35:49

    1. 任务切换相关API函数

    函数描述
    xPortPendSVHandler()PendSV中断服务函数,其实函数原型为PendSV_Handler()
    vTaskSwitchContext()检查任务堆栈使用是否溢出,和查找下一个优先级高的任务,如果使能运行时间统计功能,会计算任务运行时间

    2. 任务切换的基本知识

    在FreeRTOS任务管理中,最主要的目的就是找到就绪态优先级最高的任务,然后执行任务切换,从而能保持优先级最高的任务一直占用CPU资源。为了达到最优性能,任务切换部分程序使用汇编代码编写。

    FreeRTOS有两种方法触发任务切换:

    • 系统节拍时钟中断(SysTick定时器)。切换过程参考《FreeRTOS原理剖析:系统节拍时钟分析》
    • 执行系统调用代码。普通任务使用taskYIELD()强制任务切换;中断服务程序使用portYIELD_FROM_ISR()强制任务切换;在应用程序里也可以通过设置xYieldPending的值来通知调度器进行任务切换。

    其中:

    // 对于普通任务时
    #define taskYIELD()				portYIELD()
    #define portYIELD_WITHIN_API 	portYIELD
    
    // 对于中断服务程序时
    #define portEND_SWITCHING_ISR( xSwitchRequired ) if( xSwitchRequired != pdFALSE ) portYIELD()
    #define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x )
    

    可以看出,最终执行的代码段为:

    #define portYIELD()											\
    {															\
    	/* 														\
    	 * 通过向中断控制及状态寄存器ICSR的第28位写入1,触发PendSV中断	\
    	 * 地址为0xE000 ED04 									\
    	 */														\
    	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;			\
    															\
    	/* dsb和isb 完成数据同步隔离和指令同步隔离					\
    	 * 保证之前存储器访问操作和指令都执行完						\
    	 */														\
    	__dsb( portSY_FULL_READ_WRITE );						\
    	__isb( portSY_FULL_READ_WRITE );						\
    }
    

    通过向中断控制及状态寄存器ICSR(地址:0xE000 ED04)的第28位写入1,触发PendSV中断,从而执行任务切换。

    3. 任务切换过程

    3.1 PendSV中断服务函数分析

    在FreeRTOS中,有:

    #define xPortPendSVHandler 	PendSV_Handler
    

    源代码如下:

    __asm void xPortPendSVHandler( void )
    {
    	extern uxCriticalNesting;
    	extern pxCurrentTCB;		/* 永远会指向当前激活的任务 */
    	extern vTaskSwitchContext;
    
    	PRESERVE8
    
    	mrs r0, psp					/* 读取进程栈指针PSP,保存在R0中,此时SP的值为MSP */
    	isb							/* 指令同步隔离 */
    
    	/* 这两句使R2中保存当前激活的任务TCB首地址 */
    	ldr	r3, =pxCurrentTCB		/* 将pxCurrentTCB储存的地址保存到R3,注意的是pxCurrentTCB的储存地址是固定不变的,但指向是可变的 */
    	ldr	r2, [r3]				/* 将R3地址所处数据保存在R2,即TCB的首地址 */
    
    	/* 
    	 * 前两句判断是否使能了FPU,如果使能了,则手动将s16~s31压入栈中
    	 * 其中s0~s15和FPSCR硬件自动完成 
    	 */
    	tst r14, #0x10
    	it eq
    	vstmdbeq r0!, {s16-s31}
    
    	/* 将当前激活任务的寄存器值入栈,并更新R0,另外硬件自动将xPSR、PC、LR、R12、R0~R3入栈 */
    	stmdb r0!, {r4-r11, r14}	
    
    	/* R0为PSP地址,R2为激活任务的TCB地址,R0的值写入R2所保存的地址去,即TCB第一个成员指向线程堆栈指针,在每次任务切换最后都会更新PSP */
    	str r0, [r2]				
    
    	stmdb sp!, {r3}				/* 将R3临时压入堆栈,R3保存了pxCurrentTCB地址,函数调用后会用到,因此要入栈保护 */
    
    	/* 关中断,中断优先级号大于等于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断都会被屏蔽 */
    	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY	
    	msr basepri, r0				
    	
    	dsb							/* 数据同步隔离 */
    	isb							/* 指令同步隔离 */
    	
    	bl vTaskSwitchContext		/* 切换到vTaskSwitchContext,查找下一个任务 */
    
    	/* 开中断 */	
    	mov r0, #0	
    	msr basepri, r0				
    	
    	ldmia sp!, {r3}				/* 恢复R3,R3保存了pxCurrentTCB的地址,这里pxCurrentTCB的地址固定,但指向改变了 */
    
    	/* 当前激活的TCB栈顶值存入R0 */
    	ldr r1, [r3]				/* 将pxCurrentTCB指向的地址赋值给R1,即将TCB的首地址赋值给R1 */
    	ldr r0, [r1]				/* 将R1地址所处的数据赋值给R0,即当前激活任务TCB的第一项的值赋给R0 */
    
    	ldmia r0!, {r4-r11, r14}	/* 将寄存器R4~R11出栈,并同时更新R0的值 */
    
    
    	/* 
    	 * 前两句判断是否使能了FPU,如果使能了,手动恢复s16-s31浮点寄存器
    	 * 其中s0~s15和FPSCR硬件自动完成 
    	 */
    	tst r14, #0x10
    	it eq
    	vldmiaeq r0!, {s16-s31}
    
    	msr psp, r0					/* 将最新的任务堆栈栈顶赋值给线程堆栈指针PSP */
    	isb							/* 指令同步隔离,清流水线 */
    	
    	#ifdef WORKAROUND_PMU_CM001
    		#if WORKAROUND_PMU_CM001 == 1
    			push { r14 }
    			pop { pc }
    			nop
    		#endif
    	#endif
    	
    	bx r14	/* 当调用 bx r14指令退出中断,堆栈指针PSP指向了新任务堆栈的正确位置 */
    
    }
    

    说明:

    • 在Cortex-M处理器中有两个栈指针,一个是主栈指针(Main Stack Pointer,即MSP),它可用于线程模式,在中断模式下只能用MSP;另一个是进程堆栈指针(Processor Stack Pointer,即PSP),PSP总是用于线程模式。在任何时刻只能使用到其中一个。
    • 复位后处于线程模式特权级,默认使用MSP。在FreeRTOS中,MSP用于OS内核和异常处理,PSP用于应用任务。
    • 通过设置CONTROL寄存器的bit[1]选择使用哪个堆栈指针。CONTROL[1]=0选择主堆栈指针;CONTROL[1]=1选择进程堆栈指针。

    3.2 函数vTaskSwitchContext()

    该函数会更新当前任务运行时间,检查任务堆栈使用是否溢出,然后调用宏 taskSELECT_HIGHEST_PRIORITY_TASK()获取更高优先级的任务。

    函数源代码如下:

    void vTaskSwitchContext( void )
    {
    	/* 如果任务调度器已经挂起 */
    	if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
    	{
    		xYieldPending = pdTRUE;		/* 标记任务调度器挂起,不允许任务切换 */
    	}
    	else
    	{
    		xYieldPending = pdFALSE;	/* 标记任务调度器没有挂起 */
    		
    		traceTASK_SWITCHED_OUT();
    
    		/* 
    		 * 如果启用运行时间统计功能,设置configGENERATE_RUN_TIME_STATS为1
    		 * 如果使用了该功能,要提供以下两个宏:
    		 * portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()
    		 * portGET_RUN_TIME_COUNTER_VALUE()
    		 */
    		#if ( configGENERATE_RUN_TIME_STATS == 1 )
    		{
    				#ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
    					portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );
    				#else
    					ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
    				#endif
    
    				/*  
    				 * ulTotalRunTime记录系统的总运行时间,ulTaskSwitchedInTime记录任务切换的时间
    				 * 如果系统节拍周期为1ms,则ulTotalRunTime要497天后才会溢出
    				 * ulTotalRunTime < ulTaskSwitchedInTime表示可能溢出
    				 */
    				if( ulTotalRunTime > ulTaskSwitchedInTime )
    				{
    					/* 记录当前任务的运行时间 */
    					pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
    				}
    				else
    				{
    					mtCOVERAGE_TEST_MARKER();
    				}
    
    				/* 更新ulTaskSwitchedInTime,下个任务时间从这个值开始 */
    				ulTaskSwitchedInTime = ulTotalRunTime;	
    		}
    		#endif /* configGENERATE_RUN_TIME_STATS */
    
    		/* 核查堆栈是否溢出 */
    		taskCHECK_FOR_STACK_OVERFLOW();
    
    		/* 寻找更高优先级的任务 */
    		taskSELECT_HIGHEST_PRIORITY_TASK();
    		
    		traceTASK_SWITCHED_IN();
    
    		/* 如果使用Newlib运行库,你的操作系统资源不够,而不得不选择newlib,就必须打开该宏 */
    		#if ( configUSE_NEWLIB_REENTRANT == 1 )
    		{
    			_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
    		}
    		#endif /* configUSE_NEWLIB_REENTRANT */
    	}
    }
    

    3.2 寻找下一个任务的方式

    PendSV中会调用vTaskSwitchContext(),最后调用函数taskSELECT_HIGHEST_PRIORITY_TASK()寻找优先级最高的任务。

    对于FreeRTOS的调度器,它有两种方式寻找下一个最高优先级的任务,分别为特殊方式和常用方式,在FreeRTOSConfig.h中可通过宏定义设置,如下:

    /* 0:使用常用方式来选择下一个要运行的任务;1:使用特殊方法来选择下一个要运行的任务 */
    #define configUSE_PORT_OPTIMISED_TASK_SELECTION	1
    

    在FreeRTOS中,通用方法不依赖某些硬件等限制,适用于多种MCU中。特殊方式是使用了某些硬件的特性,只针对部分MCU而使用。

    3.2.1 常用方法

    uxTopReadyPriority 记录就绪态中最高优先级值,创建任务时会更新值,有任务添加到就绪表时也会更新值。这种方法对任务的数量无限制。

    #define taskSELECT_HIGHEST_PRIORITY_TASK()												\
    {																						\
    	UBaseType_t uxTopPriority = uxTopReadyPriority;										\
    																						\
    	while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )				\
    	{																					\
    		configASSERT( uxTopPriority );													\
    		--uxTopPriority;																\
    	}																					\
    																						\
    	/* 获取优先级最高任务的任务控制块 */														\
    	listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );\
    	
    	uxTopReadyPriority = uxTopPriority;	 												\
    }
    

    3.2.2 特殊方式

    使用此方法,uxTopReadyPriority 每个bit位表示一个优先级,bit0表示优先级0,bit31表示优先级31,使用此方式优先级最大只能是32个。

    #define taskSELECT_HIGHEST_PRIORITY_TASK()														\
    {																								\
    	UBaseType_t uxTopPriority;																		\
    																								\
    	/* 获取优先级最高的任务 */								\
    	portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );								\
    	configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 );		\
    	
    	/* 获取优先级最高任务的任务控制块 */
    	listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );		\
    } 
    
    

    其中:

    #define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
    

    __clz( ( uxReadyPriorities ) 是计算uxReadyPriorities 的前导零个数,如:
    二进制0001 1010 0101 1111的前导零个数为3,可以知道,最高优先级uxTopPriority 等于 31减去前导零个数。
    知道最高优先级的优先级,则通过listGET_OWNER_OF_NEXT_ENTRY()对应最高优先级的列表项,将pxCurrentTCB指向对应的控制块。


    参考资料:

    【1】: 正点原子:《STM32F407 FreeRTOS开发手册V1.1》
    【2】: 野火:《FreeRTOS 内核实现与应用开发实战指南》
    【3】: 《Cortex M3权威指南(中文)》

    更多相关内容
  • freertos内核 任务定义与切换 原理分析主程序任务控制块任务创建函数任务栈初始化就绪列表调度器总结任务切换 主程序 这个程序目的就是,使用freertos让两个任务不断切换。看两个任务中变量的变化情况(波形)。 下面...

    主程序

    这个程序目的就是,使用freertos让两个任务不断切换。看两个任务中变量的变化情况(波形)。

    下面这个图是任务函数里面delay(100)的结果。

    在这里插入图片描述

    下面这个图是任务函数里面delay(2)的结果.

    在这里插入图片描述

    多任务系统,CPU好像在同时做两件事,也就是说,最好预期就是,两变量的波形应该是完全相同的。

    这个实验,delay减少了,他们两变量波形中间间距仍然没有减少,说明这个实验只是一个入门,远没达到RTOS的效能。

    这个实验特点,就是具有任务主动切换能力,这是如何实现的呢,值得研究。

    下面两个图,直观显示了程序的主动切换。观察CurrentTCB这个参数,可以发现它是一直变动的。

    在这里插入图片描述

    它究竟为什么变动呢,采用逐步debug的方式,可找到,是因为调用了一个SwitchContext函数。

    在这里插入图片描述

    那么先看一下main里面都有啥:

    从下面可知,这里面有任务栈、任务控制块、有任务函数、还得创建任务。有就绪列表、有调度器。

    任务栈:

    #define TASK1_STACK_SIZE                    20
    StackType_t Task1Stack[TASK1_STACK_SIZE];
    #define TASK2_STACK_SIZE                    20
    StackType_t Task2Stack[TASK2_STACK_SIZE];
    

    任务函数(任务入口):

    void Task1_Entry( void *p_arg )
    {
    	for( ;; )
    	{
    		flag1 = 1;
    		delay( 100 );		
    		flag1 = 0;
    		delay( 100 );
    		/* 任务切换,这里是手动切换 */
            taskYIELD();
    	}
    }
    void Task2_Entry( void *p_arg )
    {
    	for( ;; )
    	{
    		flag2 = 1;
    		delay( 100 );		
    		flag2 = 0;
    		delay( 100 );
    		/* 任务切换,这里是手动切换 */
            taskYIELD();
    	}
    }
    

    任务控制块:

    TCB_t Task1TCB;
    TCB_t Task2TCB;
    

    就绪列表初始化:

    prvInitialiseTaskLists();
    

    创建任务:

    typedef void * TaskHandle_t;
    TaskHandle_t Task1_Handle;
    
    Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry,   /* 任务入口 */
    					                  (char *)"Task1",               /* 任务名称,字符串形式 */
    					                  (uint32_t)TASK1_STACK_SIZE ,   /* 任务栈大小,单位为字 */
    					                  (void *) NULL,                 /* 任务形参 */
    					                  (StackType_t *)Task1Stack,     /* 任务栈起始地址 */
    					                  (TCB_t *)&Task1TCB );          /* 任务控制块 */
    

    任务添加到就绪列表:

    vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
    

    启动调度器:

    vTaskStartScheduler(); 
    

    任务控制块

    多任务系统,任务执行由系统调度。任务的信息很多,于是就用任务控制块表示任务,这样方便系统调度。

    任务控制块类型,包含了任务的所有信息,比如栈顶指针pxTopOfStack、任务节点xStateListItem、任务栈起始地址pxStack、任务名称pcTaskName。

    typedef struct tskTaskControlBlock
    {
    	volatile StackType_t    *pxTopOfStack;    /* 栈顶 */
    
    	ListItem_t			    xStateListItem;   /* 任务节点 */
        
        StackType_t             *pxStack;         /* 任务栈起始地址 */
    	                                          /* 任务名称,字符串形式 */
    	char                    pcTaskName[ configMAX_TASK_NAME_LEN ];  
    } tskTCB;
    typedef tskTCB TCB_t;
    

    任务创建函数

    main里面调用xTaskCreateStatic创建了任务,观察可知这个函数其实改变的是Task1TCB任务控制块,这个任务控制块诞生之初,就没有进行过初始化。调用任务创建函数目的就是初始化任务控制块。

    Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry,   /* 任务入口 */
    					                  (char *)"Task1",               /* 任务名称,字符串形式 */
    					                  (uint32_t)TASK1_STACK_SIZE ,   /* 任务栈大小,单位为字 */
    					                  (void *) NULL,                 /* 任务形参 */
    					                  (StackType_t *)Task1Stack,     /* 任务栈起始地址 */
    					                  (TCB_t *)&Task1TCB );          /* 任务控制块 */
    

    直观表述这个函数内部:

    在这里插入图片描述

    任务控制块里面的任务节点:下面代码是初始化过程,其实就是进行链表的普通节点初始化。

        /* 初始化TCB中的xStateListItem节点 */
        vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
        /* 设置xStateListItem节点的拥有者 */
    	listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
    

    这个任务入口体现在哪呢,其实是体现在任务栈里面。在main.c里面初始化任务栈,仅仅开辟了一段内存空间,里面放什么东西都没有具体说明。调用任务创建函数之后,其实也一并初始化了任务栈(往里面放东西),任务入口就放到这个栈里了。任务栈也初始化完的时候,任务控制块才算圆满的初始化完了。

    所以任务创建函数里面还得调用任务栈初始化函数。

    任务栈初始化

    初始化任务栈的函数代码在下面:

    StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
    {
        /* 异常发生时,自动加载到CPU寄存器的内容 */
    	pxTopOfStack--;
    	*pxTopOfStack = portINITIAL_XPSR;	                                    /* xPSR的bit24必须置1 */
    	pxTopOfStack--;
    	*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;	/* PC,即任务入口函数 */
    	pxTopOfStack--;
    	*pxTopOfStack = ( StackType_t ) prvTaskExitError;	                    /* LR,函数返回地址 */
    	pxTopOfStack -= 5;	/* R12, R3, R2 and R1 默认初始化为0 */
    	*pxTopOfStack = ( StackType_t ) pvParameters;	                        /* R0,任务形参 */
        
        /* 异常发生时,手动加载到CPU寄存器的内容 */    
    	pxTopOfStack -= 8;	/* R11, R10, R9, R8, R7, R6, R5 and R4默认初始化为0 */
    
    	/* 返回栈顶指针,此时pxTopOfStack指向空闲栈 */
        return pxTopOfStack;
    }
    
    static void prvTaskExitError( void )
    {
        /* 函数停止在这里 */
        for(;;);
    }
    

    栈顶指针就是pxTopOfStack。pxStack是一个指针指向任务栈起始地址,ulStackDepth是任务栈大小。下面是获取栈顶指针的代码。栈是后进先出,先进去的后出。其实也就是,先进栈的被压到最底下去了(下标最靠后)。所以,如果栈里面什么都没有,栈顶的位置得在最后面(也就是地址最高的哪个位置)。

    /* 获取栈顶地址 */
    pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
    

    下面两个图表述的都是一个意思,只不过右边的可能好懂点(先进栈的被压到最底下去了)。

    在这里插入图片描述

    初始化任务栈的函数运行完,栈就发生了变化,里面有内容了,如下图所示。可以看到任务入口地址存进去了,任务形参也存进去了。

    #define portINITIAL_XPSR			        ( 0x01000000 )
    

    在这里插入图片描述

    至此,通过任务创建函数,已经圆满的初始好了任务控制块,同时填充了任务栈,任务栈联系了任务入口地址(任务的函数实体)。任务控制块成员变量里面有栈顶指针,联系了任务栈。那么,任务的栈、任务的函数实体、任务的控制块通过任务创建函数就联系起来了。

    这里面插一句:任务栈一个元素占四个字节!上面那个图,如果r0地址是0x40,那么pxTopOfStack地址就是0x20(因为0x40-0x20=32),32÷4=8,也就是说八个元素。

    #define portSTACK_TYPE	uint32_t
    typedef portSTACK_TYPE StackType_t;
    StackType_t Task1Stack[TASK1_STACK_SIZE];
    
    uint32_t
    u:代表 unsigned 即无符号,即定义的变量不能为负数;
    int:代表类型为 int 整形;
    32:代表四个字节,即为 int 类型;
    _t:代表用 typedef 定义的;
    整体代表:用 typedef 定义的无符号 int 型宏定义;
    位(bit):每一位只有两种状态01。计算机能表示的最小数据单位。
    字节(Byte)8位二进制数为一个字节。计算机基本存储单元内容用字节表示。
    

    就绪列表

    下面是main里面就绪列表的定义、初始化,添加任务到就绪列表。

    首先绪列表的定义,简而言之,就绪列表是一个List_t类型的数组(其实数组中每个元素就相当于根节点),数组下标对应任务的优先级。

    #define configMAX_PRIORITIES		            
    /* 任务就绪列表 */
    List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
    /* 初始化与任务相关的列表,如就绪列表 */
    prvInitialiseTaskLists();
    /* 将任务添加到就绪列表 */                                 
    vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
    /* 将任务添加到就绪列表 */                                 
    vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );
    

    就绪列表初始化函数如下,简而言之,就是对List_t类型的数组里面每个元素进行初始化(根节点初始化)。

    /* 初始化任务相关的列表 */
    void prvInitialiseTaskLists( void )
    {
        UBaseType_t uxPriority;
        
        for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
    	{
    		vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
    	}
    }
    

    添加任务到就绪列表的函数是vListInsertEnd,这个在之前双向循环链表说过,其实就是把普通节点插到根节点后。

    就绪列表在不同任务之间建立一种联系,图示如下。

    在这里插入图片描述

    调度器

    启动调度器,是用了一个SVC中断。

    从下面代码可以看出,pxCurrentTCB指向的是Task1TCB(任务控制块)的地址。

    typedef struct tskTaskControlBlock
    {
    	volatile StackType_t    *pxTopOfStack;    /* 栈顶 */
    
    	ListItem_t			    xStateListItem;   /* 任务节点 */
        
        StackType_t             *pxStack;         /* 任务栈起始地址 */
    	                                          /* 任务名称,字符串形式 */
    	char                    pcTaskName[ configMAX_TASK_NAME_LEN ];  
    } tskTCB;
    typedef tskTCB TCB_t;
    
    //void vTaskStartScheduler( void )函数里
    pxCurrentTCB = &Task1TCB;
    

    下面这个svc的中断函数,里面第一步就是把任务栈的栈顶指针给r0寄存器。

    可以认为:r0=pxTopOfStack(任务栈的栈顶指针的地址)。

    //__asm void vPortSVCHandler( void )函数里
    ldr	r3, =pxCurrentTCB	//加载pxCurrentTCB的地址到r3
    ldr r1, [r3]			//把r3指向的内容给r1,内容就是Task1TCB的地址
    ldr r0, [r1]  //把r1指向的内容给r0,内容就是Task1TCB的地址里面的第一个内容,也就是pxTopOfStack
    

    接下来:以r0(任务栈的栈顶指针的地址)为基地址,将任务栈里面向上增长的8字节内容加载到CPU寄存器r4-r11。

    ldmia r0!, {r4-r11}
    

    然后将r0存到psp里。

    msr psp, r0
    

    下面这个代码,目的是改EXC_RETURN值为0xFFFFFFD,这样的话中断返回就进入线程模式,使用线程堆栈(sp=psp)。

    orr r14, #0xd
    

    看下面这个图,异常返回时,出栈用的是PSP指针。PSP指针把任务栈里面剩余的内容(没有读到寄存器里的内容)全部给弄出去(自动将栈中的剩余内容加载到cpu寄存器)。那么任务函数的地址就给到了PC,程序就跳到任务函数的地方继续运行。

    在这里插入图片描述

    图1如下:注意,动的是psp,pxTopOfStack是不动的。

    在这里插入图片描述

    下面是实验证明上面关于psp指针运动描述的正确性:

    r0一开始存的就是pxTopOfStack的值(任务栈的栈顶指针的地址)

    在这里插入图片描述

    接下来把运动过的r0给psp,此时的psp位置就在图1psp2那个地方。

    在这里插入图片描述

    下图这个psp地址仍然是0x40。

    在这里插入图片描述

    程序运行完bx r14,就跑到任务函数里面了,此时的psp=0x60,位置就在图1的psp3。

    在这里插入图片描述

    现在程序跑到任务函数里面去了,任务函数里面调了taskYIELD()函数,目的就是触发PendSV中断(优先级最低,没有其他中断运行时才响应)。下面这个图是进到PendSV中断服务函数之前的寄存器组状态。

    在这里插入图片描述

    下面这个图是进到PendSV中断服务函数时的寄存器组状态。可以观察psp,从0x60变成了0x40。

    在这里插入图片描述

    现在psp的位置就可以知道了,如下图所示。这是因为,进到xPortPendSVHandler函数之后,上个任务运行的环境将会自动存储到任务的栈中,同时psp自动更新。

    在这里插入图片描述

    下面这个代码,把psp的值存到r0里面。

    //__asm void xPortPendSVHandler( void )函数
    mrs r0, psp
    
    //void vTaskStartScheduler( void )函数里
    pxCurrentTCB = &Task1TCB;/*pxCurrentTCB有一个地址,这个地址里面的内容是当前任务的地址*/
    						  /*当前任务地址的第一个内容就是当前任务的栈顶指针*/
    
    //__asm void xPortPendSVHandler( void )函数里
    ldr	r3, =pxCurrentTCB		/* 加载pxCurrentTCB的地址到r3 */
    ldr	r2, [r3]         /* 把r3指向的内容给r2,内容就是Task1TCB(当前任务)的地址*/
    					  /*[r2]是当前任务栈的栈顶指针*/
    
    stmdb r0!, {r4-r11}			/* 将CPU寄存器r4~r11的值存储到r0指向的地址 */
    str r0, [r2]                /* 把r0的地址给当前任务栈的栈顶指针 */	
    

    经过上面这个代码,现在r0的位置如下。psp在上面这个过程是没变化的,变的只有r0。

    在这里插入图片描述

    对照着下面这个图,更清晰点。r2存的是当前任务的地址。r0存的是栈顶指针的地址。

    在这里插入图片描述

    下面对r3进行说明:r3=0x2000000C,这个地址里面存的第一个内容是当前任务块的地址0x20000068如下图所示。

    在这里插入图片描述

    下面对当前任务块的地址进行说明:当前任务块的地址0x20000068里面存的第一个内容就是栈顶指针的地址。

    在这里插入图片描述

    下面对栈顶指针的地址进行说明:栈顶指针地址里面内容刚好就是当前任务的任务栈。

    在这里插入图片描述

    可以对比下图,观察当前任务栈里面的内容,与此同时内容也对应了地址,地址就可以通过上图推出,比如,0x20000060地址里面存的就是0x10000000。

    在这里插入图片描述

    下面这个代码:目的是将r3和r14临时压入主栈(MSP指向的栈),因为接下来需要调用任务切换函数,调用函数时,返回地址自动保存到r14里面。r3的内容是当前任务块的地址(ldr r3, =pxCurrentTCB),调用函数后,pxCurrentTCB会被更新。

    stmdb sp!, {r3, r14}
    

    执行代码之前,MSP指向0x20000058这个地址。

    在这里插入图片描述

    执行代码之后,MSP指向的地址少了8个字节,与此同时r3和r14存到了MSP指向的地址里面。

    在这里插入图片描述

    msp指向的栈里面的具体信息其实可以反推出来,如下绿字:

    在这里插入图片描述

    下面这个代码:basepri是中断屏蔽寄存器,下面这个设置,优先级大于等于11的中断都将被屏蔽。相当于关中断进入临界段。

    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY 
    msr basepri, r0
    /*
    #define configMAX_SYSCALL_INTERRUPT_PRIORITY 	191   /* 高四位有效,即等于0xb0,或者是11 */
    191转成二进制就是11000000,高四位就是1100
    */
    

    下面这个代码:调用了函数vTaskSwitchContext,这个函数目的是选择优先级最高的任务,然后更新pxCurrentTCB。目前这里面使用的是手动切换。

    bl vTaskSwitchContext 
    
    void vTaskSwitchContext( void )
    {    
        /* 两个任务轮流切换 */
        if( pxCurrentTCB == &Task1TCB )
        {
            pxCurrentTCB = &Task2TCB;
        }
        else
        {
            pxCurrentTCB = &Task1TCB;
        }
    }
    

    现在说明一下调用这个函数产生什么后果:

    从下图可知,此时r3=0x2000000C,这个地址里面的的内容就是当前任务块的地址。

    在这里插入图片描述

    进行到下面这一步,当前任务块的地址变了,与此同时,0x2000000C地址里面的的内容也变了。也就是说,走出调用函数之后,通过r3就能找到变化后新的任务地址了。

    那么此时豁然开朗,为什么调用函数前要把r3入栈呢,看下图正中间上方的汇编代码,这个c语言背后的汇编代码是调用寄存器r0、r1存一些中间变量,为了防止运行函数时往r3寄存器里面存中间变量,才把r3入栈保护起来。想一下,如果往r3寄存器里面存中间变量,那么0x2000000C地址就不存到r3寄存器里了,那也无法通过r3找到变化后新的任务地址了。

    在这里插入图片描述

    下面这个代码:优先级高于0的中断被屏蔽,相当于是开中断退出临界段。

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

    下面这个代码恢复r3和r14

    ldmia sp!, {r3, r14}        /* 恢复r3和r14 */
    

    如下图,r3和r14被恢复,而且MSP从0x20000550变成了0x20000558。

    在这里插入图片描述

    这里面有个细节,MSP变动之后,MSP指向的栈前面的数(存的r3和r14)却被留了下来。这让人不禁思考出栈究竟是什么意思,这里不就只是动了MSP指针吗。

    此时观察psp地址里面的内容,可发现,还是之前的那个任务栈。看了出栈和c语言里面实体的出(c语言里面出栈后,出去的内容就不在栈里面了)还不太一样,这个出栈,动的是指针,内容还在栈里面。

    在这里插入图片描述

    下面这个代码,进行完,r0里面存的是当前任务栈的栈顶指针的地址。

    ldr r1, [r3]
    ldr r0, [r1] 				/* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
    

    在这里插入图片描述

    下面是当前的任务栈里面的内容。

    在这里插入图片描述

    ldmia r0!, {r4-r11}			/* 出栈 */
    

    这个时候r0位置变到了0x200000c0。

    在这里插入图片描述

    然后下面把r0给了psp。记得吧,之前psp指向的可是0x20000040,也就是上一个任务的任务栈,这里面切到了另一个任务的任务栈里面了。也就是psp指向0x200000c0。

    msr psp, r0
    

    下面这个代码运行完效果如下图。

    bx r14  
    

    仔细观察,异常退出时,会以psp作为基地址,将任务栈里面剩下的内容自动加载到CPU寄存器。然后PC指针就拿到了任务栈里面的任务函数地址,然后就跳到任务函数里了。至此,切换完成。

    在这里插入图片描述

    最后,观察一下psp:由下面两张图,就明白了,psp出栈是什么意思。

    下面是返回Thread Mode后(进入到了任务函数里面)psp的指向。

    在这里插入图片描述

    下图是没有返回到Thread Mode时psp的指向。

    在这里插入图片描述

    总结任务切换

    总结一下核心思路:

    1.首先是这张图,在任务函数里面,处于Thread Mode状态(为什么呢,因为bx r14 指令,里面r14的值设置的是0xFFFFFFFD),然后通过任务函数里面的taskYIELD()函数,进入Handler Mode状态,里面进行了任务切换操作,就是说,psp指向的任务栈切换了(所以一会pc指向的任务函数也改了),然后结束异常的时候,psp出栈,pc现在指向的是切换后的任务函数地址,于是就又跳到另一个任务函数里。

    在这里插入图片描述

    2.要明白切到任务函数里面的原理

    之前创建任务时,已经把任务函数保存在了任务栈内。

    出栈的话,psp指向的栈里面剩下的东西,会加载到寄存器里面,如下图所示:那么任务函数地址就给到pc指针了,那么异常返回之后,程序就跳到任务函数的地方继续运行,那么就切到任务函数里了。

    在这里插入图片描述

    展开全文
  • 一、CortexM3中断优先级 CortexM3支持多达240个外部中断和16个内部中断,每一个中断都对应一个中断都对应一个...PendSV用于任务切换; 对于实时操作系统而言,我们一般外部中断优先得到响应,所以SysTick和PendSV的

    一、CortexM3中断优先级

    CortexM3支持多达240个外部中断和16个内部中断,每一个中断都对应一个中断都对应一个优先级寄存器。每一个优先级寄存器占用8位,STM32采用其中的高四位来表示优先级,低四位不可用。
    在这里插入图片描述
    在这里插入图片描述

    FreeRtos一共会使用到三种中断:SysTick、SVC、PendSV。

    • SVC在启动任务调度的时候使用;
    • SysTIck定时器用于周期性的中断,为系统提供心跳;
    • PendSV用于任务切换;

    对于实时操作系统而言,我们一般外部中断优先得到响应,所以SysTick和PendSV的优先级通常设置为最低。

        /* 使 PendSV and SysTick 的优先级最低. */
        portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
        portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
    

    portNVIC_PENDSV_PRI表示(0xf<<4)<<16,portNVIC_SYSTICK_PRI表示(0xf<<4)<<24,刚好将PendSV和SysTick优先级寄存器的最高4位全部置一(CortexM3的优先级寄存器值越大优先级越低)。

    二、PendSV

    SVC(系统服务调用,亦简称系统调用)和 PendSV(可悬起系统调用),它们多用于在操作系统之上的软件开发中。 SVC产生的中断必须立即得到响应,否则将触发硬Fault。PendSV是可悬挂的系统调用,如果有更高优先级的中断产生,PendSV中断会挂起,直到更高优先级的中断处理完成。
    悬起 PendSV 的方法是: 手工往 NVIC 的 PendSV 悬起寄存器中写 1。

    假设某个OS系统中存在比SysTick优先级更低的中断,那么当低优先级的IRQ在执行时会被SysTick打断,并在SyStick中断中执行上下文切换。由于执行上下文切换的时间在真实系统中所需要的时间是不可知的,所以低优先级的中断将会被延时执行。这种行为在任何一种实时操作系统中都是不能容忍的,在CortexM3中如果 OS 在某中断活跃时尝试切入线程模式,将触发fault 异常。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wr3mswWP-1611658781428)(en-resource://database/616:1)]
    为解决此问题,早期的 OS 大多会检测当前是否有中断在活跃中,只有没有任何中断需要响应时,才执行上下文切换(切换期间无法响应中断)。然而,这种方法的弊端在于,它可以把任务切换动作拖延很久(因为如果抢占了 IRQ,则本次 SysTick 在执行后不得作上下文切换,只能等待下一次 SysTick 异常),尤其是当某中断源的频率和 SysTick 异常的频率比较接近时,会发生“共振”。
    现在好了, PendSV 来完美解决这个问题了。PendSV 异常会自动延迟上下文切换的请求,直到其它的 ISR 都完成了处理后才放行。为实现这个机制,需要把 PendSV 编程为最低优先级的异常。在FreeRtos中,每一次进入SysTick中断,系统都会检测是否有新的进入就绪态的任务需要运行,如果有,则悬挂PendSV异常, 以便缓期执行上下文切换。如图 7.17 所示

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nrHpZdKc-1611658781431)(en-resource://database/618:1)]

    关于任务切换的内容,强烈建议参考《CortexM3权威指南》第9章内容。

    三、FreeRtos任务切换的两种场景

    FreeRtos任务切换有两种场景:

    1. 在SysTick定时器中监测是否有新的就绪态任务需要运行,如果有则进行任务切换;
    void xPortSysTickHandler( void )
    {
        /* The SysTick runs at the lowest interrupt priority, so when this interrupt
         * executes all interrupts must be unmasked.  There is therefore no need to
         * save and then restore the interrupt mask value as its value is already
         * known. */
        portDISABLE_INTERRUPTS();
        {
            /* Increment the RTOS tick. */
            if( xTaskIncrementTick() != pdFALSE )
            {
                /* A context switch is required.  Context switching is performed in
                 * the PendSV interrupt.  Pend the PendSV interrupt. */
                portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
            }
        }
        portENABLE_INTERRUPTS();
    }
    

    从上面的代码中可以看出,FreeRtos在进入SysTick中断后会屏蔽所有的其它中断,如果我们不使用用PendSV而在SysTick中断中来进行任务切换,那么SysTick中断会占用无法预知的时间,即使其它中断的优先级高于Systick,也依然要等到SysTick中断执行结束,从而导致系统发生不可预知的异常。
    任务的被动切换很依赖于每一个任务中调用的系统延时或者阻塞,如果某个较高优先级的任务一直不停的运行,比如你在最高优先级的任务中写入了如下代码:

    while(1);
    

    那么你的系统有可能就会一直卡死在里面,或者不停的触发看门狗。
    2. 用户主动调用portYIELD函数进行任务切换

    /* Scheduler utilities. */
        #define portYIELD()                                 \
        {                                                   \
            /* Set a PendSV to request a context switch. */ \
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
            __DSB();                                        \
            __ISB();                                        \
        }
    

    从代码中可以看出,这两种任务切换方式的原理一样,都是向PendSV中断寄存器写1,触发一次PendSV中断。接下来我们看下PendSV中断函数:

    xPortPendSVHandler:
    	mrs r0, psp
    	isb
    	/* Get the location of the current TCB. */
    	ldr	r3, =pxCurrentTCB
    	ldr	r2, [r3]
    
    	/* Is the task using the FPU context?  If so, push high vfp registers. */
    	tst r14, #0x10
    	it eq
    	vstmdbeq r0!, {s16-s31}
    
    	/* Save the core registers. */
    	stmdb r0!, {r4-r11, r14}
    
    	/* Save the new top of stack into the first member of the TCB. */
    	str r0, [r2]
    
    	stmdb sp!, {r0, r3}
    	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
    	msr basepri, r0
    	dsb
    	isb
    	bl vTaskSwitchContext
    	mov r0, #0
    	msr basepri, r0
    	ldmia sp!, {r0, r3}
    
    	/* The first item in pxCurrentTCB is the task top of stack. */
    	ldr r1, [r3]
    	ldr r0, [r1]
    
    	/* Pop the core registers. */
    	ldmia r0!, {r4-r11, r14}
    
    	/* Is the task using the FPU context?  If so, pop the high vfp registers
    	too. */
    	tst r14, #0x10
    	it eq
    	vldmiaeq r0!, {s16-s31}
    
    	msr psp, r0
    	isb
    	#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
    		#if WORKAROUND_PMU_CM001 == 1
    			push { r14 }
    			pop { pc }
    		#endif
    	#endif
    
    	bx r14
    

    博主汇编知识有限,所以就不一一介绍每句代码的含义了,感兴趣的同学可以自行百度。这里只介绍vTaskSwitchContext函数。vTaskSwitchContext的核心任务是找到当前处于就绪态的最高优先级的任务,代码如下:

    if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
        {
            /* 如果当前任务调度器处于挂起状态- 不允许任务切换 */
            xYieldPending = pdTRUE;
        }
        else
        {
            xYieldPending = pdFALSE;
            traceTASK_SWITCHED_OUT();
    
            //此处省略部分代码
    
            /* 使用c或者汇编代码选择最高优先级的就绪态任务作为下一个将要运行的任务*/
            taskSELECT_HIGHEST_PRIORITY_TASK(); 
    
        }
    

    接下来我们看下taskSELECT_HIGHEST_PRIORITY_TASK函数是如何寻找最高优先级的任务的。

        #define taskSELECT_HIGHEST_PRIORITY_TASK()           \
        {                                                                     \
            UBaseType_t uxTopPriority;                                \
                                                                              \
            /* Find the highest priority list that contains ready tasks. */                         \
            portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );                          \
            configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
            listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );   \
        } /* taskSELECT_HIGHEST_PRIORITY_TASK() */
    

    首先我们看下portGET_HIGHEST_PRIORITY函数,这个函数的作用是返回系统中最高有优先级任务的优先级值,其源码为:

            #define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities )    uxTopPriority = ( 31UL - ( ( uint32_t ) __CLZ( ( uxReadyPriorities ) ) ) )
    

    __CLZ是一条汇编指令,用于计算最高符号位与第一个1之间的0的个数。比如( ( uint32_t ) __CLZ( ( 0x00FFFFF0 ) ) )得到的值为8。
    由于相同优先级的任务可能会存在多个,所以接下来便需要从就绪任务列表中找到位于链表最前面的优先级,将其赋值给pxCurrentTCB。至此,vTaskSwitchContext函数分析完毕。

    FreeRtos带注释源码Gitee地址:https://gitee.com/zBlackShadow/FreeRtos10.4.3.git

    展开全文
  • FreeRtos学习笔记(10)任务切换原理刨析 STM32 单片机启动流程中介绍了SP和PC寄存器, STM32单片机bootloader扫盲中说过如何通过控制SP和PC寄存器从而控制程序从bootLoader跳转到APP,RTOS任务切换和BootLoader与...

    FreeRtos学习笔记(10)任务切换原理刨析

    STM32 单片机启动流程中介绍了SP和PC寄存器,
    STM32单片机bootloader扫盲中说过如何通过控制SP和PC寄存器从而控制程序从bootLoader跳转到APP,RTOS任务切换和BootLoader与APP之间的跳转类似,也是通过控制SP和PC指针实现任务之间跳转。

    MSP和PSP

    在中断服务函数使用MSP作为堆栈指针,如果工程中没有特殊设置(即非RTOS工程)整个工程都会默认使用MSP。如果工程使用了RTOS,则除了中断服务函数外,其他任务使用PSP作为堆栈指针。

    Cortex‐M3 拥有两个堆栈指针,然而它们是banked,因此任一时刻只能使用其中的一个。

    主堆栈指针(MSP):复位后缺省使用的堆栈指针,用于操作系统内核以及异常处理例程(包括中断服务例程)

    进程堆栈指针(PSP):由用户的应用程序代码使用。

    堆栈指针的最低两位永远是 0,这意味着堆栈总是 4 字节对齐的。 在 ARM 编程领域中,凡是打断程序顺序执行的事件,都被称为异常(exception)。除了外部中断外,当有指令执行了“非法操作”,或者访问被禁的内存区间,因各种错误产生的 fault,以及不可屏蔽中断发生时,都会打断程序的执行,这些情况统称为异常。在不严格的上下文中,异常与中断也可以混用。另外,程序代码也可以主动请求进入异常状态的(常用于系统调用)。

    为什么堆栈指针有两个?

    1. 可以将用户应用程序的堆栈与特权级/操作系统内核(kernel)的堆栈分开,阻止用户程序访问内核的堆栈,消除了内核数据被破坏的可能。(举个例子,windos系统下,一个软件卡死并不会使整个windos操作系统卡死)
    2. 可以使RTOS实现任务间“可抢占的系统调用”,大幅提高实时性能(中断前使用PSP,进入中断服务函数后会自动使用MSP,在中断中修改PSP值,退出中断服务函数后SP会自动切换到PSP,而PSP的值在中断中修改过,退出中断时会根据新的PSP, POP出PC寄存器及其他寄存器值,从而完成任务切换)

    MSP和PSP之间如何切换?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-agGdomIW-1635424021495)(https://note.youdao.com/yws/res/788/WEBRESOURCEce164f885d4f75a261f561bd1b2c2229)]

    M3权威指南中指出MSP和PSP之间的切换有两种方法:

    • 在特权级线程模式下写CONTROL[1]
    • 在中断服务函数结束时修改LR寄存器(R14),下图为LR寄存器低四位所代表的含义

    在这里插入图片描述

    FreeRtos中就是通过修改LR寄存器值实现从MSP切换到PSP的。

    上电后默认使用MSP,然后进行外设初始化,创建任务,最后调用vTaskStartScheduler()启动RTOS,在vTaskStartScheduler()中会调用xPortStartScheduler()函数,xPortStartScheduler()函数中会调用port.c中的prvPortStartFirstTask();启动第一个任务。

    prvPortStartFirstTask()为一个汇编函数,主要功能就是触发SVC中断

    static void prvPortStartFirstTask( void )
    {
        __asm volatile (
            " ldr r0, =0xE000ED08 	\n"/* Use the NVIC offset register to locate the stack. */
            " ldr r0, [r0] 			\n"
            " ldr r0, [r0] 			\n"
            " msr msp, r0			\n"/* Set the msp back to the start of the stack. */
            " cpsie i				\n"/* Globally enable interrupts. */
            " cpsie f				\n"
            " dsb					\n"
            " isb					\n"
            " svc 0					\n"/* 触发SVC异常,在SVC中断服务函数中启动第一个任务. */
            " nop					\n"
            " .ltorg				\n"
            );
    }
    

    SVC中断服务函数-- vPortSVCHandler()也是一个汇编函数,主要干了两件事,恢复任务现场(也就是将任务栈中保存的寄存器值POP到对应寄存器);将MSP切换为PSP;汇编语句具体含义可以对照M3权威指南中第四章指令集自行翻译。

    void vPortSVCHandler( void )
    {
        __asm volatile (
            "	ldr	r3, pxCurrentTCBConst2		\n"/* Restore the context. */
            "	ldr r1, [r3]					\n"/* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
            "	ldr r0, [r1]					\n"/* The first item in pxCurrentTCB is the task top of stack. */
            "	ldmia r0!, {r4-r11}				\n"/* 将R4-R11的值从栈中弹出到对应寄存器,其他寄存器编译器会自动弹出 */
            "	msr psp, r0						\n"/* Restore the task stack pointer. */
            "	isb								\n"
            "	mov r0, #0 						\n"
            "	msr	basepri, r0					\n"
            "	orr r14, #0xd					\n"/* 返回线程模式,并使用线程堆栈(SP从MSP切换到PSP) */
            "	bx r14							\n"
            "									\n"
            "	.align 4						\n"
            "pxCurrentTCBConst2: .word pxCurrentTCB				\n"
            );
    }
    

    任务之间如何切换?

    任务之间切换时需要保存现场,以便下次跳转回来时可以恢复现场,继续执行。所谓的现场就是内核的寄存器,而保存现场就是将寄存器组的当前值PUSH到栈中保存,恢复现场就是将栈中保存的寄存器值POP到对应寄存器,下图为cortex-M3寄存器组。

    在这里插入图片描述

    FreeRtos的任务切换在PendSV中断服务函数中完成的,该中断服务函数在port.c中。

    void xPortPendSVHandler( void )
    {
        /* This is a naked function. */
    
        __asm volatile
        (
        /**************************第一部分保存现场****************************/
            "	mrs r0, psp							\n"
            "	isb									\n"
            "										\n"
            "	ldr	r3, pxCurrentTCBConst			\n"/* R3指向pxCurrentTCBConst,pxCurrentTCBConst指向当前任务控制块 */
            "	ldr	r2, [r3]						\n"
            "										\n"
            "	stmdb r0!, {r4-r11}					\n"/* 保存现场,将R4-R11的值压入栈中. */
            "	str r0, [r2]						\n"/* 由于上面将R4-R11压入栈中,栈顶PSP也会随之改变,这里将新的栈顶PSP存入任务控制块 */
            "										\n"
            "	stmdb sp!, {r3, r14}				\n"/* 将R3入栈,后面还要用的R3,但是后面会调用 vTaskSwitchContext函数,防止vTaskSwitchContext中修改了R3的值 */
            
        /*******************第二部分找到当前就绪任务中优先级最高的****************/
            "	mov r0, %0							\n"
            "	msr basepri, r0						\n"/* 关中断 进入临界区 要修改pxCurrentTCBConst */
            "	bl vTaskSwitchContext				\n"/* 查找就绪任务中优先级最高的任务 把pxCurrentTCBConst指向该任务控制块 */
            "	mov r0, #0							\n"
            "	msr basepri, r0						\n"/* 开中断 */
            
        /**************************第三部分恢复现场****************************/
            "	ldmia sp!, {r3, r14}				\n"
            "										\n"/* 将R3从栈中取出,R3指向pxCurrentTCBConst,但此时pxCurrentTCBConst可能已经在vTaskSwitchContext中修改过了 */
            "	ldr r1, [r3]						\n"
            "	ldr r0, [r1]						\n"
            "	ldmia r0!, {r4-r11}					\n"/* 从栈中POP出R4-R11 */
            "	msr psp, r0							\n"/* 更新psp */
            "	isb									\n"
            "	bx r14								\n"
            "										\n"
            "	.align 4							\n"
            "pxCurrentTCBConst: .word pxCurrentTCB	\n"
            ::"i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY )
        );
    }
    

    xPortPendSVHandler函数大致可以分为三部分:

    1. 保存现场

      进入中断服务函数前,xPSR, PC, LR, R12以及R3‐R0由硬件自动压入适当的堆栈中,而R4-R11就需要我们自行编写代码进行入栈。

    在这里插入图片描述

    1. 找到当前就绪任务中优先级最高的

    怎么找到就绪比较复杂,后面会具体介绍,这里只需要知道pxCurrentTCBConst已经指向了就绪任务中任务优先级最高的任务控制块。

    在这里插入图片描述

    1. 恢复现场
      和保存现场一样,从中断服务函数中退出时,堆栈会自动弹出恢复xPSR, PC, LR, R12以及R3‐R0寄存器的值。R4-R11需要在退出中断服务函数前自行编写代码恢复。

    因此在FreeRtos中想要进行上下文切换(任务切换)只需要触发PendSV中断即可。

    在这里插入图片描述

    任务控制块和任务堆栈

    RTOS的任务都是死循环,每个任务都拥有自己独立的栈,任务的栈从哪里来?
    freeRtos在heap_4.c中声明了一个大数组,创建任务时会根据指定的栈大小从该数组分配一段空间作为该任务的栈。

    在这里插入图片描述

    任务控制块是一个包含任务所有信息的结构体,通过宏定义对内核进行剪裁时,任务控制块内的成员也会有所增减,重要的结构体成员已经添加了注释。任务间的切换主要用到了pxTopOfStack来保存栈顶。至于xStateListItem、xEventListItem、uxPriority和查找就绪任务中优先级最高任务有关,后面会具体介绍。

    typedef struct tskTaskControlBlock       /* The old naming convention is used to prevent breaking kernel aware debuggers. */
    {
        volatile StackType_t * pxTopOfStack; /*< 指向任务的栈顶.任务之间切换需要用到 */
    
        #if ( portUSING_MPU_WRAPPERS == 1 )
            xMPU_SETTINGS xMPUSettings; /*< 使用MPU时需要用到. */
        #endif
    
        ListItem_t xStateListItem;                  /*< 状态链表节点,可以将该节点挂在不同状态(就绪、堵塞、挂起)链表中 */
        ListItem_t xEventListItem;                  /*< 链表节点,可以将该节点挂在不同队列链表中,实现队列堵塞等功能 */
        UBaseType_t uxPriority;                     /*< 任务优先级 */
        StackType_t * pxStack;                      /*< 指向任务栈起始位置 */
        char pcTaskName[ configMAX_TASK_NAME_LEN ]; /*< 任务名字. */
    
        #if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
            StackType_t * pxEndOfStack; /*< Points to the highest valid address for the stack. */
        #endif
    
        #if ( portCRITICAL_NESTING_IN_TCB == 1 )
            UBaseType_t uxCriticalNesting; /*< Holds the critical section nesting depth for ports that do not maintain their own count in the port layer. */
        #endif
    
        #if ( configUSE_TRACE_FACILITY == 1 )
            UBaseType_t uxTCBNumber;  /*< Stores a number that increments each time a TCB is created.  It allows debuggers to determine when a task has been deleted and then recreated. */
            UBaseType_t uxTaskNumber; /*< Stores a number specifically for use by third party trace code. */
        #endif
    
        #if ( configUSE_MUTEXES == 1 )
            UBaseType_t uxBasePriority; /*< The priority last assigned to the task - used by the priority inheritance mechanism. */
            UBaseType_t uxMutexesHeld;
        #endif
    
        #if ( configUSE_APPLICATION_TASK_TAG == 1 )
            TaskHookFunction_t pxTaskTag;
        #endif
    
        #if ( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
            void * pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
        #endif
    
        #if ( configGENERATE_RUN_TIME_STATS == 1 )
            uint32_t ulRunTimeCounter; /*< Stores the amount of time the task has spent in the Running state. */
        #endif
    
        #if ( configUSE_NEWLIB_REENTRANT == 1 )
    
            /* Allocate a Newlib reent structure that is specific to this task.
             * Note Newlib support has been included by popular demand, but is not
             * used by the FreeRTOS maintainers themselves.  FreeRTOS is not
             * responsible for resulting newlib operation.  User must be familiar with
             * newlib and must provide system-wide implementations of the necessary
             * stubs. Be warned that (at the time of writing) the current newlib design
             * implements a system-wide malloc() that must be provided with locks.
             *
             * See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
             * for additional information. */
            struct  _reent xNewLib_reent;
        #endif
    
        #if ( configUSE_TASK_NOTIFICATIONS == 1 )
            volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
            volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
        #endif
    
        /* See the comments in FreeRTOS.h with the definition of
         * tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE. */
        #if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) /*lint !e731 !e9029 Macro has been consolidated for readability reasons. */
            uint8_t ucStaticallyAllocated;                     /*< Set to pdTRUE if the task is a statically allocated to ensure no attempt is made to free the memory. */
        #endif
    
        #if ( INCLUDE_xTaskAbortDelay == 1 )
            uint8_t ucDelayAborted;
        #endif
    
        #if ( configUSE_POSIX_ERRNO == 1 )
            int iTaskErrno;
        #endif
    } tskTCB;
    
    展开全文
  • 前言 FreeRTOS是一个是实时内核,任务是程序执行的最小单位,也是调度器处理的基本单位,移植了FreeRTOS,则避免不了对任务的管理,在多个任务运行的时候,任务切换显得尤为重要。而任务切换的效率会决定了系统的...
  • FreeRTOS中的任务切换

    2022-06-07 20:26:15
    FreeRTOS中的任务切换
  • 默认情况下,FreeRTOS使用固定优先级抢占式调度策略,并对同等优先级的任务进行循环时间切片: "固定优先级"意味着调度程序不会一直更改任务的优先级,但是由于优先级继承,它可能会暂时提高任务的优先级。 ...
  • FreeRTOS工作原理

    2018-11-05 10:54:49
    备注:资源清晰度有所欠缺,但是不影响观看,内容对于初学者来说,理解FreeRTOS是非常很有帮助。...2. 任务的上下文切换,以一个实际的例子说明多任务运行机制; 3. 举了一个实例,说明RTOS运行机制;
  • 1.Cortex-M4中SysTick的重要性 2.Cortex-M4中的中断管理 3.Cortex-M4中影子栈指针 4.Cortex-M4中SVC和PendSV异常 5.多任务启动 6.PendSV业务流程 7.系统时钟节拍详解 8.SysTick中断服务函数 9.SysTick任务调度
  • 嵌入式操作系统RTOS任务调度原理分析,基于Cortex-M内核
  • FreeRTOS的调度原理

    千次阅读 多人点赞 2020-03-29 21:29:22
    以个人理解,FreeRTOS内核调度的本质是利用了从异常中断返回时,切换任务栈的机制,使得进入新的任务下进行执行任务,实现内核调度功能。 内核进入第一个空闲任务分析 内核版本:FreeRTOS V9.0.0 硬件平台:STM32...
  • FreeRTOS任务调度

    2022-07-23 19:42:53
    时间片调度(不同优先级):每个任务都有相同的优先级,任务会运行固定的时间片个数或者遇到阻塞式的API函数,比如vTaskDelay,才会执行同优先级任务之间的任务切换。 2. 抢占式调度器 在实际的应用中,不同的任务...
  • FreeRTOS基本原理

    千次阅读 2020-07-09 16:26:04
    一个常规的处理器只能在某一时刻执行一个任务,但是多任务操作系统通过快速的任务切换,可以让多个任务看起来是并发执行,如下。 调度原理 调用度是内核中负责决定在某一时刻该执行什么任务的部分。内核可以挂起...
  • 文章目录任务控制块数据结构任务创建函数定义就绪表就绪表初始化启动调度器 任务控制块数据结构 任务控制块数据结构在task.c声明 typedef struct tskTaskControlBlock { volatile StackType_t * pxTopOfStack; //...
  • FreeRTOS任务管理

    千次阅读 2022-07-01 10:42:03
    FreeRTOS任务管理系统,任务的创建、删除、挂起和恢复等操作。
  • 继上一篇后,今天看了下运行过程中的任务切换。然后让我觉得我上一篇对于从PCXI读取地址的理解是错误的,我之前理解PCXI读取值时候会将它释放move到FCX,其实这个理解是错误的。PCXI为当前上下文,可以通过指针方式...
  • FreeRTOS任务调度机制

    千次阅读 2022-04-24 23:06:43
    FreeRTOS任务调度 不同于前后台轮询系统,FreeRTOS正是使用任务调度机制来选择某个任务运行。那么到底如何选择任务,就需要根据任务的优先级的状态来决定。 文章目录FreeRTOS任务调度1 【优先级与任务状态】1.1 ...
  • 主要介绍了任务管理方面的基础知识,包括前后台与多任务系统、协程的基本特性、任务任务状态、任务优先级、任务控制块和任务相关的全局变量。本篇重点介绍了任务控制块和任务相关的全局变量。
  • freertos任务调度简介

    2021-03-28 17:13:37
    1.listLIST_IS_EMPTY()查找任务就绪列表数组pxReadyTasksLists[]中的最高优先级任务列表是否为空,为空即没有列表项,没有列表项即没有线程。为空的情况几乎是不可能出现的,所以这个查找算法只是存在可能的时间不...
  • 现在创建任务(xTaskCreate)、启动调度器(vTaskStartScheduler),任务控制(xTaskDelay),以及Tick 中断(xPortSysTickHandler),都分析完成了,SysTick,PendSV 中断已经使能,接下来第一个任务便可以自由的奔跑...
  • 一般的RTOS,例如FreeRTOS、UCOS、OneOS等等操作系统无论发生任务切换还是挂起任务等操作都是先获取相应的任务控制块然后做相应的处理。 **任务入口函数:***实现的功能在任务函数中执行。 项目实现的功能都在任务...
  • [导读] 学习梳理一下FreeRTOS任务管理单元实现思路,代码分析基于V10.4.3。从本文开始计划写个图解freeRTOS内核系列笔记分享给朋友们,希望大家喜欢。文章中或有错误,也请留...
  • 在前面几篇文章我们已经对FreeRTOS任务API和任务调度原理进行了相对深入的分析 这篇文章主要针对任务与任务之间的交互,信息传递相关的API组件进行分析
  • 浅析FreeRTOS_v4.5.0的任务切换原理和栈结构文章来源:http://gliethttp.cublog.cn[转载请声明出处]FreeRTOS的更新速度很快,基本2、3个月就会出现一个新版本,看来一直在完善和各种功能增加中,截止2007/09/27日为止,...
  • 一、学习FreeROTS 1.Free 即免费的,RTOS 全称是 Real Time Operating System,中文就是实时操作系统。注意,RTOS 不是指某一个确定的系统,而是指一类系统。...任务调度在各个任务之间的切换非常快,就给人们造成了
  • FreeRTOS原理剖析:任务调度器启动

    千次阅读 2019-10-08 06:52:35
    介绍了任务调度器启动的过程。
  • FreeRTOS创建任务(1)

    2021-10-18 09:06:44
    一、任务基础知识 在STM32F4中以前的单片机裸机(未使用系统)的时候一般都是在mai函数中用循环来处理所有事物,循环调用相应的函数完成事物的处理。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 695
精华内容 278
关键字:

freertos任务切换原理