u C / O S 是一种免费公开源代码、结构小巧、具有可剥夺实时内核的实时操作系统。
μC/OS-II 的前身是μC/OS,最早出自于1992 年美国嵌入式系统专家Jean J.Labrosse 在《嵌入式系统编程》杂志的5 月和6 月刊上刊登的文章连载,并把μC/OS 的源码发布在该杂志的B B S 上。
μC/OS 和μC/OS-II 是专门为计算机的嵌入式应用设计的, 绝大部分代码是用C语言编写的。CPU 硬件相关部分是用汇编语言编写的、总量约200行的汇编语言部分被压缩到最低限度,为的是便于移植到任何一种其它的CPU 上。用户只要有标准的ANSI 的C交叉编译器,有汇编器、连接器等软件工具,就可以将μC/OS-II嵌人到开发的产品中。μC/OS-II 具有执行效率高、占用空间小、实时性能优良和可扩展性强等特点, 最小内核可编译至 2KB 。μC/OS-II 已经移植到了几乎所有知名的CPU 上。
严格地说uC/OS-II只是一个实时操作系统内核,它仅仅包含了任务调度,任务管理,时间管理,内存管理和任务间的通信和同步等基本功能。没有提供输入输出管理,文件系统,网络等额外的服务。但由于uC/OS-II良好的可扩展性和源码开放,这些非必须的功能完全可以由用户自己根据需要分别实现。
uC/OS-II目标是实现一个基于优先级调度的抢占式的实时内核,并在这个内核之上提供最基本的系统服务,如信号量,邮箱,消息队列,内存管理,中断管理等。
任务管理
uC/OS-II 中最多可以支持 个任务,分别对应优先级0~63,其中0 为最高优先级。63为最低级,系统保留了4个最高优先级的任务和4个最低优先级的任务,所有用户可以使用的任务数有56个。
uC/OS-II提供了任务管理的各种函数调用,包括创建任务,删除任务,改变任务的优先级,任务挂起和恢复等。
系统初始化时会自动产生两个任务:一个是空闲任务,它的优先级最低,改任务仅给一个整形变量做累加运算;另一个是系统任务,它的优先级为次低,改任务负责统计当前cpu的利用率。
时间管理
uC/OS-II的时间管理是通过定时中断来实现的,该定时中断一般为10毫秒或100毫秒发生一次,时间频率取决于用户对硬件系统的定时器编程来实现。中断发生的时间间隔是固定不变的,该中断也成为一个时钟节拍。
uC/OS-II要求用户在定时中断的服务程序中,调用系统提供的与时钟节拍相关的系统函数,例如中断级的任务切换函数,系统时间函数。
内存管理
在ANSI C中是使用malloc和free两个函数来动态分配和释放内存。但在嵌入式实时系统中,多次这样的错作会导致内存碎片,且由于内存管理算法的原因,malloc和free的执行时间也是不确定。
uC/OS-II中把连续的大快内存按分区管理。每个分区中包含整数个大小相同的内存块,但不同分区之间的内存快大小可以不同。用户需要动态分配内存时,系统选择一个适当的分区,按块来分配内存。释放内存时将该块放回它以前所属的分区,这样能有效解决碎片问题,同时执行时间也是固定的。
任务间通信与同步
对一个多任务的操作系统来说,任务间的通信和同步是必不可少的。uC/OS-II中提供了4中同步对象,分别是信号量,邮箱,消息队列和事件。所有这些同步对象都有创建,等待,发送,查询的接口用于实现进程间的通信和同步。
任务调度
uC/OS-II 采用的是可剥夺型实时多任务内核。可剥夺型的实时内核在任何时候都运行就绪了的最高优先级的任务。
uC/os-II的任务调度是完全基于任务优先级的抢占式调度,也就是最高优先级的任务一旦处于就绪状态,则立即抢占正在运行的低优先级任务的处理器资源。为了简化系统设计,uC/OS-II规定所有任务的优先级不同,因为任务的优先级也同时唯一标志了该任务本身。
任务调度将在以下情况下发生:
1) 高优先级的任务因为需要某种临界资源,主动请求挂起,让出处理器,此时将调度就绪状态的低优先级任务获得执行,这种调度也称为任务级的上下文切换。
2) 高优先级的任务因为时钟节拍到来,在时钟中断的处理程序中,内核发现高优先级任务获得了执行条件(如休眠的时钟到时),则在中断态直接切换到高优先级任务执行。这种调度也称为中断级的上下文切换。
这两种调度方式在uC/OS-II的执行过程中非常普遍,一般来说前者发生在系统服务中,后者发生在时钟中断的服务程序中。
调度工作的内容可以分为两部分:最高优先级任务的寻找和任务切换。其最高优先级任务的寻找是通过建立就绪任务表来实现的。u C / O S 中的每一个任务都有的堆栈空间,并有一个称为任务控制块TCB(Task Control Block)的数据结构,其中第一个成员变量就是保存的任务堆栈指针。任务调度模块首先用变量OSTCBHighRdy 记录当前最高级就绪任务的TCB 地址,然后调用OS_TASK_SW()函数来进行任务切换。
μC/OS-II的组成部分
μC/OS-II可以大致分成核心、任务处理、时间处理、任务同步与通信,CPU的移植等5个部分。
1) 核心部分(OSCore.c)
是操作系统的处理核心,包括操作系统初始化、操作系统运行、中断进出的前导、时钟节拍、任务调度、事件处理等多部分。能够维持系统基本工作的部分都在这里。
2) 任务处理部分(OSTask.c)
任务处理部分中的内容都是与任务的操作密切相关的。包括任务的建立、删除、挂起、恢复等等。因为μC/OS-II是以任务为基本单位调度的,所以这部分内容也相当重要。
3) 时钟部分(OSTime.c)
μC/OS-II中的最小时钟单位是timetick(时钟节拍)。任务延时等操作是在这里完成的。
4) 任务同步和通信部分
为事件处理部分,包括信号量、邮箱、邮箱队列、事件标志等部分;主要用于任务间的互相联系和对临界资源的访问。
5) 与CPU的接口部分
是指μC/OS-II针对所使用的CPU的移植部分。由于μC/OS-II是一个通用性的操作系统,所以对于关键问题上的实现,还是需要根据具体CPU的具体内容和要求作相应的移植。这部分内容由于牵涉到SP等系统指针,所以通常用汇编语言编写。主要包括中断级任务切换的底层实现、任务级任务切换的底层实现、时钟节拍的产生和处理、中断的相关处理部分等内容。
6.3 μC/OS-II 实时操作系统
μC/OS是一个特殊风格的嵌入式操作系统,它有多个版本,可以适应从x86到8051的各种不同类型不同规模的嵌入式系统,原先代码开放,但某些改进版本,代码不开放。
1、μC/OS-II 的特点
可移植性:绝大部分μC/OS的源码是用移植性很强的ANSI C写的,和微处理器硬件相关的那部分是用汇编语言写的,汇编语言写的部分已经压到最低限度。
可固化:μC/OS是为嵌入式应用而设计的,用户可以通过固化手段将μC/OS嵌入到产品中成为产品的一部分。
可裁减:μC/OS系统由多个相对的、短小精炼的目标模块组成,用户可根据需要选择适当模块来裁剪和配置系统,这样,通过目标模块之间的按需组合,可以减少产品中的μC/OS所需的存储空间,这种裁减性是靠条件编译实现的。
占先式:μC/OS完全是占先式的实时内核,即μC/OS总是运行就绪条件下优先级最高的任务。
多任务:μC/OS可以管理个任务,每个任务的优先级必须是不同的,其中系统占用8个,应用程序最多可以有56个任务。
可确定性:全部μC/OS的函数调用与服务的执行时间是可知的,即μC/OS系统服务的执行时间不依赖于应用程序任务的多少。
任务栈:μC/OS允许每个任务有不同的堆栈空间,以便压低应用程序对RAM的需求。
系统服务:μC/OS有多个相对的、短小精炼的目标模块组成,这些模块有:任务管理、时间管理、任务间的通信与同步、内存管理。其中:任务管理提供建立任务、删除任务、请求删除任务、任务的堆栈检查、改变任务的优先级、挂起任务、恢复任务和任务信息查询的系统调用;时间管理提供任务延时、取消任务延时和查询系统时间的系统调用;任务间通信与同步提供基于信号量、邮箱和消息队列机制的系统调用;内存管理提供内存分区的建立、分配、释放和查询的系统调用。
中断管理:中断可以使正在执行的任务暂时挂起,如果优先级更高的任务被该中断唤醒,则高优先级的任务在中断嵌套全部退出后立即执行,中断嵌套层数可达255层。
稳定性和可靠性:μC/OS自1992年以来已经有好几百个商业应用。
2、μC/OS-II 内核
实时操作系统对系统资源进行管理。主要包括任务调度、时间管理、内存管理、资源管理(信号灯、邮箱、消息队列)四大部分。μC/OS所有系统服务均由内核提供。内核将应用系统和底层硬件结合成一个完整的实时系统。
⑴ 任务调度
任务可以是一个无限的循环,也可以是在一次执行完毕后被删除掉。这里要注意的是,任务代码并不是被真正的删除了,而只是μC/OS-Ⅱ不再理会该任务代码,所以该任务代码不会再运行。
任务看起来与任何C函数一样,具有一个返回类型和一个参数,只是它从不返回。任务的返回类型必须被定义成void型。
任务的调度包括如何在用户的应用程序中建立任务、删除任务、改变任务的优先级、挂起和恢复任务,以及获得有关任务的信息。它主要由下列函数组成:
◇ 建立任务OSTaskCreate()
◇ 建立任务OSTaskCreateExt()
◇ 堆栈检验OSTaskStkChk()
◇ 删除任务OSTaskDel()
◇ 请求删除任务OSTaskDelReq()
◇ 改变任务的优先级OSTaskChangePrio()
◇ 挂起任务OSTaskSuspend()
◇ 恢复任务OSTaskResume()
◇ 获得有关任务的信息OSTaskQuery()
以上函数可以在OS_TASK文件中找到。
① 建立任务函数OSTaskCreate()或OSTaskCreateExt()
想让μC/OS-Ⅱ管理用户的任务,用户必须要先建立任务。用户可以通过传递任务地址和其它参数到以下两个函数之一来建立任务:OSTaskCreate() 或OSTaskCreateExt()。OSTaskCreate()与μC/OS是向下兼容的,OSTaskCreateExt()是OSTaskCreate()的扩展版本,提供了一些附加的功能。用两个函数中的任何一个都可以建立任务。
任务可以在多任务调度开始前建立,也可以在其它任务的执行过程中被建立。在开始多任务调度(即调用OSStart())前,用户必须建立至少一个任务。任务不能由中断服务程序(ISR)来建立。
OSTaskCreate() 格式为:
INT8U OSTaskCreate (void (*task)(void *pd), void *pdata, OS_STK *ptos, INT8U prio)
OSTaskCreate()需要四个参数:
task 是任务代码的指针,
pdata 是当任务开始执行时传递给任务的参数的指针,
ptos 是分配给任务的堆栈的栈顶指针,
prio 是分配给任务的优先级。
返回值:OS_NO_ERR 任务成功建立
OS_PRIO_EXIST 此优先级上已有一个任务
OS_PRIO_INVALID 此优先级上已有一个休眠的任务
OSTaskCreateExt() 格式为:
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)
OSTaskCreateExt()需要九个参数!前四个参数(task,pdata,ptos和prio)与OSTaskCreate()的四个参数完全相同,连先后顺序都一样。
id 参数为要建立的任务创建一个特殊的标识符。该参数在μC/OS以后的升级版本中可能会用到,但在μC/OS-Ⅱ中还未使用。这个标识符可以扩展μC/OS-Ⅱ功能,使它可以执行的任务数超过目前的个。但在这里,用户只要简单地将任务的id设置成与任务的优先级一样的值就可以了。
pbos 是指向任务的堆栈栈底的指针,用于堆栈的检验。
stk_size 用于指定堆栈成员数目的容量。也就是说,如果堆栈的入口宽度为4字节宽,那么stk_size为10000是指堆栈有40000个字节。该参数与pbos一样,也用于堆栈的检验。
pext 是指向用户附加的数据域的指针,用来扩展任务的OS_TCB。
opt 用于设定OSTaskCreateExt()的选项,指定是否允许堆栈检验,是否将堆栈清零,任务是否要进行浮点操作等等。μCOS_Ⅱ.H文件中有一个所有可能选项(OS_TASK_OPT_STK_CHK,OS_TASK_OPT_STK_CLR和OS_TASK_OPT_SAVE_FP)的常数表。每个选项占有opt的一位,并通过该位的置位来选定(用户在使用时只需要将以上OS_TASK_OPT_?选项常数进行位或(OR)操作就可以了)。
② 任务堆栈和堆栈检验函数OSTaskStkChk()
每个任务都有自己的堆栈空间。堆栈必须声明为OS_STK类型,并且由连续的内存空间组成。用户可以静态分配堆栈空间(在编译的时候分配)也可以动态地分配堆栈空间(在运行的时候分配)。
堆栈检验函数OSTaskStkChk()可以用来统计从堆栈栈底开始的堆栈空闲空间。这样用户就可以避免为任务分配过多的堆栈空间,从而减少自己的应用程序代码所需的RAM(内存)数量。
OSTaskStkChk() 格式为:
INT8U OSTaskStkChk (INT8U prio, OS_STK_DATA *pdata)
prio 是想检验的任务的优先级。
0S_STK_DATA数据结构用来保存有关任务堆栈的信息。
③ 删除任务OSTaskDel()
通过调用OSTaskDel()就可以完成删除任务的功能。OSTaskDel()一开始应确保用户所要删除的任务并非是空闲任务,因为删除空闲任务是不允许的。
OSTaskDel() 格式为:
INT8U OSTaskDel (INT8U prio)
prio是想删除的任务的优先级。
④ 请求删除任务OSTaskDelReq()
有时候,如果任务A拥有内存缓冲区或信号量之类的资源,而任务B想删除该任务,这些资源就可能由于没被释放而丢失。在这种情况下,用户可以想法子让拥有这些资源的任务在使用完资源后,先释放资源,再删除自己。用户可以通过OSTaskDelReq()函数来完成该功能。
发出删除任务请求的任务(任务B)和要删除的任务(任务A)都需要调用OSTaskDelReq()函数。
任务B在需要请求删除任务的情况下调用OSTaskDelReq()函数。如果要被删除的任务不存在(即任务已被删除或是还没被建立),OSTaskDelReq()返回OS_TASK_NOT_EXIST。如果OSTaskDelReq()的返回值为OS_NO_ERR,则表明请求已被接受但任务还没被删除。
任务A通过调用OSTaskDelReq(OS_PRIO_SELF)来确认自己是否需要被删除。
⑤ 改变任务的优先级OSTaskChangePrio()
在用户建立任务的时候会分配给任务一个优先级。在程序运行期间,用户可以通过调用OSTaskChangePrio()来改变任务的优先级。μC/OS-Ⅱ允许用户动态的改变任务的优先级。
OSTaskChangePrio() 格式为:
INT8U OSTaskChangePrio (INT8U oldprio, INT8U newprio)
oldprio 旧的优先级
newprio 新的优先级
μC/OS-Ⅱ不允许多个任务具有相同的优先级,所以OSTaskChangePrio()需要检验新优先级是否是合法的(即不存在具有新优先级的任务)。如果新优先级是合法的,则保留这个优先级。
⑥ 挂起任务OSTaskSuspend()
通过调用OSTaskSuspend()函数可以挂起任务。被挂起的任务只能通过调用OSTaskResume()函数来恢复。
OSTaskSuspend() 格式为:
INT8U OSTaskSuspend (INT8U prio)
prio是想挂起的任务的优先级。
返回值:OS_NO_ERR 任务被成功挂起
OS_SUSPEND_IDLE 任务已休眠,不能被挂起
OS_PRIO_INVALID 此优先级大于OS_MAX_TASK
OS_TASK_SUSP_PRIO 此优先级没有被注册的任务
⑦ 恢复任务OSTaskResume()
通过调用OSTaskResume()函数来恢复被挂起的任务。
OSTaskResume() 格式为:
INT8U OSTaskResume (INT8U prio)
prio是想恢复的任务的优先级。
返回值:OS_NO_ERR 任务被成功激活
OS_TASK_NOT_SUSP 任务并没有被挂起
OS_PRIO_INVALID 此优先级大于OS_MAX_TASK
OS_TASK_NOT_EXIST 此优先级没有被注册的任务
⑧ 获得有关任务的信息OSTaskQuery()
用户的应用程序可以通过调用OSTaskQuery()来获得自身或其它应用任务的信息。
OSTaskQuery() 格式为:
INT8U OSTaskQuery (INT8U prio, OS_TCB *pdata)
prio是想获得信息的任务的优先级。
OS_TCB是想获得信息的任务控制块
⑵ 时间管理
μC/OS-Ⅱ(其它内核也一样)要求用户提供定时中断来实现延时与超时控制等功能。这个定时中断叫做时钟节拍,它应该每秒发生10至100次。时钟节拍的实际频率是由用户的应用程序决定的。时钟节拍的频率越高,系统的负荷就越重。
与时钟节拍有关的系统服务主要有:
◇ 任务延时函数 OSTimeDly()
◇ 按时分秒延时函数OSTimeDlyHMSM()
◇ 结束延时函数 OSTimeDlyResume()
◇ 设置系统时间 OSTimeGet()
◇ 返回系统时间 OSTimeSet()
它们都包含在OS_TIME.C文件中。
① 任务延时函数 OSTimeDly()
OSTimeDly()函数可以使用户按时钟节拍数来定义延时时间。该函数的参数是延时的时钟节拍数_____一个1 到65535之间的数。使用该函数,用户的应用程序需要知道延时时间对应的时钟节拍的数目。
任务调用OSTimeDly()后,一旦规定的时间期满或者有其它的任务通过调用OSTimeDlyResume()取消了延时,它就会马上进入就绪状态。注意,只有当该任务在所有就绪任务中具有最高的优先级时,它才会立即运行。
OSTimeDly() 格式为:
void OSTimeDly (INT16U ticks)
ticks 延时的时钟节拍数(1~65535)
② 按时分秒延时函数 OSTimeDlyHMSM()
OSTimeDlyHMSM()函数可以使用户按小时(H)、分(M)、秒(S)和毫秒(m)来定义延时时间。
任务调用OSTimeDlyHMSM()后,一旦规定的时间期满或者有其它的任务通过调用OSTimeDlyResume()取消了延时,它就会马上处于就绪态。同样,只有当该任务在所有就绪态任务中具有最高的优先级时,它才会立即运行。
OSTimeDlyHMSM() 格式为:
INT8U OSTimeDlyHMSM (INT8U hours, INT8U minutes, INT8U seconds, INT16U milli)
hours 小时
minutes 分
seconds 秒
milli 毫秒
③ 结束延时函数 OSTimeDlyResume()
μC/OS-Ⅱ允许用户结束延时正处于延时期的任务。通过调用OSTimeDlyResume()可以取消指定任务的延时,不等待延时期满,就使指定的任务处于就绪态。实际上,OSTimeDlyResume()也可以唤醒正在等待事件的任务,虽然这一点并没有提到过。在这种情况下,等待事件发生的任务会考虑是否终止等待事件。
该函数的参数是要恢复的任务的优先级。
OSTimeDlyResume() 格式为:
INT8U OSTimeDlyResume (INT8U prio)
prio 是想唤醒的任务的优先级
返回值:OS_NO_ERR 任务被成功唤醒
OS_TIME_DLY 任务并没有休眠
OS_PRIO_INVALID 此优先级大于OS_MAX_TASK
OS_TASK_NOT_EXIST 此优先级没有被注册的任务
④ 设置系统时间 OSTimeSet()
通过调用OSTimeSet()可以改变时钟节拍计数器的值。
该函数的参数是时钟节拍计数器的新值。
OSTimeSet() 格式为:
void OSTimeSet (INT32U ticks)
ticks 是时钟节拍计数器的新值
⑤ 返回系统时间 OSTimeGet()
通过调用OSTimeGet()可以获得时钟节拍计数器的当前值。
OSTimeGet() 格式为:
INT32U OSTimeGet (void)
返回值:ticks 时钟节拍计数器的当前值
⑶ 内存管理
内存管理模块用来对需要管理的内存块进行简单的管理:分配(动态分配)和释放(动态回收)。它主要由一个数据结构体和五个函数组成:
◇ 内存控制块数据结构OS_MEM
◇ 内存分区建立函数OSMemCreate()
◇ 内存块分配函数OSMemGet()
◇ 内存块释放函数OSMemPut()
◇ 内存分区状态查询函数OSMemQuery()
◇ 内存控制块链表初始化函数OSMemInit()
① 内存控制块数据结构OS_MEM
为了便于内存的管理,在μC/OS-II中使用内存控制块(memory control blocks)的数据结构来跟踪每一个内存分区。
其定义如下:
typedef struct {
void *OSMemAddr;
void *OSMemFreeList;
INT32U OSMemBlkSize;
INT32U OSMemNBlks;
INT32U OSMemNFree;
} OS_MEM;
系统中每个内存分区必须有一个属于自己的内存控制块,只有这样,内存管理模块中的五个函数才能对这个内存分区进行管理和操作。
.OSMemAddr是指向内存分区起始地址的指针。它在建立内存分区时被初始化,在此之后就不能更改了。
.OSMemFreeList是指向下一个空闲内存控制块或者下一个空闲的内存块的指针,具体含义要根据该内存分区是否已经建立来决定。
.OSMemBlkSize是内存分区中内存块的大小,是用户建立该内存分区时指定的。
.OSMemNBlks是内存分区中总的内存块数量,也是用户建立该内存分区时指定的。
.OSMemNFree是内存分区中当前可以得空闲内存块数量。
如果要在μC/OS-II中使用内存管理,需要:
Ⅰ.打开配置文件OS_CFG.H,将开关量OS_MEM_EN设置为1:#define OS_MEM_EN 0
Ⅱ.打开配置文件OS_CFG.H,设置系统要建立的任务分区的数量:#define OS_MAX_MEM_PART,该常数值至少应为2。
② 内存分区建立函数OSMemCreate()
在使用一个内存分区之前,必须先建立该内存分区。这个操作可以通过调用OSMemCreate()函数来完成。
OSMemCreate() 格式为:
OSMemCreate(OSMemAddr, OSMemNBlks, OSMemBlkSize, &err)
该函数共有4个参数:内存分区的起始地址、分区内的内存块总块数、每个内存块的字节数和一个指向错误信息代码的指针。
③ 内存块分配函数OSMemGet()
应用程序调用OSMemGet()函数可以从已经建立的内存分区中申请一个内存块。该函数的唯一参数是指向特定内存分区的指针,该指针在建立内存分区时,由OSMemCreate()函数返回。显然,应用程序必须知道内存块的大小,并且在使用时不能超过该容量。
OSMemGet() 格式为:
void *OSMemGet (OS_MEM *pmem, INT8U *err)
*pmem 是指向特定内存分区的指针
*err 是指向错误信息代码的指针
④ 内存块释放函数OSMemPut()
用户创建的任务不再使用申请来的内存块的时候,必须及时的调用OSMemPut()来把内存块释放到相应的内存分区中去。需要注意的是,这个内存块从那个内存分区中申请来的就必须释放到那个内存分区中去,否则会造成系统崩溃;这个用户在编写任务的时候注意就可以避免了。
OSMemGet()和OSMemPut()应该成对使用;
OSMemPut() 格式为:
INT8U OSMemPut (OS_MEM *pmem, void *pblk)
*pmem 是指向特定内存分区的指针
*pblk 空闲内存块链表
⑤ 内存分区状态查询函数OSMemQuery()
在μC/OS-II 中,可以使用OSMemQuery()函数来查询一个特定内存分区的相关信息。通过该函数可以知道特定内存分区中内存块的大小、可用内存块数和正在使用的内存块数等信息。所有这些信息都放在一个叫OS_MEM_DATA的数据结构中。
OSMemQuery() 格式为:
INT8U OSMemQuery (OS_MEM *pmem, OS_MEM_DATA *pdata)
*pmem 是指向特定内存分区的指针
*pdata OS_MEM_DATA的数据结构
⑷ 资源管理
μC/OS-II 中的资源管理主要包括:信号灯、邮箱、消息队列等。
① 信号灯
信号灯管理模块主要由五个函数组成:
◇ 信号灯初始化函数OSSemInit()
◇ 信号灯获取函数OSSemPend()
◇ 信号灯计数器函数OSSemAccept()
◇信号灯释放函数OSSemPost()
◇ 信号灯删除函数OSSemClear()
Ⅰ、信号灯初始化函数OSSemInit()
OSSemInit()函数初始化一个信号灯,可用来同步对公共资源的存取。
OSSemInit() 格式为:
UBYTE OSSemInit (OS_SEM *psem, UWORD cnt)
*psem 是信号灯指针
cnt 可同时支持的最大进程数
返回值:OS_NO_ERR 信号灯被初始化
Ⅱ、信号灯获取函数OSSemPend()
OSSemPend()函数用来获取一个信号灯以及对其保护资源的存取。
OSSemPend() 格式为:
UBYTE OSSemPend(OS_SEM *psem, UWORD timeout)
*psem 是信号灯指针
timeout 以内核时钟节拍为单位的等待时间(1~65534)
返回值:OS_NO_ERR 获得信号灯
OS_SEM_NODATA 信号灯被占用( timeout =OS_NO_SUSP时)
OS_TIMEOUE 信号灯被占用(经过等待timeout 后)
Ⅲ、信号灯计数器函数OSSemAccept()
OSSemAccept()函数可用来利用信号灯实现事件计数器的功能。
OSSemAccept() 格式为:
UBYTE OSSemAccept(OS_SEM *psem, UWORD *cnt, UWORD timeout)
*psem 是信号灯指针
*cnt 变量指针,该变量获得计数值
timeout 以内核时钟节拍为单位的等待时间(1~65534)
返回值:OS_NO_ERR 至少有一次事件发生
OS_SEM_NODATA 计时器为0( timeout =OS_NO_SUSP时)
OS_TIMEOUE 计时器为0(经过等待timeout 后)
Ⅳ、信号灯释放函数OSSemPost()
OSSemPost()函数用来释放使用完的信号灯,释放被保护资源。
OSSemPost() 格式为:
UBYTE OSSemPost(OS_SEM *psem)
*psem 是信号灯指针
返回值:OS_NO_ERR 信号灯被释放
OS_SEM_OVF 信号灯操作出错(计数值太大)
Ⅴ、信号灯删除函数OSSemClear()
OSSemClear()函数用来删除信号灯的计数值。
OSSemClear() 格式为:
UBYTE OSSemClear(OS_SEM *psem)
*psem 是信号灯指针
② 邮箱
邮箱管理模块主要由三个函数组成:
◇ 邮箱初始化函数OSMboxInit()
◇ 邮箱获取函数OSMboxPend()
◇邮箱释放函数OSMboxPost()
Ⅰ、邮箱初始化函数OSMboxInit()
OSMboxInit()函数用来初始化一个信箱。
OSMboxInit() 格式为:
UBYTE OSMboxInit(OS_MBOX*pmbox)
*pmbox 是信箱指针
返回值:OS_NO_ERR 信箱被初始化
Ⅱ、邮箱获取函数OSMboxPend()
OSMboxPend()函数用来从信箱获取信息。
OSMboxPend()格式为:
UBYTE OSMboxPend(OS_MBOX*pmbox, void OS_FAR *msg, UWORD timeout)
*pmbox 是信箱指针
*msg 接收缓冲区指针
timeout 以内核时钟节拍为单位的等待时间(1~65534)
返回值:OS_NO_ERR 成功获取信息
OS_MBOX_NODATA 信箱中没有信息( timeout =OS_NO_SUSP时)
OS_TIMEOUE 信箱中没有信息(经过等待timeout 后)
Ⅲ、邮箱释放函数OSMboxPost()
OSMboxPost()函数用来向信箱发送一个信息。
OSMboxPost()格式为:
UBYTE OSMboxPost(OS_MBOX*pmbox, void OS_FAR *msg, UWORD timeout)
*pmbox 是信箱指针
*msg 发送缓冲区指针
timeout 以内核时钟节拍为单位的等待时间(1~65534)
返回值:OS_NO_ERR 成功发送信息
OS_MBOX_FULL 信箱已满( timeout =OS_NO_SUSP时)
OS_TIMEOUE 信箱已满(经过等待timeout 后)
③ 消息队列
消息队列管理模块主要由六个函数组成:
◇ 队列初始化函数OSQueueInit()
◇ 队列状态获取函数OSQueueInfo()
◇ 队列获取函数OSQueuePend()
◇ 队列发送函数OSQueuePost()
◇ 队列头发送函数OSQueueFrontPost()
◇ 队列删除函数OSQueueClear()
Ⅰ、队列初始化函数OSQueueInit()
OSQueueInit()函数用来初始化一个队列。
OSQueueInit() 格式为:
UBYTE OSQueueInit(OS_Q*pq, void OS_HUGE*buffer, UWORD size)
*pq 是队列指针
*buffer 是内核缓冲区指针
size 以字节为单位的内核缓冲区大小
返回值:OS_NO_ERR 队列被初始化
Ⅱ、队列状态获取函数OSQueueInfo()
OSQueueInfo()函数用来获取一个队列的状态。
OSQueueInfo() 格式为:
UBYTE OSQueueInfo(OS_Q*pq, UWORD*size, UWORD*used, UBYTE *prio)
*pq 是队列指针
*size 指向变量的指针,将获得队列大小
*used 指向变量的指针,将获得队列中已用字节数
*prio 指向变量的指针,将获得处于等待状态的任务的优先级
返回值:OS_NO_ERR 无错误
Ⅲ、队列获取函数OSQueuePend()
OSQueuePend()函数用来从队列中获取一个字节。
OSQueuePend() 格式为:
UBYTE OSQueuePend(OS_Q*pq, UBYTE OS_FAR *msg, UWORD timeout)
*pq 是队列指针
*msg 接收字节指针
timeout 以内核时钟节拍为单位的等待时间(1~65534)
若使 timeout=OS_NO_SUSP ,没有可用字节,函数也会立即返回;若使 timeout=OS_SUSPEND,没有可用字节,函数将一直等到有可用字节才返回。
返回值:OS_NO_ERR 成功获取信息
OS_Q_NODATA 队列中没有信息( timeout =OS_NO_SUSP时)
OS_TIMEOUE 队列中没有信息(经过等待timeout 后)
Ⅳ、队列发送函数OSQueuePost()
OSQueuePost()函数用来发送一个字节到队列中。
OSQueuePost() 格式为:
UBYTE OSQueuePost(OS_Q*pq, UBYTE OS_FAR *msg, UWORD timeout)
*pq 是队列指针
*msg 接收字节指针
timeout 以内核时钟节拍为单位的等待时间(1~65534)
若使 timeout=OS_NO_SUSP ,队列没有可用空间,函数也会立即返回;若使 timeout=OS_SUSPEND,队列没有可用空间,函数将一直等到有可用空间才返回。
返回值:OS_NO_ERR 成功发送
OS_Q_FULL 队列满( timeout =OS_NO_SUSP时)
OS_TIMEOUE 队列满(经过等待timeout 后)
Ⅴ、队列头发送函数OSQueueFrontPost()
OSQueueFrontPost()函数用来发送一个字节到队列头部。
OSQueueFrontPost() 格式为:
UBYTE OSQueueFrontPost(OS_Q*pq, UBYTE OS_FAR *msg, UWORD timeout)
*pq 是队列指针
*msg 接收字节指针
timeout 以内核时钟节拍为单位的等待时间(1~65534)
若使 timeout=OS_NO_SUSP ,队列没有可用空间,函数也会立即返回;若使 timeout=OS_SUSPEND,队列没有可用空间,函数将一直等到有可用空间才返回。
返回值:OS_NO_ERR 成功发送
OS_Q_FULL 队列满( timeout =OS_NO_SUSP时)
OS_TIMEOUE 队列满(经过等待timeout 后)
Ⅵ、队列删除函数OSQueueClear()
OSQueueClear()函数用来删除一个队列中的内容。
OSQueueClear() 格式为:
UBYTE OSQueueClear(OS_Q*pq)
*pq 是队列指针
返回值:OS_NO_ERR 队列中的内容被删除
3、μC/OS 51移植
移植的时候内核是不变的,开发者根据自己应用系统的需要来选择实时操作系统内核,开发者不能对内核随意访问,只能使用内核提供的功能服务来开发自己的应用系统。内核确定,那么所提供的系统管理能力,系统服务也就得到了限定。开发者只能在规定的范围内对系统作些改动。
μC/OS 51移植涉及到两个方面:与处理器相关的代码和与应用相关的代码。
⑴ 与处理器相关的代码
这是移植中最关键的部分。内核将应用系统和底层硬件有机的结合成一个实时系统,要使同一个内核能适用于不同的硬件体系,就需要在内核和硬件之间有一个中间层,这就是与处理器相关的代码。处理器不同,这部分代码也不同。
我们在移植时需要自己处理这部分代码,可以自己编写,也可以直接使用已经成功移植的代码。
在μC/OS中这一部分代码包括三个文件:OS_CPU.H, OS_CPU_A.ASM,OS_CPU_C.C。
① OS_CPU.H
包括了用#define定义的与处理器相关的常量,宏和类型定义。
具体来讲有系统数据类型定义,栈增长方向定义,关中断和开中断定义,系统软中断的定义等等。
② OS_CPU_A.ASM
这部分需要对处理器的寄存器进行操作,所以必须用汇编语言来编写。包括四个子函数:OSStartHighRdy(),OSCtxSw(),OSIntCtxSw(),OSTickISR()。
OSStartHighRdy()
在多任务系统启动函数OSStart()中调用。完成的功能是:设置系统运行标志位OSRunning = TRUE;将就绪表中最高优先级任务的栈指针Load到SP中,并强制中断返回。这样就绪的最高优先级任务就如同从中断里返回到运行态一样,使得整个系统得以运转。
OSCtxSw()
在任务级任务切换函数中调用的。任务级切换是通过SWI或者TRAP人为制造的中断来实现的ISR的向量地址必须指向OSCtxSw()。这一中断完成的功能:保存任务的环境变量(主要是寄存器的值,通过入栈来实现),将当前SP存入任务TCB中,载入就绪最高优先级任务的SP,恢复就绪最高优先级任务的环境变量,中断返回。这样就完成了任务级的切换。
OSIntCtxSw()
在退出中断服务函数OSIntExit()中调用,实现中断级任务切换。由于是在中断里调用,所以处理器的寄存器入栈工作已经做完,就不用作这部分工作了。具体完成的任务:调整栈指针(因为调用函数会使任务栈结构与系统任务切换时堆栈标准结构不一致),保存当前任务SP,载入就绪最高优先级任务的SP,恢复就绪最高优先级任务的环境变量,中断返回。这样就完成了中断级任务切换。
OSTickISR()
系统时钟节拍中断服务函数,这是一个周期性中断,为内核提供时钟节拍。频率越高系统负荷越重。其周期的大小决定了内核所能给应用系统提供的最小时间间隔服务。一般只限于ms级(跟MCU有关),对于要求更加苛刻的任务需要用户自己建立中断来解决。该函数具体内容:保存寄存器(如果硬件自动完成就可以省略),调用OSIntEnter(),调用OSTimeTick(),调用OSIntExit(),恢复寄存器,中断返回。
③ OS_CPU_C.C
μC/OS定义了6个函数在该文件中。但是最重要的是OSTaskStkInit(),其他都是对系统内核的扩展时用的。
OSTaskStkInit()
是在用户建立任务时系统内部自己调用的,对用户任务的堆栈进行初始化。使建立好的进入就绪态任务的堆栈与系统发生中断并且将环境变量保存完毕时的栈结构一致。这样就可以用中断返回指令使就绪的任务运行起来。
具体的入栈方式要根据不同mcu而定。需要参考用户使用的mcu说明书。同时还要考虑mcu的栈生成方式。这需要根据具体问题来分析,在此不做过多论述。
⑵ 与应用相关的代码
这一部分是用户根据自己的应用系统来定制合适的内核服务功能。包括两个文件:OS_CFG.H,INCLUDES.H。
OS_CFG.H
配置内核,用户根据需要对内核进行定制,留下需要的部分,去掉不需要的部分,设置系统的基本情况。比如系统可提供的最大任务数量,是否定制邮箱服务,是否需要系统提供任务挂起功能,是否提供任务优先级动态改变功能等等。
INCLUDES.H
系统头文件,整个实时系统程序所需要的文件,包括了内核和用户的头文件。
4、用户应用系统编写的模式
用户应用系统是整个实时系统的最高层,用户通过利用实时操作系统提供的服务来开发自己的具体程序。
kernel提供给用户一些功能函数,使得用户的系统建立更加方便,但是kernel内部不会处理用户的工作,对于整个系统的具体应用工作还得需要用户自己去考虑,如何利用好这些功能服务函数就成为一个比较重要的问题。
⑴ main函数的结构
void main (void)
{
初始化系统的硬件;
OSInit();
任务的建立,消息机制的建立;
OSStart();
}
这里需要的是在OSStart()执行之前不得启动中断,硬件系统还不能工作,必须先让软件系统进入工作状态后才行。
⑵ 中断的结构
ISR:
{
保存处理器寄存器的值;
调用OSIntEnter();
执行用户的工作;
调用OSIntExit();
恢复处理器寄存器的值;
RTI;
}
用户的中断形式和以前一样,没有什么大的变化,仅仅是在原来用户ISR的基础上在固定的位置加了两个函数:OSIntEnter(), OSIntExit()。
⑶ 各个任务的结构
void YourTask (void)
{
for(;;)
{
用户代码
调用的系统服务
}
}
在任务启动函数执行完后,系统会切换到最高优先级的任务去执行,此时,可以将系统硬件部分的启动放在该任务的最前边,仅仅是启动时执行一次,主要是启动系统的节拍中断,或者一些必须在多任务系统调度后才能初始化的部分,使系统的真正开始工作,达到软件硬件的基本同步。
Void HighestPrioTask(void)
{
OSStartHardware();
For (;;)
{
用户代码
调用的系统服务
}
}
用户可以按照这些格式去编写自己的任务,建立自己的应用系统。
习题六
1、什么是实时操作系统?
2、实时多任务操作系统与分时多任务操作系统有什么区别?
3、实时操作系统应具有哪些基本功能?
4、实时操作系统中的任务(Task)有哪几种基本状态?
5、请列举几种针对51CPU的实时操作系统。
6、RTX51实时操作系统有哪两种不同的版本,它们之间有什么区别?
7、RTX51是怎样在多个任务之间切换的?
8、μC/OS是一种什么样的操作系统?
9、μC/OS-II有哪些基本特点?
10、μC/OS-II的内核主要包括哪些组成部分?
11、μC/OS 51的移植主要涉及到哪几个代码文件?
实时操作系统(上) |
通用计算机具有完善的操作系统(OS)和应用程序接口(API),是计算机基本组成不可分离的一部分,应用程序的开发以及完成后的软件都在OS平台上面运行,但一般不是实时的。嵌入式系统则不同,应用程序可以没有操作系统直接在芯片上运行;但是为了合理地调度多任务、利用系统资源、系统函数以及和专家库函数接口,用户必须自行选配RTOS开发平台,这样才能保证程序执行的实时性、可靠性,并减少开发时间,保障软件质量。
§6.1 关于实时操作系统 1、实时多任务操作系统 实时多任务操作系统(Real Time Operating System)是根据操作系统的工作特性而言的。实时是指物理进程的真实时间。实时操作系统是指具有实时性,能支持实时控制系统工作的操作系统。首要任务是调度一切可利用的资源完成实时控制任务,其次才着眼于提高计算机系统的使用效率,重要特点是要满足对时间的和要求。 2、实时多任务操作系统与分时多任务操作系统 它们有明显的区别。具体的说,对于分时操作系统,软件的执行在时间上的要求,并不严格,时间上的错误,一般不会造成灾难性的后果。而对于实时操作系统,主要任务是对事件进行实时的处理,虽然事件可能在无法预知的时刻到达,但是软件上必须在事件发生时能够在严格的时限内作出响应(系统响应时间),即使是在尖峰负荷下,也应如此,系统时间响应的超时就意味着致命的失败。另外,实时操作系统的重要特点是具有系统的可确定性,即系统能对运行情况的最好和最坏等的情况能做出精确的估计。 3、实时操作系统的基本功能 ⑴ 任务管理(多任务和基于优先级的任务调度) ⑵ 任务间同步和通信(信号量和邮箱等) ⑶ 存储器优化管理(含ROM的管理) ⑷ 实时时钟服务 ⑸ 中断管理服务 4、实时操作系统的工作特性 实时操作系统中的任务(Task)等同于分时操作系统中的进程(Process)的概念。系统中的任务有四种状态:运行(Executing),就绪(Ready),挂起(Suspended),冬眠(Dormant)。 运行:获得CPU控制权。 就绪:进入任务等待队列。通过调度转为运行状态。 挂起:任务发生阻塞,移出任务等待队列,等待系统实时事件的发生而唤醒。从而转为就绪或运行。 冬眠:任务完成或错误等原因被清除的任务。也可以认为是系统中不存在了的任务。系统中只能有一个任务在运行状态。各任务按级别通过时间片分别获得对CPU的访问权。 5、51实时操作系统 针对51CPU构成的嵌入式系统,有RTX51及UC/OS等操作系统。由于受CPU本身资源的,功能都相对比较简单,但其性能完全能够满足以51系列单片机为核心的嵌入式系统的应用需求。
§6.2 RTX51实时操作系统 RTX51是一个专门针对8051 家族设计的实时多任务操作系统,代码完全开放。RTX51使复杂的系统和软件设计以及有时间的工程开发变得简单。RTX51是一个强大的工具,它可以在单个CPU上管理几个作业(任务)。 1、概述 ⑴ 版本 RTX51 有两种不同的版本。 RTX51 Full 允许4个优先权任务的循环和切换,并且还能并行的利用中断功能。RTX51支持信号传递,以及与系统邮箱和信号量进行消息传递。RTX51的os_wait 函数可以等待以下事件:中断、时间到、来自任务或中断的信号、来自任务或中断的消息、信号量。 RTX51 Tiny 是RTX51 Full 的一个子集。RTX51 Tiny 可以很容易的运行在没有扩展外部存储器的单片机系统上。但是,使用RTX51 Tiny的程序可以访问外部存储器。RTX51 Tiny允许循环任务切换,并且支持信号传递,还能并行的利用中断功能。RTX51 Tiny 的os_wait函数可以等待以下事件:时间到、时间间隔、来自任务或者中断的信号。 ⑵ RTX51 的性能参数 表6-1 RTX51 的性能参数
2、RTX51的多任务结构 ⑴ 单任务程序 一个标准C程序从主函数开始执行。在嵌入式应用里,主函数经常被编写为一个无穷循环,也可以被认为是一个连续执行的单个任务。例如: int counter; void main(void) { counter=0; while(1) { counter++; } } ⑵ 伪任务系统 不利用操作系统,也可以实现一种伪任务系统。如:循环(Round-Robin)方式的多任务系统。参见下例: int counter; void main(void) { counter=0; while(1) { check_serial_io(); process_serial_cmds(); check_kbd_io(); process_kbd_cmds(); adjust_ctrlr_parms(); counter++; } } ⑶ 循环任务切换 RTX51 Tiny 允许“准并行”的同时执行几个任务。每一个任务在预先定义好的时间片内得以执行。时间到使正在执行的任务挂起,并使另一个任务开始执行。下面的例子使用了循环任务切换的技术。见下例: #include int counter0; int counter1; void job0(void)_task_0 { os_create(1); while(1) { counter0++; } } void job1(void)_task_1 { while(1) { counter1++; } } 3、RTX51程序的控制机制 ⑴ os_wait 函数 os_wait 函数提供了一种更为有效的方式来给几个任务分配可使用的处理器时间。 os_wait函数中断当前正在运行的任务,并且等待特定的事件。在一个任务等待事件的时间里,其他任务可以被执行。 ⑵ 等待时间到 RTX51使用8051 的一个定时器来产生一个循环的中断(时钟周期)。响应os_wait 的最简单事件是时间到,当前正在执行的任务被指定的时钟周期所中断。下面的延时例子使用的是等待时间到。 使用os_wait 函数编程的例子如下: #include int counter0; int counter1; void job0(void)_task_0 { os_create(1); while(1) { counter0++; os_wait(K_TMO,3); } } void job1(void)_task_1 { while(1) { counter1++; os_wait(K_TMO,5); } } ⑶ 等待信号 os_wait 函数的另一个事件是信号。信号被用来协调任务。直到另一个任务发出信号,在 os_wait 函数控制下的任务才结束等待状态。如果信号预先就被发送出来,那么任务将立即继续执行。 #include int counter0; int counter1; void job0(void)_task_0 { os_create(1); while(1) { if(++counter0==0) os_send_signal(1); } } void job1(void)_task_1 { while(1) { os_wait(K_SIG,0,0); counter1++; } } ⑷ 优先权机制 RTX51 Full 提供了优先权机制,RTX51 Tiny 不具备这个功能。 在上一个例子中,任务1收到一个信号后不会立即开始,只有当任务0 发生了时间到事件后,任务1才会启动。如果任务1被赋予了比任务0 高的优先级,通过抢先任务切换,如果任务1收到了信号,就会立即开始。优先级在任务定义中被指定(默认的优先级是0)。 4、RTX51函数 ⑴ RTX51 函数 表6-2 RTX51 函数 ⑵ RTX51 Full里附加的调试和支持函数见下表: 表6-3 调试函数 ⑶ CAN 函数 CAN 函数仅在RTX-51 Full中提供。CAN控制器支持非利浦82C200和80C592以及英特尔82526。更多的CAN控制器正在准备中。 表6-4 CAN 函数 5、RTX51 Tiny程序 编写RTX51 Tiny程序,要求将rtx51tny.h头标文件包含在你的C语言程序的\\c51\\inc\子目录下,而且使用_task_函数属性声明你的任务。 RTX51 Tiny程序不需要一个C语言主函数(main) ,连接过程将包含首先执行任务0的程序代码。 ⑴ RTX51 Tiny 配置 RTX51 Tiny的配置文件conf_tny.a51 在\\c51\\lib\子目录下,你可以在这个配置文件中改变下列参数。 ·用于系统时钟报时中断的寄存器组 ·系统计时器的间隔时间 ·时间片轮转超时值 ·内部数据存储器容量 · RTX51 Tiny运行之后释放的堆栈大小 ⑵ 编译 RTX51 Tiny 程序 RTX51 Tiny应用程序不需要专门的编译程序,你可以象编译普通的C语言源文件一样编译你的RTX51 Tiny源文件。 ⑶ 连接RTX51 Tiny 程序 RTX51 Tiny应用程序必须使用BL51 code banking linker/locator进行连接。 必须在所有目标文件后,在命令行上规定RTX51 Tiny指令。 ⑷ 优化RTX51 Tiny 程序 在建立rtx51应用程序时,应该注意以下事项: ·对于多重任务应尽可能采用os_wait函数触发,而不用时间片轮。 因为,使用时间片轮切换任务需要13字节的堆栈空间来存储任务环境(寄存器等),如果由os_wait函数触发,则不需要环境存储器。os_wait函数还会改善系统反应时间。 ·不要将报时信号的中断速率设得太快。 因为,每个时钟报时中断约需100到200个CPU周期,所以应该把时间报时信号速率设的足够高,以使中断等待时间减到最少。 6、RTX51 Tiny 应用程序实例 ⑴ 实例名称:TRAFFIC TRAFFIC是一个定时控制的红绿灯控制器。 ⑵ 基本控制要求 在一个用户定义的时间段内,红绿灯正常运行;超过正常运行时段时,黄色信号灯闪烁;如果一个行人按下请求按钮,红绿灯立即变成“walk”状态,以便行人通过;此后,红绿灯继续正常工作。 ⑶ 红绿灯控制命令 红绿灯控制器通过8051的串口进行通讯,其通讯命令见表6-5。这些命令由ASCII字符组成,所有命令必须用回车结束。 表6-5 红绿灯控制命令 ⑷ 应用程序组成 TRAFFIC应用程序由三个文件组成,这三个文件在\\C51V4\\RTX_TINY\\TRAFFIC或\\CDEMO\\51\\RTX_TINY\\TRAFFIC 目录中可以找到。 ①TRAFFIC.C 包括红绿灯控制程序,由下面几个任务组成: · 任务 0 : 初始化 初始化串行接口,并启动其他任务,初始化仅需执行一次,执行完以后,任务0自己删除。 · 任务 1 : 命令处理 这是红绿灯控制器的命令处理程序,用于控制和处理接收到的串口命令。 · 任务 2 : 定时器 · 任务 3 : 黄灯闪烁控制 在非正常运行时段,使黄色灯光闪烁。 · 任务 4 :红绿灯控制 在正常运行时段(在开始和结束时间之间),控制红绿灯。 · 任务 5 : 按钮控制 读取行人操作的按钮状态,并发送信号给任务4。 · 任务 6 : 退出 在串口字符流中检查ESC字符,如果发现ESC字符,就终止前一个显示命令。 ② SERIAL.C 实现串行接口的中断程序。其中包括函数putchar()和getkey() ,其它高层的输入输出函数printf()和getline()将调用这两个基本的输入输出子程序。 ③ GETLINE.C 用于对来自串口的字符命令行进行处理。 |