我将uCOS-II 移植到了EPSON 的C33209的平台上,接下来我就基于我移植好的代码讲解如何将uCOS-II从一种MCU移植到另一种MCU。
首先介绍uCOS-II的文件,如下表:
ucos_ii.h
os_cfg.h
os_cpu.h
os_core.c
os_dbg_r.c
os_flag.c
os_mbox.c
os_mem.c
os_mutex.c
os_q.c
os_sem.c
os_task.c
os_time.c
ucos_ii.c
os_cpu_c.c
os_cpu_a.asm
其中我们和硬件平台相关的文件的文件名被加粗了,也就是说若要将uCOS-II移植到新的平台上只要关心被以上四个文件就行了。当然你也可以根据需要再添加你自己的和平台相关的文件,事实上我也是这么做的。在我移植的例子中就添加了四个和平台相关的文件,文件如下表:
crt0.c
drv_rtc.c
vector.c
ext.s
crt0.c是用来初始化系统的比如说MCU的一些特殊寄存器、设置外围的总线接口,等。drv_rtc.c是用来初始化系统中的一个RTC的,这个RTC可以为内核提供必要的基于时间片调度的时基。同时提供了对RTC开始和停止的操作函数。在我的例子中RTC会每秒产生32次中断。vector.c顾名思义,它是系统上电后为系统提供矢量入口表的文件,当然也包括中断向量表。ext.s是为uc/OS-II提供OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()函数的具体实现以及在用户程序的中断函数出入时要调用的状态保护和状态恢复函数OS_SAVEALL ()和OS_RESTOREALL ()。前面两个函数的功能是:OS_ENTER_CRITICAL()屏蔽中断;OS_EXIT_CRITICAL()恢复原来的中断使能状态。
1. os_cpu_a.asm的说明
要想顺利的移植首先要了解uCOS-II的一些基本概念。
uCOS-II实质上是一个嵌入式操作系统内核,她只负责管理各个任务,为每个任务分配CPU时间,并且负责任务之间的通讯。内核提供的基本服务是任务切换。这是个很重要的概念,可以说你只要掌握了任务切换的本质,可以说你就掌握了移植uCOS-II的技术。至于任务之间的通讯他们是建立在任务切换之上的或者说和系统平台关系不大(当然这也和操作系统通讯机制的实现相关,至少uCOS-II是这样的)。
接下来我们就有针对性的介绍什么是uCOS-II里的任务。一个任务通常是一个无限循环,如下程序所示。
- void Task1(void *data)
- {
- INT8U err;
- char *rxmsg;
- data = data; /* Prevent compiler warning */
- while(1) //这是一个无限循环
- {
- rxmsg = (char *)OSMboxPend(MAIL1, 0, &err); /* Wait for message from Task #2 */
- OSTimeDlyHMSM(0, 0, 1, 0); /* Wait 1 second */
- OSMboxPend(MAIL3, 0, &err); /* Wait for message from Task #3*/
- OSMboxPost(MAIL2, (void *)1); /* Acknowledge reception of msg*/
- }
- }
可以通过内核的专用函数来建立、删除、挂起、激活任务,在这里我们的重点在如何移植,所以具体的使用方式和原理可以看JEAN J.LABROSSE 著、邵贝贝译的《uCOS-II—源码共开的实时嵌入式系统》一书。
(1). OSCtxSw()函数
在上面的例子里你也看到了任务和其他的C函数一样,有函数的返回类型,有形式参数变量,只是任务是绝不会返回。事实上任务也就是一个函数,内核在调度时是以这个函数为基础的,为了和其他函数区分,我们给了她另外一个名字——任务。也正因她是一个特殊的函数,而且和内核调度直接相关,所以不能随便返回和被用户调用,而要用内核的专用函数来“建立”和“删除”。所谓的“建立任务”其实是在内核处对该函数进行注册和相关数据结构的填充,比如该函数的入口地址、为函数分配专门的堆栈空间(为什么要为函数分配专门的地址空间呢?我们马上就会谈到)。“任务调度”就是根据情况(比如时间片被用完),来调用另一个被称为任务的函数(我们暂时称之为函数TA),同时停止当前的一个任务(其实也是一个函数,我们称之为TB)。问题出来了,若内核象普通函数那样直接调用TA,那么当内核要重新调用TB时怎么知道刚才TB执行到哪里了呢?若内核为TA和TB分配专用的两块空间,当内核要调用其他任务(其实就是函数)的时候先将当前任务(函数)运行的地址和状态保存起来,然后当要返回前再恢复,当然每个被称之为任务的函数都要有自己独立的保存运行地址和状态的空间,以免混乱。那问题就很好解决了。这也就是为什么任务都有自己的堆栈空间的原因。
那么新的问题来了,内核是如何调度的呢?在这里我们只关心内核要进行任务调度时发生的情况,而不关心内核为什么及何时要调度任务。这是因为这和移植关系不大,各种内核对任务的调度算法是不同的,解决方案也不同。但这些只是些算法上的区别,和平台关系不大。我们只需要将精力集中在内核决定要调度时会发生的事情。在uc/OS-II中若内核决定要对任务实行调度时最终会调用这个关键的函数void OSCtxSw(void),该函数位于os_cpu_a.asm中。它其实是一个软件中断或陷阱。因此有必要在中断矢量表里分配一个软件中断向量或陷阱给向量该函数。在我例子中的Vector.c文件中可以很清楚的看到我分配了一个软件中断向量给该函数。在os_cpu_a.asm文件中除了OSCtxSw()函数外你还看到了三个用汇编编写的函数,我会依次介绍。如下是OSCtxSw()函数的源代码。
- OSCtxSw:
- pushn %r15 ; 将r1~r15寄存器压入当前任务堆栈,(r1~r15是C33中的CPU寄存器)
- ld.w %r0,%ahr ; 将状态寄存器的内容转存入r0,r1寄存器
- ld.w %r1,%alr ;
- pushn %r1 ; 将状态寄存器压入堆栈
- ld.w %r4,%sp ; 将当前的SP指针内容保存入r4
- xld.w %r5,[OSTCBCur] ; 将当前SP指针内容存入uc/OS-II的一个数据结构:
- ld.w [%r5],%r4 ; OSTCBCur->OSTCBStkPtr中
- xcall OSTaskSwHook ; 调用用户接口函数,允许用户在任务切换时做一些工作
- xld.w %r4,[OSTCBHighRdy] ; 得到要切换的任务的TCB块
- xld.w %r5,OSTCBCur ; 将要切换到的任务TCB块放到当前TCB块
- xld.w [%r5], %r4 ;
- xld.w %r5,OSPrioHighRdy ; OSPrioCur = OSPrioHighRdy,保存要切换到的任务优先级
- ld.b %r4,[%r5]
- xld.w %r5,OSPrioCur
- xld.b [%r5],%r4
- xld.w %r5,[OSTCBHighRdy] ; SP = OSTCBHighRdy->OSTCBStkPtr,得到要切换到的
- ld.w %r4,[%r5] ; 任务SP指针
- ld.w %sp, %r4
- popn %r1
- ld.w %alr,%r1 ; 从要切换到的任务SP指针中恢复状态寄存器
- ld.w %ahr,%r0
- popn %r15 ; 从要切换到的任务SP指针中恢复r1~r15寄存器
- reti ; 从要切换到的任务SP指针中中断返回,这时自然就回到了要切换到的任务
该函数是用汇编写的,这就很直接的说明了一个问题——这个函数和uCOS-II的移植直接相关。OSCtxSw()人为的模仿了一次中断,大多数MCU提供软件中断或陷阱指令来实现这样的操作。必须提供中断向量给汇编语言函数OSCtxSw()。任务切换很简单,将被挂起任务的微处理器寄存器推入堆栈,然后将较高优先级的任务的寄存器值从堆栈中恢复到寄存器中。在uCOS-II中,就绪任务的堆栈结构总是看起来跟刚刚发生过中断一样,所有的微处理器的寄存器都保存在堆栈中。
(2). OSStartHighRdy()函数
在掌握了最关键的一个汇编函数后我们再来看看其他汇编函数。OSStartHighRdy(),顾名思义是操作系统开始工作时调用最高优先级任务的函数。它是在OSStart ()中被调用的,其实它的原理很简单,你只要理解了OSCtxSw()函数就能很轻易的理解它。我们先回顾一下刚才的话“就绪任务的堆栈结构总是看起来跟刚刚发生过中断一样”,那么在操作系统初始化结束,但还未进行调度时任务的堆栈结构又是什么样子的呢?uCOS-II是这样做的,她在初始化时将所有已建立任务的堆栈结构初始化,并把任务的首地址放在堆栈中。同时任务的堆栈指针指向栈顶。当系统启动开始执行第一个任务(当然是最高优先级的任务)时就调用OSStartHighRdy(),该函数会恢复要执行的任务的状态。在它返回时并没有使用普通的ret指令而是利用reti指令将初始化时由操作系统添入的任务的首地址和状态寄存器弹弹出,(单片机在进入中断是一般会自动将状态寄存器和PC指针同时入栈,所以在中断返回时要调用专用的reti指令,它会将状态寄存器和PC指针同时出栈。而正常的函数调用时状态寄存器是不会自动保存的,所以ret函数也不会同时恢复状态寄存器)这样第一个任务就启动了。从中你应该可以看出OSCtxSw()和OSStartHighRdy()的相似之处了吧?OSCtxSw()是要将挂起任务的状态保存,然后恢复要运行的任务的状态。而OSStartHighRdy()只需要将要运行的任务的状态恢复就行了。所以这部分源代码也非常相似,你自己也一定看得懂。
(3). OSIntCtxSw()函数
接下来我们来看看OSIntCtxSw()函数,该函数是在中断中对任务进行切换时被OSIntExit()调用的。注意因为是在中断中被调用的,所以OSIntCtxSw()认为所有状态寄存器已经被保存。用户在中断中要进行任务调度时尤其要注意这点。还有,要强调OSIntCtxSw()是在OSIntExit()中被调用的,而OSIntExit()要和OSIntEnter()成对使用,即用户想在中断函数中调度任务的话一定要在进入中断时调用OSIntEnter()在离开中断前调用OSIntExit()。别忘了!还要在一进入中断是最先调用OS_SAVEALL()它会帮你把所有的寄存器都保存起来,在即将退出中断前调用OS_RESTOREALL()。OSIntCtxSw()的原理也和OSCtxSw()相似,只是少了保存状态寄存器这一环而已。值得一提的是OSIntCtxSw()是在OSIntExit()中被调用的,而在OSIntCtxSw()返回时就进入了新的任务,并不是从中断返回时再进入新的任务的,因此在OSIntCtxSw()里首先要调整堆栈指针的位置。
(4). OSTickISR()函数
接下来我们来看看最后一个函数OSTickISR(),这个函数其实就是一个时钟中断函数,就是它为系统提供所谓的时间片。既然作为一个中断函数你就必须给他分配中断向量。在我的Vector.c文件中你也能看到。它还会为一个称之为OSIntNesting的全局变量加一,为什么加一我们就不讨论了。反正你要移植的时候也别忘了给OSIntNesting变量加一就行了。
2. os_cpu_c.c的说明
和os_cpu_a.asm一样,os_cpu_c.c也是和移植密切相关的一个文件,只不过是用C语言写的。在该文件中最重要的是如下这个函数:
- OS_STK *OSTaskStkInit (INT32U *pd, void *pdata, INT32U *ptos, INT16U opt)
- {
- INT32U *stk;
- opt = opt; /* 'opt' is not used, prevent warning */
- stk = (INT32U)ptos; /* Load stack pointer */
- *stk-- = (INT32U)pd;//return address
- *stk-- = (INT32U)0x10;//psr, Interrupts enabled
- *stk-- = (INT32U)0;//r15
- *stk-- = (INT32U)0;//r14
- *stk-- = (INT32U)0;//r13
- *stk-- = (INT32U)0;//r12
- *stk-- = (INT32U)0;//r11
- *stk-- = (INT32U)0;//r10
- *stk-- = (INT32U)0;//r9
- *stk-- = (INT32U)0;//r8
- *stk-- = (INT32U)0;//r7
- *stk-- = (INT32U)0;//r6
- *stk-- = (INT32U)0;//r5
- *stk-- = (INT32U)0;//r4
- *stk-- = (INT32U)0;//r3
- *stk-- = (INT32U)0;//r2
- *stk-- = (INT32U)0;//r1
- *stk-- = (INT32U)0;//r0
- *stk-- = (INT32U)0;//alr
- *stk = (INT32U)0;//ahr
- return ((void *)stk);
- }
我们再次回顾一下“就绪任务的堆栈结构总是看起来跟刚刚发生过中断一样”这句话,那么在你要移植的系统初始化时要将任务堆栈变成什么样子呢?通过这个函数!操作系统在调用这个函数时会传递一个堆栈指针给它,利用这个堆栈指针你就可以根据你系统的要求将堆栈初始化。并且最后返回该堆栈指针。比如在我的MCU中有16个通用寄存器、1个状态寄存器和2个专用寄存器。每次中断(或任务调度)时要将她们全部入栈,而且我的MCU的堆栈生长方向是向下的。参数pd就是任务的首地址,通常它应该放在栈底,紧接着任务首地址的是状态寄存器。再接下来就是16个通用寄存器和2个专用寄存器。16个通用寄存器的和2个专用寄存器的保存顺序一旦固定,那么你在进入中断时的入栈顺序就要和这个函数一致当然出栈顺序也要匹配。在这里要提醒你一点,我的MCU是32位的,对堆栈操作也是4字节对齐的,所以要将stk指针定义成INT32U。你的MCU若是16位,且对堆栈访问也是2字节对齐的,就要将stk定义成INT16U。否则,呵呵呵……!
3. os_cpu.h的说明
在该头文件中定义了许多操作系统要用到的基本的数据类型和变量。最重要的是你要实现如下宏:
#define OS_TASK_SW()
该宏是操作系统在进行任务切换时调用的,一般都定义成一个软件中断。在我的系统中定义成如下形式:
- #define OS_TASK_SW() asm("int 3") //任务切换时调用软件中断
- 还有就是要定义堆栈的生长方向:
- #define OS_STK_GROWTH 1 //1 表示从高到低方向生长,0表示从低到高方向生长
StatusReg变量是我自己添加的,它被OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()使用,主要为了解决通过状态寄存器开关中断的问题。
4. os_cfg.h的说明
该文件是用来配置操作系统的,每个配置后面都有比较清晰的注解,我就不罗嗦了。而且一般情况下你不必修改。当然还是应该看看!
5. ext.s的说明
ext.s文件是我自己添加的,它实现了OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()函数,因为我的系统没有办法用一条指令来屏蔽中断。用户只要能实现OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()这两个函数对中断进行屏蔽和开放就行了。
至此和移植相关的代码全部搞定,你还有什么不清楚的吗?可以联系我:sean_wang@21cn.com