1 嵌入式实时操作系统概述[1] 所 谓嵌入式系统是以应用为中心,以计算机技术为基础,软硬件可裁减,从而能够适应实际应用中对功能、可靠性、成本、体积、功耗等严格要求的专用计算机系统。 它一般由嵌入式微处理器、外围硬件设备、嵌入式操作系统以及用户的应用软件等四个部分组成,用于实现对其他设备的控制、监视或管理等功能。在大型嵌入式应 用系统中,为了使嵌入式开发更方便、快捷,需要具备一种稳定、安全的软件模块集合,用以管理存储器分配、中断处理、任务间通信和定时器响应,以及提供多任 务处理等,即嵌入式操作系统。嵌入式操作系统的引入大大提高了嵌入式系统的功能,方便了应用软件的设计,但同时也占用了宝贵的嵌入式系统资源。嵌入式操作 系统常常有实时要求,所以嵌入式操作系统往往又是“实时操作系统”。早期的嵌入式系统几乎都用于控制目的,从而或多或少都有些实时要求,所以从前“嵌入式 操作系统”实际上是“实时操作系统”的代名词。近年来,由于手持式计算机和掌上电脑等设备的出现,也有了许多不带实时要求的嵌入式操作系统。另一方面,由 于CPU速度的提 高,一些原先以为是“实时”的反应速度现在已经很普遍了。这样,一些原先需要在“实时”操作系统上才能实现的应用,现在已不难在常规的操作系统上实现。在 这样的背景下,“嵌入式操作系统”和“实时操作系统”就成了不同的概念和名词。而实时操作系统能及时响应外部事件的请求,在规定的时间内完成对该事件的处 理,并控制所有实时任务协调一致地运行,具有独立性、及时性、可靠性的特点。顾名思义,嵌入式实时操作系统则是在综合了以上的两种操作系统的特点之后形成 的,嵌入式实时操作系统没有一般的计算机操作系统的文件管理等庞大内容,一般也没有内存管理,它所拥有的是实时操作系统中最重要的内容,即多任务实时调度 和任务的定时、同步操作。其二进制代码的大小通常为几KB到几十KB,是纯粹为嵌入式应用而设计的,具有很短的任务切换时间和很高的实时响应速度。而嵌入式实时操作系统的核心是实时多任务内核。 2 嵌入式实时操作系统?C/OS-II简介[2] ?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(); /* ?C/OS-II临界区代码 */ 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寄存器中,然后执行一条中断返回指令,中断返回指令强制执行该任务代码。
|