在多任务系统中,我们根据功能的不同,把整个系统分割 成一个个独立的且无法返回的函数,这个函数我们称为任务。
在多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分 配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一 段内存空间,但它们都存在于 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 ] ) );
}
}
接下来创建任务
涉及到静态任务创建函数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了。而且栈是向下生长的,高地址往低地址走。
异常返回的函数在这里,死循环。
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的地址。
调用节点插入链表的尾巴函数,参考链表的实现。把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 )++;
}
启动调度器,成功后不再返回。
手动指定第一个要运行的任务为Task1TCB,进入调度器.设置PendSV和SysTick中断优先级为最低
/*
* 参考资料《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的值也将更新,即指向任务栈的栈顶 */
}
分析代码: 把pxCurrentTCB的地址存到r3寄存器。
ldr r3, =pxCurrentTCB /* 加载pxCurrentTCB的地址到r3 */
分析代码:把r3寄存器的值存到r1
ldr r1, [r3] /* 加载pxCurrentTCB到r1 */
分析代码:把r1寄存器的值存到r0。而此时的r1寄存器的值为Task1TCB的值。Task1TCB结构体第一个成员变量是记录栈顶的变量,因此也是把任务1Task1TCB的栈顶给r0。
ldr r0, [r1] /* 加载pxCurrentTCB指向的值到r0,目前r0的值等于第一个任务堆栈的栈顶 */
分析代码:从r0作为基址开始出栈,恢复寄存器。栈是从高地址往低地址,向下增长的,因此出栈是先把当前栈顶恢复,然后栈顶每次+4,因为是32位4个字节。此时的r0指向了任务栈里存储的旧r0。
ldmia r0!, {r4-r11} /* 以r0为基地址,将栈里面的内容加载到r4~r11寄存器,同时r0会递增 */
分析代码:把r0赋值给psp,psp又是指向栈顶的,准备触发中断返回恢复硬件保存的8个寄存器
msr psp, r0 /* 将r0的值,即任务的栈指针更新到psp */
分析代码:打开中断,向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的值也将更新,即指向任务栈的栈顶 */
任务1死循环,手动调用切换函数taskYIELD()。出发PendSV异常,进入中断服务函数xPortPendSVHandler
#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
分析代码:把pxCurrentTCB的地址存到r3寄存器。
ldr r3, =pxCurrentTCB /* 加载pxCurrentTCB的地址到r3 */
分析代码:取r3寄存器的地址内容赋值给r2。
ldr r2, [r3] /* 加载pxCurrentTCB到r2 */
分析代码:由于r0已经被psp赋值了,而进入中断的时候,xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形 参)这些 CPU 寄存器的值会自动存储到任务的栈中,这8个是硬件自动帮我们做的,而剩下的 r11-r4 需要手动保存寄存器。stmdb将寄存器的值存储到任务栈,执行入栈操作,指针先递减,再压入栈。至此,当前任务的所有寄存器内容已经压入任务栈。
stmdb r0!, {r4-r11} /* 将CPU寄存器r4~r11的值存储到r0指向的地址 */
分析代码:将r2指向的内容即当前任务控制块tcb的第一个成员变量栈顶,之所以是栈顶因为str是一次取四个字节。r0保存了当前任务控制块的栈顶。
str r0, [r2] /* 将任务栈的新的栈顶指针存储到当前任务TCB的第一个成员,即栈顶指针 */
分析代码:
stmdb sp!, {r3, r14} /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,
调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护;
R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护 */
分析代码:进入临界段,宏的值位191,高四位有效,高于11的中断被屏蔽
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY /* 进入临界段 */
分析代码:调用切换函数,切换pxCurrentTCB的指向。
bl vTaskSwitchContext /* 调用函数vTaskSwitchContext,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */
分析代码:退出临界段
mov r0, #0 /* 退出临界段 */
msr basepri, r0
分析代码:恢复r3和r14
ldmia sp!, {r3, r14} /* 恢复r3和r14 */
分析代码:取r3里的内容给r1。r3已经是当前pxCurretnTCB的地址,存的是Task2TCB地址,先把Task2TCB地址赋值给r1
ldr r1, [r3]
分析代码:从r1获取内容给r0,r1已经是Task2TCBd的地址,现在读取4个字节,则是把栈顶的值给r0。此时的r0已经是Task2TCB的栈顶了指向的是上一次保存的r4。
ldr r0, [r1] /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
分析代码:开始恢复Task2TCB之前保存的寄存器,从r4开始到r11是需要手动恢复保存的寄存器内容。此时r0递增到指向Task2TCB任务栈保存的r0的位置。
ldmia r0!, {r4-r11} /* 出栈 */
分析代码:此时的r0指向地址更新到psp栈顶。
msr psp, r0
分析代码:psp记录到Task2TCB任务栈存储的r0位置。开始由硬件自动从任务栈恢复r0,r1,r2,r3,r13,r14,r15,xPSR八个寄存器,任务切换成功。
bx r14 /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、
使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,
然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,
当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/
总结一下
配置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。任务切换成功,周而复始。