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指向