uC/OS-ll
μC/OS 和μC/OS-II 是专门为计算机的嵌入式套用设计的, 绝大部分代码是用C语言编写的。CPU 硬体相关部分是用彙编语言编写的、总量约200行的彙编语言部分被压缩到最低限度,为的是便于移植到任何一种其它的CPU 上。
基本介绍
- 中文名:uC/OS-ll
- 具有:可剥夺实时核心的实时作业系统
- 商业套用:需要付费
- 前身:μC/OS
- 出自于:1992
uC/OS-II简介
u C / O S 是一种公开原始码、结构小巧、具有可剥夺实时核心的实时作业系统,商业套用需要付费。
μC/OS-II 的前身是μC/OS,最早出自于1992 年美国嵌入式系统专家Jean J.Labrosse 在《嵌入式系统编程》杂誌的5 月和6 月刊上刊登的文章连载,并把μC/OS 的源码发布在该杂誌的B B S 上。
用户只要有标準的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工作原理
uC/OS-II是一种基于优先权的可抢先的硬实时核心。
要实现多任务机制,那幺目标CPU必须具备一种在运行期更改PC的途径,否则无法做到切换。不幸的是,直接设定PC指针,目前还没有哪个CPU支持这样的指令。但是一般CPU都允许通过类似JMP,CALL这样的指令来间接的修改PC。我们的多任务机制的实现也正是基于这个出发点。事实上,我们使用CALL指令或者软中断指令来修改PC,主要是软中断。但在一些CPU上,并不存在软中断这样的概念,所以,我们在那些CPU上,使用几条PUSH指令加上一条CALL指令来模拟一次软中断的发生。
在uC/OS-II里,每个任务都有一个任务控制块(Task Control Block),这是一个比较複杂的数据结构。在任务控制块的偏移为0的地方,存储着一个指针,它记录了所属任务的专用堆叠地址。事实上,在uC/OS-II内,每个任务都有自己的专用堆叠,彼此之间不能侵犯。这点要求程式设计师再他们的程式中保证。一般的做法是把他们申明成静态数组。而且要申明成OS_STK类型。当任务有了自己的堆叠,那幺就可以将每一个任务堆叠在那里记录到前面谈到的任务控制快偏移为0的地方。以后每当发生任务切换,系统必然会先进入一个中断,这一般是通过软中断或者时钟中断实现。然后系统会先把当前任务的堆叠地址保存起来,仅接着恢复要切换的任务的堆叠地址。由于哪个任务的堆叠里一定也存的是地址(还记得我们前面说过的,每当发生任务切换,系统必然会先进入一个中断,而一旦中断CPU就会把地址压入堆叠),这样,就达到了修改PC为下一个任务的地址的目的。
任务管理
uC/OS-II 中最多可以支持64 个任务,分别对应优先权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等系统指针,所以通常用彙编语言编写。主要包括中断级任务切换的底层实现、任务级任务切换的底层实现、时钟节拍的产生和处理、中断的相关处理部分等内容。
uC/OS-II的任务切换机理及中断调度最佳化
引 言
在嵌入式作业系统领域,由Jean J. Labrosse开发的μC/OS,由于开放原始码和强大而稳定的功能,曾经一度在嵌入式系统领域引起强烈反响。而其本人也早已成为了嵌入式系统会议(美国)的顾问委员会的成员。
不管是对于初学者,还是有经验的工程师,μC/OS开放原始码的方式使其不但知其然,还知其所以然。通过对于系统内部结构的深入了解,能更加方便地进行开发和调试;并且在这种条件下,完全可以按照设计要求进行合理的裁减、扩充、配置和移植。通常,购买RTOS往往需要一大笔资金,使得一般的学习者望而却步;而μC/OS对于学校研究完全免费,只有在套用于盈利项目时才需要支付少量的着作权费,特别适合一般使用者的学习、研究和开发。自1992第1版问世以来,已有成千上万的开发者把它成功地套用于各种系统,安全性和稳定性已经得到认证,现已经通过美国FAA认证。
μC/OS-II的几大组成部分
μC/OS-II可以大致分成核心、任务处理、时间处理、任务同步与通信,CPU的移植等5个部分。
核心部分(OSCore.c) 是作业系统的处理核心,包括作业系统初始化、作业系统运行、中断进出的前导、时钟节拍、任务调度、事件处理等多部分。能够维持系统基本工作的部分都在这里。
任务处理部分(OSTask.c) 任务处理部分中的内容都是与任务的操作密切相关的。包括任务的建立、删除、挂起、恢复等等。因为μC/OS-II是以任务为基本单位调度的,所以这部分内容也相当重要。
时钟部分(OSTime.c) μC/OS-II中的最小时钟单位是timetick(时钟节拍)。任务延时等操作是在这里完成的。
任务同步和通信部分 为事件处理部分,包括信号量、信箱、信箱伫列、事件标誌等部分;主要用于任务间的互相联繫和对临界资源的访问。
与CPU的接口部分 是指μC/OS-II针对所使用的CPU的移植部分。由于μC/OS-II是一个通用性的作业系统,所以对于关键问题上的实现,还是需要根据具体CPU的具体内容和要求作相应的移植。这部分内容由于牵涉到SP等系统指针,所以通常用彙编语言编写。主要包括中断级任务切换的底层实现、任务级任务切换的底层实现、时钟节拍的产生和处理、中断的相关处理部分等内容。
对于MSP430的中断处理
2.1 函式调用和中断调用的操作
MSP430最常使用的C编译器应该就是IAR Embedd-ed WorkBench。对于这一编译器来说,通过分析和研究,发现它有以下规律。
(1)函式调用
如果是函式级调用,编译器会在函式调用时先把当前函式PC压栈,然后调用函式,PC值改变。
如果被调用的函式带有参数,那幺,编译器按照以下的规则进行。
最左边的两个参数如果不是struct(结构体)或者union(联合体),将被赋值到暂存器,否则将被压栈。函式剩下的参数都将被压栈。根据最左边的那两个参数的类型,分别赋值给R12(对于32位类型赋值给R12:R13)和R14(对于32位类型赋值给R14:R15)。
(2)中断调用
如果是在中断中调用中断服务子程式的话,编译器将把当前执行语句的PC压栈,同时再把SR压栈。接着,根据中断服务子程式的複杂程度,选择把R12~R15中的暂存器压栈。然后,执行中断服务子程式。中断处理结束后再把Rx暂存器出栈,SR出栈,PC出栈。把系统恢复到中断前的状态,使程式接着被中断的部分继续运行。
图3 中断髮生时的任务栈压栈操作
2.2 任务级和中断级的任务切换步骤和原理
(1)任务级的任务切换原理
μC/OS-II是一个多任务的作业系统,在没有用户自己定义的中断情况下,任务间的切换步骤是这样的:任务间的切换一般会调用OSSched()函式。函式的结构如下:
void OSSched(void){
关中断
如果(不是中断嵌套并且系统可以被调度){
确定优先权最高的任务
如果(最高级的任务不是当前的任务){
调用OSCtxSw();
}
}
开中断
}
我们把这个函式称作任务调度的前导函式。它先判断要进行任务切换的条件,如果条件允许进行任务调度,则调用OSCtxSw()。这个函式是真正实现任务调度的函式。由于期间要对堆叠进行操作,所以OSCtxSw()一般用彙编语言写成。它将正在运行的任务的CPU的SR暂存器推入堆叠,然后把R4~R15压栈。接着把当前的SP保存在TCB->OSTCBStkPtr中,然后把最高优先权的TCB->OSTCBStkPtr的值赋值给SP。这时候,SP就已经指到最高优先权任务的任务堆叠了。然后进行出栈工作,把R15~R4出栈。接着使用RETI返回,这样就把SR和PC出栈了。简单地说,μC/OS-II切换到最高优先权的任务,只是恢复最高优先权任务所有的暂存器并运行中断返回指令(RETI),实际上,所作的只是人为地模仿了一次中断。
(2)中断级的任务切换原理
μC/OS-II的中断服务子程式和一般前后台的操作有少许不同,往往需要这样操作:
保存全部CPU暂存器
调用OSIntEnter()或OSIntNesting++
开放中断
执行用户代码
关闭中断
调用OSIntExit();
恢复所有CPU暂存器
RETI
OSIntEnter()就是将全局变数OSIntNesting加1。OSIntNesting是中断嵌套层数的变数。μC/OS-II通过它确保在中断嵌套的时候,不进行任务调度。执行完用户的代码后,μC/OS-II调用OSIntExit(),一个与OSSched()很像的函式。在这个函式中,系统首先把OSIntNesting减1,然后判断是否中断嵌套。如果不是的话,并且当前任务不是最高优先权的任务,那幺找到优先权最高的任务,执行OSIntCtxSw()这一出中断任务切换函式。因为,在这之前已经做好了压栈工作;在这个函式中,要进行R15~R4的出栈工作。而且,由于在之前调用函式的时候,可能已经有一些暂存器被压入了堆叠。所以要进行堆叠指针的调整,使得能够从正确的位置出栈。
使用μC/OS-II存在的问题和解决方法
由于μC/OS-II在套用的时候会占用单片机上的一些资源,如系统时钟、RAM、Flash或者ROM,从而减少了用户程式对资源的利用。对于MSP430来说,RAM的占用是特别突出的问题。对于8、16位的单片机来说,片内的RAM容量都很小,MSP430也是如此(最大的片内RAM也只有2KB,例如MSP430F149)。如果使用扩展记忆体,会大大增加设计难度。
通过对μC/OS-II的分析可以得知,μC/OS-II占用的RAM主要是用在每个任务的TCB、每个任务的堆叠等方面。通过进一步分析,发现任务堆叠大的原因是因为MSP430的硬体设计中没有把中断堆叠和任务堆叠分开。这样就造成了在套用μC/OS-II的时候,考虑每个任务的任务堆叠大小时,不单单需要计算任务中局部变数和函式嵌套层数,还需要考虑中断的最大嵌套层数。因为,对于μC/OS-II原始的中断处理的设计、中断处理过程中的中断嵌套中所需要压栈的暂存器大小和局部变数的记忆体大小,都需要算在每个任务的任务堆叠中,则对于每一个任务都需要预留这一部分记忆体,所以大量的RAM被浪费。从这里可以看出,解决这一问题的直接方法就是把中断堆叠和每个任务自己的堆叠分开。这样,在计算每个任务堆叠的时候,就不需要把中断处理中(包括中断嵌套过程中)的记忆体的占用计算到每个任务的任务堆叠中,只需要计算每个任务本身需要的记忆体大小,从而提高了RAM的利用率,可以缓解记忆体紧张的问题。
在这种设计方案中,中断堆叠区也就是利用原有的MSP430中的系统堆叠区。在前后台的设计形式中,中断中的压栈和出栈的操作都是在系统的堆叠区完成的。基于μC/OS-II的任务切换的原理,我们对于任务堆叠的功能和系统堆叠的功能做了以下划分:任务在运行过程中产生中断和任务切换的时候,PC和SR以及暂存器Rx都保存在各个任务自己的任务堆叠中;而中断嵌套产生的压栈和出栈的操作都是放在系统堆叠中进行的。这种划分方式是基于儘量将中断任务与普通任务分开的思想设计的。
从前面对于IAR EW的默认操作分析来看,堆叠的结构可以有两种。一种是把μC/OS-II的任务堆叠设计成图1所示的形式。这种方法是把编译器默认的压栈操作放在前面,然后再把剩下的暂存器进栈。但是,由于编译器在处理複杂程度不同的中断服务程式的时候,压入栈的暂存器的数量不定,所以会对以后其余暂存器的压栈和出栈操作增加複杂度。这里,我们採用了图2所示的方式生成堆叠。在这种堆叠中,PC和SR压栈后,通过调整SP指针,使得R4~R15暂存器覆盖编译器默认压栈的暂存器。这样,处理的难度会小一点。
对于这样的设计方式,CPU必须能够:
◆ 有相应的CPU暂存器能够模仿SP的一些功能,能使用相应的指令来完成类似SP的一些操作;
◆ 作为SP使用的暂存器在编译过程中最好不被编译器默认使用。在IAR的编译器中,有一个选项可以避免在编译过程中使用到R4、R5。
这两点MSP430都可以做到。
下面对一个正在运行的优先权为6的任务中断后,会发生的几种情况进行分析。
1)在中断的处理过程中没有更高优先权的中断产生,即不会产生中断嵌套。
图3所示为中断髮生后对于任务优先权为6的任务堆叠所进行的操作。中断髮生后,PC和SR被系统压栈②,对于IAR C编译器来说,会按照複杂度不同的中断服务程式的要求,默认地进行一些暂存器的压栈操作③。因为我们要求的堆叠格式是如图2所示的,我们要把SP调整到SR后面④,然后进行R4~R15的压栈操作,形成我们所要求的堆叠格式⑤。
进行任务堆叠的压栈工作以后,就可以调整SP的指针到系统堆叠了,如图4所示。压栈后的SP指向最后一个压栈内容①。我们把SP的值赋值给优先权6任务的TCB->OSTCBStkPtr,以便进行任务调度的时候出栈使用②。接着,就把SP调整到系统堆叠处③。在中断处理过程中,可能会出现压栈的操作,那幺这种情况下SP的指针会随之移动。由于现在是中断堆叠中,所以不会破坏任务堆叠的格式。
由于没有中断嵌套,在中断处理中没有别的中断髮生,那幺返回的步骤和上述的进栈操作正好相反。在中断处理完了以后,SP会自动回到图4中③的SP位置。接着,系统会查询到优先权最高的任务,然后把SP的指针移到优先权最高的任务的任务堆叠,进行R15~R4的出栈工作,最后用RETI中断返回指令返回到新的任务。因为我们把所有的任务堆叠都规定成相同的格式,所以它们之间不会产生问题。这里需要注意的是,因为系统在C编译器的中断处理中会对中断进入时默认压栈的暂存器出栈,所以在设计出栈的程式时,要先把这些内容压栈,这样才能正确出栈。
2)在中断的处理过程中,有别的中断产生,产生中断嵌套。
如图5所示,由于在处理中断的时候,SP已经被移到系统堆叠去了,只有当中断退出的时候才可能把SP移到别的任务的任务堆叠中。所以在中断的时候进行中断嵌套,那幺对于中断的处理和第一次是一样的,所不同的是,这次保存在堆叠中的不是任务运行中的暂存器,而是中断处理中的暂存器,而且是保存在系统堆叠中而不是任务堆叠中。从这里就可以看出最佳化记忆体的效果。所有的中断嵌套中的暂存器压栈都压在系统堆叠中,这样对于任务堆叠记忆体大小的要求大大降低。
因为μC/OS-II在进入中断中,会把全局变数OSIntNesting++;在退出中断的时候,又会把OSIntNesting--。在退出中断进行任务切换之前,μC/OS-II会先判断OSIntNesting是否为0,是0才会进行任务调度。当第二中断运行结束以后,退出中断嵌套的时候,OSIntNesting不为0,也就不会进行任务调度。因此,仍旧在系统堆叠出栈,那幺系统会继续前面没有完成的中断服务程式。
接着退出中断的顺序和非中断嵌套的顺序是一样的。在中断处理完以后,SP会自动回到图4中③的SP位置。接着,系统会查询到优先权最高的任务,然后把SP的指针移到优先权最高的任务的任务堆叠。进行R15~R4的出栈工作,最后用RETI中断返回指令返回到新的任务。
中断的情况基本上就是上述两种。对于有些文献中提到的在中断中会调度到更高优先权的任务的情况,笔者觉得是不应该发生的。因为从上面的分析可以看出,默认的(μC/OS-II的设计思路)中断处理会同时对全局变数OSIntNesting进行增减处理,以给出是否需要任务调度的条件。那幺即使在中断服务程式中把更高优先权的任务就绪,也会等到中断退出以后再进行调度,除非是在中断中直接调用更高优先权的任务函式。但这种方法应该是和μC/OS-II的原则相违背的,沿用的是以前前后台设计的思路。
对于这样的设计方式,时钟节拍的处理方式必须和一般的中断处理方式是一样的。一般来说,MSP430使用WATCHDOG时钟中断作为时钟节拍的产生源。从本质上来说,时钟节拍本身也是中断处理过程,所以对于时钟节拍的处理应该和其它的中断处理过程相同。实际上,在时钟节拍的处理过程中也可能会存在中断嵌套的问题。
中断堆叠和任务堆叠分离设计的程式流程如图6所示。
几点建议
① 编写中断程式的时候,有条件儘量使用彙编语言。因为这样可以避免一些编译器自己进行的操作,减少指针调整的次数。
② 在用C编写中断服务的时候,因为有些功能必须调用彙编的函式才能实现。调用函式时,有些时候压栈的PC会破坏堆叠的结构。这个时候需要把堆叠进行适当的调整,保证堆叠格式的正确。
③ 中断处理过程中调用OSIntExit()的时候,由于 μC/OS-II的原始设计中SP指针有时是不调整的,所以在OSIntExit()返回了以后,还要判断一下是否中断嵌套。因为有的时候是需要切换任务的。
(综合电子论坛)