UCOSII操作系统详解.docx
《UCOSII操作系统详解.docx》由会员分享,可在线阅读,更多相关《UCOSII操作系统详解.docx(18页珍藏版)》请在冰点文库上搜索。
![UCOSII操作系统详解.docx](https://file1.bingdoc.com/fileroot1/2023-5/18/843bf844-e71c-4111-ad9b-e98c5de1022b/843bf844-e71c-4111-ad9b-e98c5de1022b1.gif)
UCOSII操作系统详解
μC/OS和μC/OS-II是专门为计算机的嵌入式应用设计的,绝大部分代码是用C语言编写的。
CPU硬件相关部分是用汇编语言编写的、总量约200行的汇编语言部分被压缩到最低限度,为的是便于移植到任何一种其它的CPU上。
工作原理编辑
uC/OS-II是一种基于优先级的可抢先的硬实时内核。
要实现多任务机制,那么目标CPU必须具备一种在运行期更改PC的途径,否则无法做到切换。
不幸的是,直接设置PC指针,还没有哪个CPU支持这样的指令。
但是一般CPU都允许通过类似JMP,CALL这样的指令来间接的修改PC。
我们的多任务机制的实现也正是基于这个出发点。
事实上,我们使用CALL指令或者软中断指令来修改PC,主要是软中断。
但在一些CPU上,并不存在软中断这样的概念,所以,我们在那些CPU上,使用几条PUSH指令加上一条CALL指令来模拟一次软中断的发生。
在uC/OS-II里,每个任务都有一个任务控制块(TaskControlBlock),这是一个比较复杂的数据结构。
在任务控制块的偏移为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要求用户在定时中断的服务程序中,调用系统提供的与时钟节拍相关的系统函数,例如中断级的任务切换函数,系统时间函数。
内存管理
在ANSIC中是使用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的执行过程中非常普遍,一般来说前者发生在系统服务中,后者发生在时钟中断的服务程序中。
调度工作的内容可以分为两部分:
最高优先级任务的寻找和任务切换。
其最高优先级任务的寻找是通过建立就绪任务表来实现的。
uC/OS中的每一个任务都有独立的堆栈空间,并有一个称为任务控制块TCB(TaskControlBlock)的数据结构,其中第一个成员变量就是保存的任务堆栈指针。
任务调度模块首先用变量OSTCBHighRdy记录当前最高级就绪任务的TCB地址,然后调用OS_TASK_SW()函数来进行任务切换。
中断机理编辑
引言
在嵌入式操作系统领域,由JeanJ.Labrosse开发的μC/OS,由于开放源代码和强大而稳定的功能,曾经一度在嵌入式系统领域引起强烈反响。
而其本人也早已成为了嵌入式系统会议(美国)的顾问委员会的成员。
不管是对于初学者,还是有经验的工程师,μC/OS开放源代码的方式使其不但知其然,还知其所以然。
通过对于系统内部结构的深入了解,能更加方便地进行开发和调试;并且在这种条件下,完全可以按照设计要求进行合理的裁减、扩充、配置和移植。
通常,购买RTOS往往需要一大笔资金,使得一般的学习者望而却步;而μC/OS对于学校研究完全免费,只有在应用于盈利项目时才需要支付少量的版权费,特别适合一般使用者的学习、研究和开发。
自1992第1版问世以来,已有成千上万的开发者把它成功地应用于各种系统,安全性和稳定性已经得到认证,现已经通过美国FAA认证。
组成部分
μ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等系统指针,所以通常用汇编语言编写。
主要包括中断级任务切换的底层实现、任务级任务切换的底层实现、时钟节拍的产生和处理、中断的相关处理部分等内容。
中断处理
2.1函数调用和中断调用的操作
MSP430最常使用的C编译器应该就是IAREmbedd-edWorkBench。
对于这一编译器来说,通过分析和研究,发现它有以下规律。
函数调用
如果是函数级调用,编译器会在函数调用时先把当前函数PC压栈,然后调用函数,PC值改变。
如果被调用的函数带有参数,那么,编译器按照以下的规则进行。
最左边的两个参数如果不是struct(结构体)或者union(联合体),将被赋值到寄存器,否则将被压栈。
函数剩下的参数都将被压栈。
根据最左边的那两个参数的类型,分别赋值给R12(对于32位类型赋值给R12:
R13)和R14(对于32位类型赋值给R14:
R15)。
中断调用
如果是在中断中调用中断服务子程序的话,编译器将把当前执行语句的PC压栈,同时再把SR压栈。
接着,根据中断服务子程序的复杂程度,选择把R12~R15中的寄存器压栈。
然后,执行中断服务子程序。
中断处理结束后再把Rx寄存器出栈,SR出栈,PC出栈。
把系统恢复到中断前的状态,使程序接着被中断的部分继续运行。
图3中断发生时的任务栈压栈操作
2.2任务级和中断级的任务切换步骤和原理
(1)任务级的任务切换原理
μC/OS-II是一个多任务的操作系统,在没有用户自己定义的中断情况下,任务间的切换步骤是这样的:
任务间的切换一般会调用OSSched()函数。
函数的结构如下:
voidOSSched(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在应用的时候会占用单片机上的一些资源,如系统时钟、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都保存在各个任务自己的任务堆栈中;而中断嵌套产生的压栈和出栈的操作都是放在系统堆栈中进行的。
这种划分方式是基于尽量将中断任务与普通任务分开的思想设计的。
从前面对于IAREW的默认操作分析来看,堆栈的结构可以有两种。
一种是把μ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被系统压栈②,对于IARC编译器来说,会按照复杂度不同的中断服务程序的要求,默认地进行一些寄存器的压栈操作③。
因为我们要求的堆栈格式是如图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()返回了以后,还要判断一下是否中断嵌套。
因为有的时候是需要切换任务的。
(综合电子论坛)
优先级编辑
运行机制
在嵌入式系统的应用中,实时性是一个重要的指标,而优先级翻转是影响系统实时性的重要问题。
本文着重分析优先级翻转问题的产生和影响,以及在uC/OS-II中的解决方案。
[2]
uC/OS-II采用基于固定优先级的占先式调度方式,是一个实时、多任务的操作系统。
系统中的每个任务具有一个任务控制快OS_TCB,任务控制块记录任务执行的环境,包括任务的优先级,任务的堆栈指针,任务的相关事件控制块指针等。
内核将系统中处于就绪态的任务在就绪表(readylist)进行标注,通过就绪表中的两个变量OSRdyGrp和OSRdyTbl[]可快速查找系统中就绪的任务。
在uC/OS-II中每个任务有唯一的优先级,因此任务的优先级也是任务的唯一编号(ID),可以作为任务的唯一标识。
内核可用控制块优先级表OSTCBPrioTbl[]由任务的优先级查到任务控制块的地址。
uC/OS-II主要就是利用任务控制快OS_TCB、就绪表(readylist)和控制块优先级表OSTCBPrioTbl[]来进行任务调度的。
任务调度程序OSSched()首先由就绪表(readylist)中找到当前系统中处于就绪态的优先级最高的任务,然后根据其优先级由控制块优先级表OSTCBPrioTbl[]取得相应任务控制块的地址,由OS_TASK_SW()程序进行运行环境的切换。
将当前运行环境切换成该任务的运行环境,则该任务由就绪态转为运行态。
当这个任务运行完毕或因其它原因挂起时,任务调度程序OSSched()再次到就绪表(readylist)中寻找当前系统中处于就绪态中优先级最高的任务,转而执行该任务,如此完成任务调度。
若在任务运行时发生中断,则转向执行中断程序,执行完毕后不是简单的返回中断调用处,而是由OSIntExit()程序进行任务调度,执行当前系统中优先级最高的就绪态任务。
当系统中所有任务都执行完毕时,任务调度程序OSSched()就不断执行优先级最低的空闲任务OSTaskIdle(),等待用户程序的运行。
[2]
优先翻转
在uC/OS-II中,多个任务按照优先级高低由内核调度执行,而且任务调度所花的时间是常数,与应用程序中建立的任务数无关。
对于占先式内核,任务的响应时间是确定的,而且是最优化的,占先式内核保证最高优先级的任务最先执行。
[2]
任务的响应时间=寻找最高优先级任务的时间+任务切换时间
在uC/OS-II中寻找进入就绪态的最高优先级任务是通过查就绪表实现的,这减少了所需时间。
y=OSUnMapTbl[OSRdyGrp];
x=OSUnMapTbl[OSRdyTbl[y]];
prio=(y<<3)+x;
任务切换是通过调用汇编函数OS_TASK_SW()来实现的,主要完成两个任务运行环境的保存和恢复。
因此用户可以通过安排任务的优先级,保证系统的实时性。
当涉及到共享资源的互斥访问时,多任务实时操作系统常常会出现优先级翻转问题(priorityinversion),不能保证高优先级任务的响应时间,影响系统的实时性,uC/OS-II中也存在同样问题。
所谓优先级翻转问题(priorityinversion)即当一个高优先级任务通过信号量机制访问共享资源时,该信号量已被一低优先级任务占有,而这个低优先级任务在访问共享资源时可能又被其它一些中等优先级的任务抢先,因此造成高优先级任务被许多具有较低优先级的任务阻塞,实时性难以得到保证。
例如:
有优先级为A、B和C的三个任务,优先级A>B>C,任务A,B处于挂起状态,等待某一事件的发生,任务C正在运行,此时任务C开始使用某一共享资源S。
在使用中,任务A等待的事件到来,任务A转为就绪态,因为它比任务C优先级高,所以立即执行。
当任务A要使用共享资源S时,由于其正在被任务C使用,因此任务A被挂起,任务C开始运行。
如果此时任务B等待的事件到来,则任务B转为就绪态。
由于任务B的优先级比任务C高,因此任务B开始运行,直到其运行完毕,任务C才开始运行。
直到任务C释放共享资源S后,任务A才得以执行。
在这种情况下,优先级发生了翻转,任务B先于任务A运行。
这样便不能保证高优先级任务的响应时间,解决优先级翻转问题有优先级天花板(priorityceiling)和优先级继承(priorityinheritance)两种办法。
[2]
优先级天花板是当任务申请某资源时,把该任务的优先级提升到可访问这个资源的所有任务中的最高优先级,这个优先级称为该资源的优先级天花板。
这种方法简单易行,不必进行复杂的判断,不管任务是否阻塞了高优先级任务的运行,只要任务访问共享资源都会提升任务的优先级。
在uC/OS-II中,可以通过OSTaskChangePrio()改变任务的优先级,但是改变任务的优先级是很花时间的。
如果不发生优先级翻转而提升了任务的优先级,释放资源后又改回原优先级,则无形中浪费了许多CPU时间,也影响了系统的实时性。
优先级继承是当任务A申请共