freertos内核 任务定义与切换 原理分析

freertos内核 任务定义与切换 原理分析,第1张

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

直观表述这个函数内部:

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

    
    vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
    
	listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );

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

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

任务栈初始化

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

StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
    
	pxTopOfStack--;
	*pxTopOfStack = portINITIAL_XPSR;	                                    
	pxTopOfStack--;
	*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;	
	pxTopOfStack--;
	*pxTopOfStack = ( StackType_t ) prvTaskExitError;	                    
	pxTopOfStack -= 5;	
	*pxTopOfStack = ( StackType_t ) pvParameters;	                        
    
        
	pxTopOfStack -= 8;	

	
    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):每一位只有两种状态0或1。计算机能表示的最小数据单位。
字节(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;
						  

//__asm void xPortPendSVHandler( void )函数里
ldr	r3, =pxCurrentTCB		
ldr	r2, [r3]         
					  
stmdb r0!, {r4-r11}			
str r0, [r2]                	

经过上面这个代码,现在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

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被恢复,而且MSP从0x20000550变成了0x20000558。

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

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

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

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

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

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指针了,那么异常返回之后,程序就跳到任务函数的地方继续运行,那么就切到任务函数里了。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5714426.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-18
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存