【FreeRTOS】任务的定义与切换

113 阅读17分钟
在多任务系统中,我们根据功能的不同,把整个系统分割 成一个个独立的且无法返回的函数,这个函数我们称为任务。

image.png

在多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分 配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一 段内存空间,但它们都存在于 RAM 中。

需要事先声明的全局变量,任务句柄,任务控制块,任务栈

/* 任务句柄 */
typedef void * TaskHandle_t;

//栈类型
#define portSTACK_TYPE	uint32_t
typedef portSTACK_TYPE StackType_t;
//任务控制块
typedef struct tskTaskControlBlock
{
	volatile StackType_t    *pxTopOfStack;    /* 栈顶 */

	ListItem_t			    xStateListItem;   /* 任务节点 */
    
    StackType_t             *pxStack;         /* 任务栈起始地址 */
	                                          /* 任务名称,字符串形式 */
	char                    pcTaskName[ configMAX_TASK_NAME_LEN ];  
} tskTCB;
typedef tskTCB TCB_t;
TaskHandle_t Task1_Handle;
#define TASK1_STACK_SIZE                    20
StackType_t Task1Stack[TASK1_STACK_SIZE];
TCB_t Task1TCB;

TaskHandle_t Task2_Handle;
#define TASK2_STACK_SIZE                    20
StackType_t Task2Stack[TASK2_STACK_SIZE];
TCB_t Task2TCB;

TCB结构体如何理解呢,第一个成员pxTopOfStack用来记录栈顶指针,TCB的地址即为第一个成员的地址。第二个成员xStateListItem为链表的节点,通过这个节点可以把该任务控制块挂接到各种链表中。第三个成员以及第四个注释解释很清楚。

初始化任务相关的列表, pxReadyTasksLists[uxPriority] 是任务就绪列表,uxPriority是优先级,默认为5。就绪列表的类型是链表结构体。每一个就绪列表的元素管理挂载在该元素链表下的所有任务节点。 初始化就是便利所有的就绪列表,对每一个元素(链表结构体)初始化,调用vListInitialise(List*const pxList)链表结构体初始化函数。

/* 链表初始化 */
void vListInitialise( List_t * const pxList )
{
	/* 将链表索引指针指向最后一个节点 */
	pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );

	/* 将链表最后一个节点的辅助排序的值设置为最大,确保该节点就是链表的最后节点 */
	pxList->xListEnd.xItemValue = portMAX_DELAY;

    /* 将最后一个节点的pxNext和pxPrevious指针均指向节点自身,表示链表为空 */
	pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );
	pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );

	/* 初始化链表节点计数器的值为0,表示链表为空 */
	pxList->uxNumberOfItems = ( UBaseType_t ) 0U;
}
/* 任务就绪列表 */
List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
/* 初始化任务相关的列表 */
void prvInitialiseTaskLists( void )
{
    UBaseType_t uxPriority;
    
    
    for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
	{
		vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
	}
}

接下来创建任务

image.png

涉及到静态任务创建函数xTaskCreateStatic(...)

TaskHandle_t xTaskCreateStatic(	TaskFunction_t pxTaskCode,           /* 任务入口 */
					            const char * const pcName,           /* 任务名称,字符串形式 */
					            const uint32_t ulStackDepth,         /* 任务栈大小,单位为字 */
					            void * const pvParameters,           /* 任务形参 */
					            StackType_t * const puxStackBuffer,  /* 任务栈起始地址 */
					            TCB_t * const pxTaskBuffer )         /* 任务控制块指针 */
{
	TCB_t *pxNewTCB;
	TaskHandle_t xReturn;

	if( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) )
	{		
		pxNewTCB = ( TCB_t * ) pxTaskBuffer; 
		pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;

		/* 创建新的任务 */
		prvInitialiseNewTask( pxTaskCode,        /* 任务入口 */
                              pcName,            /* 任务名称,字符串形式 */
                              ulStackDepth,      /* 任务栈大小,单位为字 */ 
                              pvParameters,      /* 任务形参 */
                              &xReturn,          /* 任务句柄 */ 
                              pxNewTCB);         /* 任务栈起始地址 */      

	}
	else
	{
		xReturn = NULL;
	}

	/* 返回任务句柄,如果任务创建成功,此时xReturn应该指向任务控制块 */
    return xReturn;
}

