一、uC/OS-II的简介
uC/OS是一个微型的实时操作系统,包括了一个操作系统最基本的一些特性,如任务调度、任务通信、内存管理、中断管理等,而且这是一个代码完全开放的实时操作系统,简单明了的结构和严谨的代码风格,非常适合初涉嵌入式操作系统的人士学习,它可以让我们以最快的速度来了解操作系统的概念、结构和模块工作原理,并由浅入深逐步推广到商用操作系统上。同时对于那些对操作系统感兴趣的爱好者来说,uC/OS的浅显易懂,给我们提供了一个很好的研究标本。uC/OS功能不是很完整,比如缺少文件系统、设备管理、网络协议栈、图形用户接口等,需要我们自己去完善它。
二、uC/OS-II的特点
μC/OS-Ⅱ的源码清晰易读且结构协调,组织有序且注解详尽。
绝大部分μC/OS-Ⅱ的源码是用移植性很强的ANSI C写的。和微处理器硬件相关的那部分是用汇编语言写的。汇编语言写的部分已经压到最低限度,使得μC/OS-Ⅱ便于移植到其他微处理器上。如同μC/OS 一样,μC/OS-Ⅱ可以移植到许许多多微处理器上。条件是,只要该微处理器有堆栈指针,由CPU内部寄存器入栈、出栈指令。另外,使用的C编译器必须支持内嵌汇编(inline assembly)或者该C语言可扩展、可连接汇编模块,使得关中断、开中断能在C语言程序中实现。μC/OS-Ⅱ可以在绝大多数8位、16位、32位以至64位微处理器、微控制器、数字信号处理器(DSP)上运行。 从移植了的μC/OS升级到μC/OS-Ⅱ,全部工作一个小时左右就可完成。因为μC/OS-Ⅱ和μC/OS是向下兼容的,应用程序从μC/OS升级到 μC/OS-Ⅱ几乎不需要改动或根本不需要改动。移植的范例可以从互联网上找到,网址是www.uCOS-Ⅱ.com。
μC/OS- Ⅱ是为嵌入式应用而设计的,这就意味着,只要有固化手段(C编译、连接、下载和固化),μC/OS-Ⅱ可以嵌入到产品中成为产品的一部分。
可以只使用μC/OS-Ⅱ中应用程序需要的那些]系统服务。也就是说某产品可以只使用很少几个μC/OS-Ⅱ调用,而另一个产品则使用了几乎所有μC/OS-Ⅱ的功能。这样可以减少产品中的] μC/OS-Ⅱ所需的存储空间(RAM和ROM),这种可裁剪性是靠条件编译实现的。只要在用户的应用程序中(用#define constants 语句)定义哪些μC/OS-Ⅱ中的功能是应用程序需要的就可以了。程序和数据两部分的存储用量已被最大努力的压低了。
μC/OS- Ⅱ完全是占先式的实时内核。这意味着μC/OS-Ⅱ总是运行就绪条件下优先级最高的任务。大多数商业内核也是占先式的,μC/OS-Ⅱ在性能上和它们类似。
μC/OS- Ⅱ可以管理64个任务,然而,目前这一版本保留8个给系统。应用程序最多可以有56个任务。赋予每个任务的优先级必须是不同的,这意味着μC/OS-Ⅱ不支持时间片轮转调度法(Round-robin Scheduling)。
全部μC/OS-Ⅱ的函数调用与服务的执行时间具有其可确定性。也就是说,全部μC/OS-Ⅱ的函数调用与服务的执行时间是可知的。进而言之,μC/OS系统服务的执行时间不依赖于应用程序任务的多少。
每个任务有自己单独的栈,μC/OS-Ⅱ允许每个任务有不同的栈空间。以便压低应用程序对RAM的需求。使用μC/OS-Ⅱ的栈空间校验函数,可以确定每个任务到底需要多少栈空间。
μC/OS-Ⅱ提供很多系统服务,例如邮箱、消息队列、信号量、块大小固定的内存的申请与释放、时间相关函数等。
中断可以使正在执行的任务暂时挂起。如果优先级更高的任务被该中断唤醒,则高优先级的任务在中断嵌套全部退出后立即执行,中断嵌套层数可达255层。
μC/OS-Ⅱ是基于μC/OS的,μC/OS自1992年以来已经有好几百个商业应用。μC/OS-Ⅱ与μC/OS的内核是一样的,只不过提供了更多的功能。
三、uC/OS-II源码目录结构
μC/OS-Ⅱ内核源码的主目录是UCOS- II,内核源码主要集中在其下面的两个子目录:SOURCE和一个表示处理器类型的子目录(例如:当代码运行在80x86实模式下是,该子目录名为Ix86L)。SOURCE子目录中包括的是与处理器类型无关的源代码,这些代码完全可以移植到其它架构的处理器上。而表示处理器类型的子目录中则包含移植时需要改动的代码。下面分别对这两个目录下的文件进行解释。
3.1 与处理器类型相关的内核代码文件
UCOS-IIOS_CPU.H
该文件首先对一些常用的数据类型进行了重新定义,如下所示:
typedef unsigned char BOOLEAN;
typedef unsigned char INT8U;
typedef signed char INT8S;
typedef unsigned int INT16U;
typedef signed int INT16S;
typedef unsigned long INT32U;
typedef signed long INT32S;
typedef float FP32;
typedef double FP64;
因为不同的微处理器有不同的字长,所以进行以上对数据类型的重新定义以确保可移植性。µCOS-II不使用C语言中的short,int,long等数据类型的定义,因为它们与处理器类型有关,隐含着不可移植性。代之以移植性强的整数数据类型,这样,既直观又可移植。为了方便起见,还定义了浮点数数据类型,虽然µC/OS-II中没有使用浮点数。
以INT16U数据类型为例,它代表16位无符号整数数据类型。µC/OS-II和用户的应用代码可以定义这种类型的数据,范围从0到65,535。如果将µCO/S-II移植到32位处理器中,那就意味着INT16U不再不是一个无符号整型数据,而是一个无符号短整型数据。然而将无论µC/OS-II用到哪里,都会当作 INT16U处理。
接下来该文件定义了进入和离开临界区的宏:OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()。OS_ENTER_CRITICAL() 关中断;而OS_EXIT_CRITICAL()开中断。关中断和开中断是为了保护临界段代码。这些代码很显然与处理器有关。µCO/S-II提供了两种进入和离开临界区的方法,程序员可以自行决定使用哪种方法。
方法1
第一种方法,也是最简单的方法,是直接将OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()定义为处理器的关闭(CLI)和打开(STI)中断指令。但这种方法有一个隐患,如果在关闭中断后调用µC/OS-II函数,当函数返回后,中断将被打开!严格意义上的关闭中断应该是执行 OS_ENTER_CRITICAL()后中断始终是关闭的,方法1显然不满足要求。但方法1的最大优点是简单,执行速度快(只有一条指令),在此类操作频繁的时候更为突出。如果在任务中并不在意调用函数返回后是否被中断,推荐用户采用方法1。此时需要将OSIntCtxSw()中的常量由10改到8(见文件OS_CPU_A.ASM)。
方法2
执行 OS_ENTER_CRITICAL()的第二种方法是先将中断关闭的状态保存到堆栈中,然后关闭中断。与之对应的OS_EXIT_CRITICAL() 的操作是从堆栈中恢复中断状态。采用此方法,不管用户是在中断关闭还是允许的情况下调用µC/OS-Ⅱ中的函数,在调用过程中都不会改变中断状态。如果用户在中断关闭的情况下调用µC/OS-Ⅱ函数,其实是延长了中断响应时间。虽然OS_ENTER_CRITICAL()和 OS_EXIT_CRITICAL()可以保护代码的临界段。但如此用法要小心,特别是在调用OSTimeDly()一类函数之前关闭了中断。此时任务将处于延时挂起状态,等待时钟中断,但此时时钟中断是禁止的!则系统可能会崩溃。很明显,所有的PEND调用都会涉及到这个问题,必须十分小心。所以建议用户调用µC/OS-Ⅱ的系统函数之前打开中断。
宏 OS_STK_GROWTH定义了堆栈增长的方向,定义为1时表示从高地址向低地址增长,定义为0则相反。UCOS 则定义为进行进程切换(上下文切换)的中断向量。
在 µC/OS-II中, 就绪任务的堆栈初始化应该模拟一次中断发生后的样子,堆栈中应该按进栈次序设置好各个寄存器的内容。OS_TASK_SW()函数模拟一次中断过程,在中断返回的时候进行任务切换。
UCOS-IIOS_CPU_C.C
OS_CPU_C.C中实现了µC/OS-II移植时需要改写的六个函数:
OSTaskStkInit()
OSTaskCreateHook()
OSTaskDelHook()
OSTaskSwHook()
OSTaskStatHook()
OSTimeTickHook()
实际需要修改的其实只有OSTaskStkInit()函数,该函数由 OSTaskCreate()或OSTaskCreateExt()调用,用来初始化任务的堆栈。初始状态的堆栈模拟发生一次中断后的堆栈结构。下图说明了OSTaskStkInit()初始化后的堆栈内容。请注意,图中的堆栈结构不是调用OSTaskStkInit()任务的,而是新创建任务的。
该文件中除了OSTaskStkInit()函数以外的其它5个函数需要声明,但不一定有实际内容。这5个函数都是由用户定义的钩子函数。以OS_CPU_C.C中没有给出代码。如果用户需要使用这些函数,请将文件OS_CFG.H中的#define constant OS_CPU_HOOKS_EN设为1,设为0表示不使用这些函数。
UCOS-IIOS_CPU_A.ASM
OS_CPU_A.ASM中实现了µC/OS-II移植时需要用户改写的四个函数:
OSStartHighRdy()
OSCtxSw()
OSIntCtxSw()
OSTickISR()
这四个函数与处理器类型的关系比较密切,是用汇编语言实现的。
OSStartHighRdy()函数由SStart()函数调用,功能是运行优先级最高的就绪任务,在调用OSStart()之前,用户必须先调用 OSInit(),并且已经至少创建了一个任务。OSStartHighRdy()默认指针OSTCBHighRdy指向优先级最高就绪任务的任务控制块(OS_TCB)。
OSCtxSw()是一个任务级的任务切换函数(在任务中调用,区别于在中断程序中调用的OSIntCtxSw())。在80x86系统上,它通过执行一条软中断的指令来实现任务切换。软中断向量指向 OSCtxSw()(通过应用程序中的PC_VectSet(uCOS, OSCtxSw);语句实现)。在µC/OS-II中,如果任务调用了某个函数,而该函数的执行结果可能造成系统任务重新调度(例如试图唤醒了一个优先级更高的任务),则在函数的末尾会调用OSSched(),如果OSSched()判断需要进行任务调度,会找到该任务控制块OS_TCB的地址,并将该地址拷贝到OSTCBHighRdy,然后通过宏OS_TASK_SW()执行软中断进行任务切换。
在µC/OS- II中,由于中断的产生可能会引起任务切换,在中断服务程序的最后会调用OSIntExit()函数检查任务就绪状态,如果需要进行任务切换,将调用OSIntCtxSw()。所以OSIntCtxSw()又称为中断级的任务切换函数。OSIntCtxSw()的代码大部分与OSCtxSw()的代码相同,不同之处是,第一,由于中断已经发生,此处不需要再保存CPU寄存器(没有PUSHA, PUSH ES, 或PUSH DS);第二,OSIntCtxSw()需要调整堆栈指针,去掉堆栈中一些不需要的内容,以使堆栈中只包含任务的运行环境。
OSTickISR()是µC/OS-II的时钟中断服务函数。
3.2 与处理器类型无关的内核代码文件
SOURCEUCOS-II.H
该头文件定义了µC/OS-II中常用的常量和数据结构。并对内核函数进行了声明。
SOURCE OS_CORE.C
从文件名就可以看出,该文件是整个操作系统比较核心的部分,文件中包括了操作系统中很多比较重要的函数,比如用于任务间通信的事件函数:OSEventTaskRdy()、OSEventTaskWait()、OSEventTO()、 OSEventWaitListInit()。进程调度函数:OSSched()。对操作系统进行初始化的函数OSInit()。对任务控制块进行初始化的函数OSTCBInit()。时钟中断的主体部分OSTimetick(),该函数在时钟中断中被调用。开始运行操作系统的函数OSStart(),即开始运行优先级最高的就绪任务。另外,该文件中还包含了显示版本信息,进行临界区控制的其它一些函数。
SOURCE OS_MBOX.C
该文件中提供了µC/OS-II的5种对邮箱的操作:OSMboxCreate(),OSMboxPend(),OSMboxPost(),OSMboxAccept() 和OSMboxQuery()函数。
邮箱是µC/OS-II中另一种通讯机制,它可以使一个任务或者中断服务子程序向另一个任务发送一个指针型的变量。该指针指向一个包含了特定“消息”的数据结构。为了在µC/OS-II中使用邮箱,必须将OS_CFG.H中的OS_MBOX_EN常数置为1。
OSMboxCreate() 建立一个邮箱,OSMboxPend()等待一个邮箱中的消息,OSMboxPost()发送一个消息到邮箱中,OSMboxAccept()无等待的从邮箱中得到一个消息,OSMboxQuery()查询一个邮箱的状态。
SOURCE OS_MEM.C
在µC/OS-II中,操作系统把连续的大块内存按分区来管理。每个分区中包含有整数个大小相同的内存块。每个内存分区由一个 OS_MEM结构的内存控制块描述。如果要在µC/OS-II中使用内存管理,需要在OS_CFG.H文件中将开关量OS_MEM_EN设置为1。这样µC/OS-II 在启动时就会对内存管理器进行初始化由OSInit()调用OSMemInit()实现。
OS_MEM.C中用于内存管理的函数有五个:
OSMemCreate()
OSMemGet()
OSMemInit()
OSMemPut()
OSMemQuery()
OSMemCreate( )用于建立一个内存分区,可以指定分区中内存块的个数和大小。程序通过调用OSMemGet( )函数申请分配一个内存块,函数的参数中指定了请求的内存分区。OSMemPut( )释放一个内存块到指定分区的空闲内存块链表中。OSMemQuery()函数查询一个特定内存分区的有关消息。通过该函数可以知道特定内存分区中内存块的大小、可用内存块数和正在使用的内存块数等信息。所有这些信息都放在一个叫OS_MEM_DATA的数据结构中。
SOURCE OS_Q.C
该文件提供了对消息队列通信机制的支持。消息队列是µC/OS-II中另一种通讯机制,它可以使一个任务或者中断服务子程序向另一个任务发送以指针方式定义的变量。因具体的应用有所不同,每个指针指向的数据结构变量也有所不同。为了使用µC/OS-II的消息队列功能,需要在OS_CFG.H 文件中,将OS_Q_EN常数设置为1,并且通过常数OS_MAX_QS来决定µC/OS-II支持的最多消息队列数。
OS_Q.C提供了8个对消息队列进行操作的函数:OSQCreate(),OSQPend(),OSQPost(),OSQPostFront(),OSQAccept(),OSQInit(),OSQFlush() 和OSQQuery()函数。
OSQCreate()函数建立一个消息队列,OSQInit()初始化一个消息队列,OSQPend()等待一个消息队列中的消息,OSQPost()以FIFO的方式向消息队列发送一个消息,OSQPostFront()以LIFO的方式向消息队列发送一个消息,OSQAccept()无等待地从一个消息队列中取得一个消息,OSQFlush()清空一个消息队列,OSQQuery()查询一个消息队列的状态。
SOURCE OS_SEM.C
信号量是任务间通信的一种机制,该文件提供了对信号量机制的支持。µC/OS-II中的信号量由两部分组成:一个是信号量的计数值,它是一个16位的无符号整数(0 到65,535之间);另一个是由等待该信号量的任务组成的等待任务表,每个信号量用一个OS_EVENT结构表示。用户要在OS_CFG.H中将 OS_SEM_EN开关量常数置成1,这样µC/OS-II才能支持信号量。
在 OS_SEM.C中,µC/OS-II提供了5个对信号量进行操作的函数。它们是:OSSemCreate(),OSSemPend(),OSSemPost(),OSSemAccept()和OSSemQuery()函数。
OSSemCreate()建立一个信号量,OSSemPend()等待一个信号量,OSSemPost()发送一个信号量,OSSemAccept()无等待地请求一个信号量,OSSemQuery()查询一个信号量的当前状态。
SOURCE OS_TASK.C
该文件包括了对任务进行各种操作的函数,包括文件的建立、删除、改变优先级、挂起和恢复等。µC/OS-Ⅱ可以管理多达64个任务,并从中保留了四个最高优先级和四个最低优先级的任务供自己使用,所以用户可以使用的只有56个任务。任务的优先级越高,反映优先级的值则越低。
对任务进行操作的函数如下:OSTaskChangePrio()、OSTaskCreate()、OSTaskCreateExt()、OSTaskDel()、 OSTaskDelReq()、OSTaskResume()、OSTaskStkChk()、OSTaskSuspend()和 OSTaskQuery()。
在用户程序中通过传递任务地址和其它参数到以下两个函数之一来建立任务:OSTaskCreate()或OSTaskCreateExt()。OSTaskCreate()用于向下兼容,OSTaskCreateExt()是OSTaskCreate()的扩展版本,提供了一些附加的功能。在开始多任务调度(即调用 OSStart())前,用户必须建立至少一个任务。UBuFb
OSTaskStkChk()用于进行任务的堆栈检验。在对一个任务分配堆栈前,可以调用该函数大致检验一下任务实际所需堆栈的大小,这样可以避免为任务分配过多的堆栈空间。
OSTaskDel()用于删除任务。删除任务,是说任务将返回并处于休眠状态,并不是说任务的代码被删除了,只是任务的代码不再被µC/OS-Ⅱ调用。
有时候,如果任务A拥有内存缓冲区或信号量之类的资源,而任务B想删除该任务,这些资源就可能由于没被释放而丢失。在这种情况下,用户可以想法子让拥有这些资源的任务在使用完资源后,先释放资源,再删除自己。用户可以通过OSTaskDelReq()函数来完成该功能。
OSTaskChangePrio()用于改变任务的优先级。
挂起任务通过调用OSTaskSuspend()函数来完成。被挂起的任务通过调用OSTaskResume()函数来恢复。
用户的应用程序可以通过调用OSTaskQuery()来获得自身或其它应用任务的信息。实际上,OSTaskQuery()获得的是对应任务的任务控制块OS_TCB中内容的拷贝。
SOURCE OS_TIME.C2W
µC/OS-Ⅱ要求用户提供定时中断来实现延时与超时控制等功能。这个定时中断叫做时钟节拍,它的实际频率是由用户的应用程序决定的。
OS_TIME.C文件中实现了五个和时钟节拍相关的系统函数:
OSTimeDly()
OSTimeDlyHMSM()
OSTimeDlyResume()
OSTimeGet()
OSTimeSet()
OSTimeDly() 函数可以将当前任务延时一段时间,这段时间的长短是用时钟节的数目来决定的。调用该函数会使µC/OS-Ⅱ进行一次任务调度,并且执行下一个优先级最高的就绪态任务。任务调用OSTimeDly()后,一旦规定的时间期满或者有其它的任务通过调用OSTimeDlyResume()取消了延时,它就会马上进入就绪状态。
OSTimeDlyHMSM()函数也是延时当前任务,不过是通过用小时、分、秒和毫秒为单位来指定延时的时间。
µC/OS- Ⅱ可以通过调用OSTimeDlyResume()和指定要恢复的任务的优先级来结束正处于延时期间的任务。
在每个时钟节拍中断时,µC/OS-Ⅱ都会将一个32位的计数器OSTime加1。这个计数器在用户调用OSStart初始化系统时清零。用户可以通过调用 OSTimeGet()来获得该计数器的当前值。也可以通过调用OSTimeSet()来改变该计数器的值。
原文地址为:http://safelab.nku.cn:8080/cgi-bin/topic.cgi?forum=30&topic=11&show=100
转载这篇文章是因为它可以让自己在宏观上把握uC/OS-II的结构以及它的特点,文中未指出所使用uC/OS-II的版本,可能与2.52版本的有出入。另外需要注意的是文中提到的官网地址已经更改为http://micrium.com/page/home。
通读完此文档可以对uC/OS-II的代码组织形式有了整体的了解。从移植性上来说,代码可以分为两类:与平台有关的代码OS_CPU.H,OS_CPU_C.C,OS_CPU_A.ASM;与平台无关的代码UCOS-II.H,UCOS-II.C,OS_CORE.C,OS_MBOX.C,OS_MEM.C,OS_Q.C,OS_SEM.C,OS_TASK.C,OS_FLAG.C,OS_MUTEX.C,OS_TIME.C。