原创 嵌入式实时操作系统μC/OS-II

2010-9-19 16:07 1480 2 2 分类: MCU/ 嵌入式
嵌入式实时操作系统μC/OS-II简介
μC/OS-II是著名的源代码公开的实时内核,是一个完整的,可移植、固化、裁剪的占先式实时多任务内核。

μC/OS-II是用ANSI C编写的,包含一小部分与微处理器类型相关的汇编语言代码,使之可供不同架构的微处理器


使用。虽然μC/OS-II是在PC机上开发和测试的,但μC/OS-II的实际对象是嵌入式系统,并且很容易移植到不同架


构的微处理器上。至今,从8位到64位,μC/OS-II已在超过40中不同架构的微处理器上运行。
3  嵌入式实时操作系统μC/OS-II内核结构
3.1 临界区(Critical Sections),OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()
同其他内核一样,μC/OS-II为了处理临界区代码,必须关中断,处理完毕后再开中断。关中断使得μC/OS-II能够


避免同时有其他任务或中断服务进入临界区代码。关中断的时间是实时内核开发商应提供的最重要的指标之一,


因为这个指标影响用户系统对实时事件的响应特性。μC/OS-II努力使关中断时间降至最短,但就使用μC/OS-II而


言,关中断的时间在很大程度上取决于微处理器的结构以及C编译器所生成的代码质量。
微处理器一般都有关中断和开中断指令,用户使用的C编译器必须具有某种机制,能够在C源代码中直接实现关中


断/开中断操作。有些C编译器允许在用户的C源代码中嵌入汇编语言的语句,使得关中断/开中断很容易实现;而


有些C编译器把从C语言中关中断/开中断的操作放在语言的扩展部分,从而直接从C语言中可以关中断/开中断。
μC/OS-II定义了2个宏来关中断和开中断,以便避免不同C编译器厂商使用不同的方法来处理关中断和开中断。


μC/OS-II中的这2个宏分别是OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()。因为这2个宏的定义取决于使用的微


处理器,故在文件OS_CPU.H中可以找到相应的宏定义。每种微处理器都有自己的OS_CPU.H文件。
OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()总是成对使用的,把临界区代码封装起来,如以下代码所示:
{
    ……
    ……
    OS_ENTER_CRITICAL();
   
    OS_EXIT_CRITICAL();
    ……
    ……
}
OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()也可以用来保护应用程序中的临界区代码。
3.2 任务(Tasks)
任务通常是一个无限循环,但是当任务完成后,任务可以自我删除。μC/OS-II可以管理多达64个任务,但是


μC/OS-II的作者建议用户不要使用优先级为0,1,2,3的任务,以及优先级为OS_LOWEST_PRIO-3,


OS_LOWEST_PRIO-2, OS_LOWEST_PRIO-1和 OS_LOWEST_PRIO的任务,因为在未来的μC/OS-II版本中可能会用到这


些任务。因此,如果遵循作者的建议,不使用以上优先级最高的4个任务和优先级最低的4个任务,则用户最多可


以有56个自己的任务。
3.3 任务状态(Task States)
下图是μC/OS-II控制下的任务状态转换图[3]。在任一给定的时刻,任务的状态应为以下5种状态之一。
睡眠态(DORMANT)——指任务驻留在程序空间,还没有交给μC/OS-II来管理。把任务交给μC/OS-II,是通过调


用下述2个函数之一:OSTaskCreate()或OSTaskCreateExt()来实现的。这些调用只是用于告诉μC/OS-II,任务的


起始地址在哪里;任务建立时,用户给任务赋予的优先级是多少;任务要使用多少栈空间等。
就绪态(READY)——任务一旦建立,这个任务就进入了就绪态,准备运行。任务的建立可以是在多任务运行开


始之前,也可以动态地由一个运行着地任务建立。如果多任务已经启动,且一个任务是被另一个任务建立的,而


新建立的任务优先级高于建立它的任务的优先级,则这个刚刚建立的任务将立即得到CPU的使用权。一个任务可


