UCOS-II操作系统详解 下载本文

SR压栈。接着,根据中断服务子程序的复杂程度,选择把R12~R15中的寄存器压栈。然后,执行中断服务子程序。中断处理结束后再把Rx寄存器出栈,SR出栈,PC出栈。把系统恢复到中断前的状态,使程序接着被中断的部分继续运行。

图3 中断发生时的任务栈压栈操作 2.2 任务级和中断级的任务切换步骤和原理 (1)任务级的任务切换原理

μC/OS-II是一个多任务的操作系统,在没有用户自己定义的中断情况下,任务间的切换步骤是这样的:任务间的切换一般会调用OSSched()函数。函数的结构如下:

void OSSched(void){ 关中断

如果(不是中断嵌套并且系统可以被调度){ 确定优先级最高的任务

如果(最高级的任务不是当前的任务){ 调用OSCtxSw(); } } 开中断 }

我们把这个函数称作任务调度的前导函数。它先判断要进行任务切换的条件,如果条件允许进行任务调度,则调用OSCtxSw()。这个函数是真正实现任务调度的函数。由于期间要对堆栈进行操作,所以OSCtxSw()一般用汇编语言写成。它将正在运行的任务的CPU的SR寄存器推入堆栈,然后把R4~R15压栈。接着把当前的SP保存在TCB->OSTCBStkPtr中,然后把最高优先级的TCB->OSTCBStkPtr的值赋值给SP。这时候,SP就已经指到最高优先级任务的任务堆栈了。然后进行出栈工作,把R15~R4出栈。接着使用RETI返回,这样就把SR和PC出栈了。简单地说,μC/OS-II切换到最高优先级的任务,只是恢复最高优先级任务所有的寄存器并运行中断返回指令(RETI),实际上,所作的只是人为地模仿了一次中断。

(2)中断级的任务切换原理

μC/OS-II的中断服务子程序和一般前后台的操作有少许不同,往往需要这样操作: 保存全部CPU寄存器

调用OSIntEnter()或OSIntNesting++ 开放中断 执行用户代码 关闭中断 调用OSIntExit(); 恢复所有CPU寄存器 RETI

OSIntEnter()就是将全局变量OSIntNesting加1。OSIntNesting是中断嵌套层数的变量。μC/OS-II通过它确保在中断嵌套的时候,不进行任务调度。执行完用户的代码后,μC/OS-II调用OSIntExit(),一个与OSSched()很像的函数。在这个函数中,系统首先把OSIntNesting减1,然后判断是否中断嵌套。如果不是的话,并且当前任务不是最高优先级的任务,那么找到优先级最高的任务,执行OSIntCtxSw()这一出中断任务切换函数。因为,在这之前已经做好了压栈工作;在这个函数中,要进行R15~R4的出栈工作。而且,由于在之前调用函数的时候,可能已经有一些寄存器被压入了堆栈。所以要进行堆栈指针的调整,使得能够从正确的位置出栈。

6存在问题编辑

由于μC/OS-II在应用的时候会占用单片机上的一些资源,如系统时钟、RAM、Flash或者ROM,从而减少了用户程序对资源的利用。对于MSP430来说,RAM的占用是特别突出的问题。对于8、16位的单片机来说,片内的RAM容量都很小,MSP430也是如此(最大的片内RAM也只有2KB,例如MSP430F149)。如果使用扩展内存,会大大增加设计难度。

通过对μC/OS-II的分析可以得知,μC/OS-II占用的RAM主要是用在每个任务的TCB、每个任务的堆栈等方面。通过进一步分析,发现任务堆栈大的原因是因为MSP430的硬件设计中没有把中断堆栈和任务堆栈分开。这样就造成了在应用μC/OS-II的时候,考虑每个任务的任务堆栈大小时,不单单需要计算任务中局部变量和函数嵌套层数,还需要考虑中断的最大嵌套层数。因为,对于μC/OS-II原始的中断处理的设计、中断处理过程中的中断嵌套中所需要压栈的

寄存器大小和局部变量的内存大小,都需要算在每个任务的任务堆栈中,则对于每一个任务都需要预留这一部分内存,所以大量的RAM被浪费。从这里可以看出,解决这一问题的直接方法就是把中断堆栈和每个任务自己的堆栈分开。这样,在计算每个任务堆栈的时候,就不需要把中断处理中(包括中断嵌套过程中)的内存的占用计算到每个任务的任务堆栈中,只需要计算每个任务本身需要的内存大小,从而提高了RAM的利用率,可以缓解内存紧张的问题。

在这种设计方案中,中断堆栈区也就是利用原有的MSP430中的系统堆栈区。在前后台的设计形式中,中断中的压栈和出栈的操作都是在系统的堆栈区完成的。基于μC/OS-II的任务切换的原理,我们对于任务堆栈的功能和系统堆栈的功能做了以下划分:任务在运行过程中产生中断和任务切换的时候,PC和SR以及寄存器Rx都保存在各个任务自己的任务堆栈中;而中断嵌套产生的压栈和出栈的操作都是放在系统堆栈中进行的。这种划分方式是基于尽量将中断任务与普通任务分开的思想设计的。

