3.5 任务切换
当调用OSStartHighRdy()函数,触发PendSV异常后,就需要编写PendSV异常服务函数,然后在其中进行任务切换。PendSV异常服务函数具体参见代码清单3-27。PendSV异常服务函数名称必须与启动文件向量表中PendSV的向量名一致,如果不一致,则内核无法响应用户编写的PendSV异常服务函数,只响应启动文件中默认的PendSV异常服务函数。启动文件中为每个异常都编写好了默认的异常服务函数,函数体都是一个死循环,当发现代码跳转到这些启动文件中默认的异常服务函数时,就要检查异常函数名称是否写错了,是否与向量表中的一致。PendSV_Handler函数中涉及的ARM汇编指令的讲解如表3-3所示。
代码清单3-27 PendSV异常服务函数
1 ;*********************************************************************** 2 ; PendSVHandler异常 3 ;*********************************************************************** 4 PendSV_Handler 5 ; 关中断,NMI和HardFault除外,防止上下文切换被中断 6 CPSID I(1) 7 8 ; 将PSP的值加载到R0 9 MRS R0, PSP(2) 10 11 ; 判断R0,如果值为0,则跳转到OS_CPU_PendSVHandler_nosave 12 ; 进行第一次任务切换时,R0肯定为0 13 CBZ R0, OS_CPU_PendSVHandler_nosave (3) 14 15 ;-------------------------保存上文----------------------------- 16 ; 任务的切换,即把下一个要运行的任务栈的内容加载到CPU寄存器中 17 ;-------------------------------------------------------------- 18 ; 进入PendSV异常时,当前CPU的xPSR、PC(任务入口地址)、 19 ; R14、R12、R3、R2、R1、R0会自动存储到当前任务栈, 20 ;同时递减PSP的值,可通过代码:MRS R0, PSP 把PSP的值传给R0 21 22 ; 手动存储CPU寄存器R4~R11的值到当前任务栈 23 STMDB R0!, {R4-R11}(15) 24 25 26 ; 加载 OSTCBCurPtr 指针的地址到R1,这里LDR属于伪指令 27 LDR R1, = OSTCBCurPtr(16) 28 ; 加载 OSTCBCurPtr 指针到R1,这里LDR属于ARM指令 29 LDR R1, [R1](17) 30 ; 存储R0的值到OSTCBCurPtr->OSTCBStkPtr,这时R0存储的是任务空闲栈的栈顶 31 STR R0, [R1] (18) 32 33 ;-------------------------切换下文----------------------------- 34 ; 实现 OSTCBCurPtr = OSTCBHighRdyPtr 35 ; 把下一个要运行的任务栈的内容加载到CPU寄存器中 36 ;-------------------------------------------------------------- 37 OS_CPU_PendSVHandler_nosave (4) 38 39 ; 加载 OSTCBCurPtr 指针的地址到R0,这里LDR属于伪指令 40 LDR R0, = OSTCBCurPtr(5) 41 ; 加载 OSTCBHighRdyPtr 指针的地址到R1,这里LDR属于伪指令 42 LDR R1, = OSTCBHighRdyPtr(6) 43 ; 加载 OSTCBHighRdyPtr 指针到R2,这里LDR属于ARM指令 44 LDR R2, [R1](7) 45 ; 存储 OSTCBHighRdyPtr 到 OSTCBCurPtr 46 STR R2, [R0](8) 47 48 ; 加载 OSTCBHighRdyPtr 到 R0 49 LDR R0, [R2](9) 50 ; 加载需要手动保存的信息到CPU寄存器R4~R11 51 LDMIA R0!, {R4-R11}(10) 52 53 ; 更新PSP的值,这时PSP指向下一个要执行的任务栈的栈底 54 ;(这个栈底已经加上刚刚手动加载到CPU寄存器R4~R11的偏移) 55 MSR PSP, R0(11) 56 57 ; 确保异常返回使用的栈指针是PSP,即LR寄存器的位2要为1 58 ORR LR, LR, #0x04 (12) 59 60 ; 开中断 61 CPSIE I (13) 62 63 ; 异常返回,这时任务栈中剩下的内容将会自动加载到xPSR、 64 ; PC(任务入口地址)、R14、R12、R3、R2、R1、R0(任务的形参) 65 ; 同时PSP的值也将更新,即指向任务栈的栈顶 66 ; 在STM32中,栈是由高地址向低地址生长的 67 BX LR (14)
代码清单3-27中,PendSV异常服务中主要完成两项工作,一是保存上文,即保存当前正在运行的任务的环境参数;二是切换下文,即把下一个需要运行的任务的环境参数从任务栈中加载到CPU寄存器,从而实现任务的切换。
PendSV异常服务中用到了OSTCBCurPtr和OSTCBHighRdyPtr这两个全局变量,它们在os.h中定义,要想在汇编文件os_cpu_a.s中使用,必须将这两个全局变量导入os_cpu_a.s中,具体导入方法参见代码清单3-28。
代码清单3-28 导入OSTCBCurPtr和OSTCBHighRdyPtr到os_cpu_a.s
1 ;******************************************************************* 2 ; 全局变量&函数 3 ;******************************************************************* 4 IMPORT OSTCBCurPtr ; 外部文件引入的参考(1) 5 IMPORT OSTCBHighRdyPtr 6 7 EXPORT OSStartHighRdy ; 该文件定义的函数(2) 8 EXPORT PendSV_Handler
代码清单3-28(1):使用IMPORT关键字将os.h中的OSTCBCurPtr和OSTCBHighRdyPtr这两个全局变量导入该汇编文件,从而该汇编文件可以使用这两个变量。如果是函数,也可以使用IMPORT导入。
代码清单3-28(2):使用EXPORT关键字导出该汇编文件中的OSStartHighRdy和PendSV_Handler函数,让外部文件可见。除了使用EXPORT导出外,还要在某个C的头文件中声明这两个函数(在μC/OS-III中是在os_cpu.h中声明),这样才可以在C文件中调用这两个函数。
表3-3 PendSV_Handler函数中涉及的ARM汇编指令
接下来具体讲解代码清单3-27中主要代码的含义。
代码清单3-27(1):关中断,NMI和HardFault除外,防止上下文切换被中断。在上下文切换完毕之后,会重新开中断。
代码清单3-27(2):将PSP的值加载到R0寄存器。MRS是ARM 32位数据加载指令,功能是加载特殊功能寄存器的值到通用寄存器。
代码清单3-27(3):判断R0,如果值为0,则跳转到OS_CPU_PendSVHandler_nosave。进行第一次任务切换时,PSP在OSStartHighRdy初始化为0,所以这时R0肯定为0,因此跳转到OS_CPU_PendSVHandler_nosave。CBZ是ARM 16位转移指令,用于比较,结果为0则跳转。
代码清单3-27(4):当第一次切换任务时,会跳转到这里运行。当执行过一次任务切换之后,则顺序执行到这里。这个标号以后的内容属于下文切换。
代码清单3-27(5):加载OSTCBCurPtr指针的地址到R0。在ARM汇编中,操作变量都属于间接操作,即要先获取这个变量的地址。这里LDR属于伪指令,不是ARM指令。例如“LDR Rd,=label”,如果label是立即数,那么Rd等于立即数;如果label是一个标识符,比如指针,那么存到Rd的就是label这个标识符的地址。
代码清单3-27(6):加载OSTCBHighRdyPtr指针的地址到R1,这里LDR也属于伪指令。
代码清单3-27(7):加载OSTCBHighRdyPtr指针到R2,这里LDR属于ARM指令。
代码清单3-27(8):存储OSTCBHighRdyPtr到OSTCBCurPtr,实现将下一个要运行的任务的TCB存储到OSTCBCurPtr。
代码清单3-27(9):加载OSTCBHighRdyPtr到R0。TCB中第一个成员是栈指针StkPtr,所以这时R0等于StkPtr,后续操作任务栈都是通过操作R0来实现,不需要操作StkPtr。
代码清单3-27(10):将任务栈中需要手动加载的内容加载到CPU寄存器R4~R11,同时会递增R0,让R0指向空闲栈的栈顶。LDMIA中的I是increase的缩写,A是after的缩写,R0后面的“!”表示会自动调节R0中存储的指针。当任务被创建时,任务的栈会被初始化,初始化的流程是先让栈指针StkPtr指向栈顶,然后从栈顶开始依次存储异常退出时会自动加载到CPU寄存器的值和需要手动加载到CPU寄存器的值,具体代码实现参见代码清单3-12中的OSTaskStkInit()函数,栈空间的分布情况如图3-3所示。当把需要手动加载到CPU的栈内容加载完毕之后,栈空间的分布图和栈指针指向如图3-4所示,注意这时StkPtr不变,改变的是R0。
图3-3 任务创建成功后栈空间的分布图
图3-4 手动加载栈内容到CPU寄存器后的栈空间分布图
代码清单3-27(11):更新PSP的值,这时PSP与图3-4中R0的指向一致。
代码清单3-27(12):设置LR寄存器的位2为1,确保异常退出时使用的栈指针是PSP。当异常退出后,就切换到就绪任务中优先级最高的任务继续运行。
代码清单3-27(13):开中断。上下文切换已经完成了3/4,剩下的就是异常退出时自动保存的部分。
代码清单3-27(14):异常返回,这时任务栈中的剩余内容将会自动加载到xPSR、PC(任务入口地址)、R14、R12、R3、R2、R1、R0(任务的形参)寄存器,同时PSP的值也将更新,即指向任务栈的栈顶,这样就切换到了新的任务。这时栈空间的分布具体如图3-5所示。
图3-5 刚切换完成即将运行的任务的栈空间分布和栈指针指向
代码清单3-27(15):手动存储CPU寄存器R4~R11的值到当前任务栈。当出现异常,进入PendSV异常服务函数时,当前CPU寄存器xPSR、PC(任务入口地址)、R14、R12、R3、R2、R1、R0会自动存储到当前任务栈,同时递减PSP的值,这个时候当前任务的栈空间分布如图3-6所示。当执行“STMDB R0!,{R4-R11}”后,当前任务的栈空间分布图如图3-7所示。
图3-6 进入PendSV异常时,当前任务的栈空间分布
图3-7 当前任务执行完上文保存时的栈空间分布
代码清单3-27(16):加载OSTCBCurPtr指针的地址到R1,这里LDR属于伪指令。
代码清单3-27(17):加载OSTCBCurPtr指针到R1,这里LDR属于ARM指令。
代码清单3-27(18):存储R0的值到OSTCBCurPtr->OSTCBStkPtr,这时R0存储的是任务空闲栈的栈顶。执行到了这里,才完成了上文的保存。这时当前任务的栈空间分布和栈指针指向如图3-8所示。
图3-8 当前任务执行完上文保存时的栈空间分布和StkPtr指向