philips investment:uCOSII几种移植方案的分析比较
ucosII的移植主要是修改os_cpu.h、os_cpu.s、os_cpu.c三个文件。
一、头文件《os_cpu.h》
头文件《os_cpu.h》中定义使用的数据类型,堆栈的增长方向以及开关中断的实现。中断的打开与关闭,在MCU-51移植中这样实现:
#define OS_ENTER_CRITICAL() EA=0 //关中断
#define OS_EXIT_CRITICAL() EA=1 //开中断
#define OS_STK_GROWTH 0 //堆栈向上增长
通过直接调用处理器指令来关闭打开中断。存在一定的问题是:当系统调用后返回中断有可能是打开的。另一种实现方法是,在OS_ENTER_CRITICAL()时先将中断禁止状态保存到堆栈中,然后禁止中断;在OS_EXIT_CRITICAL()时,恢复保存的状态。在ARM移植中可以这样实现的,通过软件中断调用一段汇编代码,软件中断时就已经将cpsr保存到spsr中,在汇编代码中关闭中断即可。如:
#define OS_ENTER_CRITICAL() OsSwiHandle1(1);宏定义,参数1的软中断
ENTER_CRITICAL;汇编代码实现关中断
LDR R1, =OsEnterSum
LDRB R2, [R1]
ADD R2, R2, #1
STRB R2, [R1]
MRS R0, SPSR
ORR R0, R0, #NoInt
MSR SPSR_c, R0
MOVS PC, LR
宏定义OS_TASK_SW(),它是在μC/OS-Ⅱ从低优先级任务切换到最高优先级任务时被调用的。OS_TASK_SW()总是在任务级代码中被调用的。另一个函数OSIntExit()被用来在ISR使得更高优先级任务处于就绪状态时,执行任务切换功能。任务切换只是简单的将处理器寄存器保存到将被挂起的任务的堆栈中,并且将更高优先级的任务从堆栈中恢复出来。这里只是宏定义,真正的实现在下面的文件中。《ucosII》书中讲最方便的方式是:设置swi的向量地址为OSCtxSw(),需要切换任务时,通过软件中断实现调用。但是,在ARM的移植实例中,没有这样实现的,在调用OSCtxSw()前都有一段程序进行现场保存操作,以确保堆栈结构。
ARM在移植时可以这样实现:
TASK_SW
MRS R3, SPSR ;保存任务的CPSR
MOV R2, LR ;保存任务的PC
MSR CPSR_c, #(NoInt | SYS32Mode) ;切换到系统模式
STMFD SP!, {R2} ;保存PC到堆栈
STMFD SP!, {R0-R12, LR} ;保存R0-R12,LR到堆栈
B OSIntCtxSw_0 ;真正进行任务切换
二、汇编文件《os_cpu.s》
汇编文件中实现了几个功能模块,下面分析几个有代表意义的移植实例。
1. OSStartHighRdy()模块。
要想运行最高优先级任务,用户所要做的是将所有处理器寄存器按顺序从任务堆栈中恢复出来,并且执行中断的返回。OSStartHighRdy()模块的原型是这样的:
void OSStartHighRdy (void)
{ 调用用户定义的OSTaskSwHook();
获取要运行任务的堆栈指针;
从新任务堆栈中恢复所有的寄存器;
执行中断返回指令;
}
MCU-51中是这样实现的: C51中堆栈的结构要十分清楚,TCB结构体中OSTCBStkPtr总是指向用户堆栈最低地址,该地址空间内存放用户堆栈长度,其上空间存放系统堆栈映像,即:用户堆栈空间大小=系统堆栈空间大小+1。用户堆栈初始化时从下向上依次保存:用户堆栈长度(15),PCL,PCH,PSW,ACC,B,DPL,DPH,R0,R1,R2,R3,R4,R5,R6,R7。不保存SP,任务切换时根据用户堆栈长度计算得出。 SP总是先加1再存数据,因此,SP初始时指向系统堆栈起始地址(OSStack)减1处(OSStkStart)。很明显系统堆栈存储空间大小=SP-OSStkStart。
恢复最高优先级任务系统堆栈的方法是:通过最高优先级任务的TCB指针获得此任务户用户堆栈最低地址,从中取出“长度”,以最高优先级任务用户堆栈最低地址为起址,以OSStkStart为系统堆栈起址,由用户栈向系统栈拷贝数据,循环“长度”数值指示的次数,每次拷贝前先将各自栈指针增1。
任务切换时,先保存当前任务堆栈内容。方法是:用SP-OSStkStart得出保存字节数,将其写入用户堆栈最低地址内,以用户堆栈最低地址为起址,以OSStkStart为系统堆栈起址,由系统栈向用户栈拷贝数据,循环SP-OSStkStart次,每次拷贝前先将各自栈指针增1。
指针占3字节。+0类型+1高8位数据+2低8位数据,低位地址存高8位值,高位地址存低8位值。例如0x1234,基址+0:0x12 基址+1:0x34。
ARM系统移植时,各模块的实现不能割裂来分析,要放到一块儿来看,在后面有介绍。
二、OSCtxSw()与OSIntCtxSw()
两个模块实现的功能在一定程度上有所重叠,故编写代码时可以重复使用同一代码,在C51移植ucosII系统和ARM上做系统移植时都可以这样。
void OSCtxSw(void)
{
保存处理器寄存器;
将当前任务的堆栈指针保存到当前任务的OS_TCB中:
OSTCBCur->OSTCBStkPtr = Stack pointer;
调用用户定义的OSTaskSwHook();
OSTCBCur = OSTCBHighRdy;
OSPrioCur = OSPrioHighRdy;
得到需要恢复的任务的堆栈指针:
Stack pointer = OSTCBHighRdy->OSTCBStkPtr;
将所有处理器寄存器从新任务的堆栈中恢复出来;
执行中断返回指令;
}
void OSIntCtxSw(void)
{
调整堆栈指针来去掉在调用时的变化:
OSIntExit(),OSIntCtxSw()过程中压入堆栈的多余内容;
将当前任务堆栈指针保存到当前任务的OS_TCB中:
OSTCBCur->OSTCBStkPtr = 堆栈指针;
调用用户定义的OSTaskSwHook();
OSTCBCur = OSTCBHighRdy;
OSPrioCur = OSPrioHighRdy;
得到需要恢复的任务的堆栈指针:
堆栈指针 = OSTCBHighRdy->OSTCBStkPtr;
将所有处理器寄存器从新任务的堆栈中恢复出来;
执行中断返回指令;
}
C5108移植代码的关键流程:(配合文档来看)
执行最高优先级任务:
OSStartHighRdy:=======》关中断,调用钩子程序;
OSCtxSw_in:========》通过最高优先级任务的TCB指针获得此任务户用户堆栈最低地址,从中取出“长度”,以最高优先级任务用户堆栈最低地址为起址,以OSStkStart为系统堆栈起址,由用户栈向系统栈拷贝数据,循环“长度”数值指示的次数,每次拷贝前先将各自栈指针增1。
恢复所有寄存器;开中断;返回RETI。
任务级切换:
OSCtxSw:=========》所有寄存器入栈保存,(入的是系统堆栈);
OSIntCtxSw_in:======》获得系统堆栈长度和起址;
获得当前TCB指针;
获得用户堆栈指;保存堆栈长度;
将系统堆栈数据拷贝到用户堆栈中;
调用用户程序OSTaskSwHook;
更改当前任务的指针OSTCBCur和优先级OSPrioCur为将要运行的任务;
跳转:LJMP OSCtxSw_in;后面的操作可以跳到前面代码某一处重复执行。
中断返回时的任务切换:
OSIntCtxSw:
调整SP指针去掉在调用OSIntExit(),OSIntCtxSw()过程中压入堆栈的多余内容;
跳转:LJMP OSIntCtxSw_in;切换任务的工作前面已经实现。
OSTickISR: =======》关中断,保存现场;
LCALL _?OSIntEnter
LCALL _?OSTimeTick
LCALL _?OSIntExit
恢复现场,返回。
注意:整个流程出现多次跳转。运行的任务堆栈是系统堆栈,挂起的任务堆栈都在用户堆栈空间。中断退出需要判断是否需要进行任务切换,这个过程在OSIntExit()中完成,因而移植是按照《ucosII》书中介绍实现的,中断退出先调用OSIntExit(),OSIntExit()判断是否需要进行任务切换,若需要则调用模块来OSIntCtxSw()实现。
ARM上移植系统的关键流程:
l Lumit网站下载得版本:
OSStartHighRdy:=====》调用用户钩子程序;
OsRunning置1,获取最高优先级任务TCB指针和堆栈指针;
恢复状态寄存器和所有寄存器;pc从新地址开始执行(不需要恢复cpsr)。
(根本没有考虑系统模式的问题么!)
OSCtxSw:==============》依次保存pc,lr,R12-R0,cpsr,spsr;
_OSCtxSw:========》OSPrioCur = OSPrioHighRdy;
保存当前任务堆栈指针到当前任务的TCB;
调用用户钩子程序;
获得最高优先级任务的TCB指针,赋值给当前任务TCB;
按照相反的顺序将新任务的堆栈恢复;PC从新的地址开始执行;
―――――――――――――――――――――――――――――――――――――――――――――
OSIntCtxSw:=======》OSIntCtxSwFlag = true,赋值为真,表示需要进行任务切换;
返回调用语句;MOV pc, lr
―――――――――――――――――――――――――――――――――――――――――――――
OSTickISR:=======》保存R0-R12、LR(返回地址)、spsr;
如果是时钟中断,则跳转到handler_event_timer;
否则,按照与入栈相反的顺序出栈恢复寄存器;
pc回到原程序继续执行;(cpsr也同时恢复,回到任务模式)
handler_event_timer:==》依次调用OSIntEnter、OSTimeTick、timer_irq、OSIntExit;
如果OSIntCtxSwFlag = true,跳转到_IntCtxSw;
_IntCtxSw:=======》清除任务切换标志OSIntCtxSwFlag;
spsr出栈并且保存到SAVED_SPSR,R0-R12、LR依次出栈;
将LR保存到SAVED_LR_IRQ(通过此变量将状态传递到管理模式);
切换到管理模式,将SAVED_LR_IRQ(返回地址)入栈;
R0-R12、LR、SAVED_SPSR、SAVED_SPSR依次入栈;
跳转:B _OSCtxSw
总结:堆栈的结构都是统一的,自栈底到栈顶顺序是:PC、LR、R12-R0、SPSR、CPSR。中断退出时,若需要进行任务切换则置位OSIntCtxSwFlag,切换任务时,先将进入中断时保存的寄存器值恢复,再切换到管理模式,在管理模式重新入栈,????任务级的切换和中断返回时的切换最后一段代码是重复的,都是在管理模式下完成的么??
l ARM7和ARM9通用版本
OSStartHighRdy:=====》调用用户定义的钩子程序OSTaskSwHook;
获得最高优先级任务的TCB指针;
按照spsr、cpsr、r0-r12、lr、pc顺序恢复寄存器;
可见:堆栈自栈底到栈顶顺序是pc、lr、r12、――――、r0、cpsr、spsr。
――――――――――――――――――――――――――――――――――――――――――――
OSCtxSw:========》按照顺序各寄存器入栈保存现场;
赋值OSPrioCur = OSPrioHighRdy;
将原任务的sp保存到TCB的开始位置;
调用用户钩子程序;
获得新任务的堆栈指针和优先级数;
按照次序恢复新任务的寄存器,PC从新地址继续执行。
―――――――――――――――――――――――――――――――――――――――――――――
OSIntCtxSw:=======》给OSIntCtxSwFlag置为真;然后pc从调用处继续执行。
―――――――――――――――――――――――――――――――――――――――――――――
UCOS_IRQHandler:=====》r0-r3,r12,lr依次入栈保存;
关中断,中断处理,中断退出;
若OSIntCtxSwFlag为真,则跳到_IntCtxSw;
否则,r0-r3,r12,lr出栈;
pc=lr-4,继续执行;(当前IRQ模式,cpsr不恢复???)
_IntCtxSw========》清除中断标志,r0-r3,r12,lr出栈;
构造返回地址PC,关中断;
pc、lr、r12――r0、cpsr、spsr依次入栈;(SP_IRQ???)
保存当前任务sp到当前任务PCB的0地址;
获取新任务的TCB及堆栈地址;
恢复新任务的所有寄存器;PC从新地址继续运行;
问题:中断时,系统自动进入IRQ模式,堆栈指针不指向任务堆栈,为何没有考虑这些,直接入栈保存?????
总结:用一个变量来标志是否需要进行任务切换;中断后不保存所有寄存器的值。
l 周立功的标准版本
SoftwareInterrupt:====》设置sp为管理模式堆栈指针;
保存R0-R3, R12, LR;MRS R3, SPSR;
(直接用R3保存spsr,管理模式不响应中断么?在IRQ响应时可不是这样的,是入栈保存的)
读取swi中断号,0:进行任务级的任务切换,则跳转OSIntCtxSw;
1:运行最高级任务,则跳转__OSStartHighRdy;
其他情况,跳转到SWI_Exception,软件选择;
LDMFD SP!, {R0-R3, R12, PC}^,恢复寄存器;
―――――――――――――――――――――――――――――――――――――――――――――
OSIntCtxSw:=======》(参照中断响应入口时保存入栈的寄存器顺序才可以理解)
系统在IRQ、sys两种模式间多次切换,保存所有寄存器到原任务的堆栈;
保存当前任务堆栈指针到当前任务的TCB;
调用钩子函数;
OSPrioCur <= OSPrioHighRdy,OSTCBCur <= OSTCBHighRdy;
OSIntCtxSw_1:======》将新任务TCB指针保存到R4;
切换到管理模式;
sp《=R4,使指向新任务堆栈;
恢复所有寄存器;恢复CPSR
LDMFD SP!, {R0-R12, LR, PC }^;运行新任务
(自栈底到栈顶的顺序:pc、lr、r12-r0、cpsr、OsEnterSum)
―――――――――――――――――――――――――――――――――――――――――――――
__OSStartHighRdy:======》切换到系统模式;
设置运行标志,调用用户钩子程序;
获得新任务TCB指针,跳转到OSIntCtxSw_1;
――――――――――――――――――――――――――――――――――――――――――
总结:使用软件中断进行任务级的切换及调用最高级任务,软件中断保存了R0-R3、R12、LR且R3中保存的是进入中断前的cpsr,这样的堆栈同中断响应后IRQ堆栈的内容一致,因而,任务级切换和中断后切换调用了同一段代码OSIntCtxSw。另外,__OSStartHighRdy也是调用OSIntCtxSw_1实现运行新任务的,运行新任务为什么一定要在管理模式呢???
前面几个方案中,没有使用软件中断,运行最高级任务是单独调用的,都不需要恢复CPSR的。
没有时钟中断汇编代码,处理程序是c语言写的,中断入口汇编代码在《IRQ.INI》。
l 周立功的高效版本
SoftwareInterrupt:===》SWI中断号通过R0传递过来,通过R0跳转到对应的处理;
SwiFunction:=======》DCD TASK_SW ;0
DCD ENTER_CRITICAL ;1
DCD EXIT_CRITICAL ;2
DCD ISRBegin ;3
DCD ChangeToSYSMode ;4
DCD ChangeToUSRMode ;5
DCD __OSStartHighRdy ;6
DCD TaskIsARM ;7
DCD TaskIsTHUMB ;8
DCD OSISRNeedSwap ;9
DCD GetOSFunctionAddr ;10
DCD GetUsrFunctionAddr ;11
TASK_SW:========》保存spsr到R2;保存LR到R3;
切换到系统模式(关中断);
保存R2(spsr),按顺序保存lr、r12-r0;
跳转到OSIntCtxSw_0,真正进行任务切换;
(很好,软件中断0进行任务级切换,但寄存器保存到原任务堆栈中)
ENTER_CRITICAL:
EXIT_CRITICAL:
ISRBegin:======》中断嵌入层次数加1,MOVS PC, LR;返回调用处继续,并恢复CPSR;
―――――――――――――――――――――――――――――――――――――――――――――
OSIntCtxSw:======》(此时处于中断模式,堆栈自栈底到栈顶:pc、r12、r3-r0)
获取此时堆栈中的R12放到R12,获取此时堆栈中的PC保存到R0;
切换到系统模式(关中断);
保存当前任务的PC、LR、R12-R4;
回到中断模式,获取当前任务的R3-R0;
切换到系统模式(关中断),保存当前任务的R3-R0;
OSIntCtxSw_0:======》(中断入口可知,此时R3保存的是spsr,即进入中断前的cpsr)
保存R3、sEnterSum到任务堆栈;
LDR R1, =OSTCBCur
LDR R1, [R1]
STR SP, [R1];将当前堆栈指针保存到当前任务TCB开始地址;
调用钩子程序;
OSPrioCur <= OSPrioHighRdy;
OSTCBCur <= OSTCBHighRdy;
OSIntCtxSw_1:======》切换到管理模式,但指针指向新任务的TCB;
依次恢复OsEnterSum、SPSR、R0-R12、LR、PC;
恢复PC的同时恢复了CPSR;
(问题:为什么在切换到管理模式前,要将LR取出来,放到任务模式的LR?后面出栈时有这样的操作!)
__OSStartHighRdy:====》切换到系统模式(关中断);
OsRunnig=true;
调用用户钩子程序OSTaskSwHook;
获得新任务的TCB指针;
跳转到OSIntCtxSw_1;
总结:这一移植版本,是最完美的,暂时只有一处感到困惑!
最后总结:c51移植的方案感觉没有问题,主要是堆栈比较简单,只有任务堆栈和系统堆栈。ARM的几种移植方案都有些疑惑。几种模式的堆栈操作,比较复杂。