通过最简单的任务切换函数讲解,工程使用《[野火®]《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 ; 汇编文件结束
通过这一个过程之后,任务切换时保存任务当前的运行环境,并将下一个任务的运行环境加载出来,实现了任务的切换。