查看原文
其他

月薪20K不是梦!学会RTOS给你的身价增增值,嵌入式操作系统UCOS-II总览

嵌入式ARM 嵌入式ARM 2021-01-31

如果,你最近关注一些嵌入式招聘职位描述,你可能会经常看到看到使用过uCOS、Vxworks、QNX等RTOS者优先。


随便打开一个20K的嵌入式开发工作职责: 

你会发现熟悉RTOS的开发、移植、剪裁真的很吃香!  

今天,我们就来介绍一下实时操作系统UCOS-II。

 

一、  嵌入式操作系统概览

嵌入式操作系统的主要好处就是屏蔽了底层硬件的差别,给上层应用提供统一的接口,并管理进程调度和资源(如CPU时间、内存)分配等。并且可以充分利用硬件资源,如在单任务时(大循环结构,如大部分51程序)遇到delay函数时,CPU在空转。而在多任务系统,遇到delay或需等待资源时系统会自动运行下一个任务,等条件满足再回来运行先前的任务,这样就充分利用了CPU,提高了效率。


uC/OS操作系统与裸机程序的最大不同点就在于uC/OS有任务调度,可以根据任务的重要程度(优先级)优先执行重要的任务,从而确保能及时处理最重要的数据。(所以对于一个系统有必要使用OS的判断是能否划分一个个的任务,并且各任务间的耦合很小)可以思考下裸机程序中断的时候发生的过程。利用堆栈可以很自由的在A、B中切换,如果切换足够快,A、B看以来好像同时在执行,这就是并行,A、B就是任务。如果这个切换操作放到定时器函数中来做,就可以严格按照时间来切换。另外,各个任务之间有存在一定的关系,有逻辑上的先后等,必须引进全局的结构体、变量来标记一些信息,全局的这些数据是不会被释放的,所以所有的任务可以去通过读、写这些数据来实现各个程序块交流信息,实现所谓的同步、互斥。这就是操作系统的原理,而这些不同的通信方式按功能细分就成事件管理、内存管理等。


二、ucos的运行概览

首先是主函数,然后是OSInit(),这个函数就是对那些全局的数据结构初始化,建立希望的链表等数据结构,为后面全局变量通信做好准备,并且创建了1-2个系统任务(空闲任务必须,为了不让操作系统返回。统计任务可选),而所谓的创建任务OSTaskCreate就是把一个函数的函数地址、自己的栈建立联系、优先级、任务控制块等弄好,为任务切换做好准备。设置好定时切换的相关信息类似定时器,按照节拍在中断中进行任务切换判断(这个节拍是给延时函数提供计时基准,一个任务的延时时间到或等待的资源满足而进入就绪表就会检查优先级看是否可以执行),可以的话就发生切换,这个时候还没有开启开关,所以等任务创建完成后,启动多任务函数OSStart(),这个函数是让SP指到其中的一个栈,然后出栈就跳到一个任务函数里去了,接下来就是正常的任务运行了。


对于操作系统,主要是任务怎么释放CPU(延时、中断、等待资源),其他的任务怎么获得CPU(进入就绪表),如何找到某个任务(优先级及任务控制列表)。


三、ucos各部分介绍

μC/OS-II的各种服务都以任务的形式来出现的。在μC/OS-II中,每个任务都有一个唯一的优先级。它是基于优先级可剥夺型内核,适合应用在对实时性要求较高的地方。


3.1 μC/OS-II的任务

μC/OS-II的核心部分就是它的任务,它也是通过任务来对不同事件进行响应和处理的。从代码上来看,μC/OS-II的任务一般为如下形式(C语言描述,后同):

void uCOSTask(void *p)

{

while(1)

{

任务具体的功能;

}

}

创建任务的函数有个是OSTaskCreate (void (*task)(void *pd), void *pdata, OS_STK *ptos, INT8U prio),OSTaskCreate()需要四个参数:task是任务代码的指针,pdata是当任务开始执行时传递给任务的参数的指针,ptos是分配给任务的堆栈的栈顶指针,prio是分配给任务的优先级。