解释该函数,五个参数,第一个参数的类型函数指针,TaskFuntion_t类型,返回值为void,参数为void*,

typedef void (*TaskFunction_t)( void * );

第五个参数,任务栈起始地址,传进来的是 (StackType_t *)Task1Stack, 预先就创立的全局变量,早已分配过地址.以及第六个参数任务控制块,但是还没完全初始化赋值,只是声明了,未指定地址。

第一个pxNewTCB为临时TCB变量,从参数pxTaskBuffer赋值给pxNewTCB,从参数puxStackBuffer任务栈起始地址赋值给pxNewTCB的任务栈起始地址。从而实现了一个TCB的伪复制(其实就是通过间接创造一个指针去绑定传进来的TCB)。

然后调用创建新任务函数prvInitialiseNewTask(...)

static void prvInitialiseNewTask( 	TaskFunction_t pxTaskCode,              /* 任务入口 */
									const char * const pcName,              /* 任务名称,字符串形式 */
									const uint32_t ulStackDepth,            /* 任务栈大小,单位为字 */
									void * const pvParameters,              /* 任务形参 */
									TaskHandle_t * const pxCreatedTask,     /* 任务句柄 */
									TCB_t *pxNewTCB );
static void prvInitialiseNewTask( 	TaskFunction_t pxTaskCode,              /* 任务入口 */
									const char * const pcName,              /* 任务名称,字符串形式 */
									const uint32_t ulStackDepth,            /* 任务栈大小,单位为字 */
									void * const pvParameters,              /* 任务形参 */
									TaskHandle_t * const pxCreatedTask,     /* 任务句柄 */
									TCB_t *pxNewTCB )                       /* 任务控制块指针 */

{
	StackType_t *pxTopOfStack;
	UBaseType_t x;	
	
	/* 获取栈顶地址 */
	pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
	//pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
	/* 向下做8字节对齐 */
	pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );	

	/* 将任务的名字存储在TCB中 */
	for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
	{
		pxNewTCB->pcTaskName[ x ] = pcName[ x ];

		if( pcName[ x ] == 0x00 )
		{
			break;
		}
	}
	/* 任务名字的长度不能超过configMAX_TASK_NAME_LEN */
	pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';

    /* 初始化TCB中的xStateListItem节点 */
    vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
    /* 设置xStateListItem节点的拥有者 */
	listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
    
    
    /* 初始化任务栈 */
	pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );   


	/* 让任务句柄指向任务控制块 */
    if( ( void * ) pxCreatedTask != NULL )
	{		
		*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
	}
}

解释一下获取栈顶地址这里, pxTopOfStack的栈顶地址等于栈的起始地址+栈大小-1。那为什么要这么做呢,静态创建的时候是预先分配的内存,任务结束了不删除释放空间,动态分配的才需要删除释放空间。

而任务栈定义成:
#define TASK1_STACK_SIZE                    20
StackType_t Task1Stack[TASK1_STACK_SIZE];
用数组的方式,来得到一块地址连续的空间,这片地址空间就是任务栈。因此不必纠结于每个元素啥啥啥的。

8字节对齐,是考虑到如果出现浮点数运算的话,需要8字节,能够兼容它采用8字节对齐。也就是地址要是8的整数倍,整数倍CPU就可以用一次性的读取全部内容。比如某int变量地址为0x00000003,假定cpu一次可以读4个字节,因此先读0x00-0x03 4个字节,int变量第一个字节都进来了,接着第二次读从0x04-0x07地址读三个字节 这才凑成一个完整的int变量。但是内存4字节对齐后,就可以一次性读完int的内容,提高运行的效率。

(xxx)&(~(0x0007))消除后三位就一定是8的整数倍。

初始化TCB中的任务节点,表明该节点还未挂载在任何一个链表上,同时另该节点的拥有者为自身TCB。


/*
*************************************************************************
*                              任务栈初始化函数
*************************************************************************
*/

