源码请在https://github.com/ifreecoding/MbedRtos.git下载
本节中新增的重要内容已经介绍的差不多了,下面我们来看看使用tick中断的任务上下文切换过程。
在本节中,tick中断被配置为使用Timer1产生IRQ中断。产生tick中断时,硬件会自动将PC指针指向IRQ中断向量表,IRQ中断向量里存放的是一条跳转指令,会跳转到IRQ中断服务函数,这个中断服务函数需要由我们来编写,它的功能就是备份、恢复寄存器组。在Mindows里我们使用MDS_TickContextSwitch函数来实现这个功能,在start.s文件里需要把中断向量表的IRQ中断服务函数修改为MDS_TickContextSwitch。
由于C函数会自动生成入栈出栈的指令,任务切换时调用C函数会破坏寄存器组中的数据,因此,MDS_TickContextSwitch函数需要使用汇编语言来写。备份、恢复寄存器组的原理与Wanlix中讲述是一样的,由于是在IRQ中断中执行的,实现的细节上还是有区别的,我们来看代码:
00013 .func MDS_TickContextSwitch
00014 MDS_TickContextSwitch:
00015
00016 @保存接口寄存器
00017 STMDB R13!, {R0 - R3, R12, R14}
00018
00019 @调用C语言TICK中断处理函数
00020 LDR R0, =MDS_TickIsr
00021 MOV R14, PC
00022 BX R0
00023
00024 @保存当前任务的堆栈信息
00025 LDR R0, =gpstrCurTaskSpAddr
00026 LDR R14, [R0]
00027 MRS R0, SPSR
00028 STMIA R14!, {R0}
00029 LDMIA R13!, {R0 - R3, R12}
00030 STMIA R14, {R0 - R14}^
00031 ADD R14, R14, #0x3C
00032 LDMIA R13!, {R0}
00033 STMIA R14, {R0}
00034
00035 @任务调度完毕, 恢复将要运行任务现场
00036 LDR R0, =gpstrNextTaskSpAddr
00037 LDR R14, [R0]
00038 LDMIA R14, {R0}
00039 MSR SPSR, R0
00040 LDMIB R14, {R0 - R14}^
00041 NOP
00042 ADD R14, R14, #0x40
00043 LDMIA R14, {R14}
00044 SUBS PC, R14, #4
00045
00046 .endfunc
00017行,保存切换前任务的寄存器。本函数在第22行会调用MDS_TickIsr函数,在2.3节中我们已经介绍了AAPCS参数传递的规则,MDS_TickIsr函数可能会破坏接口寄存器,因此在本函数中需要保存R0~R3、R12寄存器。其余的寄存器,本函数在备份它们之前并没有破坏它们的数据,因此不需要备份。这段代码是在IRQ模式下执行的,此时的SP是SPIRQ,数据被保存到IRQ的栈中。
00020~00022行,执行MDS_TickIsr函数。MDS_TickIsr函数是使用C语言写的函数,这个函数里才是真正的IRQ中断处理函数,如果不使用操作系统,这个函数是应该直接挂到IRQ中断向量表上的。MDS_TickIsr函数会判断产生IRQ的中断源,根据IRQ不同的中断源走不同的程序分支,其中包括产生tick中断的Timer1中断。MDS_TickIsr函数最终调用了MDS_TaskSched函数,做任务切换前的准备工作,包括获取将要运行的最高优先级任务,将切换前任务和切换后任务的寄存器组所在的地址存入全局变量gpstrCurTaskSpAddr和gpstrNextTaskSpAddr中等操作。
00025行,将全局变量gpstrCurTaskSpAddr的地址放入R0,gpstrCurTaskSpAddr的内容是切换前任务的寄存器组存放在栈中的地址。
00026行,将切换前任务的寄存器组存放在栈中的地址赋给LR寄存器,注意,这段程序是在IRQ中断中执行的,这个LR是LRIRQ。
00027行,将SPSRIRQ也就是CPSRUSR的值存入R0。
00028行,将R0的内容存入LRIRQ指向的地址,也就是将CPSRUSR存入切换前任务的寄存器组所在栈中的第一个地址,也就是将切换前任务的CPSRUSR存入到切换前任务TCB中的寄存器组中的CPSR的位置,这条指令就是备份寄存器组中的CPSR。
00029行,从IRQ堆栈中恢复R0~R3和R12寄存器,执行完这条指令,USR模式下的R0~R14都已经恢复为中断发生那一时刻的值了。
00030行,将USR模式下的R0~R14备份到栈中的寄存器组中对应的位置。这条指令的第一个参数LR是LRIRQ,指向的是切换前任务的栈中寄存器组的R0所在位置,第二个参数里的LR是LRUSR,因为这条指令里有“^”符号,这个我们在2.2节中介绍过。
00031行,将LRIRQ更新到栈中的寄存器组的PC所在位置。
00032行,从IRQ栈中取出中断发生时的LRIRQ。在17行时向IRQ栈存入了6个寄存器,在29行取出了5个,剩下的最后一个LRIRQ在这里取出,它的值就是中断返回时的PC值。
00033行,将LRIRQ存入切换前任务的栈中寄存器组的PC位置,也就是将中断返回的PC值存入栈中寄存器组的PC位置。
自此,备份寄存器组的工作已经完成。
00036行,将全局变量gpstrNextTaskSpAddr的地址放入R0,gpstrNextTaskSpAddr的内容是切换后任务的寄存器组存放在栈中的地址。
00037行,将切换后任务的寄存器组存放在栈中的地址赋给LRIRQ寄存器。
00038行,从切换后任务的栈中寄存器组取出CPSR的值,存入R0。
00039行,将CPSR的值赋给SPSRIRQ,这就是恢复CPSR寄存器的过程,但真正恢复CPSR的操作还需要从IRQ模式返回到USR模式时才能执行,那时候硬件会自动将SPSRIRQ中数值恢复到CPSR中。
00040行,从栈中寄存器组中恢复USR模式下的R0~R14寄存器,此时,USR模式下的R0~R14已经全部恢复为该任务上次切出去时候的值了。
00041行,NOP,指令空闲一个周期什么也不做。别小看这个NOP指令,当初就因为没有这条指令Mindows程序老是跑飞,卡住了我好几天,最后才定位到这里,但又找不到原因。观察41行和42行,这两行指令里都使用了LR寄存器,而且是LRUSR和LRIRQ连在一起使用的,估计可能是芯片内部指令总线上起了冲突才导致程序跑飞的,最后加了这条NOP指令程序才正常,但一直不知道为什么,没有查到相关的资料,哪位知道的话请到论坛上反馈一下,多谢!求真相!
00042行,将LRIRQ更新到切换后任务的栈中的寄存器组的PC所在位置。
00043行,将返回的PC值存入LRIRQ。
00044行,跳转到USR模式下中断前的下条指令继续执行,同时将SPSRIRQ中的数值恢复到CPSR中。IRQ中断发生时,会将刚执行完的指令的地址+8存入到LRIRQ中,当中断返回时,需要执行的是+4地址的指令,因此,从IRQ返回时需要使用SUBS指令将再LRIRQ-4存入到PC中,这样才能正好跳转到中断前的下条指令继续执行。这个过程在参考资料2中有描述。
下面我们来跟踪一个tick中断产生的过程,用以了解Mindows任务的调度流程,见图37:
MDS_TickIsr、MDS_TaskTick函数比较简单,MDS_TaskSwitch函数与Wanlix中的这个函数没有本质的区别,这3个函数就不介绍了。我们来看看任务调度函数MDS_TaskSched:
00209 void MDS_TaskSched(void)
00210 {
00211 M_TCB* pstrTcb;
00212 M_TCBQUE* pstrTaskQue;
00213 U8 ucTaskPrio;
00214
00215
00216 ucTaskPrio = MDS_TaskHighestPrioGet(&gstrReadyTab.strFlag);
00217 pstrTaskQue = (M_TCBQUE*)MDS_ChainEmpInq(&gstrReadyTab.astrChain[ucTaskPrio]);
00218 pstrTcb = pstrTaskQue->pstrTcb;
00219
00220
00221 MDS_TaskSwitch(pstrTcb);
00222 }
00216行,从ready表标志中获取最高的优先级。
00217行,获取最高优先级对应的链表根节点,并将根节点的M_CHAIN型指针强制转换成M_TCBQUE型指针。
00218行,通过M_TCBQUE指针类型获取任务TCB的指针。
00221行,调用MDS_TaskSwitch函数,做寄存器组备份、恢复前的准备工作。
由于增加了ready表,在创建任务时需要对相关的变量进行初始化,MDS_TaskCreate函数代码如下:
00015 M_TCB* MDS_TaskCreate(VFUNC vfFuncPointer, U8* pucTaskStack, U32 uiStackSize,
00016 U8 ucTaskPrio)
00017 {
00018 M_TCB* pstrTcb;
00019
00020
00021 if(NULL == vfFuncPointer)
00022 {
00023
00024 return (M_TCB*)NULL;
00025 }
00026
00027
00028 if((NULL == pucTaskStack) || (0 == uiStackSize))
00029 {
00030
00031 return (M_TCB*)NULL;
00032 }
00033
00034
00035 if(MDS_RootTask == vfFuncPointer)
00036 {
00037
00038 if(NULL != gpstrRootTaskTcb)
00039 {
00040 return (M_TCB*)NULL;
00041 }
00042 }
00043
00044
00045 pstrTcb = MDS_TaskTcbInit(vfFuncPointer, pucTaskStack, uiStackSize, ucTaskPrio);
00046
00047 return pstrTcb;
00048 }
00015行,函数返回值是新创建任务的TCB指针,若为NULL,代表创建任务失败,其它值则为创建任务成功,为新任务的TCB指针。入口参数ucTaskPrio是新创建任务的优先级,其它入口参数与以前一致,没有变化。
00021~00025行,入口参数判断,若创建任务的函数指针为NULL,返回失败。
00028~00032行,入口参数判断,若创建任务的堆栈不合法,返回失败。
00035~00042行,若重复创建根任务,则返回失败。根任务只能创建一次,在操作系统初始函数MDS_SystemVarInit里会将根任务的TCB指针gpstrRootTaskTcb初始化为NULL,当根任务创建成功后gpstrRootTaskTcb就被更改为根任务的TCB值,即便根任务运行结束后gpstrRootTaskTcb中保持的TCB值仍然不变,在创建任务时可根据gpstrRootTaskTcb来判断根任务是否被重复创建。
00045行,初始化任务的TCB。
00047行,任务创建成功,返回任务的TCB指针。
建立任务时的初始化操作主要集中在MDS_TaskTcbInit函数里面,由于寄存器组被放到了TCB里面,因此MDS_TaskStackInit函数也由MDS_TaskTcbInit函数调用,来看一下MDS_TaskTcbInit函数的代码:
00059 M_TCB* MDS_TaskTcbInit(VFUNC vfFuncPointer, U8* pucTaskStack, U32 uiStackSize,
00060 U8 ucTaskPrio)
00061 {
00062 M_TCB* pstrTcb;
00063 M_CHAIN* pstrChain;
00064 M_CHAIN* pstrNode;
00065 M_PRIOFLAG* pstrPrioFlag;
00066 U8* pucStackBy4;
00067
00068
00069 pucStackBy4 = (U8*)(((U32)pucTaskStack + uiStackSize) & 0xFFFFFFFC);
00070
00071
00072 pstrTcb = (M_TCB*)(((U32)pucStackBy4 - sizeof(M_TCB)) & 0xFFFFFFFC);
00073
00074
00075 MDS_TaskStackInit(pstrTcb, vfFuncPointer);
00076
00077
00078 pstrTcb->strTcbQue.pstrTcb = pstrTcb;
00079
00080
00081 pstrTcb->ucTaskPrio = ucTaskPrio;
00082
00083 pstrChain = &gstrReadyTab.astrChain[ucTaskPrio];
00084 pstrNode = &pstrTcb->strTcbQue.strQueHead;
00085 pstrPrioFlag = &gstrReadyTab.strFlag;
00086
00087
00088 (void)MDS_IntLock();
00089
00090
00091 MDS_TaskAddToSchedTab(pstrChain, pstrNode, pstrPrioFlag, ucTaskPrio);
00092
00093
00094 (void)MDS_IntUnlock();
00095
00096 return pstrTcb;
00097 }
00059行,函数返回值是新创建任务的TCB指针,若为NULL,代表创建任务失败,其它值则为创建任务成功,为新任务的TCB指针。
00075行,初始化任务栈。
00078行,将任务TCB指针赋给TCB队列中的指针变量。
00081行,将任务的优先级保存到TCB中。
00083行,根据任务优先级,获取ready中同等的优先级链表根节点指针。
00084行,获取TCB中可加入ready链表的链表结构指针。
00085行,获取ready表的标志结构指针。
00088行,锁Timer1中断,防止执行下面的代码时发生任务调度。在91行会对ready表进行操作,修改它的链表和标志,这个过程是要分几个步骤进行的,ready表及其上面挂接的各个任务节点对所有任务都是同时可见的,如果在这过程中切换到了其它任务,而其它任务也来对ready表进行操作,那么ready表就可能会因为多个任务同时修改它的数据而导致数据异常。因此,我们要防止这种情况发生,必须保证每次只有一个任务对ready操作,当这个任务对ready表操作完成之后才能允许其它任务再次操作ready表。为了实现这个功能,我们可以使用MDS_IntLock函数将tick中断锁住,这样就不能产生tick中断了,因此也就不会发生任务调度了,因此也就保证了只有一个任务可以对ready进行操作,保证了ready表操作的串行性。在ready表操作完之后,需要调用MDS_IntUnlock函数打开tick中断,这样操作系统又可以继续进行任务调度了。注意,锁tick中断的过程中操作系统丧失了任务调度功能,因此锁tick中断的时间一定要尽可能的短。不但是tick中断,不但是在操作系统状态下,任何情况下,我们都需要将任何锁中断的时间做的尽可能的短。
00091行,将新建立的任务挂接到ready表对应的优先级链表中,新建立的任务处于ready态,准备运行。
00094行,解锁中断,恢复任务调度功能。
00096行,任务创建成功,返回新建任务的TCB指针。
前面说过进入IRQ时,存入LRIRQ的是刚执行完的指令地址+8,在退出中断时需要使用SUBS指令将LRIRQ-4存入PC寄存器,但在USR模式下函数调用时是没有这种+8和-4操作的,而Mindows操作系统从非操作系统状态转换为操作系统状态正是在USR模式下依靠函数调用实现的,开始运行第一个任务MDS_RootTask,但其它的任务第一次调用时则是依靠IRQ中断开始调度的,因此,在初始化寄存器组中的PC值时要分别对待,来看栈初始化函数MDS_TaskStackInit的代码:
00011 void MDS_TaskStackInit(M_TCB* pstrTcb, VFUNC vfFuncPointer)
00012 {
00013 STACKREG* pstrRegSp;
00014
00015 pstrRegSp = &pstrTcb->strStackReg;
00016
00017
00018 pstrRegSp->uiCpsr = MODE_USR;
00019 pstrRegSp->uiR0 = 0;
00020 pstrRegSp->uiR1 = 0;
00021 pstrRegSp->uiR2 = 0;
00022 pstrRegSp->uiR3 = 0;
00023 pstrRegSp->uiR4 = 0;
00024 pstrRegSp->uiR5 = 0;
00025 pstrRegSp->uiR6 = 0;
00026 pstrRegSp->uiR7 = 0;
00027 pstrRegSp->uiR8 = 0;
00028 pstrRegSp->uiR9 = 0;
00029 pstrRegSp->uiR10 = 0;
00030 pstrRegSp->uiR11 = 0;
00031 pstrRegSp->uiR12 = 0;
00032 pstrRegSp->uiR13 = (U32)pstrTcb;
00033 pstrRegSp->uiR14 = 0;
00034
00035
00036 if(MDS_RootTask != vfFuncPointer)
00037 {
00038 pstrRegSp->uiR15 = (U32)vfFuncPointer + 4;
00039 }
00040 else
00041 {
00042 pstrRegSp->uiR15 = (U32)vfFuncPointer;
00043 }
00044 }
00011~00033行,初始化寄存器组中的CPSR、R0~R14,与以前一致,没有变化。
00036~00039行,判断创建的若不是root任务,则将创建任务所用函数的指针+4存入寄存器组中PC的位置。若创建的不是root任务,需要由IRQ中断开始调度,在IRQ中断返回时会使用SUBS指令将返回的PC地址-4,因此创建非root任务需要将PC多+4。
00042行,创建的任务是root任务,将创建任务所用函数的指针直接存入寄存器组中PC的位置。因为root任务是在USR模式下采用函数调用的方式开始运行的,不需要多+4。
上面介绍了本节中关键部分的代码,其它代码请读者自行参考源代码,这里不再介绍了。接下来,我们使用测试函数来看看本节的成果。
本节只引入了ready表,没有其它可以控制任务调度的方法,一旦最高优先级任务开始运行就无法切换到其它任务了。
我们使用3个测试函数TEST_TestTask1、TEST_TestTask2和TEST_TestTask3,每个函数都是循环执行“打印字符串,延迟时间”的操作。
00015 void TEST_TestTask1(void)
00016 {
00017 while(1)
00018 {
00019 DEV_PutString((U8*)"\r\nTask1 is running!");
00020
00021 DEV_DelayMs(1000);
00022 }
00023 }
00030 void TEST_TestTask2(void)
00031 {
00032 while(1)
00033 {
00034 DEV_PutString((U8*)"\r\nTask2 is running!");
00035
00036 DEV_DelayMs(2000);
00037 }
00038 }
00045 void TEST_TestTask3(void)
00046 {
00047 while(1)
00048 {
00049 DEV_PutString((U8*)"\r\nTask3 is running!");
00050
00051 DEV_DelayMs(3000);
00052 }
00053 }
这3个函数都由MDS_RootTask任务创建,创建后都延迟1秒。
00013 void MDS_RootTask(void)
00014 {
00015
00016 DEV_SoftwareInit();
00017
00018
00019 DEV_HardwareInit();
00020
00021
00022 (void)MDS_TaskCreate((VFUNC)TEST_TestTask1, gaucTask1Stack, TASKSTACK, 3);
00023
00024 DEV_DelayMs(1000);
00025
00026 (void)MDS_TaskCreate((VFUNC)TEST_TestTask2, gaucTask2Stack, TASKSTACK, 2);
00027
00028 DEV_DelayMs(1000);
00029
00030 (void)MDS_TaskCreate((VFUNC)TEST_TestTask3, gaucTask3Stack, TASKSTACK, 0);
00031
00032 DEV_DelayMs(1000);
00033 }
我们将MDS_RootTask任务的优先级设为1,TEST_TestTask1任务的优先级设定为3,TEST_TestTask2任务的优先级设定为2,TEST_TestTask3任务的优先级设定为0,我们按照本节的任务调度方式,从系统上电运行开始,推算一下任务的切换过程,如图38:
t0时刻,系统开始运行。
t1时刻,从非操作系统状态切换到操作系统状态,开始运行MDS_RootTask任务,这时候MDS_RootTask任务处于running态。在MDS_RootTask任务里初始化了tick中断,在tick中断里开始任务调度,此时,ready表中只有MDS_RootTask一个任务。
t2时刻,创建了TEST_TestTask1任务,此时ready表中有MDS_RootTask和TEST_TestTask1共2个任务,现在还是在运行MDS_RootTask任务。MDS_RootTask任务处于running态,TEST_TestTask1任务处于ready态。
t3时刻,tick中断到来,调度任务,由于MDS_RootTask任务比TEST_TestTask1任务的优先级高,因此调度的结果还是执行MDS_RootTask任务。MDS_RootTask任务处于running态,TEST_TestTask1任务处于ready态。
t3~t4之间不断产生tick中断,调度任务,由于MDS_RootTask任务比TEST_TestTask1任务的优先级高,因此调度的结果还是执行MDS_RootTask任务。MDS_RootTask任务处于running态,TEST_TestTask1任务处于ready态。
t4时刻,创建了TEST_TestTask2任务,此时ready表中有MDS_RootTask、TEST_TestTask1和TEST_TestTask2共3个任务,现在还是在运行MDS_RootTask任务。MDS_RootTask任务处于running态,TEST_TestTask1和TEST_TestTask2任务处于ready态。
t5时刻,tick中断到来,调度任务,由于MDS_RootTask任务优先级最高,因此调度的结果还是执行MDS_RootTask任务。MDS_RootTask任务处于running态,TEST_TestTask1和TEST_TestTask2任务处于ready态。
t5~t6之间不断产生tick中断,调度任务,由于MDS_RootTask任务优先级最高,因此调度的结果还是执行MDS_RootTask任务。MDS_RootTask任务处于running态,TEST_TestTask1和TEST_TestTask2任务处于ready态。
t6时刻,创建了TEST_TestTask3任务,此时ready表中有MDS_RootTask、TEST_TestTask1、TEST_TestTask2和TEST_TestTask3共4个任务,现在还是在运行MDS_RootTask任务。MDS_RootTask任务处于running态,TEST_TestTask1和TEST_TestTask2和TEST_TestTask3任务处于ready态。
t7时刻,tick中断到来,调度任务,由于TEST_TestTask3任务优先级最高,因此发生任务切换,TEST_TestTask3任务从ready态变为running态,而MDS_RootTask任务则从running态变为ready态,调度的结果变为执行TEST_TestTask3任务了。TEST_TestTask3任务处于running态,MDS_RootTask、TEST_TestTask1和TEST_TestTask2任务处于ready态。
t7之后不断产生tick中断,调度任务,由于TEST_TestTask3任务优先级最高,因此调度的结果还是执行TEST_TestTask3任务。TEST_TestTask3任务处于running态,MDS_RootTask、TEST_TestTask1和TEST_TestTask2任务处于ready态。
TEST_TestTask3任务每隔3秒打印一次,因此,我们最终从串口打印看到的只能是TEST_TestTask3任务每隔3秒一次的打印,而看不到TEST_TestTask1和TEST_TestTask2任务的打印。
下面是本节输出的打印截图:
大家可以到ifreecoding_新浪博客网站下载视频,观看本节的动态打印输出。从视频中可以看到TEST_TestTask3任务每隔3秒打印一次,与我们分析的情况一致。
从图38中可以看到,拥有最高优先级的TEST_TestTask3任务在t6时刻就已经创建了,但还需要等到t7时刻tick中断到来时才能够执行,实时性还是差了一些。实时操作系统的调度周期并非完全是由tick周期决定,在下节我们将了解实时操作系统的另一种调度方式——实时事件触发的随机调度,这种调度方式对提高操作系统的实时性也是有帮助的。