分类:
2009-06-02 19:41:16
OSTaskStkInit():任务堆栈结构的初始化
OSTaskCreate()和OSTaskCreateExt()通过调用OSTaskStkInit(),初始化任务的栈结构。因此,堆栈看起来就像中断刚发生过一样,所有寄存器都保存在堆栈中。OSTaskStkInit()的示意性代码如下所示。
OSTaskStkInit()的示意性代码L:
/**********************************************************/
OS_STK *OSTaskStkInit (void (*task)(void *pd), void *pdata, OS_STK *ptos, INT16U opt)
{
模拟带参数(pdata)的函数调用; // (1)
模拟ISR向量; // (2)
按照预先设计的寄存器值初始化堆栈结构; // (3)
返回栈顶指针给调用该函数的函数; // (4)
}
/**********************************************************/
下图显示了OSTaskStkInit()在建立任务时,任务栈应该初始化成何种形式。
注意:在这里假定堆栈是从上往下递减的。下面的讨论同样适用于以相反方向从下往上递增的堆栈结构。
以下的程序清单给出了OSTaskCreate(),OSTaskCreateExt()及OSTaskStkInit()的函数原型,其中参数task,pdata,ptos及opt是调用OSTaskStkInit()函数时须传递过去的参数。因为只有OSTaskCreate()函数不支持附加的opt选项,因此,当OSTaskCreate()调用OSTaskStkInit()时,将opt设置为0x0000。
程序清单:
/**********************************************************/
INT8U OSTaskCreate(void (*task)(void *pd), void *pdata, OS_STK *ptos, INT8U prio);
INT8U OSTaskCreateExt(void (*task)(void *pd), void *pdata, OS_STK *ptos, INT8U prio, INT16U id, OS_STK *pbos, INT32U stk_size, void *pext, INT16U opt);
OS_STK *OSTaskStkInit(void (*task)(void *pd), void *pdata, OS_STK *ptos, INT16U opt);
/**********************************************************/
回顾一下,任务可以是一个无限的循环,也可以在一次执行完毕后被删除掉。这里要注意的是,任务代码并不是被真正地删除了,而只是uC/OS-II不再理会该任务代码,所以该任务代码不会再运行。任务看起来与任何C函数一样,具有一个返回类型和一个参数,只是它决不返回。任务的返回类型必须定义成void型。
也就是说,在uC/OS-II中,任务是一个无限的循环,其他部分看起来与别的C函数没有什么不同。
当uC/OS-II让任务开始执行时,任务就会收到一个参数,好像是被其他任务调用了,如以下代码所列。
任务代码:
/**********************************************************/
void MyTask(void *pdata)
{
/* 用pdata参数做某些操作 */
for(;;){
/* 任务代码 */
}
}
/**********************************************************/
如果是从其他函数中调用MyTask(),那么C编译器就会先将调用MyTask()函数的返回地址保存到堆栈中,再将参数保存到堆栈中。OSTaskStkInit()需要模仿编译器的这种动作。实际上有些编译器会将pdata参数放在一个或多个寄存器中传递,后面会讨论这类情况。
F(1)
L(1)假定pdata会被编译器保存到堆栈中,OSTaskStkInit()就会模仿编译器的这种行为,将pdata保存到堆栈中。
F(2)
L(1)与C函数的调用不一样的是,调用者的返回地址是未知的,因为该函数根本就没有被调用,只是为了建立一个任务的堆栈结构,好像这段任务代码被调用过一样。OSTaskStkInit()函数仅仅知道任务的起始地址(是作为参数传递过来的)。换言之,也不需要知道返回地址,因为任务并不再返回。
F(3)
L(2)这时,OSTaskStkInit()需将处理器的寄存器保存到堆栈中。当处理器识别并开始执行中断时,它会自动地完成该过程。一些处理器会将所有的寄存器都推入堆栈,而另一些处理器只将部分寄存器推入堆栈。一般而言,处理器至少需将程序计数器的值(中断返回地址)和处理器的状态字存入堆栈。很明显,处理器是按一定的顺序将寄存器推入堆栈的,而用户在将寄存器推入堆栈时,也就必须依照这一顺序。
F(4)
L(3)OSTaskStkInit()需要将剩下的处理器寄存器保存到堆栈中。入栈的顺序取决于处理器是否允许按不同顺序操作。有些处理器用一条指令就可以一次将全部寄存器都保存起来,有些则要几条指令。必须严格按照处理器要求的入栈顺序完成这一过程。例如,Intel 80x86有PUSHA指令,可将8个寄存器推入堆栈;对Motorola 68HC11处理器而言,在中断响应期间,所有的寄存器都会按一定顺序自动地保存到堆栈中。所以在初始化堆栈时,也必须符合这一顺序。
F(5)
L(4)在初始化堆栈以后,OSTaskStkInit()应当返回堆栈指针所指向的地址。OSTaskCreate()或OSTaskCreateExt()得到这个地址,并且保存在任务控制块中。处理器的文档应该告诉用户,堆栈指针是指向下一个可以使用的堆栈空间,还是指向上次入栈的数据。例如80x86就是指向上次存储的数据,而Motorola的68HC11处理器是指向下一个可以使用的栈空间。
现在是讨论这个问题的时候了:如果用户的C编译器用寄存器传递pdata参数,而不使用堆栈传递参数,该怎么办?请看下图。
F(1)
L(1)同上一种情况类似,OSTaskStkInit()模仿调用应用程序任务代码的情况,将任务起始地址保存在堆栈中。
F(2)
L(2)同样地,OSTaskStkInit()将寄存器保存到堆栈中。当处理器识别并开始执行中断时,自动地将寄存器推入堆栈中。一些处理器会将所有的寄存器都推入堆栈,而另外一些处理器只将部分寄存器推入堆栈。一般而言,处理器至少需将程序计数器的值(中断返回地址)和处理器的状态字存入堆栈。很明显,处理器是按一定的顺序将寄存器推入堆栈的,而用户在将寄存器推入堆栈时,也必须严格依照同样的顺序。
F(3)
L(3)OSTaskStkInit()会将其余的处理器寄存器保存到堆栈中。入栈的顺序取决于处理器是否允许按不同顺序操作。有些处理器可以使用一条指令将很多寄存器推入堆栈,有些则需要几条指令。这时,应该模拟出这些指令。因为编译器是使用寄存器传递参数的(至少部分参数是通过寄存器传递的),所以应从相应文档中弄清楚pdata是通过哪个寄存器传递的,并且将该寄存器也推入堆栈。
F(4)
L(4)在初始化堆栈后,OSTaskStkInit()函数应当返回堆栈指针所指向的地址。OSTaskCreate()或OSTaskCreateExt()得到这个地址并且保存在任务控制块中。再次强调,应该清楚堆栈指针是指向下一个空的堆栈空间,还是指向上次推入堆栈的值。
总结:OSTaskStkInit()函数由OSTaskCreate()或OSTaskCreateExt()调用,需要传递的参数是任务代码的起始地址、参数指针(pdata)、任务堆栈顶端的地址和附加的opt选项,用来初始化任务的堆栈,初始状态的堆栈模拟发生一次中断后的堆栈结构。堆栈初始化工作结束后,OSTaskStkInit()返回新的堆栈栈顶指针,OSTaskCreate()或OSTaskCreateExt()将指针保存在任务的OS_TCB中。也就是说,调用OSTaskStkInit()给任务做一个初始的任务上下文堆栈。
uC/OS-II在80x86上的移植(OSTaskStkInit()部分)
下图说明了OSTaskStkInit()初始化后的堆栈内容。请注意,图中的堆栈结构不是调用OSTaskStkInit()任务之后的,而是新创建任务之后的。
以下为OSTaskStkInit()的程序清单:
|
L(1)
由于80x86 堆栈是16位宽的(以字为单位),OSTaskStkInit()将建立一个指向以字为单位内存区域的指针,同时要求堆栈指针指向空堆栈的顶端。
L(2)
由于笔者使用的Borland C/C++编译器配置为用堆栈而不是寄存器来传送参数pdata,所以此时参数pdata的段地址和偏移量都将被保存在堆栈中。
L(3)
堆栈中紧接着是任务函数的起始地址,理论上,此处应该为任务的返回地址,但在uC/OS-II中,任务函数必须为无限循环结构,不能有返回点。
L(4)
返回地址下面是状态字(SW),设置状态字也是为了模拟中断发生后的堆栈结构。堆栈中的SW初始化为0x0202,这将使任务启动后允许中断发生;如果设为0x0002,则任务启动后将禁止中断。需要注意的是,如果选择任务启动后允许中断发生,则所有的任务运行期间中断都允许;同样,如果选择任务启动后禁止中断,则所有的任务都禁止中断发生,而不能有所选择。
如果确实需要突破上述限制,可以通过参数pdata向任务传递希望实现的中断状态。如果某个任务选择启动后禁止中断,那么其他的任务在运行的时候需要重新开启中断。同时还要修改OS_TaskIdle()和OS_TaskStat()函数,在运行时开启中断。如果以上任何一个环节出现问题,系统就会崩溃。所以笔者还是推荐设置SW为0x0202,在任务启动时开启中断。
L(5)
堆栈中还要留出各个寄存器的空间,注意寄存器在堆栈中的位置要和运行指令PUSHA,PUSH ES和PUSH DS的压入堆栈的次序相同。上述指令在每次进入中断服务程序时都会调用。
AX,BX,CX,DX,SP,BP,SI和DI的次序是和指令PUSHA的压栈次序相同的。如果使用没有PUSHA指令的80x86处理器,就要使用多个PUSH指令压入上述寄存器,且顺序要与PUSHA相同。在程序清单中每个寄存器被初始化为不同的值,这是为了调试方便。
L(6)
Borland编译器支持伪寄存器变量操作,可以用_DS关键字取得CPU DS寄存器的值,程序清单中的标记处是用_DS直接把DS寄存器拷贝到堆栈中。
堆栈初始化工作结束后,OSTaskStkInit()返回新的堆栈栈顶指针,OSTaskCreate()或 OSTaskCreateExt()将指针保存在任务的OS_TCB中。
另外注意:OSStartHighRdy()将永远不返回到OSStart(),因为OSStartHighRdy()被OSStart()调用的函数返回地址以及相关的上下文环境都没有被保存。