static void prvTaskExitError( void )
{
    /* 函数停止在这里 */
    for(;;);
}

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


这一片栈空间因为是当全局变量声明的,因此全部被自动初始化为0了。而且栈是向下生长的,高地址往低地址走。

image.png

异常返回的函数在这里,死循环。

static void prvTaskExitError( void )
{
    /* 函数停止在这里 */
    for(;;);
}

异常发生时,异常发生时,CPU 自动从栈中加载到 CPU 寄存器的内容。包括 8 个寄存器,分别为 R0、R1、R2、R3、R12、R14、R15 和 xPSR 的位 24,且顺序不能变。 需要手动加载到 CPU 寄存器的内容,总共有 8 个, 分别为 R4、R5、R6、R7、R8、R9、R10和 R11,默认初始化为 0。

返回指向空闲栈的栈顶指针给TCB的第一个成员变量,下次就从这个位置恢复寄存器的值。 让任务句柄指向当前的进程控制块TCB,记录和绑定TCB的地址。

image.png

调用节点插入链表的尾巴函数,参考链表的实现。把TCB的节点成员挂载到就绪链表里

void vListInitialise( List_t * const pxList );

/* 链表结构体定义 */
typedef struct xLIST
{
        UBaseType_t uxNumberOfItems;    /* 链表节点计数器 */
        ListItem_t *  pxIndex;			/* 链表节点索引指针 */
        MiniListItem_t xListEnd;		/* 链表最后一个节点 */
} List_t;
/* 将节点插入到链表的尾部 */
void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
{
        ListItem_t * const pxIndex = pxList->pxIndex;

        pxNewListItem->pxNext = pxIndex;
        pxNewListItem->pxPrevious = pxIndex->pxPrevious;
        pxIndex->pxPrevious->pxNext = pxNewListItem;
        pxIndex->pxPrevious = pxNewListItem;

        /* 记住该节点所在的链表 */
        pxNewListItem->pvContainer = ( void * ) pxList;

        /* 链表节点计数器++ */
        ( pxList->uxNumberOfItems )++;
}

启动调度器,成功后不再返回。
image.png

image.png

image.png

手动指定第一个要运行的任务为Task1TCB,进入调度器.设置PendSV和SysTick中断优先级为最低

image.png


/*
 * 参考资料《STM32F10xxx Cortex-M3 programming manual》4.4.3,百度搜索“PM0056”即可找到这个文档
 * 在Cortex-M中,内核外设SCB的地址范围为:0xE000ED00-0xE000ED3F
 * 0xE000ED008为SCB外设中SCB_VTOR这个寄存器的地址,里面存放的是向量表的起始地址,即MSP的地址
 */
__asm void prvStartFirstTask( void )
{
	PRESERVE8

	/* 在Cortex-M中,0xE000ED08是SCB_VTOR这个寄存器的地址,
       里面存放的是向量表的起始地址,即MSP的地址 */
	ldr r0, =0xE000ED08
	ldr r0, [r0]
	ldr r0, [r0]

	/* 设置主堆栈指针msp的值 */
	msr msp, r0
    
	/* 使能全局中断 */
	cpsie i
	cpsie f
	dsb
	isb
	
    /* 调用SVC去启动第一个任务 */
	svc 0  
	nop
	nop
}

进入SVC中断服务函数去调用第一个任务.

__asm void vPortSVCHandler( void )
{
    extern pxCurrentTCB;
    
    PRESERVE8

    ldr	r3, =pxCurrentTCB	/* 加载pxCurrentTCB的地址到r3 */
    ldr r1, [r3]			/* 加载pxCurrentTCB到r1 */
    ldr r0, [r1]			/* 加载pxCurrentTCB指向的值到r0,目前r0的值等于第一个任务堆栈的栈顶 */
    ldmia r0!, {r4-r11}		/* 以r0为基地址,将栈里面的内容加载到r4~r11寄存器,同时r0会递增 */
    msr psp, r0				/* 将r0的值,即任务的栈指针更新到psp */
    isb
    mov r0, #0              /* 设置r0的值为0 */
    msr	basepri, r0         /* 设置basepri寄存器的值为0,即所有的中断都没有被屏蔽 */
    orr r14, #0xd           /* 当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,
                           使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态 */

    bx r14                  /* 异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器:
                           xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
                           同时PSP的值也将更新,即指向任务栈的栈顶 */
}