μC/OS-II的任务是在内存中来看,任务由三个部分构成:任务的代码部分、任务堆栈和任务控制块。其中任务控制块保存任务的属性;任务堆栈在任务进行切换时保存任务运行的环境;任务代码部分就是宏观上看到的C语言代码。

  


嵌入式设备中一般只有一个处理器,所以在某一具体的时刻只能有一个任务占用处理器。μC/OS-II的任务一共有5种状态:睡眠、就绪、运行、等待和中断服务。


3.2 任务控制块

μC/OS-II中参与调度和管理的最小单位是任务。而任务是通过任务控制块的形式管理的。任务控制块是一个结构体,它包含了任务的堆栈信息,任务控制块的指针,前一个任务控制块和后一个任务控制块的指针(利用优先级一个个查找是否是要找的任务,所以优先级是唯一的),任务的优先级(根据优先级查找到任务控制块,从而就找到该任务),任务需要等待的时间(任务延时的时候时钟节拍中断来的时候会对等待时间做减,为零的时候就放入就绪表,查看一下是否需要切换)等信息。


任务控制块包含了除了指向任务代码的所有信息。而任务的代码地址在任务运行时是怎么获得的呢?其实,任务代码的地址是通过任务的堆栈储存的。


3.3 任务堆栈

任务在创建时候,必须指明该任务的堆栈。任务的堆栈大小由用户根据实际情况自行定义。μC/OS-II的堆栈实际上是一个连续的内存块,任务在创建的时候,由函数OSTaskCreate()将任务的代码和用户为任务定义的堆栈联系起来。由于堆栈按照增长方向可以分为两种类型,故在创建任务的时候调用的堆栈初始化函数实际上也跟微处理器类型有关的。故这些代码也是移植时需要修改的。 


3.4系统任务

μC/OS-II提供了两个系统任务:空闲任务和统计任务。其中空闲任务是必要的。因为在某一时刻可能所有的用户任务都不处于就绪状态,这样微处理器会因为没有任何任务运行造成系统崩溃。


3.5 临界区

μC/OS-II还有一个临界的概念,所谓临界区,就是一段特殊的代码。在这段代码内不允许中断的响应,以此来保证这段代码的原子性。临界代码段通过调用开关中断两个宏来实现的。


3.6 μC/OS-II任务的管理


3.6.1 对就绪任务的管理

μC/OS-II定义了一个就绪表的数据结构,跟普通的数组非常像(也就是一维数组),但是被赋予了特殊的意义。就绪表中每一位表示一个优先级的任务是否处于就绪状态。而每一位的下标则表示任务的优先级。通过特殊的数据结构和意义,就绪任务的管理效率很高。  


任务就绪表是由一个OSRdyTbl数组表示,数组大小(OS_RDY_TBL_SIZE)由最低优先级(OS_LOWEST_PRIO)确定, 这样可以减少不必要的空间浪费,节约RAM资源。OSRdyTbl[]是INT8U 类型数组,每一个元素占8位。每一位表示一个优先级状态(1为就绪,0则未就绪)。8个元素则可以表示64个优先级(8*8=64)。为加速就续表的查找,Labrosse把每个OSRdyTbl元素划为每一优先级组,8个元素则有8个优先级组,它定义了一个INT8U类型的8位变量OSRdyGrp ,OSRdyGrp的每一位对应每个优先级组。如下图:

 任务优先级的第三位用于确定任务在osRdyTb1中在元素的第几位,接着的三位用于指定是第几个元素。


假设优先级31的任务第一个加入了就绪任务表,此时OSRdyGrp和OSRdyTbl的情况:

 


OSRdyGrp的第3位为1,表示第3优先级组有就绪任务。OSRdyTbl的第7位为1,表示第31优先级的任务被就绪。


