8.03.03OS_STK_GROWTH
绝大多数的微处理器和微控制器的堆栈是从上往下长的。 但是某些处理器是用另外一种方式工作的。μC/OS-Ⅱ被设计成两种情况都可以处理,只要在结构常量OS_STK_GROWTH[L8.1(4)]中指定堆栈的生长方式(如下所示)就可以了。
置OS_STK_GROWTH为0表示堆栈从下往上长。
置OS_STK_GROWTH为1表示堆栈从上往下长。
8.03.04OS_TASK_SW()
OS_TASK_SW()[L8.1(5)]是一个宏,它是在μC/OS-Ⅱ从低优先级任务切换到最高优先级任务时被调用的。OS_TASK_SW()总是在任务级代码中被调用的。另一个函数OSIntExit()被用来在ISR使得更高优先级任务处于就绪状态时,执行任务切换功能。任务切换只是简单的将处理器寄存器保存到将被挂起的任务的堆栈中, 并且将更高优先级的任务从堆栈中恢复出来。
在μC/OS-Ⅱ中,处于就绪状态的任务的堆栈结构看起来就像刚发生过中断并将所有的寄存器保存到堆栈中的情形一样。换句话说,μC/OS-Ⅱ要运行处于就绪状态的任务必须要做的事就是将所有处理器寄存器从任务堆栈中恢复出来,并且执行中断的返回。为了切换任务可以通过执行OS_TASK_SW()来产生中断。大部分的处理器会提供软中断或是陷阱(TRAP)指令来完成这个功能。ISR或是陷阱处理函数(也叫做异常处理函数)的向量地址必须指向汇编语言函数OSCtxSw()(参看8.04.02)。
例如,在Intel或者AMD80x86处理器上可以使用INT指令。但是中断处理向量需要指向OSCtxSw()。Motorola68HC11处理器使用的是SWI指令,同样,SWI的向量地址仍是OSCtxSw()。还有,Motorola680x0/CPU32可能会使用16个陷阱指令中的一个。当然,选中的陷阱向量地址还是OSCtxSw()。
一些处理器如ZilogZ80并不提供软中断机制。在这种情况下,用户需要尽自己的所能将堆栈结构设置成与中断堆栈结构一样。OS_TASK_SW()只会简单的调用OSCtxSw()而不是将某个向量指向OSCtxSw()。μC/OS已经被移植到了Z80处理器上,μC/OS-Ⅱ也同样可以。
8.04OS_CPU_A.ASM
μC/OS-Ⅱ的移植实例要求用户编写四个简单的汇编语言函数:
OSStartHighRdy()
OSCtxSw()
OSIntCtxSw()
OSTickISR()
如果用户的编译器支持插入汇编语言代码的话, 用户就可以将所有与处理器相关的代码放到OS_CPU_C.C文件中,而不必再拥有一些分散的汇编语言文件。
8.04.01OSStartHighRdy()
使就绪状态的任务开始运行的函数叫做OSStart(),如下所示。在用户调用OSStart()之前,用户必须至少已经建立了自己的一个任务(参看OSTaskCreate()和OSTaskCteateExt())。OSStartHighRdy()假设OSTCBHighRdy指向的是优先级最高的任务的任务控制块。前面曾提到过,在μC/OS-Ⅱ中处于就绪状态的任务的堆栈结构看起来就像刚发生过中断并将所有的寄存器保存到堆栈中的情形一样。要想运行最高优先级任务,用户所要做的是将所有处理器寄存器按顺序从任务堆栈中恢复出来,并且执行中断的返回。为了简单一点,堆栈指针总是储存在任务控制块(即它的OS_TCB)的开头。换句话说,也就是要想恢复的任务堆栈指针总是储存在OS_TCB的0偏址内存单元中。
voidOSStartHighRdy(void)
{
CalluserdefinableOSTaskSwHook();
Getthestackpointerofthetasktoresume:
Stackpointer=OSTCBHighRdy->OSTCBStkPtr;
OSRunning=TRUE;
Restoreallprocessorregistersfromthenewtask'sstack;
Executeareturnfrominterruptinstruction;
}
注意,OSStartHighRdy()必须调用OSTaskSwHook(),因为用户正在进行任务切换的部分工作——用户在恢复最高优先级任务的寄存器。而OSTaskSwHook()可以通过检查OSRunning来知道是OSStartHighRdy()在调用它(OSRunning为FALSE)还是正常的任务切换在调用它(OSRunning为TRUE).
OSStartHighRdy()还必须在最高优先级任务恢复之前和调用OSTaskSwHook()之后设置OSRunning为TRUE。
8.04.02OSCtxSw()
如前面所述, 任务级的切换问题是通过发软中断命令或依靠处理器执行陷阱指令来完成的。中断服务例程,陷阱或异常处理例程的向量地址必须指向OSCtxSw()。
如果当前任务调用μC/OS-Ⅱ提供的系统服务,并使得更高优先级任务处于就绪状态,μC/OS-Ⅱ就会借助上面提到的向量地址找到OSCtxSw()。在系统服务调用的最后,μC/OS-Ⅱ会调用OSSched(),并由此来推断当前任务不再是要运行的最重要的任务了。OSSched()先将最高优先级任务的地址装载到OSTCBHighRdy中,再通过调用OS_TASK_SW()来执行软中断或陷阱指令。注意,变量OSTCBCur早就包含了指向当前任务的任务控制块(OS_TCB)的指针。 软中断(或陷阱)指令会强制一些处理器寄存器(比如返回地址和处理器状态字)到当前任务的堆栈中,并使处理器执行OSCtxSw()。OSCtxSw()的原型如程序清单L8.2所示。这些代码必须写在汇编语言中,因为用户不能直接从C中访问CPU寄存器。注意在OSCtxSw()和用户定义的函数OSTaskSwHook()的执行过程中,中断是禁止的。
程序清单 L8.2 OSCtxSw()的原型
voidOSCtxSw(void)
{
保存处理器寄存器;
将当前任务的堆栈指针保存到当前任务的OS_TCB中:
OSTCBCur->OSTCBStkPtr=Stackpointer;
调用用户定义的OSTaskSwHook();
OSTCBCur=OSTCBHighRdy;
OSPrioCur=OSPrioHighRdy;
得到需要恢复的任务的堆栈指针:
Stackpointer=OSTCBHighRdy->OSTCBStkPtr;
将所有处理器寄存器从新任务的堆栈中恢复出来;
执行中断返回指令;
}
8.04.03OSIntCtxSw()
OSIntExit()通过调用OSIntCtxSw()来从ISR中执行切换功能。 因为OSIntCtxSw()是在ISR中被调用的,所以可以断定所有的处理器寄存器都被正确地保存到了被中断的任务的堆栈之中。实际上除了我们需要的东西外,堆栈结构中还有其它的一些东西。OSIntCtxSw()必须要清理堆栈,这样被中断的任务的堆栈结构内容才能满足我们的需要。
要想了解OSIntCtxSw(),用户可以看看μC/OS-Ⅱ调用该函数的过程。用户可以参看图8.2来帮助理解下面的描述。假定中断不能嵌套(即ISR不会被中断),中断是允许的,并且处理器正在执行任务级的代码。当中断来临的时候,处理器会结束当前的指令,识别中断并且初始化中断处理过程, 包括将处理器的状态寄存器和返回被中断的任务的地址保存到堆栈中[F8.2(1)]。至于究竟哪些寄存器保存到了堆栈上,以及保存的顺序是怎样的,并不重要。
图 8.2 在ISR执行过程中的堆栈内容.
接着,CPU会调用正确的ISR。μC/OS-Ⅱ要求用户的ISR在开始时要保存剩下的处理器寄存器[F8.2(2)]。一旦寄存器保存好了,μC/OS-Ⅱ就要求用户或者调用OSIntEnter(),或者将变量OSIntNesting加1。在这个时候,被中断任务的堆栈中只包含了被中断任务的寄存器内容。现在,ISR可以执行中断服务了。并且如果ISR发消息给任务(通过调用OSMboxPost()或OSQPost()), 恢复任务(通过调用OSTaskResume()), 或者调用OSTimeTick()或OSTimeDlyResume()的话,有可能使更高优先级的任务处于就绪状态。
假设有一个更高优先级的任务处于就绪状态。μC/OS-Ⅱ要求用户的ISR在完成中断服务的时候调用OSIntExit()。OSIntExit()会告诉μC/OS-Ⅱ到了返回任务级代码的时间了。
调用OSIntExit()会导致调用者的返回地址被保存到被中断的任务的堆栈中[F8.2(3)]。
OSIntExit()刚开始时会禁止中断,因为它需要执行临界段的代码。根据OS_ENTER_CRITICAL()的不同执行过程(参看8.03.02),处理器的状态寄存器会被保存到被中断的任务的堆栈中[F8.2(4)]。OSIntExit()注意到由于有更高优先级的任务处于就绪状态,被中断的任务已经不再是要继续执行的任务了。在这种情况下,指针OSTCBHighRdy会被指向新任务的OS_TCB,并且OSIntExit()会调用OSIntCtxSw()来执行任务切换。调用OSIntCtxSw()也同样使返回地址被保存到被中断的任务的堆栈中[F8.2(5)]。
在用户切换任务的时候,用户只想将某些项([F8.2(1)]和[F8.2(2)])保留在堆栈中,并忽略其它项(F8.2(3),(4)和(5)) 。这是通过调整堆栈指针(加一个数在堆栈指针上)来完成的[F8.2(6)]。加在堆栈指针上的数必须是明确的,而这个数主要依赖于移植的目标处理器(地址空间可能是16,32或64位),所用的编译器,编译器选项,内存模式等等。另外,处理器状态字可能是8,16,32甚至64位宽,并且OSIntExit()可能会分配局部变量。有些处理器允许用户直接增加常量到堆栈指针中,而有些则不允许。在后一种情况下,可以通过简单的执行一定数量的pop(出栈)指令来实现相同的功能。一旦堆栈指针完成调整,新的堆栈指针会被保存到被切换出去的任务的OS_TCB中[F8.2(7)]。
OSIntCtxSw()的原型如程序清单L8.3所示。这些代码必须写在汇编语言中,因为用户不能直接从C语言中访问CPU寄存器。如果用户的编译器支持插入汇编语言代码的话,用户就可以将OSIntCtxSw()代码放到OS_CPU_C.C文件中,而不放到OS_CPU_A.ASM文件中。正如用户所看到的那样,除了第一行以外,OSIntCtxSw()的代码与OSCtxSw()是一样的。这样在移植实例中,用户可以通过“跳转”到OSCtxSw()中来减少 OSIntCtxSw()代码量。
程序清单 L8.3 OSIntCtxSw()的原型
voidOSIntCtxSw(void)
{
调整堆栈指针来去掉在调用:
OSIntExit(),
OSIntCtxSw()过程中压入堆栈的多余内容;
将当前任务堆栈指针保存到当前任务的OS_TCB中:
OSTCBCur->OSTCBStkPtr= 堆栈指针;
调用用户定义的OSTaskSwHook();
OSTCBCur=OSTCBHighRdy;
OSPrioCur=OSPrioHighRdy;
得到需要恢复的任务的堆栈指针:
堆栈指针 =OSTCBHighRdy->OSTCBStkPtr;
将所有处理器寄存器从新任务的堆栈中恢复出来;
执行中断返回指令;
}
OSIntCtxSw()是μC/OS-Ⅱ(和μC/OS)中唯一的与编译器相关的函数;在我收到的e-mail中,关于该函数的e-mail明显多于关于μC/OS其它方面的。如果在多次任务切换后用户的系统崩溃了,用户应该怀疑堆栈指针在OSIntCtxSw()中是否被正确地调整了。
8.04.04OSTickISR()
μC/OS-Ⅱ要求用户提供一个时钟资源来实现时间的延时和期满功能。时钟节拍应该每秒钟发生10-100次。 为了完成该任务, 可以使用硬件时钟, 也可以从交流电中获得50/60Hz的时钟频率。
用户必须在开始多任务调度后(即调用OSStart()后)允许时钟节拍中断。换句话说,就是用户应该在OSStart()运行后,μC/OS-Ⅱ启动运行的第一个任务中初始化节拍中断。通常所犯的错误是在调用OSInit()和OSStart()之间允许时钟节拍中断(如程序清单L8.4所示)。
程序清单 L8.4 在不正确的位置启动时钟节拍中断
voidmain(void)
{
.
.
OSInit();/* 初始化 ? μC/OS-II*/
.
.
/* 应用程序初始化代码 ...*/
/*... 调用OSTaskCreate()建立至少一个任务 */
.
.
允许时钟节拍中断;/* 千万不要在这里允许!!! */
.
.
OSStart();/* 开始多任务调度 */
}
有可能在μC/OS-Ⅱ开始执行第一个任务前时钟节拍中断就发生了。在这种情况下,μC/OS-Ⅱ的运行状态不确定,用户的应用程序也可能会崩溃。
时钟节拍ISR的原型如程序清单L8.5所示。这些代码必须写在汇编语言中,因为用户不能直接从C语言中访问CPU寄存器。如果用户的处理器可以通过单条指令来增加OSIntNesting,那么用户就没必要调用 OSIntEnter()了。增加OSIntNesting要比通过函数调用和返回快得多。OSIntEnter()只增加OSIntNesting,并且作为临界段代码中受到保护。
程序清单 L8.5 时钟节拍ISR的原型
voidOSTickISR(void)
{
保存处理器寄存器;
调用OSIntEnter()或者直接将 OSIntNesting加1;
调用OSTimeTick();
调用OSIntExit();
恢复处理器寄存器;
执行中断返回指令;
}
8.05OS_CPU_C.C
μC/OS-Ⅱ的移植实例要求用户编写六个简单的C函数:
OSTaskStkInit()
OSTaskCreateHook()
OSTaskDelHook()
OSTaskSwHook()
OSTaskStatHook()
OSTimeTickHook()
唯一必要的函数是OSTaskStkInit(),其它五个函数必须得声明但没必要包含代码。