以通过调用OSTaskDel()返回到睡眠态,或通过调用该函数让另一个任务进入睡眠态。
运行态(RUNNING)——调用OSStart()可以启动多任务。OSStart()函数只能在启动时调用一次,该函数运行用


户初始化代码中已经建立的、进入就绪态的优先级最高的任务。优先级最高的任务就这样进入了运行态。任何时


刻只能有一个任务处于运行态。就绪的任务只有当所有优先级高于这个任务的任务都转为等待状态,或者是被删


除了,才能进入运行态。
等待状态(WAITING)——正在运行的任务可以通过调用以下2个函数之一:OSTimeDly()或OSTimeDlyHMSM(),将


自身延迟一段时间。这个任务于是进入等待状态,一直到函数中定义的延迟时间到。这2个函数会立即强制执行


任务切换,让下一个优先级最高的、并进入了就绪态的任务运行。等待的时间过去以后,系统服务函数


OSTimeTick()使延迟了的任务进入就绪态。而正在运行的任务可能需要等待某一事件的发生,可以通过调用以下


函数之一实现:OSFlagPend()、OSSemPend()、OSMutexPend()、OSMboxPend()或OSQPend()。如果该事件并未发


生,调用上述函数的任务就进入了等待状态,直到等待的事件发生了。当任务因等待事件而被挂起时,下一个优


先级最高的任务立即得到了CPU的使用权。当事件发生了或等待超时使,被挂起的任务就进入就绪态。事件发生


的报告可能来自另一个任务,也可能来自中断服务子程序。
中断服务态(ISR)——正在运行的任务是可以被中断的,除非该任务将中断关闭,或者μC/OS-II将中断关闭。


被中断了的任务于是进入了中断服务态。相应中断时,正在执行的任务被挂起,中断服务子程序得到了CPU的使


用权。中断服务子程序可能会报告一个或多个事件的发生,而使一个或多个任务进入就绪态。在这种情况下,从


中断服务子程序返回之前,μC/OS-II要判定被中断的任务是否还是就绪态任务中优先级最高的。如果中断服务子


程序使另一个优先级更高的任务进入就绪态,则新进入就绪态的这个优先级更高的任务将得以运行;否则,原来


被众多拉的任务将继续运行。
当所有的任务都在等待事件的发生或等待延迟时间的结束时,μC/OS-II执行被称为空闲任务(idle task)的内


部任务,即OSTaskIdle()。
3.4 任务控制块(Task Control Blocks)
一旦任务建立,一个任务控制块OS_TCB就被赋值。任务控制块是一个数据结构,当任务的CPU使用权被剥夺是,μ


C/OS-II用它保存该任务的状态。当任务重新得到CPU使用权时,任务控制块能确保任务从当时被中断的那一点丝


毫不差地继续执行。OS_TCB全部驻留在RAM中。
3.5 就绪表(Ready List)
每个任务被赋予不同的优先级等级,从0到最低优先级OS_LOWEST_PRIO,包括0和OS_LOWEST_PRIO在内。当μC/OS


-II初始化时,最低优先级OS_LOWEST_PRIO总是被赋给空闲任务。
每个就绪的任务都放在就绪表中,就绪表中有2个变量,OSRdyGrp和OSRdyTbl[]。在OSRdyGrp中,任务按优先级


分组,8个任务为一组。OSRdyGrp中的每一位表示8组任务中每一组是否有进入就绪态的任务。任务进入就绪态时


,就绪表OSRdyTbl[]中相应元素的相应位也置为1。OSRdyGrp和OSRdyTbl[]之间的关系见下图[3]:
就绪表OSRdyTbl[]数组的大小取决于OS_LOWEST_PRIO。当应用程序中任务数目比较少时,这种安排可以减小


OS_LOWEST_PRIO的值,可以降低μC/OS-II对RAM的需求量。
为确定下一次该哪个优先级的任务运行了,μC/OS-II中的调度器总是将最低优先级的任务在就绪表中相应字节的


相应位置1。
从上图可以看出,任务优先级的低3位用于确定任务在总就绪表OSRdyTbl[]中的所在位。接下去的3位用于确定是