用此可以使任务加入或脱离就绪表。调度的时候即是查找此表,找出最高的优先级,从而找到任务控制块,执行该任务。


3.6.2 任务的创建、挂起和其他操作


μC/OS-II提供了两个函数可以创建任务,它们是OSTaskCreate()和OSTaskCreateExt(),任务在创建之后也可以挂起或者恢复,这同样要使用μC/OS-II提供的系统函数。挂起任务使用函数OSTsakSuspend(),恢复被挂起的任务使用函数OSTaskResume()。μC/OS-II还提供了任务的删除,优先级的修改,查询任务信息等其他功能的函数。 


3.6.3 任务的调度


μC/OS-II任务的调度是由调度器完成的。所谓调度器实际上是一个函数OSShed();此函数通过搜索任务就绪表来获得最高优先级的就绪任务,在由该任务的优先级来获得任务的控制块再来实现任务的切换。            

任务的调度不是任何时刻都进行的,而是有时机的。μC/OS-II任务当有以下情况发生时将产生一次任务调度:

           

● 创建了新任务,并在就绪表中进行了登记

● 有任务被删除

● 有处于等待的任务被唤醒

● 中断退出的时候

● 正在运行的任务等待某事件而进入等待状态

● 正在运行的任务自愿放弃微处理器占有权而等待一段时间       


3.6.4任务的初始化和启动


μC/OS-II中定义了大量的全局变量和数据结构。在μC/OS-II运行以前需要对这些全局变量和数据结构进行初始化。为了完成μC/OS-II的初始化,系统提供了初始化函数OSInit()。μC/OS-II的启动也是通过系统提供的函数OSStart()来实现的。OSStart()在判断系统没有在运行后来获得就绪表中最高优先级的就绪任务,并调用函数OSStartHighRdy()来启动系统。      


3.6.5 中断和时钟


实时系统为了能够响应异步事件,通常会采用中断。μC/OS-II也采用了中断来响应外部事件。μC/OS-II处理中断过程大致如下:当系统开中断时,系统接收到中断然后找到中断服务程序的入口地址执行中断,执行完成后退出中断。这里要提到的一点是,当要退出中断时,系统会查找就绪表是否有比处于中断服务状态任务的优先级更高的任务进入就绪状态。如果有将会一发一次调度,否则返回被中断的任务继续运行。关于中断的一些细节在后面的移植的部分还会讨论。


在所有的中断源中最重要的一个就是时钟中断,它为系统提供时间服务以此来实现任务的延时。     


3.6.6 任务间的通信


对于一个完整的嵌入式操作系统来说,任务间的通信机制必不可少。μC/OS-II提供了相应的数据结构和机制来实现任务之间的同步和通信。


在ucos II 里任务间通信可以采用以下几种方式:

1. 共享全局变量,这是最快捷有效的方式,实现这种通信可以采用以下两种方式:一是利用宏OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()来关闭中断和打开中断,二是利用函数OSSchedLock()和OSSchedUnlock()对μC/OS-II中的任务调度函数上锁和开锁.

2. 使用信号量

3. 使用邮箱

4.  使用消息队列

创建一个任务需要给这个任务分配一个任务控制块,这个任务控制块存储着关于这个任务的重要信息。那么,事件控制块就好比任务里的任务控制块。它存储着这个事件的重要信息,我们说创建一个事件(信号,邮箱,消息队列),其本质的过程就是初始化这个事件控制块。多个任务可以同时等待同一个事件的发生。在这种情况下,当该事件发生后,所有等待该事件的任务中,优先级最高的任务得到了该事件并进入就绪状态,准备执行。


事件控制块是一个数据结构,其定义如下:

typedef struct {

    void   *OSEventPtr;                /* 指向消息或者消息队列的指针 */

    INT8U   OSEventTbl[OS_EVENT_TBL_SIZE]; /* 等待任务列表      */

 INT16U  OSEventCnt;               /* 计数器(当事件是信号量时) */  

 INT8U   OSEventType;                   /* 时间类型  */ 

 INT8U   OSEventGrp;               /* 等待任务所在的组  */ 

} OS_EVENT;

   