image.png 分析代码: 把pxCurrentTCB的地址存到r3寄存器。

ldr	r3, =pxCurrentTCB	/* 加载pxCurrentTCB的地址到r3 */	

image.png

image.png

image.png

分析代码:把r3寄存器的值存到r1

ldr r1, [r3]			/* 加载pxCurrentTCB到r1 */

image.png

分析代码:把r1寄存器的值存到r0。而此时的r1寄存器的值为Task1TCB的值。Task1TCB结构体第一个成员变量是记录栈顶的变量,因此也是把任务1Task1TCB的栈顶给r0。

ldr r0, [r1]			/* 加载pxCurrentTCB指向的值到r0,目前r0的值等于第一个任务堆栈的栈顶 */

image.png

image.png

image.png

分析代码:从r0作为基址开始出栈,恢复寄存器。栈是从高地址往低地址,向下增长的,因此出栈是先把当前栈顶恢复,然后栈顶每次+4,因为是32位4个字节。此时的r0指向了任务栈里存储的旧r0。

ldmia r0!, {r4-r11}		/* 以r0为基地址,将栈里面的内容加载到r4~r11寄存器,同时r0会递增 */

image.png

image.png

分析代码:把r0赋值给psp,psp又是指向栈顶的,准备触发中断返回恢复硬件保存的8个寄存器

msr psp, r0				/* 将r0的值,即任务的栈指针更新到psp */

image.png

分析代码:打开中断,向r14寄存器的最后4位按位或0x0D。当从 SVC 中断服务退出前,通过向 r14 寄存器最后 4 位按位或上 0x0D,使得硬件在退出时使用进程堆栈指针 PSP 完成出栈操作并返回后进入任务模式、返 回 Thumb 状态。在 SVC 中断服务里面,使用的是 MSP 堆栈指针,是处在 ARM 状态。当 r14 为 0xFFFFFFFX,执行是中断返回指令,cortext-m3 的做法,X 的 bit0 为 1 表示 返回 thumb 状态,bit1 和 bit2 分别表示返回后 sp 用 msp 还是 psp、以及返回到特权模式还 是用户模式。异常返回,这个时候出栈使用的是 PSP 指针,自动将栈中的剩下 内容加载到 CPU 寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0 (任务的形参)同时 PSP 的值也将更新,即指向任务栈的栈顶。众所周知,出栈是取值sp指针再自增,入栈是sp指针先自减,再把值压入栈。此时由于第一次指定的是Task1TCB的,因此函数入口就是任务1的。

        mov r0, #0              /* 设置r0的值为0 */
	msr	basepri, r0         /* 设置basepri寄存器的值为0,即所有的中断都没有被屏蔽 */
	orr r14, #0xd           /* 当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,
                               使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态 */
    
	bx r14                  /* 异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器:
                               xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
                               同时PSP的值也将更新,即指向任务栈的栈顶 */

image.png

任务1死循环,手动调用切换函数taskYIELD()。出发PendSV异常,进入中断服务函数xPortPendSVHandler

image.png

#define taskYIELD()			portYIELD()
#define portYIELD()																\
{																				\
	/* 触发PendSV,产生上下文切换 */								                \
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;								\
	__dsb( portSY_FULL_READ_WRITE );											\
	__isb( portSY_FULL_READ_WRITE );											\
}
/* 任务1 */
void Task1_Entry( void *p_arg )
{
	for( ;; )
	{
		flag1 = 1;
		delay( 100 );		
		flag1 = 0;
		delay( 100 );
		
		/* 任务切换,这里是手动切换 */
        taskYIELD();
	}
}

PendSV中断服务函数

