uCOS在任务切换时做了什么以及任务切换汇编代码分析

通过最简单的任务切换函数讲解,工程使用《[野火®]《uCOS-III内核实现与应用开发实战指南—基于STM32》》第5章节的工程。以下所说的地址自己做时可能有所不同。
先说明几个任务相关全局变量:

#define  TASK1_STK_SIZE       20
#define  TASK2_STK_SIZE       20

static   CPU_STK   Task1Stk[TASK1_STK_SIZE]; //Task1Stk[0]地址为0x20000028;Task1Stk[TASK1_STK_SIZE]地址为0x20000078
static   CPU_STK   Task2Stk[TASK2_STK_SIZE];//Task2Stk[0]地址为0x20000078;Task2Stk[TASK2_STK_SIZE]地址为0x200000c8

static   OS_TCB    Task1TCB;//Task1TCB的地址为0x20000008
static   OS_TCB    Task2TCB;//Task2TCB的地址为0x20000010

void     Task1( void *p_arg );//任务入口地址为0x00000401
void     Task2( void *p_arg );//任务入口地址为0x00000492

然后几个操作系统相关的全局变量:

OS_EXT OS_TCB *OSTCBCurPtr; //OSTCBCurPtr的地址为0x20000018
OS_EXT OS_TCB *OSTCBHighRdyPtr;//OSTCBHighRdyPtr的地址为0x2000001c
OS_EXT OS_RDY_LIST OSRdyList[OS_CFG_PRIO_MAX];//OSRdyList[0]的地址为0x200000c8;OSRdyList[0]的地址为0x200000d0
OS_EXT OS_STATE OSRunning;//不关注

创建任务时发生了什么?(注:把上面全局变量的地址写到一张纸上面,后面更加好分析一些)

OSTaskCreate ((OS_TCB*)      &Task1TCB, 
	              (OS_TASK_PTR ) Task1, 
	              (void *)       0,
	              (CPU_STK*)     &Task1Stk[0],
	              (CPU_STK_SIZE) TASK1_STK_SIZE,
	              (OS_ERR *)     &err);

经过这个OSTaskCreate 创建任务的函数之后,Task1TCB.StkPtr指向了Task1空闲栈的栈顶,Task1TCB.StkSize则是记录栈的大小。以下进行详细分析:

void OSTaskCreate(OS_TCB *p_tcb,
	              OS_TASK_PTR p_task,
                   void          *p_arg,
                   CPU_STK       *p_stk_base,
                   CPU_STK_SIZE  stk_size,
                   OS_ERR        *p_err)
{
     
	CPU_STK *p_sp;
	p_sp = OSTaskStkInit(p_task,p_arg,p_stk_base,stk_size);//返回值就是剩下空闲栈的栈顶
	p_tcb ->StkPtr = p_sp;//让Task1TCB.StkPtr指向剩下空闲栈的栈顶
	p_tcb->StkSize = stk_size;//Task1TCB.StkSize记录栈的大小
	*p_err = OS_ERR_NONE;
}

再对OSTaskStkInit()函数进行分析:

CPU_STK *OSTaskStkInit (OS_TASK_PTR  p_task,
                        void         *p_arg,
                        CPU_STK      *p_stk_base,
                        CPU_STK_SIZE stk_size)
{
     
	CPU_STK  *p_stk;

	p_stk = &p_stk_base[stk_size];
	/* 异常发生时自动保存的寄存器*/
	*--p_stk = (CPU_STK)0x01000000u;/* xPSR的bit24必须置1*/
	*--p_stk = (CPU_STK)p_task;     /* 任务的入口地址*/
	*--p_stk = (CPU_STK)0x14141414u;/* R14 (LR)*/
	*--p_stk = (CPU_STK)0x12121212u;/* R12*/
	*--p_stk = (CPU_STK)0x03030303u;/* R3*/
	*--p_stk = (CPU_STK)0x02020202u;/* R2*/
	*--p_stk = (CPU_STK)0x01010101u;/* R1*/
	*--p_stk = (CPU_STK)p_arg;      /* R0 : 任务形参*/
	/* 异常发生时需手动保存的寄存器*/
	*--p_stk = (CPU_STK)0x11111111u;/* R11*/
	*--p_stk = (CPU_STK)0x10101010u;/* R10*/
	*--p_stk = (CPU_STK)0x09090909u;/* R9*/
	*--p_stk = (CPU_STK)0x08080808u;/* R8*/
	*--p_stk = (CPU_STK)0x07070707u;/* R7*/
	*--p_stk = (CPU_STK)0x06060606u;/* R6*/
	*--p_stk = (CPU_STK)0x05050505u;/* R5*/
	*--p_stk = (CPU_STK)0x04040404u;/* R4*/
	return (p_stk);
}