在OSRdyTbl[]数组的第几个元素。
3.5 任务调度(Task Scheduling)
μC/OS-II总是运行进入就绪态任务中优先级最高的任务。确定哪个任务优先级最高,下面该哪个任务运行了,这


一工作是由调度器(scheduler)完成的。任务级的调度是由函数OS_Sched()完成的。中断级的调度是由另一个


函数OSIntExt()完成的。μC/OS-II任务调度的执行时间是常数,与应用程序建立了多少个任务没有关系。
任务切换很简单,由以下2步完成:将被挂起任务的处理器寄存器压入堆栈;然后将较高优先级任务的寄存器值


从堆栈中恢复到寄存器中。在μC/OS-II中,就绪任务的堆栈结构总是看起来跟刚刚发生过中断一样,所有处理器


的寄存器都保存在堆栈中。换句话说,μC/OS-II运行就绪态的任务所要做的一切,只是恢复所有的CPU寄存器并


运行中断返回指令。为了做任务切换,运行OS_TASK_SW(),人为模仿了一次中断。多数微处理器由软中断指令或


者指令陷阱来实现上述操作。中断服务子程序或陷阱处理,也称做异常处理,必须给汇编语言函数OSCtxSw()提


供中断向量。OSCtxSw()除了需要OS_TCBHighRdy指向即将被挂起的任务,还需要让当前任务控制块OSTCBCur指向


即将被挂起的任务。
OS_Sched()的所有代码都属于临界区代码。在寻找进入就绪态的优先级最高的任务过程中,为防止中断服务子程


序把一个或几个任务的就绪位置位,中断是关闭的。为缩短切换时间,OS_Sched()全部代码都可以用汇编语言写


。为增加可读性、可移植性及将汇编语言代码最少化,OS_Sched()是用C语言编写的。
3.6 给调度器上锁和开锁(Locking and Unlocking the Scheduler)
给调度器上锁函数OSSchedLock()用于禁止任务调度,直到任务完成后,调用给调度器开锁函数OSSchedUnlock()


为止。调用OSSchedLock()的任务将保持对CPU的使用权,即使有个优先级更高的任务进入了就绪态。此时,中断


仍然是可以识别的,中断服务也能得到(假设此时中断是开着的)。OSSchedLock()和OSSchedUnlock()必须成对


的使用。变量OSLockNesting跟踪OSSchedLock()函数被调用的次数,以允许嵌套的函数包含临界区代码,这段代


码其他任务不得干预。μC/OS-II允许嵌套深度达255层。当OSLockNesting=0时,任务调度重新得到允许。函数


OSSchedLock()和OSSchedUnlock()的使用要非常谨慎,因为它们影响到了μC/OS-II对任务的正常管理。调用


OSSchedLock()之后,用户应用程序不得调用可能会使当前任务挂起的系统功能函数。也就是说,用户应用程序


不得调用OSFlagPend(),OSMboxPend(),OSMutexPend(),OSQPend(),OSSemPend(),OSTaskSuspend


(OS_PRIO_SELF),OSTimeDly()或者OSTimeDlyHMSM(),直到OSLockNesting回0为止。因为OSSchedLock()给调度


器上了锁,不让其他任务运行,用户锁住了系统。
3.7 空闲任务(Idle Task)
μC/OS-II总要建立一个空闲任务(idle task),这个任务在没有其他任务进入就绪态使投入运行。这个空闲任


务(OSTaskIdle())永远被设置为最低优先级,即OS_LOWEST_PRIO。空闲任务不可能被应用软件删除。
3.8 统计任务(Statistics Task)
μC/OS-II有一个统计运行时间的任务,叫做OSTaskStat()。如果将系统配置常数OS_TASK_STAT_EN设置为1,这个


任务就会建立。一旦得到了允许,OSTaskStat()每秒运行1次,计算当前的CPU利用率。换句话说,OSTaskStat()


告诉用户应用程序使用了多少CPU时间,用百分比表示。这个值放在一个有符号8位整数OSCPUUsage中,精确度是


1%。如果应用程序打算使用统计任务,那么必须在初始化时建立唯一的任务中调用统计任务初始化函数