__asm void xPortPendSVHandler( void )
{
	extern pxCurrentTCB;
	extern vTaskSwitchContext;

	PRESERVE8

    /* 当进入PendSVC Handler时,上一个任务运行的环境即:
       xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
       这些CPU寄存器的值会自动保存到任务的栈中,剩下的r4~r11需要手动保存 */
    /* 获取任务栈指针到r0 */
	mrs r0, psp
	isb

	ldr	r3, =pxCurrentTCB		/* 加载pxCurrentTCB的地址到r3 */
	ldr	r2, [r3]                /* 加载pxCurrentTCB到r2 */

	stmdb r0!, {r4-r11}			/* 将CPU寄存器r4~r11的值存储到r0指向的地址 */
	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       /* 调用函数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
}

分析代码:psp保存的是当前任务的栈顶地址,把当前任务的栈顶地址存到r0

mrs r0, psp

image.png

image.png

分析代码:把pxCurrentTCB的地址存到r3寄存器。

ldr r3, =pxCurrentTCB /* 加载pxCurrentTCB的地址到r3 */

image.png

image.png

image.png

分析代码:取r3寄存器的地址内容赋值给r2。

ldr r2, [r3] /* 加载pxCurrentTCB到r2 */

image.png

分析代码:由于r0已经被psp赋值了,而进入中断的时候,xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形 参)这些 CPU 寄存器的值会自动存储到任务的栈中,这8个是硬件自动帮我们做的,而剩下的 r11-r4 需要手动保存寄存器。stmdb将寄存器的值存储到任务栈,执行入栈操作,指针先递减,再压入栈。至此,当前任务的所有寄存器内容已经压入任务栈。

stmdb r0!, {r4-r11}			/* 将CPU寄存器r4~r11的值存储到r0指向的地址 */

image.png

image.png

分析代码:将r2指向的内容即当前任务控制块tcb的第一个成员变量栈顶,之所以是栈顶因为str是一次取四个字节。r0保存了当前任务控制块的栈顶。

str r0, [r2]                /* 将任务栈的新的栈顶指针存储到当前任务TCB的第一个成员,即栈顶指针 */	

分析代码: image.png

image.png

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

image.png

image.png

分析代码:进入临界段,宏的值位191,高四位有效,高于11的中断被屏蔽

image.png

mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY    /* 进入临界段 */

分析代码:调用切换函数,切换pxCurrentTCB的指向。

bl vTaskSwitchContext /* 调用函数vTaskSwitchContext,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */

image.png

image.png

image.png

image.png

image.png

分析代码:退出临界段

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

分析代码:恢复r3和r14

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

image.png

image.png

分析代码:取r3里的内容给r1。r3已经是当前pxCurretnTCB的地址,存的是Task2TCB地址,先把Task2TCB地址赋值给r1

ldr r1, [r3] 

image.png

image.png

image.png

分析代码:从r1获取内容给r0,r1已经是Task2TCBd的地址,现在读取4个字节,则是把栈顶的值给r0。此时的r0已经是Task2TCB的栈顶了指向的是上一次保存的r4。

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

image.png

image.png

分析代码:开始恢复Task2TCB之前保存的寄存器,从r4开始到r11是需要手动恢复保存的寄存器内容。此时r0递增到指向Task2TCB任务栈保存的r0的位置。

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

分析代码:此时的r0指向地址更新到psp栈顶。

image.png

msr psp, r0

image.png

image.png image.png

1677646276008.jpg image.png

image.png

image.png image.png

分析代码:psp记录到Task2TCB任务栈存储的r0位置。开始由硬件自动从任务栈恢复r0,r1,r2,r3,r13,r14,r15,xPSR八个寄存器,任务切换成功。

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

1677646276008.jpg

总结一下

配置PendSV和SysTick的优先级和中断函数,当任务1要切换的时候,硬件自动保存任务1的xPSR,r15(PC),r14(LR),r13(pc),r3,r2,r1,r0寄存器的值到任务1的栈,手动保存r11-r4寄存器到任务栈,开始切换任务2,把任务2栈里保存的r4-r11出栈恢复到r4-r11寄存器,更新psp,触发中断返回,由硬件自动恢复r0,r1,r2,r3,r13,r14,r15,xPSR。任务切换成功,周而复始。