.OSEventPtr指针,只有在所定义的事件是邮箱或者消息队列时才使用。当所定义的事件是邮箱时,它指向一个消息,而当所定义的事件是消息队列时,它指向一个数据结构(因为队列要传递多个消息)。


.OSEventTbl[] 和 .OSEventGrp 很像前面讲到的OSRdyTbl[]和OSRdyGrp,只不过前两者包含的是等待某事件的任务,而后两者包含的是系统中处于就绪状态的任务


.OSEventCnt 当事件是一个信号量时,.OSEventCnt是用于信号量的计数器。


(初始化时,如果信号量是用来表示一个或者多个事件的发生,那么该信号量的初始值应设为0, 如果信号量是用于对共享资源的访问,那么该信号量的初始值应设为1)

.OSEventType定义了事件的具体类型。它可以是信号量(OS_EVENT_SEM)、邮箱(OS_EVENT_TYPE_MBOX)或消息队列(OS_EVENT_TYPE_Q)中的一种。用户要根据该域的具体值来调用相应的系统函数,以保证对其进行的操作的正确性。


信号量是什么?信号量有什么用?

信号量一是可以用来表示一个或多个事件的发生,二是用来对共享资源的访问。


有时候邮箱可以当做信号量来使用,邮箱相对信号量而言,只是多传递了一个指针变量。其实和创建一个信号量的过程几乎是一样的,先申请一个空事件控制块,接着初始化这个事件控制块。最后返回一个指向这个事件控制块的指针。不同之处在于事件控制块的类型被设置成OS_EVENT_TYPE_MBOX,以及使用.OSEventPtr域来容纳消息指针。


如果把邮箱比作是信号量的升级版,那消息队列就是邮箱的升级版。邮箱可以实现从一个任务向另外一个任务发送一个指针变量,消息队列则可以实现从一个任务向另外一个任务发送很多个指针变量。而且每个指针指向的数据结构变量也可以有所不同。(消息队列最根本的部分是一个循环缓冲区,其中的每个单元包含一个指针。)和创建邮箱,创建信号量过程是很相似的,首先申请控制块,接着初始化这个控制块,和创建邮箱,信号量不同的,创建消息队列过程是多申请了一个队列控制块。

 

最后,简单总结一下搞懂RTOS的知识储备:

 

1. 找一块别人已经移植好代码的MCU,把uCOS跑起来。然后去看别人的示例代码,先搞懂怎么使用uC/OS。

 

尝试着写一个控制三个不同的led灯,它们以不同频率闪烁。

2.等到uCOS给你提供的API用熟了, 去读关于操作系统的书。 然后再去把堆栈、上下文、调度器、锁、文件系统、网络、中断、线程、邮箱(消息队列)等等这些概念对应到实践中去。

试着写一个多任务的HTTP服务器。 试着利用uCOS重构现有的代码。

 

3. 去读一个早期一点的版本uCOS的代码, 看看uCOS是怎么实现操作系统的。

这个时候再去看操作系统原理的书。

 

4. 尝试着移植uCOS到一块新的芯片上去。 不到万不得已不要去google。 试着独立解决遇到的问题。

 

5. 改一改原始版本的uCOS代码,看看修改了之后,操作系统的行为会如何变化。


6. 去查uCOS版本进化的历史,看看为什么大家会如此修改这个代码? 为什么这么设计 ?


推荐阅读:

1.从一个不起眼的小公司逐渐成为半导体行业巨头,Microchip CEO史蒂夫桑吉不一般的“扩张”之路

2.这是一篇非常不错的介绍U-boot工作机制的好文

3.我们吃蛋糕,手中的盘子小,却可以吃掉大蛋糕,RAM就是这样运行比自身更大的程序

4.超实用!一位嵌入式高手摸索出的Linux内核编译步骤和经验


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存