OSStatInit()。换句话说,在调用系统启动函数OSStart()之前,用户初始代码中必须建立一个任务,在这个任


务中调用系统统计任务初始化函数OSStatInit(),然后再建立应用程序中的其他任务。
3.9 μC/OS-II中的中断(Interrupts under μC/OS-II)
μC/OS-II中,中断服务子程序要用汇编语言来编写。然而,如果用户使用的C语言编译器支持在线(in-line)汇


编语言,则可以直接将中断服务子程序代码放在C语言的源文件中。μC/OS-II的中断服务过程大致如下:
1)        中断到来,但还不能被CPU识别。也许是因为中断被μC/OS-II或用户应用程序关了,或者是因为CPU还


没执行完当前指令。
2)        一旦CPU响应了这个中断,CPU的中断向量被装载,跳转到中断服务子程序。
3)        中断服务子程序保存CPU的全部寄存器。
4)        保存完CPU寄存器之后,中断服务子程序通知μC/OS-II进入中断服务子程序。做法是调用OSIntEnter


(),或者给OSIntNesting家1。还应该将堆栈指针保存到当前任务控制块OS_TCB中。
5)        用户中断服务代码开始执行。中断服务所做的事应尽可能的少,应把大部分工作留给任务去做。
6)        中断服务完成后,必须调用OSIntExit(),通知μC/OS-II退出中断服务。
7)        恢复CPU的寄存器,中断返回。
3.10 时钟节拍(Clock Tick)
μC/OS-II需要提供周期性的信号源,用于实现时间延时和确认超时。节拍率应为10~20次/秒,或者说10~100Hz


。时钟节拍率越高,系统的额外负荷就越重。时钟节拍的实际频率取决于应用程序的精度。时钟节拍源可以是专


门的硬件定时器,也可以是来自50/60Hz交流电源的信号。必须在多任务系统启动之后,也就是在调用OSStart()


之后,再开启时钟节拍器。换句话说,调用OSStart()之后应做的第一件事是初始化定时器中断。通常容易犯的


错误是,将允许时钟节拍器中断放在系统初始化函数OSInit()之后,在启动多任务系统启动函数OSStart()之前


。μC/OS-II中的时钟节拍服务是通过在中断服务子程序中调用OSTimeTick()实现的。OSTimeTick()跟踪所有任务


的定时器以及超时时限,时钟节拍中断服务子程序的代码必须用汇编语言编写,因为在C语言中不能直接处理CPU


的寄存器。
3.11 μC/OS-II初始化(μC/OS-II Initialization)
在调用μC/OS-II的任何其他服务之前,μC/OS-II要求首先调用系统初始化函数OSInit()。OSInit()初始化μC/OS


-II所有的变量和数据结构。OSInit()建立空闲任务OSTaskIdle(),这个任务总是处于就绪态。空闲任务


OSTaskIdle()的优先级总是设成最低,即OS_LOWEST_PRIO。如果统计任务允许OS_TASK_STAT_EN和任务建立扩展


都设为1,则OSInit()还须建立统计任务OS_TaskStat(),并且使其进入就绪态。OS_TaskStat的优先级总是设为


OS_LOWEST_PRIO-1。
3.12 μC/OS-II的启动(Starting μC/OS-II)
多任务的启动是通过调用OSStart()实现的。然而,在启动μC/OS-II之前,必须建立至少一个应用任务。当调用


OSStart()时,OSStart()从任务就绪表中找出用户建立的优先级最高的任务的任务控制块。接着,OSStart()调


用高优先级就绪任务启动函数OSStartHighRdy()函数,后者将任务栈中保存的值弹回到CPU寄存器中,然后执行


一条中断返回指令,中断返回指令强制执行该任务代码。
4  结束语
以上简要介绍了嵌入式实时操作系统μC/OS-II的内核结构,可以看出μC/OS-II具备一个嵌入式实时操作系统内核


所必须的基本功能和良好的可移植性,目前也已经在嵌入式系统领域得到了广泛的应用。

PARTNER CONTENT

文章评论0条评论)

登录后参与讨论
我要评论
0
2
关闭 站长推荐上一条 /3 下一条