uC/OS-II


uC/OS-II

文章插图
uC/OS-II【uC/OS-II】μC/OS 和μC/OS-II 是专门为计算机的嵌入式套用设计的, 绝大部分代码是用C语言编写的 。CPU 硬体相关部分是用彙编语言编写的、总量约200行的彙编语言部分被压缩到最低限度,为的是便于移植到任何一种其它的CPU 上 。
基本介绍外文名:uC/OS-II
服务对象:计算机的嵌入式套用
简介:绝大部分代码是用C语言编写的
实质:硬实时核心 。
工作原理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()函式来进行任务切换 。中断机理引 言在嵌入式作业系统领域,由Jean J. 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编译器应该就是IAR Embedd-ed WorkBench 。对于这一编译器来说,通过分析和研究,发现它有以下规律 。函式调用如果是函式级调用,编译器会在函式调用时先把当前函式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()函式 。函式的结构如下: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暂存器RETIOSIntEnter()就是将全局变数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都保存在各个任务自己的任务堆叠中;而中断嵌套产生的压栈和出栈的操作都是放在系统堆叠中进行的 。这种划分方式是基于儘量将中断任务与普通任务分开的思想设计的 。从前面对于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()返回了以后,还要判断一下是否中断嵌套 。因为有的时候是需要切换任务的 。(综合电子论坛)优先权运行机制在嵌入式系统的套用中,实时性是一个重要的指标,而优先权翻转是影响系统实时性的重要问题 。本文着重分析优先权翻转问题的产生和影响,以及在uC/OS-II中的解决方案 。uC/OS-II採用基于固定优先权的占先式调度方式,是一个实时、多任务的作业系统 。系统中的每个任务具有一个任务控制快OS_TCB,任务控制块记录任务执行的环境,包括任务的优先权,任务的堆叠指针,任务的相关事件控制块指针等 。核心将系统中处于就绪态的任务在就绪表(ready list)进行标注,通过就绪表中的两个变数OSRdyGrp和OSRdyTbl[]可快速查找系统中就绪的任务 。在uC/OS-II中每个任务有唯一的优先权,因此任务的优先权也是任务的唯一编号(ID),可以作为任务的唯一标识 。核心可用控制块优先权表OSTCBPrioTbl[]由任务的优先权查到任务控制块的地址 。uC/OS-II主要就是利用任务控制快OS_TCB、就绪表(ready list)和控制块优先权表OSTCBPrioTbl[]来进行任务调度的 。任务调度程式OSSched()首先由就绪表(ready list)中找到当前系统中处于就绪态的优先权最高的任务,然后根据其优先权由控制块优先权表OSTCBPrioTbl[]取得相应任务控制块的地址,由OS_TASK_SW()程式进行运行环境的切换 。将当前运行环境切换成该任务的运行环境,则该任务由就绪态转为运行态 。当这个任务运行完毕或因其它原因挂起时,任务调度程式OSSched()再次到就绪表(ready list)中寻找当前系统中处于就绪态中优先权最高的任务,转而执行该任务,如此完成任务调度 。若在任务运行时发生中断,则转向执行中断程式,执行完毕后不是简单的返回中断调用处,而是由OSIntExit()程式进行任务调度,执行当前系统中优先权最高的就绪态任务 。当系统中所有任务都执行完毕时,任务调度程式OSSched()就不断执行优先权最低的空闲任务OSTaskIdle(),等待用户程式的运行 。优先翻转在uC/OS-II中,多个任务按照优先权高低由核心调度执行,而且任务调度所花的时间是常数,与应用程式中建立的任务数无关 。对于占先式核心,任务的回响时间是确定的,而且是最最佳化的,占先式核心保证最高优先权的任务最先执行 。任务的回响时间=寻找最高优先权任务的时间+任务切换时间在uC/OS-II中寻找进入就绪态的最高优先权任务是通过查就绪表实现的,这减少了所需时间 。y=OSUnMapTbl[OSRdyGrp];x= OSUnMapTbl [OSRdyTbl[y]];prio=(y<<3)+x;任务切换是通过调用彙编函式OS_TASK_SW()来实现的,主要完成两个任务运行环境的保存和恢复 。因此用户可以通过安排任务的优先权,保证系统的实时性 。当涉及到共享资源的互斥访问时,多任务实时作业系统常常会出现优先权翻转问题(priority inversion),不能保证高优先权任务的回响时间,影响系统的实时性,uC/OS-II中也存在同样问题 。所谓优先权翻转问题(priority inversion)即当一个高优先权任务通过信号量机制访问共享资源时,该信号量已被一低优先权任务占有,而这个低优先权任务在访问共享资源时可能又被其它一些中等优先权的任务抢先,因此造成高优先权任务被许多具有较低优先权的任务阻塞,实时性难以得到保证 。例如:有优先权为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运行 。这样便不能保证高优先权任务的回响时间,解决优先权翻转问题有优先权天花板(priority ceiling)和优先权继承(priority inheritance)两种办法 。优先权天花板是当任务申请某资源时,把该任务的优先权提升到可访问这个资源的所有任务中的最高优先权,这个优先权称为该资源的优先权天花板 。这种方法简单易行,不必进行複杂的判断,不管任务是否阻塞了高优先权任务的运行,只要任务访问共享资源都会提升任务的优先权 。在uC/OS-II中,可以通过OSTaskChangePrio()改变任务的优先权,但是改变任务的优先权是很花时间的 。如果不发生优先权翻转而提升了任务的优先权,释放资源后又改回原优先权,则无形中浪费了许多CPU时间,也影响了系统的实时性 。优先权继承是当任务A申请共享资源S时,如果S正在被任务C使用,通过比较任务C与自身的优先权,如发现任务C的优先权小于自身的优先权,则将任务C的优先权提升到自身的优先权,任务C释放资源S后,再恢复任务C的原优先权 。这种方法只在占有资源的低优先权任务阻塞了高优先权任务时才动态的改变任务的优先权,如果过程较複杂,则需要进行判断 。uC/OS-II不支持优先权继承,而且其以任务的优先权作为任务标识,每个优先权只能有一个任务,因此,不适宜在应用程式中使用优先权继承 。翻转解决在uC/OS-II中,为解决优先权翻转影响任务实时性的问题,可以借鉴优先权继承的方法对优先权天花板方法进行改进 。对uC/OS-II的使用,共享资源任务的优先权不是全部提升,而是先判断再决定是否提升 。即当有任务A申请共享资源S时,首先判断是否有别的的任务正在占用资源S,若无,则任务A继续执行,若有,假设为任务B正在使用该资源,则判断任务B的优先权是否低于任务A,若高于任务A,则任务A挂起,等待任务B释放该资源,如果任务B的优先权低于任务A,则提升任务B的优先权到该资源的优先权天花板,当任务B释放资源后,再恢复到原优先权 。在uC/OS-II中,每个共享资源都可看作一个事件,每个事件都有相应的事件控制块ECB 。在ECB中包含一个等待本事件的等待任务列表,该列表包括OSEventTbl[]和OSEventGrp两个域,通过对等待任务列表的判断可以很容易的确定是否有多个任务在等待该资源,同时也可判断任务的优先权与当前任务优先权的高低,从而决定是否需要用OSTaskChangePio()来改变任务的优先权 。这样,仅在优先权有可能发生翻转的情况下才改变任务的优先权,而且利用事件的等待任务列表进行判断,比用OSTaskChangePio()来改变任务的优先权速度快,并占用较少的CPU时间,有利于系统实时性的提高 。总之,优先权翻转问题是多任务实时作业系统普遍存在的问题,这个问题也存在于uC/OS-II中 。通过在应用程式中进行简单的判断,在可能出现优先权翻转的情况下动态的改变任务的优先权,可以有效地避免任务的优先权翻转,保证高优先权任务的执行,提高了系统的实时性 。开发笔记uC/OS-II是一个简洁、易用的基于优先权的嵌入式抢占式多任务实时核心 。儘管它非常简单,但是它的确在很大程度上解放了我的嵌入式开发工作 。既然是一个作业系统核心,那幺一旦使用它,就会涉及到如何基于作业系统设计套用软体的问题 。任务框架void task_xxx(void *pArg){/* 该任务的初始化工作 */……/* 进入该任务的死循环 */while(1){……}}每个用户的任务都必须符合事件驱动的编程模型,即uC/OS-II的应用程式都必须是“事件驱动的编程模型” 。一个任务首先等待一个事件的发生,事件可以是系统中断发出的,也可以是其它任务发出的,又可以是任务自身等待的时间片 。当一个事件发生了,任务再作相应处理,处理结束后又开始等待下一个事件的发生 。如此周而复始的任务处理模型就是“事件驱动的编程模型” 。事件驱动模型也涵盖了中断驱动模型,uC/OS-II事件归根结底来自三个方面:(1)中断服务函式传送的事件(2)系统延时时间到所引起的(3)其它任务传送的事件 。其中“中断服务函式传送的事件”就是指每当有硬体中断发生,那幺中断服务程式就会以事件的形式告诉任务,而等待该事件的最高优先权任务就会马上得以运行;“系统延时时间到所引起的”事件其实也是硬体中断导致的,那就是系统定时器中断 。而“其它任务传送的事件”则是由任务代码自身决定的,这是完全的“软事件” 。不管“软事件”还是“硬事件”,反正引起uC/OS-II任务切换的原因就是“事件”,所以用户编写套用代码的时候一定要体现出“事件驱动的编程模型” 。任务分配uC/OS-II的任务优先权分配需要按照不同的系统设计具体分析 。比如,对实时性要求越高的任务,则优先权要越高 。软体层次uC/OS-II会直接操纵硬体,比如:任务切换代码必然要保存和恢复CPU及协处理器的暂存器;uC/OS-II的核心时基时钟就需要硬体定时器的中断 。BSP就是“板极支持包”,它包括基于uC/OS-II而开发的事件驱动模型、支持多任务的驱动程式,这些驱动程式直接控制各个硬体模组并利用uC/OS-II的系统函式来实现多任务功能,它们应该儘量避免应用程式直接操纵硬体和uC/OS-II核心 。BSP还应该为应用程式提供标準、统一的API,以达到软体层次分明、套用软体代码可复用的目的 。应用程式就是用户为具体套用需要而开发的软体,它必须符合uC/OS-II的编程模型,即“事件驱动的编程模型” 。应用程式还应该儘量避免直接控制硬体和直接调用uC/OS-II系统函式、变数,一个完善的uC/OS-II系统是不需要应用程式来针对具体硬体而设计的 。也就是说,uC/OS-II必须拥有完备的设备驱动程式,而驱动程式和BSP共同提供完备、标準的API 。信号注意互斥信号对象(Mutual Exclusion Semaphore)简称Mutex,是uC/OS-II的核心对象之一,用于管理那些需要独占访问的资源,并使其适应多任务环境 。创建每一个Mutex,都需要指定一个空闲的优先权号,这个优先权号的优先权必须比所有可能使用此Mutex的任务的优先权都高!uC/OS-II的Mutex实现原理大致如下:当一个低优先权的任务A申请并得到了Mutex,于是它获得资源访问权 。如果此后有一个高优先权的任务B开始运行(此时任务A已经被剥夺),而且也要求得到Mutex,系统就会把任务A的优先权提高到Mutex所指定的优先权 。由于此优先权高于任何可能使用此Mutex的任务的优先权,所以任务A会马上获得CPU控制权 。一直到任务A释放Mutex,任务A才回到它原有的优先权,这时任务B就可以拥有该Mutex了 。应该注意的是:当任务A得到Mutex后,就不要再等待其它核心对象(诸如:信号量、信箱、伫列、事件标誌等等)了,而应该儘量快速的完成工作,释放Mutex 。否则,这样的Mutex就失去了作用,而且效果比直接使用信号量(Sem)更糟糕!虽然普通的信号量(Sem)也可以用于互斥访问某独占资源,但是它可能引起“优先权反转”的问题 。假设上面的例子使用的是Sem,当任务A得到Sem后,那幺任务C(假设任务C的优先权比A高,但比B低)就绪的话将获得CPU控制权,于是任务A和任务B都被剥夺CPU控制权 。任务C的优先权比B低,却优先得到了CPU!而如果任务A是优先权最低的任务,那幺它就要等到所有比它优先权高的任务都挂起之后才会拥有CPU,那幺任务B(优先权最高的任务)跟着它一起倒霉!这就是优先权反转问题,这是违背“基于优先权的抢占式多任务实时作业系统”原则的!综上所述,uC/OS-II中多个任务访问独占资源时,最好使用Mutex,但是Mutex是比较消耗CPU时间和记忆体的 。如果某高优先权的任务要使用独占资源,但是不在乎久等的情况下,就可以使用Sem,因为Sem是最高效最省记忆体的核心对象 。函式应注意uC/OS-II的OSSchedLock()和OSSchedUnlock()函式允许应用程式锁定当前任务不被其它任务抢占 。使用时应当注意的是:当你调用了OSSchedLock()之后,而在调用OSSchedUnlock()之前,千万不要再调用诸如OSFlagPend()、OSMboxPend()、OSMutexPend()、OSQPend()、OSSemPend()之类的事件等待函式!而且应当确保OSSchedLock()和OSSchedUnlock()函式成对出现,特别是在有些分支条件语句中,要考虑各种分支情况,不要有遗漏!需要一併提醒用户的是:当您调用开关中断函式OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()时也要确保成对出现,否则系统将可能崩溃!不过,在OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()函式之间调用OSFlagPend()、OSMboxPend()、OSMutexPend()、OSQPend()、OSSemPend()之类的事件等待函式是允许的 。编写规範首先应该阐明的是,我们这里讨论的是“驱动程式”,而不是“中断服务程式”,这两个词语往往被用户混淆 。(1)中断服务程式指那种硬体中断一旦发生,就会立即被硬体中断控制器调用的一小段程式,它的操作追求简单明了,越快速越精简就越好 。(2)驱动程式是指封装了某种硬体操作细节的函式集,它提供给应用程式的是统一、标準、清晰、易用的API 。对于中断服务程式的编写,往往与驱动程式的设计相关联 。比如驱动程式提供异步操作的功能,那幺就需要中断服务程式为它準备缓冲区和一个结构体,并且中断服务程式会依照这个结构体的成员参数自动完成所要求的操作 。又如,串口(UART)中断服务程式的设计有两种:基于数据包传输和基于单位元组传输,前者适用于以数据包为单位的通信程式,而后者适用于如超级终端这样的应用程式 。如果在一个系统中,要求使用同一个硬体设备完成几种不同的操作方式,就需要设计一个通用的驱动程式,而该驱动程式可以根据需要安装各种针对性很强的中断服务程式 。在设计驱动程式时,特别需要注意的是,某些外设的操作往往以一个连续而严格的时序作为原子操作,比如用I/O连线埠来访问DS1302、24C01、LM75A等等 。在这类设备的操作过程中,不允许有其它任务来控制对应的I/O连线埠,否则会引起数据错误甚至器件损坏 。所以,这种设备的驱动程式都应该仔细设计“原子操作”,把必须连贯操作的时序控制代码用互斥对象封装成一个“原子操作”,以适应多任务环境 。其实,大部分设备都是这样,需要确定“原子操作”,如LCD、RTL8019AS、Flash等等也是如此 。关于驱动程式的设计,还有很多很多的文章可作,需要具体问题具体分析 。在这里我就不列出个条条目目了,希望有兴趣的朋友多多讨论 。