约定:文中所写的硬件堆栈或系统堆栈是指51单片机SP指针所指向的堆栈空间,而用户堆栈或任务堆栈是指用来保存任务状态为每个任务分配的堆栈空间。
前一段时间一直在学习UCOS-II,看了一个月的源码感觉有了一个初步的认识,就开始着手找一个平台移植起来跑跑看,不然终究是纸上谈兵。先是看了公司的一个产品,用的UC是在ARM平台上移植的,费了好大力气终于把任务切换的过程看明白了。自以为对于UC的移植掌握的差不多了,于是拿出自己先前焊的51最小系统,想在51上面把UC跑起来,可是真正移植的时候却发现难度很大,不知如何下手,没办法只好找网上移植范例来学习。我找到的版本有两个,一个不知道是谁移植的,用的是KEIL小模式编译的;另一个是03年杨屹大侠移植的,用的是KEIL大模式编译的。随着学习的深入,发现不同平台上移植UC真是相去甚远,还发现两个版本的UCOS for 51都有不同程度的缺点。下面我会对两个版本的UCOS for 51基于自己的理解作一些阐述,并详细分析我改进的方法。
先来了解和51移植相关的三个概念:
第一,移植UCOS必须要了解编译器,我们一般使用的51编译器都是KEIL。值得一提的是KEIL对可重入函数的处理。由于51单片机的堆栈指针是8位的,所以硬件堆栈只能设置在内部RAM的DATA区和IDATA区(DATA、IDATA、PDATA、XDATA、CODE这些概念相关资料很多,我不想在此处滋述),所以51的堆栈是很紧张的。于是,KEIL将函数内的动态变量和函数传递的参数(当然有一部分参数是用寄存器直接传送的),放在分配的固定数据段中,函数执行时在固定的数据段中去取得相关的数据,而不是像传统的CPU都用堆栈来处理,这就导致了函数不可重入,因为当一个函数没执行完成时再次执行会把数据段里的内容覆盖掉。为了使函数可重入KEIL引入了仿真堆栈的概念(重入函数需在函数定义后面加上reentrant关键字),用仿真堆栈来传递参数及分配动态变量,就好像传统堆栈的入栈、出栈操作一般,如此函数第二次进入执行时,就不会覆盖掉上一次的变量和参数,仿真堆栈实现原理详见http://hi.baidu.com/lyb1900/blog/item/99b6313defc2b40abaa167fe.html 。但是,KEIL的这一机制会给我们移植造成了麻烦,任务切换时不仅要保存好硬件堆栈内容,还要保存好仿真堆栈的内容。(建议先理解仿真堆栈的概念)
第二,其他类型的CPU可以在任务切换时先将SP指针保存到被中断任务的OSTCBCur->OSTCBStkPtr中,再将高优先级任务的OSTCBCur->OSTCBStkPtr恢复到SP中就可以了,各个任务使用各自的堆栈空间,互不干扰,切换也很方便。而51的堆栈指针是8位的,SP只能指向内部RAM空间,但是内部RAM很小,根本不可能将所有任务堆栈都设置在内部RAM中(DATA和IDATA区)。所以,51只能设置一个固定的硬件堆栈,每个任务可以在外部RAM中设置各自的任务堆栈,任务切换时,将本任务所使用到的硬件堆栈的长度和内容保存到任务堆栈中,然后将高优先级任务的用户堆栈里的内容恢复到硬件堆栈中。所以51切换任务会比较慢。
第三,在KEIL的工程配置Target选项中会有一个Memory Model选项。用鼠标点击Memory Model的下拉箭头,会有3个选项.
Small:变量存储在内部ram里.
Compact:变量存储在外部ram里,使用页8位间接寻址
Large:变量存储在外部Ram里,使用16位间接寻址.
这三个变量决定了定义的变量在不加存储类型关键字时,变量存放的位置。这一点很多网站、资料都说的很明白。但是其实还有一点很多资料都是没说的。它还默认决定了上述仿真堆栈的位置。这一点在51的启动代码STARTUP.asm中能体现出来。其中有一段如下:
; Stack Space for reentrant functions in the SMALL model.
IBPSTACK EQU 1 ; set to 1 if small reentrant is used.
IBPSTACKTOP EQU 0FFH+1 ; set top of stack to highest location+1.
;
; Stack Space for reentrant functions in the LARGE model.
XBPSTACK EQU 0 ; set to 1 if large reentrant is used.
XBPSTACKTOP EQU 7FFFH+1; set top of stack to highest location+1.
;
; Stack Space for reentrant functions in the COMPACT model.
PBPSTACK EQU 0 ; set to 1 if compact reentrant is used.
PBPSTACKTOP EQU 7FFFH+1; set top of stack to highest location+1.
IF IBPSTACK <> 0
EXTRN DATA (?C_IBP)
MOV ?C_IBP,#LOW IBPSTACKTOP
ENDIF
IF XBPSTACK <> 0
EXTRN DATA (?C_XBP)
MOV ?C_XBP,#HIGH XBPSTACKTOP
MOV ?C_XBP+1,#LOW XBPSTACKTOP
ENDIF
IF PBPSTACK <> 0
EXTRN DATA (?C_PBP)
MOV ?C_PBP,#LOW PBPSTACKTOP
ENDIF
注释讲的很清楚,根据所选模式,编译器会将IBPSTACK、PBPSTACK或者XBPSTACK设置为1,就决定了仿真堆栈在IDATA区、PDAIA区还是XDATA区。对应的,KEIL会自动分配一个仿真堆栈指针,分别是?C_IBP、?C_PBP和(?C_XBP、?C_XBP+1),由于寻址XDATA区需要16位地址,所以需要两个字节。这三个指针是KEIL根据选择的Memory Model选项自动分配的。
注意:不要试图在选择好模式后将仿真堆栈设置在另一模式的空间中。比如,我用的小模式编译,仿真堆栈在IDATA区,用的仿真堆栈指针是?C_IBP,但是我现在在启动代码中将IBPSTACK定义为0,将XBPSTACK设置为1,看起来我们先把仿真堆栈设置在XDATA区了,但实际上其它代码段中使用的仿真堆栈指针任然是?C_IBP。有趣的是,KEIL还为我们的启动代码做了一个很友好的列表框选择界面。但实际上选择好编译模式后,仿真堆栈使用空间是不能更改的,不知道KEIL为什么这么做?但是我们有时候要根据单片机的型号选择仿真堆栈的起始地址。
讲了那么多,应该来看看关于堆栈的组织了,首先是不知道哪位前辈移植的,用的小模式编译的堆栈结构:
每个任务分都需要配一个任务堆栈,OSTCBCur->OSTCBStkPtr指向任务堆栈的栈底,任务堆栈的首字节是仿真堆栈指针?C_IBP(由于是小模式编译,所以使用的仿真堆栈设置在IDAIA区)。用户堆栈中紧接着存放的是该任务的仿真堆栈中的内容。再接着是系统堆栈(就是SP指针所指的堆栈)的长度,最后是系统堆栈的内容。
任务在切换时,首先将当前的?C_IBP的值保存到本任务堆栈的首地址中,然后将仿真堆栈的全部内容复制到任务堆栈中(仿真堆栈栈底固定在IDATA区的最高字节0xff,可以根据(0xff-?C_IBP+1)的值来确定所使用的仿真堆栈的长度),接着保存系统堆栈的长度(系统堆栈设置在DATA或IDATA区中,系统堆栈的栈底的地址我们可以在启动代码中设置,长度可以用(SP-Stack+1)来计算得到)最后将所用的系统堆栈中的内容复制到任务堆栈中。
然后得到高优先级的任务堆栈,首先恢复高优先级任务的?C_IBP,然后计算出高优先级任务所用仿真堆栈的长度,将保存的仿真堆栈的内容一一恢复到仿真堆栈中,然后得到系统栈的长度,再将保存的系统堆栈的内容恢复到系统堆栈中,最后恢复SP指针并执行RETI返回指令,便实现了任务切换。
任务被打断时将仿真堆栈和系统堆栈的内容全都备份到任务堆栈中,在恢复运行时将相应的内容还原到系统堆栈和仿真堆栈中。
这种方法的缺点是,任务切换将会变的很慢,因为要分别拷贝和恢复仿真堆栈和系统堆栈的全部内容。完全可以将仿真堆栈设置在XDATA区中,任务切换时,只需保存和恢复?C_XBP指针就行了,而不必每次都拷贝和恢复仿真堆栈的全部内容。由于SP指针只有8位,系统堆栈只能设置在内部RAM中。
再来看看杨屹大侠大模式编译下的堆栈结构:
同样,每个任务分配一个任务堆栈,OSTCBCur->OSTCBStkPtr指向任务堆栈的栈底,任务堆栈的首字节是系统堆栈的长度,接着是系统堆栈的全部内容。再接着是仿真堆栈指针?C_XBP的高低字节(因为是大模式编译,所以仿真堆栈在XDATA区),任务堆栈再高的字节是作为仿真堆栈用的,用户堆栈的栈顶就是仿真堆栈的栈底。
任务切换时,首先计算任务使用的系统堆栈的长度,将长度保存在任务堆栈栈底,然后将使用的系统堆栈的内容全部复制到任务堆栈中,最后保存当前的?C_XBP仿真堆栈指针的高低字节。
接着恢复高优先级任务的信息,先得到堆栈长度,将备份的堆栈内容恢复到系统堆栈中,并恢复SP指针(根据长度和系统堆栈的栈底可以计算出SP指针的值)。最后恢复?C_XBP的高低字节。便实现了任务的切换。
任务切换时将系统堆栈的内容和仿真堆栈指针保存起来,再将高优先级任务的仿真堆栈指针和系统堆栈的内容恢复。
和上述的小模式下的切换过程相比,仿真堆栈的内容在任务切换时不需要保存和恢复了,任务切换速度会提高不少。但是读过杨屹大侠代码的朋友肯定知道,每个任务堆栈的大小都要设置成相同。这对于有些堆栈使用很少的任务来说是很浪费的,而且51的RAM本来就那么紧张?仿真堆栈被设置在任务堆栈的最高地址处,细心的朋友会发现,堆栈检测函数肯定是无法运行了。
正是意识到这些缺陷,我对杨屹大侠移植的代码进行了一些改动,堆栈结构也有较大改变,使用的也是大模式编译:
同样,还是为每个任务分配一个任务堆栈。如果使能仿真堆栈检查函数,OSTCBCur->OSTCBStkPtr指向任务堆栈的第4字节(这样处理是为了汇编中处理方便),任务堆栈的最初四字节存放的是仿真堆栈长度和仿真堆栈的栈顶(这在仿真堆栈检测函数中都需要用到),第四五字节是?C_XBP指针,第六字节是系统堆栈的长度,最后是堆栈内容。如果不使用仿真堆栈检查函数时,最初的四个字节是不需要的,?C_XBP指针及以上的内容从栈底开始存放,OSTCBCur->OSTCBStkPtr还是指向任务堆栈中的?C_XBP字节(此时是任务堆栈的栈底),这样可以节省出来四个字节,并未给出这种情况下的图片,应该容易理解。
任务切换的过程和杨屹大侠的差不多,只不过仿真堆栈另外分配,不放在用户堆栈的顶部,这样任务堆栈和仿真堆栈的大小都可以随意设置,并且可以支持堆栈检测函数,额外的我还增加了仿真堆栈检测函数。
下面来具体分析移植方法和增加功能的实现。(注:全部源码我上传到CSDN下载上,下文会给出链接,由于我没有CSDN积分了,所以需要两个CSDN下载积分,真是惭愧)
一、在UCOS-II.H中我们可以对于一些变量的定义加一些存储说明:
OS_EXT INT32U data OSCtxSwCtr; /* Counter of number of context switches */
#if (OS_EVENT_EN > 0) && (OS_MAX_EVENTS > 0)
OS_EXT OS_EVENT xdata* data OSEventFreeList; /* Pointer to list of free EVENT control blocks */
OS_EXT OS_EVENT OSEventTbl[OS_MAX_EVENTS];/* Table of EVENT control blocks */
#endif
#if (OS_VERSION >= 251) && (OS_FLAG_EN > 0) && (OS_MAX_FLAGS > 0)
OS_EXT OS_FLAG_GRP OSFlagTbl[OS_MAX_FLAGS]; /* Table containing event flag groups */
OS_EXT OS_FLAG_GRP xdata* data OSFlagFreeList; /* Pointer to free list of event flag groups */
#endif
#if OS_TASK_STAT_EN > 0
OS_EXT INT8S idata OSCPUUsage; /* Percentage of CPU used */
OS_EXT INT32U idata OSIdleCtrMax; /* Max. value that idle ctr can take in 1 sec. */
OS_EXT INT32U idata OSIdleCtrRun; /* Val. reached by idle ctr at run time in 1 sec. */
OS_EXT BOOLEAN idata OSStatRdy; /* Flag indicating that the statistic task is rdy */
OS_EXT OS_STK xdata OSTaskStatStk[OS_TASK_STAT_STK_SIZE]; /* Statistics task stack */
OS_EXT OS_STK xdata XBPStartStk[TASK_STAT_XBPSTK_SIZE];
#endif
OS_EXT INT8U data OSIntNesting; /* Interrupt nesting level */
OS_EXT INT8U data OSIntExitY;
OS_EXT INT8U data OSLockNesting; /* Multitasking lock nesting level */
OS_EXT INT8U data OSPrioCur; /* Priority of current task */
OS_EXT INT8U data OSPrioHighRdy; /* Priority of highest priority task */
OS_EXT INT8U data OSRdyGrp; /* Ready list group */
OS_EXT INT8U xdata OSRdyTbl[OS_RDY_TBL_SIZE]; /* Table of tasks which are ready to run */
OS_EXT BOOLEAN data OSRunning; /* Flag indicating that kernel is running */
OS_EXT INT8U data OSTaskCtr; /* Number of tasks created */
OS_EXT INT32U idata OSIdleCtr; /* Idle counter */
OS_EXT OS_STK xdata OSTaskIdleStk[OS_TASK_IDLE_STK_SIZE]; /* Idle task stack */
OS_EXT OS_STK xdata XBPIdleSta[TASK_IDLE_XBPSTK_SIZE]; //空闲任务的仿真堆栈
/*
原来的变量定义是:
OS_EXT OS_TCB *OSTCBCur; // Pointer to currently running TCB
OS_EXT OS_TCB *OSTCBHighRdy; // Pointer to highest priority TCB R-to-R
因为这2个变量调用频繁,放在idata中比xdata中访问快
*/
OS_EXT OS_TCB xdata *data OSTCBCur; /* Pointer to currently running TCB */
OS_EXT OS_TCB xdata *data OSTCBFreeList; /* Pointer to list of free TCBs */
OS_EXT OS_TCB xdata *data OSTCBHighRdy; /* Pointer to highest priority TCB R-to-R */
OS_EXT OS_TCB xdata *data OSTCBList; /* Pointer to doubly linked list of TCBs */
OS_EXT OS_TCB xdata *xdata OSTCBPrioTbl[OS_LOWEST_PRIO + 1];/* Table of pointers to created TCBs */
OS_EXT OS_TCB OSTCBTbl[OS_MAX_TASKS + OS_N_SYS_TASKS]; /* Table of TCBs */
#if (OS_MEM_EN > 0) && (OS_MAX_MEM_PART > 0)
OS_EXT OS_MEM xdata *data OSMemFreeList; /* Pointer to free list of memory partitions */
OS_EXT OS_MEM OSMemTbl[OS_MAX_MEM_PART];/* Storage for memory partition manager */
#endif
#if (OS_Q_EN > 0) && (OS_MAX_QS > 0)
OS_EXT OS_Q xdata *data OSQFreeList; /* Pointer to list of free QUEUE control blocks */
OS_EXT OS_Q OSQTbl[OS_MAX_QS]; /* Table of QUEUE control blocks */
#endif
#if OS_TIME_GET_SET_EN > 0
OS_EXT volatile INT32U data OSTime; /* Current value of system time (in ticks) */
#endif
//#ifndef OS_GLOBALS
extern INT8U code OSMapTbl[]; /* Priority->Bit Mask lookup table */
extern INT8U code OSUnMapTbl[]; /* Priority->Index lookup table */
//#endif
尽量的把一些经常用到的全局变量定义在DATA区和IDATA区,增加访问速度。还可以指定指针指向的存储类型,可以节省指针的存储空间(顺便提一下,如果不熟悉C51指针和KEIL下C与汇编混合编程的话可以先看一下,相关资料很多),而将一些占容量较大的变量放在XDATA区(如任务控制块TCB等等)。
二、几个重要的函数
1、仿真堆栈初始化函数
void InitTaskXBPStk(OS_STK xdata * TaskStartSt,OS_STK xdata * XBPTask,INT16U XBPSize) reentrant
{
#if TASK_XBPStkChk_EN > 0 //如果使能仿真堆栈检测函数
INT16U i;
for(i=0;i<XBPSize;i++) //清零仿真堆栈内容
{
XBPTask[i]=0x00;
}
* TaskStartSt = (INT16U)XBPSize >> 8; //仿真堆栈字节数高8位 编写查询仿真堆栈剩余容量时需要用到
*(TaskStartSt+1) = (INT16U)XBPSize & 0xFF; //仿真堆栈字节数低8位
*(TaskStartSt+2) = (INT16U)XBPTask >> 8; //仿真堆栈栈底指针高8位 仿真堆栈检测时用
*(TaskStartSt+3) = (INT16U)XBPTask & 0xFF; //仿真堆栈栈底指针低8位
*(TaskStartSt+4) = (INT16U)(XBPTask + XBPSize) >> 8; //?C_XBP仿真堆栈指针高8位
*(TaskStartSt+5) = (INT16U)(XBPTask + XBPSize) & 0xFF; //?C_XBP仿真堆栈指针低8位
#else
* TaskStartSt = (INT16U)(XBPTask + XBPSize) >> 8; //?C_XBP仿真堆栈指针高8位
*(TaskStartSt+1) = (INT16U)(XBPTask + XBPSize) & 0xFF; //?C_XBP仿真堆栈指针低8位
#endif
}
这个函数是我自己增加的,在OS_CPU_C.C文件中可以找到。目的是将仿真堆栈指针、仿真堆栈栈顶指针、仿真堆栈的大小先行写入任务堆栈的前六个字节。
本函数有三个参数,分别是任务堆栈首地址、仿真堆栈栈顶地址和仿真堆栈大小。函数有一个条件编译的选项TASK_XBPStkChk_EN ,是我在OS_CFG.h中添加的,用来说明是否使用仿真堆栈检测函数,如果需要那么首先要进行仿真堆栈的清零,然后依次按照上文所述的堆栈结构将六字节内容写入任务堆栈中。如果不使用仿真堆栈检测函数,那么只需要将仿真堆栈栈底指针?C_XBP存入任务堆栈的最低两个地址中就行了。
2、堆栈初始化函数
OS_STK *OSTaskStkInit (void (*task)(void *pd) reentrant, void *ppdata, OS_STK *ptos, INT16U opt) reentrant
{
OS_STK *stk;
opt = opt; //opt没被用到,保留此语句防止告警产生
#if TASK_XBPStkChk_EN > 0 //如果使能仿真堆栈检测函数
ptos += 4; //调整用户堆栈栈底指针
#endif
stk = ptos+2; //用户堆栈最低有效地址
*stk++ = 15; //用户堆栈长度
*stk++ = (INT16U)task & 0xFF; //任务地址低8位
*stk++ = (INT16U)task >> 8; //任务地址高8位
*stk++ = 0x0A; //ACC
*stk++ = 0x0B; //B
*stk++ = 0x00; //DPH
*stk++ = 0x00; //DPL
*stk++ = 0x00; //PSW
*stk++ = 0x00; //R0
//R3、R2、R1用于传递任务参数ppdata,其中R3代表存储器类型,R2为高字节偏移,R1为低字节位移。
//通过分析KEIL汇编,了解到任务的void *ppdata参数恰好是用R3、R2、R1传递,不是通过虚拟堆栈。
*stk++ = (INT16U)ppdata & 0xFF; //R1
*stk++ = (INT16U)ppdata >> 8; //R2
*stk++ = (INT32U)ppdata >> 16; //R3
*stk++ = 0x04; //R4
*stk++ = 0x05; //R5
*stk++ = 0x06; //R6
*stk++ = 0x07; //R7
//不用保存SP,任务切换时根据用户堆栈长度计算得出。
return ((void *)ptos);
}
初始化任务堆栈函数,值得注意的是,如果使能了仿真堆栈检测函数,将会把用户栈底指针进行加4的调整操作。其余的都很容易理解。
在建立任务前必须要先调用上述的InitTaskXBPStk函数,包括空闲任务和统计任务。因为任务建立的时候,并不知道?C_XBP指针的值。是通过InitTaskXBPStk函数来把该信息放入任务堆栈的。我们可以回过头来想想,杨屹大侠为什么要固定每个任务的堆栈大小并把仿真堆栈设置在任务堆栈栈顶?因为在OSTaskStkInit函数中并不能确定仿真堆栈指针?C_XBP的值,但是如果堆栈长度是固定的,那么任务堆栈栈底+堆栈长度=任务堆栈栈顶=仿真堆栈栈底,自然的仿真堆栈指针就确定了。我们既然使仿真堆栈和任务堆栈分离开了,那么要知道本任务的仿真堆栈在哪儿,就必须要有一个参数来通知OSTaskStkInit函数,很遗憾的是现有的函数参数无法满足该要求,于是我想到了可以事先把一些信息放置在各个任务堆栈的最初几个字节中,当然这需要一个函数来处理。
这样子做可能有点麻烦,但是KEIL那么特殊,这些麻烦是必须的(真是应了“得到什么东西,必然失去些东西”这句箴言,题外话)。不过,我们可以写一个函数将所有任务的仿真堆栈都一下子都处理好
OS_STK xdata TaskStartStk1[50]; //定义三个任务的用户堆栈和仿真堆栈
OS_STK xdata XBPTask1[50];
OS_STK xdata TaskStartStk2[50];
OS_STK xdata XBPTask2[30];
OS_STK xdata TaskStartStk3[45];
OS_STK xdata XBPTask3[30];
void InitTaskXBPStk_ALL()
{
InitTaskXBPStk(OSTaskIdleStk,XBPIdleSta,TASK_IDLE_XBPSTK_SIZE); //初始化空闲任务的仿真堆栈
#if OS_TASK_STAT_EN > 0
InitTaskXBPStk(OSTaskStatStk,XBPStartStk,TASK_STAT_XBPSTK_SIZE); //初始化统计任务的仿真堆栈
#endif
InitTaskXBPStk(TaskStartStk1,XBPTask1,50); //初始化三个用户任务的仿真堆栈
InitTaskXBPStk(TaskStartStk2,XBPTask2,30);
InitTaskXBPStk(TaskStartStk3,XBPTask3,30);
}
void main(void)
{
InitTaskXBPStk_ALL(); //所用任务的仿真堆栈初始化
OSInit();
InitHardware();
OSTaskCreateExt(Task1, (void *)0, &TaskStartStk1[0],2,2,&TaskStartStk1[49],50,(void *)0,OS_TASK_OPT_STK_CHK);
OSTaskCreateExt(Task2, (void *)0, &TaskStartStk2[0],3,3,&TaskStartStk2[49],50,(void *)0,OS_TASK_OPT_STK_CHK);
OSTaskCreateExt(Task3, (void *)0, &TaskStartStk3[0],4,4,&TaskStartStk3[44],45,(void *)0,OS_TASK_OPT_STK_CHK);
OSStart();
}
在main函数中调用OSInit();前首先将所有任务的仿真堆栈全部处理好,因为OSInit中会建立空闲任务和统计任务,我们事先也必须定义空闲任务和统计任务的仿真堆栈空间,我在UCOS_II.h中定义。
由于我需要运行堆栈检测函数,所以建立任务我使用OSTaskCreateExt函数。不知读者看到这里有没有发现什么问题么?大概谁也没发现。我在调试的时候也没发现,直到任务调度不成功,回头来看才猛然惊觉。读者是否还记得OSTaskCreateExt函数建立任务时,在进行调用OSTaskStkInit函数前会将任务堆栈全部清零。我刚刚才将仿真堆栈的信息写入任务堆栈中呢。为这个问题我想了很久,因为我一开始想绝对不改内核代码的,在建立任务前首先要调用InitTaskXBPStk函数已经够别扭了。但是发现要实现我要的功能一点不该内核代码是不可能的,于是OSTaskCreateExt函数中有一段变成了(注:这是唯一一处改变内核代码的地方,上面UCOS-II.H中增加的存储类型声明除外):
if (((opt & OS_TASK_OPT_STK_CHK) != 0x0000) || /* See if stack checking has been enabled */
((opt & OS_TASK_OPT_STK_CLR) != 0x0000)) { /* See if stack needs to be cleared */
#if OS_STK_GROWTH == 1
(void)memset(pbos, 0, (stk_size-6) * sizeof(OS_STK));
#else
(void)memset(ptos+6, 0, (stk_size-6) * sizeof(OS_STK));
#endif
}
栈底开始的6个字节将不会被清零。
三、OS_CPU_A.asm
这是最重要的文件,任务切换、中断级任务切换等操作都是由本文件中的汇编完成的(读者如果不熟悉51汇编、C与51混合编程等内容,请先熟悉后再看,只是一个帖子,实在难以面面俱到)
$NOMOD51
EA BIT 0A8H.7 ;定义一些特殊功能寄存器
DATA 081H
B DATA 0F0H
ACC DATA 0E0H
DPH DATA 083H
DPL DATA 082H
PSW DATA 0D0H
TR0 BIT 088H.4
TH0 DATA 08CH
TL0 DATA 08AH
NAME OS_CPU_A ;模块名
;声明供其它代码段调用的函数
?PR?OSStartHighRdy?OS_CPU_A SEGMENT CODE ;运行初始任务函数
R?OSCtxSw?OS_CPU_A SEGMENT CODE ;任务级切换函数
R?OSIntCtxSw?OS_CPU_A SEGMENT CODE ;中断级切换函数
;声明引用全局变量和外部子程序
EXTRN DATA (?C_XBP) ;仿真堆栈指针用于重入局部变量保存,
EXTRN DATA (OSTCBCur)
EXTRN DATA (OSTCBHighRdy)
EXTRN DATA (OSRunning)
EXTRN DATA (OSPrioCur)
EXTRN DATA (OSPrioHighRdy)
EXTRN CODE (_?OSTaskSwHook)
EXTRN CODE (_?OSIntEnter)
EXTRN CODE (_?OSIntExit)
;对外声明不可重入函数
PUBLIC OSStartHighRdy
PUBLIC OSCtxSw
PUBLIC OSIntCtxSw
;定义压栈出栈宏
PUSHALL MACRO
PUSH ACC
PUSH B
PUSH DPH
PUSH DPL
PUSH PSW
MOV A,R0 ;R0-R7入栈
PUSH ACC
MOV A,R1
PUSH ACC
MOV A,R2
PUSH ACC
MOV A,R3
PUSH ACC
MOV A,R4
PUSH ACC
MOV A,R5
PUSH ACC
MOV A,R6
PUSH ACC
MOV A,R7
PUSH ACC
ENDM
POPALL MACRO
POP ACC ;R0-R7出栈
MOV R7,A
POP ACC
MOV R6,A
POP ACC
MOV R5,A
POP ACC
MOV R4,A
POP ACC
MOV R3,A
POP ACC
MOV R2,A
POP ACC
MOV R1,A
POP ACC
MOV R0,A
POP PSW
POP DPL
POP DPH
POP B
POP ACC
ENDM
;分配堆栈空间,?STACK和STARTUP.A51中同名,编译器会将两个?STACK段合并,堆栈大小在STARTUP.A51中定义
?STACK SEGMENT IDATA
RSEG ?STACK
;子程序
;-------------------------------------------------------------------------
RSEG ?PR?OSStartHighRdy?OS_CPU_A ;定义OSStartHighRd函数
SStartHighRdy:
USING 0
LCALL _?OSTaskSwHook
MOV (OSRunning),#01H
OSCtxSw_in:
;OSTCBCur ===> DPTR 获得当前TCB指针,详见C51.PDF第178页 ,OSTCBCur为指向xdata的专用指针
MOV DPH, (OSTCBCur) ;获得OSTCBCur指针低地址,由于定义OSTCBCur是指向XDATA区的,所以占2字节。+0高8位数据+1低8位数据
MOV DPL, (OSTCBCur+1)
;OSTCBCur->OSTCBStkPtr ===> DPTR 获得用户堆栈指针
INC DPTR ;OSTCBCur->OSTCBStkPtr没有指定类型,所以占3字节。+0类型+1高8位数据+2低8位数据
MOVX A,@DPTR ;.OSTCBStkPtr是void指针
MOV R0,A
INC DPTR
MOVX A,@DPTR
MOV DPL,A
MOV DPH,R0
MOVX A,@DPTR ;首先得到的是?C_XBP ,复仿真堆栈指针?C_XBP
MOV ?C_XBP,A ;?C_XBP 仿真堆栈指针高8位
INC DPTR
MOVX A,@DPTR
MOV ?C_XBP+1,A ;?C_XBP 仿真堆栈指针低8位
;*UserStkPtr ===> R5 用户堆栈起始地址内容(即用户堆栈长度放在此处)
INC DPTR
MOVX A,@DPTR ;用户堆栈中是unsigned char类型数据
MOV R5,A ;R5=用户堆栈长度
;恢复现场堆栈内容
MOV R0,#?STACK-1
restore_stack:
INC DPTR
INC R0
MOVX A,@DPTR
MOV @R0,A
DJNZ R5,restore_stack
;恢复堆栈指针SP
MOV SP,R0
POPALL ;出栈返回
RETI
;-------------------------------------------------------------------------
RSEG ?PR?OSCtxSw?OS_CPU_A ;定义OSCtxSw函数(任务级切换)
SCtxSw:
PUSHALL ;保存所用寄存器、PSW、DPTR指针、A、B等内容
OSIntCtxSw_in:
;获得堆栈长度和起址
MOV A,SP
CLR C
SUBB A,#?STACK-1 ;计算堆栈长度
MOV R5,A ;获得堆栈长度
;OSTCBCur ===> DPTR 获得当前TCB指针,OSTCBCur为指向xdata的专用指针
MOV DPH, (OSTCBCur) ;获得OSTCBCur指针低地址,指针占2节。+0高8位数据+1低8位数据
MOV DPL, (OSTCBCur+1)
;OSTCBCur->OSTCBStkPtr ===> DPTR 获得用户堆栈指针
INC DPTR ;指针占3字节。+0类型+1高8位数据+2低8位数据
MOVX A,@DPTR ;.OSTCBStkPtr是void指针,没有指定指向的存储类型所以占三字节
MOV R0,A
INC DPTR
MOVX A,@DPTR
MOV DPL,A
MOV DPH,R0
;保存仿真堆栈指针?C_XBP
MOV A,?C_XBP ;?C_XBP 仿真堆栈指针高8位
MOVX @DPTR,A
INC DPTR
MOV A,?C_XBP+1 ;?C_XBP 仿真堆栈指针低8位
MOVX @DPTR,A
INC DPTR ;保存堆栈长度
MOV A,R5
MOVX @DPTR,A
MOV R0,#?STACK-1 ;获得堆栈起址
save_stack:
INC DPTR
INC R0
MOV A,@R0
MOVX @DPTR,A
DJNZ R5,save_stack
;调用用户程序
LCALL _?OSTaskSwHook
;OSTCBCur = OSTCBHighRdy
MOV (OSTCBCur),(OSTCBHighRdy)
MOV (OSTCBCur+1),(OSTCBHighRdy+1)
;OSPrioCur = OSPrioHighRdy 使用这两个变量主要目的是为了使指针比较变为字节比较,以便节省时间。
MOV (OSPrioCur),(OSPrioHighRdy)
LJMP OSCtxSw_in
;-------------------------------------------------------------------------
RSEG ?PR?OSIntCtxSw?OS_CPU_A ;声明中断级切换函数OSIntCtxSw
OSIntCtxSw:
;调整SP指针去掉在调用OSIntExit(),OSIntCtxSw()过程中压入堆栈的多余内容
;SP=SP-4
MOV A,SP
CLR C
SUBB A,#4
MOV SP,A
LJMP OSIntCtxSw_in
END ;文件结束
这个文件有几点补充,首先KEIL编译中断函数时,首先会保存重要的寄存器,保存顺序和上面的PUSHALL宏是一样的,所以中断级的任务切换时不需要调用PUSHALL宏来保存寄存器,因为进入中断时已经保存好了。初始任务调度时也不需要调用PUSHALL宏,这是因为在堆栈初始化函数OSTaskStkInit中已经为寄存器定义了初始值。第二,进入中断级任务切换时,首先会把SP做一个减4的偏移操作,这是因为进入中断后,先是处理中断服务程序,最后调用OSIntExit()函数,进入OSIntExit()函数后,首先会保存函数返回地址(两个字节,51寻址代码是16位的),然后OSIntExit()函数又会调用OSIntCtxSw()函数进行任务切换,调用时又会保存两个字节的返回地址,所以SP要作减4的偏移。如果下次任务切换回被这个中断打断的任务中,将不再通过“OSIntCtxSw()-OSIntExit()-中断函数-被中断的地方”这样的顺序返回,而是直接跳到上次被中断的地方。
四、仿真堆栈检查函数的实现
#if TASK_XBPStkChk_EN > 0
INT8U XBPStkChk (INT8U prio, OS_STK_DATA *ppdata) reentrant
{
#if OS_CRITICAL_METHOD == 3 /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr;
#endif
OS_TCB *ptcb;
OS_STK xdata *pchk;
OS_STK *pstk;
INT32U free;
INT32U size;
INT16U addr;
#if OS_ARG_CHK_EN > 0
if (prio > OS_LOWEST_PRIO && prio != OS_PRIO_SELF) { /* Make sure task priority is valid */
return (OS_PRIO_INVALID);
}
#endif
ppdata->OSFree = 0; /* Assume failure, set to 0 size */
ppdata->OSUsed = 0;
OS_ENTER_CRITICAL();
if (prio == OS_PRIO_SELF) { /* See if check for SELF */
prio = OSTCBCur->OSTCBPrio;
}
ptcb = OSTCBPrioTbl[prio];
if (ptcb == (OS_TCB *)0) { /* Make sure task exist */
OS_EXIT_CRITICAL();
return (OS_TASK_NOT_EXIST);
}
free = 0;
pstk = ptcb->OSTCBStkPtr; //得到用户堆栈栈底指针
addr = *(pstk-2);
addr = (addr<<8)|*(pstk-1);
pchk = (OS_STK xdata *)addr;
size =*(pstk-4);
size = (size<<8)|*(pstk-3);
OS_EXIT_CRITICAL();
while (*pchk++ == (OS_STK)0 && free < size) { /* Compute the number of zero entries on the stk */
free++;
}
ppdata->OSFree = free * sizeof(OS_STK); /* Compute number of free bytes on the stack */
ppdata->OSUsed = (size - free) * sizeof(OS_STK); /* Compute number of bytes used on the stack */
return (OS_NO_ERR);
}
#endif
很简单XBPStkChk 函数和任务堆栈检查函数OSTaskStkChk()几乎是一样的, 我也只是复制过来改改,但还是稍有不同。OSTaskStkChk()函数堆栈信息是TCB块中给出的,而仿真堆栈的信息是在任务堆栈的最低几个字节中存放的。还有,任务堆栈使用量不可能为0,因为一开始在堆栈初始化中就有使用,但是仿真堆栈不一样,如果没调用过可重入函数,仿真堆栈使用量可能是0,此时调用XBPStkChk 函数检测堆栈使用量,由于程序检测到仿真堆栈栈底后,堆栈内容还是0,程序会继续检测下去,直到检测到非零的空间为止,显然得到的结果是错的。所以我将循环条件写成 while (*pchk++ == (OS_STK)0 && free < size),这样就不会有问题了,检测到栈底后就会停止检测了。
五、调试
调试花了我不少时间,上文所述的不少隐蔽的问题也都是调试时发现并一一解决的,我建立了三个任务,轮流运行。哪个任务被切换到时,就在串口终端上打印出来,在任务1中我还打印了3个任务和空闲任务所使用的任务堆栈和仿真堆栈的字节数。验证了仿真堆栈检测函数是可行的,也验证了移植是成功的。(在P89V51RD2和STC89C516上均试验成功)下面是串口终端截图和我所用的51最小系统照片。
总结:
写本文的目的是对前一阶段学习的总结,旨在抛砖引玉,希望大家能多发表一些好的心得。其实我们所学大多来自于网络,理应取之于网路,奉献于网络。源码在CSDN上的链接是http://download.csdn.net/source/2836593。源码工程请下载最新版的KEIL打开。
最后申明,该源码可以随意使用、改动,但使用过程中出现问题本人概不负责。本文整理辛苦,希望转载注明出处。
如发现有什么错误,可以联系我。QQ:513117932。 邮箱:[email protected].