从前面对于IAR EW的默认操作分析来看,堆栈的结构可以有两种。一种是把μC/OS-II的任务堆栈设计成图1所示的形式。这种方法是把编译器默认的压栈操作放在前面,然后再把剩下的寄存器进栈。但是,由于编译器在处理复杂程度不同的中断服务程序的时候,压入栈的寄存器的数量不定,所以会对以后其余寄存器的压栈和出栈操作增加复杂度。这里,我们采用了图2所示的方式生成堆栈。在这种堆栈中,PC和SR压栈后,通过调整SP指针,使得R4~R15寄存器覆盖编译器默认压栈的寄存器。这样,处理的难度会小一点。

7解决方法编辑

对于这样的设计方式,CPU必须能够:

◆ 有相应的CPU寄存器能够模仿SP的一些功能,能使用相应的指令来完成类似SP的一些操作;

◆ 作为SP使用的寄存器在编译过程中最好不被编译器默认使用。在IAR的编译器中,有一个选项可以避免在编译过程中使用到R4、R5。

这两点MSP430都可以做到。

下面对一个正在运行的优先级为6的任务中断后,会发生的几种情况进行分析。 1)在中断的处理过程中没有更高优先级的中断产生,即不会产生中断嵌套。

图3所示为中断发生后对于任务优先级为6的任务堆栈所进行的操作。中断发生后,PC和SR被系统压栈②,对于IAR C编译器来说,会按照复杂度不同的中断服务程序的要求,默认地进行一些寄存器的压栈操作③。因为我们要求的堆栈格式是如图2所示的,我们要把SP调整到

SR后面④,然后进行R4~R15的压栈操作,形成我们所要求的堆栈格式⑤。

进行任务堆栈的压栈工作以后,就可以调整SP的指针到系统堆栈了,如图4所示。压栈后的SP指向最后一个压栈内容①。我们把SP的值赋值给优先级6任务的TCB->OSTCBStkPtr,以便进行任务调度的时候出栈使用②。接着,就把SP调整到系统堆栈处③。在中断处理过程中,可能会出现压栈的操作,那么这种情况下SP的指针会随之移动。由于是中断堆栈中,所以不会破坏任务堆栈的格式。

由于没有中断嵌套,在中断处理中没有别的中断发生,那么返回的步骤和上述的进栈操作正好相反。在中断处理完了以后,SP会自动回到图4中③的SP位置。接着,系统会查询到优先级最高的任务,然后把SP的指针移到优先级最高的任务的任务堆栈,进行R15~R4的出栈工作,最后用RETI中断返回指令返回到新的任务。因为我们把所有的任务堆栈都规定成相同的格式,所以它们之间不会产生问题。这里需要注意的是,因为系统在C编译器的中断处理中会对中断进入时默认压栈的寄存器出栈,所以在设计出栈的程序时,要先把这些内容压栈,这样才能正确出栈。

2)在中断的处理过程中,有别的中断产生,产生中断嵌套。

如图5所示,由于在处理中断的时候,SP已经被移到系统堆栈去了,只有当中断退出的时候才可能把SP移到别的任务的任务堆栈中。所以在中断的时候进行中断嵌套,那么对于中断的处理和第一次是一样的,所不同的是,这次保存在堆栈中的不是任务运行中的寄存器,而是中断处理中的寄存器,而且是保存在系统堆栈中而不是任务堆栈中。从这里就可以看出优化内存的效果。所有的中断嵌套中的寄存器压栈都压在系统堆栈中,这样对于任务堆栈内存大小的要求大大降低。

因为μC/OS-II在进入中断中,会把全局变量OSIntNesting++;在退出中断的时候,又会把OSIntNesting--。在退出中断进行任务切换之前,μC/OS-II会先判断OSIntNesting是否为0,是0才会进行任务调度。当第二中断运行结束以后,退出中断嵌套的时候,OSIntNesting不为0,也就不会进行任务调度。因此,仍旧在系统堆栈出栈,那么系统会继续前面没有完成的中断服务程序。

接着退出中断的顺序和非中断嵌套的顺序是一样的。在中断处理完以后,SP会自动回到图4中③的SP位置。接着,系统会查询到优先级最高的任务,然后把SP的指针移到优先级最高的任务的任务堆栈。进行R15~R4的出栈工作,最后用RETI中断返回指令返回到新的任务。

中断的情况基本上就是上述两种。对于有些文献中提到的在中断中会调度到更高优先级的任务的情况,笔者觉得是不应该发生的。因为从上面的分析可以看出,默认的(μC/OS-II的设计思路)中断处理会同时对全局变量OSIntNesting进行增减处理,以给出是否需要任务调度的条件。那么即使在中断服务程序中把更高优先级的任务就绪,也会等到中断退出以后再进行调度,除非是在中断中直接调用更高优先级的任务函数。但这种方法应该是和μC/OS-II的原则相违背