其中自动保存和手动保存的寄存器一定要有个映像。
再回到先前OSTaskCreate()任务创建函数,通过这个函数创建任务之后,Task1TCB.StkPtr指向了Task1空闲栈的栈顶,Task1TCB.StkSize则是记录栈的大小(重复一下)
而任务1和任务2中的堆栈空间则变成了以下所示:

//Task1Stk空间
0x20000078:&Task1Stk[TASK1_STK_SIZE];堆栈的栈顶,记住这个地址

0x20000074:0x01000000u;/* xPSR的bit24必须置1*/
0x20000070:0x00000401  /* 任务的入口地址*/
0x2000006c:0x14141414u;/* R14 (LR)*/
0x20000068:0x12121212u;/* R12*/
0x20000064:0x03030303u;/* R3*/
0x20000060:0x02020202u;/* R2*/
0x2000005c:0x01010101u;/* R1*/
0x20000058:p_arg;      /* R0 : 任务形参*/需要保存的R4-R11寄存器的栈顶,记住这个值

0x20000054:0x11111111u;/* R11*/
0x20000050:0x10101010u;/* R10*/
0x2000004c:0x09090909u;/* R9*/
0x20000048:0x08080808u;/* R8*/
0x20000044:0x07070707u;/* R7*/
0x20000040:0x06060606u;/* R6*/
0x2000003c:0x05050505u;/* R5*/
0x20000038:0x04040404u;/* R4*/空闲栈的栈顶,记住这个值
0x20000034:
0x20000030:
0x2000002c:
0x20000028:
//Task2Stk空间
0x200000C8:&Task2Stk[TASK2_STK_SIZE];堆栈的栈顶,记住这个地址

0x200000C4:0x01000000u;/* xPSR的bit24必须置1*/
0x200000C0:0x00000492  /* 任务的入口地址*/
0x200000Bc:0x14141414u;/* R14 (LR)*/
0x200000B8:0x12121212u;/* R12*/
0x200000B4:0x03030303u;/* R3*/
0x200000B0:0x02020202u;/* R2*/
0x200000Ac:0x01010101u;/* R1*/
0x200000A8:p_arg;      /* R0 : 任务形参*/需要保存的R4-R11寄存器的栈顶,记住这个值

0x200000A4:0x11111111u;/* R11*/
0x200000A0:0x10101010u;/* R10*/
0x2000009c:0x09090909u;/* R9*/
0x20000098:0x08080808u;/* R8*/
0x20000094:0x07070707u;/* R7*/
0x20000090:0x06060606u;/* R6*/
0x2000008c:0x05050505u;/* R5*/
0x20000088:0x04040404u;/* R4*/空闲栈的栈顶,记住这个值
0x20000084:
0x20000080:
0x2000007c:
0x20000078:

所以说:
Task1TCB.StkPtr经过任务创建函数之后指向0x20000038;本身地址为0x20000008;
Task2TCB.StkPtr经过任务创建函数之后指向0x20000088;本身地址为0x20000010;

OSRdyList[0].HeadPtr = &Task1TCB;//指向Task1TCB(0x20000008),OSRdyList[0]本身地址为0x200000c8
OSRdyList[1].HeadPtr = &Task2TCB;//指向Task2TCB(0x20000010),OSRdyList[1]本身地址为0x200000D0

之后进行任务启动,如下所示:

void OSStart(OS_ERR *p_err)
{
     
	if( OSRunning == OS_STATE_OS_STOPPED )
	{
     
		OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
		//OSTCBHighRdyPtr 将指向&Task1TCB(0x20000008),本身的地址为0x2000001c
		//OSTCBCurPtr 指向(OS_TCB*)0,本身的地址为0x20000018
		/* 启动任务切换,不会返回 */
		OSStartHighRdy();
		/* 不会运行到这里,运行到这里表示发生了致命的错误 */
		*p_err = OS_ERR_FATAL_RETURN;
	}
	else
	{
     
		*p_err = OS_STATE_OS_RUNNING;
	}
}

OSStartHighRdy()函数分析如下:

OSStartHighRdy
	LDR		R0, = NVIC_SYSPRI14              ; 设置  PendSV 异常优先级为最低
	;此时寄存器R0的为 NVIC_SYSPRI14( 0xE000ED22
	LDR     R1, = NVIC_PENDSV_PRI
	;此时寄存器R1为 NVIC_PENDSV_PRI( 0x000000FF
	STRB    R1, [R0]
	;R1中的值作为变量,R0中的值作为地址,然后将R1写入NVIC_SYSPRI14(0xE000ED22)地址中
	;即成功设置了 PendSV 异常优先级为最低
	
	MOVS    R0, #0                           ; 设置psp的值为0,开始第一次上下文切换
	;此时R0寄存器为0
	MSR     PSP, R0
	;将寄存器R0中的值移入PSP寄存器当中,此时PSP寄存器中的值为0
	
	LDR     R0, =NVIC_INT_CTRL               ; 触发PendSV异常
	;此时寄存器R0中的值为 NVIC_INT_CTRL(0xE000ED04)
	LDR     R1, =NVIC_PENDSVSET
	;此时寄存器R1中的值为 NVIC_PENDSVSET(0x10000000)
	STR     R1, [R0]
	;将R1中的值作为变量,R0中的值作为地址,然后将R1写入NVIC_INT_CTRL(0xE000ED04)地址中
	;即触发PendSV异常
	
	CPSIE   I                                 ; 开中断
	;打开中断,将运行到PendSV中断服务函数
OSStartHang
	B       OSStartHang                       ; 程序应永远不会运行到这里	

LDR 表示从存储器中加载字到一个寄存器中;
STRB 把一个寄存器的低字节存储到存储器(即地址)中
STR 把一个寄存器按字存储到存储器(即地址)中
B 跳转到函数
PendSV中断服务函数如下所示:(其中,语句右边的注释为野火注释,语句下方的注释为本人理解)

PendSV_Handler
; 任务的保存,即把CPU寄存器的值存储到任务的堆栈中	
	CPSID   I                                 ; 关中断,NMI和HardFault除外,防止上下文切换被中断	
	MRS     R0, PSP                           ; 将psp的值加载到R0
	CBZ     R0, OS_CPU_PendSVHandler_nosave   ; 判断R0,如果值为0则跳转到OS_CPU_PendSVHandler_nosave
	;第一次进入,此时PSP的值为0,所以直接跳转到OS_CPU_PendSVHandler_nosave函数执行
	                                          ; 进行第一次任务切换的时候,R0肯定为0
	
	/*; 在进入PendSV异常的时候,当前CPU的xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0会自动存储到当前任务堆栈,同时递减PSP的值
	STMDB   R0!, {R4-R11}                     ; 手动存储CPU寄存器R4-R11的值到当前任务的堆栈
	
	LDR     R1, = OSTCBCurPtr                 ; 加载 OSTCBCurPtr 指针的地址到R1,这里LDR属于伪指令
	LDR     R1, [R1]                          ; 加载 OSTCBCurPtr 指针到R1,这里LDR属于ARM指令
	STR     R0, [R1]                          ; 存储R0的值到	OSTCBCurPtr->OSTCBStkPtr,这个时候R0存的是任务空闲栈的栈顶*/

; 任务的切换,即把下一个要运行的任务的堆栈内容加载到CPU寄存器中
OS_CPU_PendSVHandler_nosave  
	                                    ; OSTCBCurPtr = OSTCBHighRdyPtr;
	LDR     R0, = OSTCBCurPtr           
	;此时寄存器R0为&OSTCBCurPtr(0x20000018),我把它理解为R0指向OSTCBCurPtr;2级指针?
	LDR     R1, = OSTCBHighRdyPtr       
	;此时寄存器R1为&OSTCBHighRdyPtr(0x2000001C),我把它理解为R1指向OSTCBHighRdyPtr
	LDR     R2, [R1]                         
	;取R1寄存器所指向的地址中的值,赋值给R2;
	;此时,R1指向&OSTCBHighRdyPtr指向&Task1TCB(0x20000008),所以R2为&Task1TCB(0x20000008);即R2指向&Task1TCB
	STR     R2, [R0]                 
	;将R2中的值,存储到R0所指向的地址中,即将&Task1TCB(0x20000008)写入R0所指向的地址&OSTCBCurPtr(0x20000018)
	;即之后R0指向OSTCBCurPtr作为指针指向Task1TCB,也就是OSTCBCurPtr=&Task1TCB
	;此时R2中的地址依旧为&Task1TCB(0x20000008)     
	
	LDR     R0, [R2]
	;取出R2中地址中存放的值,加载到R0
	;此时R2中的地址依旧为&Task1TCB(0x20000008),即取出&Task1TCB(0x20000008)中的值加载到R0
	;Task1TCB(0x20000008)地址中的值为Task1TCB.StkPtr(0x20000038)(不加&表示指向的地址),也就是Task1Stk堆栈的空闲栈的栈顶
	;即R0指向了Task1Stk堆栈的空闲栈的栈顶Task1TCB.StkPtr(0x20000038)                          
	LDMIA   R0!, {
     R4-R11}                     
	;将R0地址中的内容出栈,4字节自增依次加载到R4-R11寄存器中
	;此时R0指向地址0x20000058,之后的内容就是退出中断会自动加载到寄存器中
	;但是出栈指针不是R0,而是PSP,所以后面还要把R0存储的值赋值给堆栈指针PSP,让PSP指向那个地址(0x20000058)
	
	MSR     PSP, R0                           ; 更新PSP的值,这个时候PSP指向下一个要执行的任务的堆栈的栈底(这个栈底已经加上刚刚手动加载到CPU寄存器R4-R11的偏移)
	ORR     LR, LR, #0x04                     ; 确保异常返回使用的堆栈指针是PSP,即LR寄存器的位2要为1
	CPSIE   I                                 ; 开中断
	BX      LR                                ; 异常返回,这个时候任务堆栈中的剩下内容将会自动加载到xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
	                                          ; 同时PSP的值也将更新,即指向任务堆栈的栈顶。在STM32中,堆栈是由高地址向低地址生长的。
	;进行BX这一步之后,会按照跳转加载到xPSR,PC(任务入口地址),R14......寄存器中的PC指针的函数去运行,也就是Task1任务
	;此时,堆栈指针PSP指向0x20000078(即Task1Stk堆栈的栈顶),等待下次触发PendSV中断
	;自动将xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0寄存器中的值压入 Task1Stk堆栈数组中   
	
	NOP                                       ; 为了汇编指令对齐,不然会有警告
	
	
	END                                       ; 汇编文件结束

之后执行Task1任务,如下:

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

之后进行任务切换,通过执行OSSched();

void OSSched(void)
{
     
	if(OSTCBCurPtr == OSRdyList[0].HeadPtr)
	{
     
		OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
	}
	else
	{
     
		OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
	}
	//到了这一步之后OSTCBCurPtr =&Task1TCB(0x20000008);
	//OSTCBHighRdyPtr  = &Task2TCB(0x20000010);
	//PSP指针指向0x20000078(即Task1Stk堆栈的栈顶)
	OS_TASK_SW();
}

之后进入PendSV中断之后,xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0根据PSP指向的地址自动入栈,同时PSP自减,等到所有自动入栈的寄存器全部入栈之后,PSP指向0x20000058,需要手动保存R4-R11寄存器的栈顶位置。见代码如下:

PendSV_Handler
; 任务的保存,即把CPU寄存器的值存储到任务的堆栈中	
	CPSID   I                                 ; 关中断,NMI和HardFault除外,防止上下文切换被中断	
	MRS     R0, PSP                           ; 将psp的值加载到R0
	;PSP指向0x20000058,经过这一步之后,R0指向0x20000058
	CBZ     R0, OS_CPU_PendSVHandler_nosave   ; 判断R0,如果值为0则跳转到OS_CPU_PendSVHandler_nosave
	;顺序执行,不跳转到OS_CPU_PendSVHandler_nosave   
	                                          
	
	; 在进入PendSV异常的时候,当前CPU的xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0会自动存储到当前任务堆栈,同时递减PSP的值
	STMDB   R0!, {
     R4-R11}                     ; 手动存储CPU寄存器R4-R11的值到当前任务的堆栈
	;{
     R4-R11}寄存器的值手动压入0x20000058指向的地址中,即保存到Task1的堆栈中
	;同时R0寄存器中的值递减,全部入栈之后R0中的值为0x20000038,即指向空闲栈的栈顶
	
	LDR     R1, = OSTCBCurPtr                 
	;R1寄存器指向&OSTCBCurPtr(0x20000018),而OSTCBCurPtr =&Task1TCB(0x20000008)
	LDR     R1, [R1]                          
	;将R1指向的地址中的值拿出来,再赋值给R1,即将&OSTCBCurPtr(0x20000018)中的值Task1TCB(0x20000008)
	;拿出来,再赋值给R1
	STR     R0, [R1]                          
	;将R0寄存器中的值存储到到R1所指向的地址Task1TCB(0x20000008)
	;即让Task1TCB.StkPtr指向0x20000038,即指向空闲栈的栈顶

; 任务的切换,即把下一个要运行的任务的堆栈内容加载到CPU寄存器中
OS_CPU_PendSVHandler_nosave  
	; OSTCBCurPtr = OSTCBHighRdyPtr;
	LDR     R0, = OSTCBCurPtr                 
	;R0指向&OSTCBCurPtr(0x20000018)
	LDR     R1, = OSTCBHighRdyPtr             
	;R1指向&OSTCBHighRdyPtr(0x2000001C)
	LDR     R2, [R1]                          
	;将R1所指向的地址中的内容赋值到R2,即将OSTCBHighRdyPtr  = &Task2TCB(0x20000010);&Task2TCB(0x20000010)赋值给寄存器R2
	STR     R2, [R0]                          
	;将R2中的值加载到R0所指向的地址中,即让OSTCBCurPtr = &Task2TCB(0x20000010)
	
	LDR     R0, [R2]                          
	;将R2所指向的地址中的内容赋值给R2,即R2指向&Task2TCB(0x20000010)指向空闲栈的栈顶,
	;参考上面保存上文时空闲栈的栈顶保存在任务控制块的地址中
	;也就是说将&Task2TCB(0x20000010)地址中的内容0x20000088地址赋值给R0
	LDMIA   R0!, {
     R4-R11}                    
	;将R0所指向的地址0x20000088中的内容出栈到R4-R11寄存器中,同时,R0所指向的地址将同步递增
	;全部出栈之后,R0所指向的地址为0x200000A8,即自动出栈的栈底
	
	MSR     PSP, R0                           
	;将R0所指向的地址赋值给PSP,当退出中断时,将从此地址中自动出栈,PSP的值同步递增,等待下一次任务切换时
	;将自动压入堆栈的寄存器中的值自动压入PSP地址中
	ORR     LR, LR, #0x04                     ; 确保异常返回使用的堆栈指针是PSP,即LR寄存器的位2要为1
	CPSIE   I                                 ; 开中断
	BX      LR                                ; 异常返回,这个时候任务堆栈中的剩下内容将会自动加载到xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
	                                          ; 同时PSP的值也将更新,即指向任务堆栈的栈顶。在STM32中,堆栈是由高地址向低地址生长的。
	
	NOP                                       ; 为了汇编指令对齐,不然会有警告
	
	
	END                                       ; 汇编文件结束

通过这一个过程之后,任务切换时保存任务当前的运行环境,并将下一个任务的运行环境加载出来,实现了任务的切换。

你可能感兴趣的:(uCOS-III,C语言,堆栈)