实时操作系统(Real-time operating system, RTOS),又称即时操作系统,它会按照排序运行、管理系统资源,并为开发应用程序提供一致的基础。
实时操作系统与一般的操作系统相比,最大的特色就是“实时性”,如果有一个任务需要执行,实时操作系统会马上(在较短时间内)执行该任务,不会有较长的延时。这种特性保证了各个任务的及时执行。
经常跟实时操作系统一起讲的,还有嵌入式操作系统这个概念,但实际上这是完全不同的两种东西,虽然大多数实时操作系统都是嵌入式操作系统,但嵌入式操作系统并不全都是实时的。
对于实时操作系统有一些常见的误区,比如:速度快,吞吐量大,代码精简,代码规模小等等。其实这些都不算是实时操作系统的特性,别的操作系统也可以做到。
只有“实时性”才是RTOS的最大特征,其它的都不算是。
维基百科上关于实时性的定义:
实时运算(Real-time computing)是计算机科学中对受到“实时约束”的计算机硬件和计算机软件系统的研究,实时约束像是从事件发生到系统回应之间的最长时间限制。实时程序必须保证在严格的时间限制内响应。
实时操作系统中都要包含一个实时任务调度器,这个任务调度器与其它操作系统的最大不同是强调:严格按照优先级来分配CPU时间,并且时间片轮转不是实时调度器的一个必选项。
提出实时操作系统的概念,可以至少解决两个问题:
早期的CPU任务切换的开销太大,实时调度器可以避免任务频繁切换导致CPU时间的浪费。
在一些特殊的应用场景中,必须要保证重要的任务优先被执行。
在这样的背景下,实时操作系统就被设计出来了,典型的实时操作系统有VxWorks,RT-Thread,uCOS,QNX,WinCE等。
实时任务调度器是实时操作系统的一个必选项,但不代表只要设计出来一个实时调度器就足够了。事实上设计一个实时调度内核并不是一个多么复杂的任务,实时操作系统的特性是在整个操作系统的设计思路上都要时刻关注实时性。
这些设计思路包括:
常规的操作系统中,消息队列都是按照FIFO(先进先出)的方式进行调度,如果有多个接受者,那么接受者也是按照FIFO的原则接受消息(数据),但实时操作系统会提供基于优先级的处理方式:两个任务优先级是分别是10和20,同时等待一个信号量,如果按照优先级方式处理,则优先级为10的任务会优先收到信号量。
实时操作系统调度器最经常遇到的问题就是优先级翻转,因此对于类似信号量一类的API,都能提供抑止优先级翻转的机制,防止操作系统死锁。
这里的锁主要是指自旋锁(spinlock)一类会影响中断的锁,也包括任何关中断的操作。在Windows和Linux的驱动中,为了同步的需要,可能会长期关闭中断,这里的长期可能是毫秒到百微秒级。但实时操作系统通常不允许长期关中断。
对于非实时操作系统来说,如果收到一个外部中断,那么操作系统在处理中断的整个过程中可能会一直关中断。但实时操作系统的通常做法是把中断作为一个事件通告给另外一个任务,interrupt handler在处理完关键数据以后,立即打开中断,驱动的中断处理程序以一个高优先级任务的方式继续执行。
对于一些系统级的服务,比如文件系统操作:
非实时系统会缓存用户请求,并不直接把数据写入设备,或者建立一系列的线程池,分发文件系统请求。
但实时系统中允许高优先级的任务优先写入数据,在文件系统提供服务的整个过程中,高优先级的请求被优先处理,这种高优先级策略直到操作完成。
这种设计实际上会牺牲性能,但实时系统强调的是整个系统层面的实时性,而不是某一个点(比如内核)的实时性,所以系统服务也要实时。
由于应用场景的差异,会出现有些用户需要实时性的驱动,有些用户需要高性能的驱动,因此实时操作系统实际上要提供多种形式的配置以满足不同实时性需求的用户。
多数实时操作系统都不支持虚拟内存(page file/swap area),主要原因是缺页中断(page fault)会导致任务调度的不确定性增加。实时操作系统很多都支持分页,但很少会使用虚拟内存,因为一次缺页中断的开销十分巨大(通常都是毫秒级),波及的代码很多,导致用户程序执行的不确定性增加。
实时操作系统的确定性是一个很重要的指标,在某些极端场景下,甚至会禁用动态内存分配(malloc/free),来保证系统不受到动态的任务变化的干扰。
比如ARINC 653标准中就针对任务调度等作出了一系列的规定,同时定义了特定的API接口和API行为,这些API不同于POSIX API,如果实时系统要在航空设备上使用,就可能需要满足ARINC 653的规范。
由于关中断等原因,通常情况下,操作系统的调度器不会太精确的产生周期性的调度,比如x86早期的默认60的时钟周期(clock rate),抖动范围可能在15-17ms之间。但一个设计优秀的实时操作系统能把调度器的抖动降低到微秒甚至百纳秒一级,在像x86这种天生抖动就很大的架构上,降低系统抖动尤其重要。
SMP(多核)场景的实时调度是很困难的,这里还涉及到任务核间迁移的开销。针对SMP场景,多数实时操作系统的设计都不算十分优秀,但比起普通操作系统来说,其实时性已经好很多了。
同时实时操作系统的虚拟化能从hypervisor层面上提供虚拟机级别的实时调度,虚拟机上可以是另外一个实时系统,也可以是一个非实时系统。
从以上的特点上看,前面提到的常见的错误认识:速度快,吞吐量大,代码精简,代码规模小,都不是实时操作系统的特征:
非实时操作系统也可以很快,实时操作系统也可能很慢;
通常来说实时操作系统的吞吐量会大一些,但非实时系统也可以做到吞吐量更大;
实时操作系统一般都比非实时操作系统要小,但规模大的实时操作系统也是存在的;
而且由于可能需要针对不同用户提供不同等级的实时服务,实时操作系统可能并不是那么精简的……
由于设备性能的发展,原来很多实时性要求高的场景,已经切换到普通的操作系统了。Linux在嵌入式设备上的推广,使用实时操作系统的很多设备已经改用Linux了,因为硬件性能的提升会让系统延迟降低到一个用户可以接受的程度。
但在某些特定的场景下,比如工业自动化、机器人、航空航天、军工领域等,仍然对实时操作系统有需求,并且应该会长期存在。
同时,由于实时操作系统的特性,它并不是一个应用场景广泛的系统,一些人认为学嵌入式就是学实时操作系统,这种认识其实是不正确的,现在嵌入式开发,不一定需要在实时操作系统下完成。
任务控制块是一个数据结构,当任务的CPU使用权被剥夺时,μC/OS-Ⅱ用它来保存该任务的状态。
当任务重新得到CPU使用权时,任务控制块能确保任务从当时被中断的那一点丝毫不差地继续执行。
typedef struct os_tcb {
OS_STK *OSTCBStkPtr; //任务当前堆栈指针
#if OS_TASK_CREATE_EXT_EN
void *OSTCBExtPtr; //指向用户可定义任务控制块扩展的指针。这允
//许您或用户的用户扩展任务控制块,而不必更
//改的源代码。
OS_STK *OSTCBStkBottom; //指向任务堆栈底部的指针
INT32U OSTCBStkSize; //保存堆栈大小的变量,以元素的数量而不是字
//节的数量表示
INT16U OSTCBOpt; //一个保存“选项”的变量,可以传递给函数
//OSTaskCreateExt()
INT16U OSTCBId; // a variable that is used to hold an
//identifier for the task
#endif
struct os_tcb *OSTCBNext; //TCB节点的后继
struct os_tcb *OSTCBPrev; //TCB节点前驱
#if (OS_Q_EN && (OS_MAX_QS >= 2)) || OS_MBOX_EN || OS_SEM_EN
OS_EVENT *OSTCBEventPtr; //指向事件控制块的指针
#endif
#if (OS_Q_EN && (OS_MAX_QS >= 2)) || OS_MBOX_EN
void *OSTCBMsg; //指向发送给任务的消息的指针
#endif
INT16U OSTCBDly; //当任务需要被延迟一定数量的时钟滴答声或任务需
//要为事件超时而付出代价时使用。即可以容忍的时
//间
INT8U OSTCBStat; //指示任务状态,当OSTCBStat为0时,任务就可以
//运行了
INT8U OSTCBPrio; //任务的优先级
//OSTCBX、OSTCBY、OSTCBBitX和OSTCBBitY用于加速使任务准备运行的过程,或使任务
//等待事件(避免在运行时计算这些值)。这些字段的值在创建任务或更改任务的优先级时
//计算
INT8U OSTCBX;
INT8U OSTCBY;
INT8U OSTCBBitX;
INT8U OSTCBBitY;
#if OS_TASK_DEL_EN
BOOLEAN OSTCBDelReq; //指示任务是否请求删除当前任务
#endif
} OS_TCB;
堆栈作用的就是用来保存局部变量,从本质上讲也就是将CPU寄存器的值保存到RAM中。在uCOS中,每一个任务都有一个独立的任务堆栈。为了深入理解任务堆栈的作用,不妨分析任务从“出生”到“消亡”的整个过程,具体就是分析任务的建立,运行,挂起几种状态中任务堆栈的变化情况。
现在假设系统运行着一个由用户创建的用以完成打印工作的任务TPrint。TPrint最初通过OSTaskCreate()函数创建,在该函数中与任务堆栈有关的第一段代码是大家比较熟悉的函数OSTaskStkInit(),这个函数是在uCOS移植过程中必须实现的,其作用是“初始化堆栈”,其实就是预先在RAM中的一块区域中把任务将来运行开始时CPU寄存器应处的状态(正确值)准备好,之后,任务第一次被内核调度器调度运行时,将这些准备好的数据(寄存器的值)推到CPU的寄存器中,如果数据设计的合理,CPU便会按照我们预先设计好的思路运行。所以,“初始化堆栈”实际上是做了一个“未雨绸缪”的工作。这个过程中有两点是必须慎重考虑的,一是PC该如何定位,二是CPU的其它寄存器(除PC之外)该怎么处理。先说第一点,因为任务是第一次运行,而任务从本质上将就是一段代码,所以PC指针应该定位到这段代码的第一行处,即所谓的入口地址(Entry
Point)处,这个地址由任务指针保存着,所以把该指针值赋给PC即可。第二,这段代码还未被执行过,所以代码中的变量与CPU的其它寄存器一点关系也没有,因此R0-R12,R14可随便给值,或者不赋值也可,让这些寄存器保持原来的值,显然后者更为简单。最后再给CPSR赋值,用户可以根据实际需要使系统运行于系统模式或管理模式。经过入栈和出栈,此时SP指向任务堆栈的最底端(就是已经定义好的任务堆栈数组的最后一个元素)。
之后任务代码开始正式运行,因为CPU的寄存器是有限的,所以在运行时不可避免地要把一些临时变量暂时保存到堆栈中。具体应保存到哪个地址呢,不用担心,SP知道(任务第一次运行时,这个地址就是任务堆栈数组的最后一个元素的地址)。任务堆栈的大小和任务代码中临时变量的数目有关,如果这段代码临变量特别多,堆栈就应设计的大一些。
然后,TPrint任务由于某种原因将要被挂起,所以应把任务的运行现场放到堆栈里保护起来,TPrint任务再次运行时再把这个现场还原,任务就能从上次断点处紧接着运行。那么,这个现场是什么呢?从本质上讲,TPrint任务的运行过程就是CPU在执行一段特定的代码,所以这个现场就是CPU的现场,也就是寄存器的值。这些寄存器的值包含了代码执行时的所有信息,包括当前运行到了这段代码的哪个位置处(由PC值指明)。因此,把CPU的寄存器的值推入堆栈,然后记住栈顶指针的位置(SP由OSTCBCur->OSTCBStkPtr保存),当任务再次将要运行前,从SP指向的地址处依次把先前保存的CPU寄存器的值放到CPU的寄存器中,任务就可以从上次中断的地方准确无误地执行。这个过程就像突然把任务冻结了,与任务有关的任何东西都不能动了,一段时间之后又把任务解冻,与它有关的东西又变得可用,于是任务又可以活蹦乱跳地跑起来了。
从以上分析可以看出,任务堆栈至始至终伴随着任务,与之生死与共,它的作用可以概括为两点:第一,当任务运行时,它用来保存一些局部变量;第二,当任务挂起时,它负责保存任务的运行现场,也就是CPU寄存器的值。有些朋友正是忽视了第一点,产生了“任务堆栈大小应是固定值的疑问”。我感觉,这可能与对函数OSTaskStkInit()的理解有关,我们都称之为堆栈初始化函数,但此处的“初始化”与我们理解的初始化不太一样,平时讲的(变量的)初始化似乎指的是将变量的所有成员都一一初始化。而此处的堆栈的初始化仅仅是初始化了很大一个堆栈的一小部分,因为当前只有这部分是有用的,而剩余的大部分用不到,所以不用初始化,就像有些变量不用初始化一样(有默认值或随机值)。更深入一点考虑,当任务挂起时,任务堆栈中保存任务挂起前CPU寄存器的这一连续的区域肯定在整个堆栈的最上面;当任务重新开始运行时,SP弹出寄存器的值,这段区域变成空白的区域。而且,任务每次挂起前用来保存当前CPU寄存器这一连续区域在整个任务堆栈空间中是浮动的。
UCOSII用来记录任务的堆栈指针、任务的当前状态、任务的优先级等一些与任务滚哪里相关的属性的表就叫做任务控制块。任务控制块相当于任务的身份证,系统就是通过任务控制块来感知和管理任务的,没有任务控制块的任务不能被系统承认和管理。
//任务控制块初始化函数
INT8U OS_TCBInit (INT8U prio,
OS_STK *ptos,
OS_STK *pbos,
INT16U id,
INT32U stk_size,
void *pext,
INT16U opt)
{
...............................
}
//在任务创建函数中对任务控制块进行初始化
INT8U OSTaskCreate (void (*task)(void *p_arg),
void *p_arg,
OS_STK *ptos,
INT8U prio)
{
..................................
err = OS_TCBInit(prio, psp, (OS_STK *)0, 0u, 0u, (void *)0, 0u);//对任务控制块进行初始化
..................................
}
任务控制块链表:
UCOSII在任务控制块的管理上需要两条链表,一条空任务块链表(其中所有任务控制块还未分配给任务)和一条任务块链表(其中所有任务控制块已分配给任务)。具体做法是:系统在调用函数OSInit()对UCOSII系统进行初始化时,就先在RAM中建立一个OS_TCB结构类型的数组OSTCBTb1[],然后把各个元素链接成一个如下图所示的链接表,从而形成一个空任务块链表。
所谓任务切换,就是从原来的任务中离开,转去执行新的任务。任务切换的核心是:保存上下文、恢复要去执行的任务的上下文、然后跳转到新任务中执行即可。
ucos是一个可剥夺性内核的操作系统。所以每一个任务都必须有一个优先级。ucos操作系统中任务的优先级使用一个8位整型数据来表示的。比如我们的0,1,2,3这些数,UCOS任务优先级的取值范围为
0 - OS_LOWEST_PRIO之间,数字越小,优先级越大。 静态优先级
这个优先级被分配以后,它在任务的运行过程中,或者说在这个系统的运行过程中,它的优先级就不能变了,比如说,我们把这个优先级分配为2,那这个任务在运行过程中就一直为2,没办法变成其他的优先级。
动态优先级
在任务的运行过程中,它本身是2这个优先级,但是在运行过程中,它还可以变成3,变成4,变成n这个数,那这个过程就被称为动态优先级,也就是说这个优先级在系统的运行过程中,会出现一个可以变化的过程,ucos是一个支持动态优先级的一个系统,也就是说,我们的ucos可以在系统运行的过程中,来更改一个任务的优先级,这点我们需要注意。
对任务优先级分配首先需要做的有哪些事情呢?
假设系统中有1,2,3,4,5,这5个任务,第一个任务对我们开关的输入信号进行扫描,第二个任务处理我们的按键,第三个任务处理我们的串口通信,第四个任务进行我们的系统逻辑处理,比如我们的开关量输出,第五个任务运行我们的LCD屏显示,假设我们对这5个任务分配优先级怎么分配呢?
第一个我们必须把前3个任务的分配优先级要比后两个要高,为什么呢?
因为从逻辑上来说,我们是首先进行开关量扫描,进行按键处理,进行串口通信,接下来,才进行逻辑处理,因为在逻辑处理当中,我们有可能用到开关量扫描,按键处理,和串口等等这些信息,所以说我们分配优先级的时候前3个任务的分配优先级要比后两个要高,对于这个输出和LCD屏显示,我们来看看这两个怎么处理,首先一般来说LCD屏显示是一个比较缓慢的过程,因为LCD屏显示是一个慢速的设备,那第四个逻辑处理和开关量输出运行速度比较快,那我们就把第四个的优先级要比第五个的高一点,那也就是说,对于一个任务,它运行的时间越短,分配的优先级越高,这是为什么呢?
非常简单,因为我们刚刚说了ucos是一个可剥夺性内核,也就是说,如果有高任务在运行,那么这个低任务它是没办法运行的,那如果这个任务占用的时间比较少,我们就把它放到一个较高的任务上,那它就能很快的执行完毕,这样我们的CPU就可以较快的执行一些其他的任务了,这个是我们在使用优先级分配的一个问题,优先级的分配不是那么容易的,我们对一个比较好的操作系统要好好来考虑这个优先级的分配,如果优先级的分配的不好,就可能出现——优先级反转。
任务优先级分配的原则 1、对于实时性要求高的任务应该分配较高的优先级。
比如我们刚刚举例的串口运行任务,我们都知道当串口接收到一个数据以后,它需要在一定的时间内把这个数据处理完,并且返回到上位机,上位机是不可能一直等待这个数据的,所以说我们对这个串口的执行它就有一个时间的要求,也就是实时性要求较高,那对于这个任务,我们就要分配一个较高的优先级。
2、对于运行速度较快的任务应该分配较高的优先级。 3、任务在逻辑之前的要分配较高的优先级。
就是我们刚刚说的,我们首先是扫描开路,扫描按键,接下来我们才能进行逻辑的处理。所以说我们的扫描开路,扫描按键要比逻辑处理的优先级要高,否则的话,我们还没有进行开关量扫描,已经开始处理逻辑了,这个时候,就发生了一个错误。
挂起一个任务,就是停止这个任务的执行。
OSTaskSuspend()函数挂起自身或者除空闲任务之外的任务。
任务在运行过程中,应外部或者内部的异步事件的请求中止当前任务,而去处理异步事件所要求的任务的过程叫做中断。应中断请求而运行的程序叫做中断服务子程序ISR,中断服务子程序的入口地址叫做中断向量。
μC/OS-II内核是可剥夺型的,必须要有任务运行。
当事件控制块成员OSEventType的值被设置成OS_EVENT_TYPE_SEM时,这个事件控制块描述的就是一个信号量,信号量由信号量计数器和等待任务表两部分组成。
1、创建信号量
OS_EVENT *OSSemCreat(INT16U cnt);
函数返回值为已经创建的信号量的指针。
2、请求信号量
任务通过调用函数OSSemPed()请求信号量
函数原型如下:
void OSSemPend( OS_EVENT *pevent, //信号量的指针
INT16U timeout, //等待时限
INT8U *err); //错误信息
为了防止任务因为得不到信号量而处于长期的等待状态,函数OSSemPend()允许用参数timeout设置一个等待时间的限制。当任务等待的时间超过timeout时,可以结束等待状态而进入就绪状态。如果参数timeout被设置为0,则表明任务的等待时间为无限长。
函数调用成功后,err的值为OS_NO_ERR。如果函数调用失败,则函数会根据在函数中出现的错误指令,令err的值分别为OS_ERR_FEND_ISR、OS_ERR_PEVENT_NULL、OS_ERR_PEVENT_NULL、OS_ERR_EVENT_TYPE和OS_TIMEOUT。
3、发送信号量
任务获得信号量,并在访问共享资源结束以后,必须释放信号量。
发送信号量需要调用函数OSSemPost()。
(1)uc/os-ii的信号量是由两个部分组成:一部分是16位的无符号整型信号量的计数值(0~65535);另一部分是等待该信号量的任务组成的等待任务表。(另外参考事件控制块ECB)
(2)信号量可以是2值的变量(称为二值信号量),使用OSMutextCreate创建, 也可以是计数式的, 使用OSSemCreate。根据信号量的值,内核跟踪那些等待信号量的任务。
(3)建立信号量的工作必须在任务级代码中或者多任务启动之前完成。
(4)任务要得到信号量的问题。
4. 信号量是如何实现任务之间的通信的?
(1)信号量的建立必须是在任务级中建立,
(2)信号量类型为OS_EVENT ,信号量值可以为1和0(Mutex二值信号量),0~65535(计数式信号量Semaphore)的值,不同的值代表不同的意义。
(3)信号量(这里仅说互斥)就两个操作,一个请求,一个释放。
(4)一个任务请求信号量时:如果被其它任务占用,则该任务等待,同时导致任务切换;
如果没有被其它任务占用,则获得,继续执行。
(5)释放信号量时,如果其它高优先级任务正在请求并等待该信号量,则导致任务切换。
(6)OSTimeDly之类,并不导致信号量的释放。只有获取信号量的那个任务调用释放功能时,信号量才会释放。
(7)OSSemAccept(信号量)起查询信号量作用,返回信号量的值。
(8)OSSemPend(sem,timeout,&err),timeout代表等待timeout个信号量后还没得到信号量,恢复运行状态,如果timeout=0,表示无限等待信号量。
OSSemPend(sem,0,&err);将暂停当前任务,等待信号量的到来。
OSSemCreate() 创建一个信号量 (注:由任务或启动代码操作)
创建工作必须在任务级代码中或者多任务启动之前完成。功能只要是先获取一个事件控制块ECB,写入一些参数。其中调用了OS_EeventWaitListInt()函数,对事件控制块的等待任务列表进行初始化。完成初始化工作后,返回一个该信号量的句柄(Handle)。
OSSemPend( )或者OSMutextPend( ) 等待/请求一个信号量 (注:只能由任务操作)
本函数应用于任务试图获得共享资源的使用权、任务需要与其他任务或中断同步及任务需要等待特定事件发生的场合。
如果任务Task_A调用OSSemPend(),且信号量的值有效(非0),那么OSSemPend()递减信号量计数器(.OSEventCnt),并返回该值。换句话说,Task_A获取到共享资源的使用权了,之后就执行该资源。
如果如果任务Task_A调用OSSemPend(),信号量无效(为0),那么OSSemPend()调用 OS_EventTaskWait()函数,把Task_A放入等待列表中。(等待到什么时候呢?要看OSSemPost()(或者等待超时情况),由它 释放信号量并检查任务执行权,见下资料)
◆OSSemPost()或者OSMutextPost( ) 发出/释放一个信号量 (注:由任务或中断操作)
本函数其中调用OS_EventTaskRdy()函数,把优先级最高的任务Task_A(在这假如是Task_A,另外假设当前调用OSSemPost()的任务是Task_B)从等待任务列表中去除,并使它进入就绪态。然后调用OSSched()进行任务调度。如果Task_A是当前就绪态中优先级最高的任务,则内核执行Task_A;否则,OSSched()直接返回,Task_B继续执行.
互斥型信号量具备uc/os-ii信号量的所有机制,但还具有其他一些特性。
任务可利用互斥型信号量来实现对共享资源的独占处理。
Mutex是二值信号量,1表示资源是可以使用的。
下面概述优先级反转原理:
假设有三个任务,分别命名为A,B,C;A的优先级最高,C的优先级最低。任务A和任务B处于挂起状态(请注意这条件),等待某一事件的发生,任务C正在运行。当任务C等待到共享资源(命名为S1)并使用后,如果任务A等待得事件到来之后,由于A的优先级最高,所以就会剥夺任务C的CPU使用权。运行过程中,任务A也要使用资源S1,但S1的信号量还被任务C占用着,所有任务A只能进入挂起状态,等待任务C对S1的信号量的释放。此时任务C得以继续运行。
同理,任务B的事件到来后,会剥夺任务C的CPU使用权。任务B把事情搞定以后,把CPU使用权归还给任务B(呵呵,优先级低就是给人欺负啊,所以做人还真的要争口气!)。任务B又得以继续运行,任务B认真处理完毕资源S1后,终于可以释放S1的信号量。而处于等待该信号量的任务A马上得到信号量并开始处理共享资源S1。
综述上面情况,任务C和任务A的优先级发生了反转。
而互斥型信号量就是具有解决优先级反转问题的特性。
3.uc/os-ii的互斥型信号量由三个部分组成:
◆一个标志,指示mutex是否可以使用(0或1)
◆一个优先级,准备一旦高优先级的任务需要这个mutex,赋予给占有mutex的任务。
◆一个等待该mutex的任务列表。
1、邮箱简介
邮箱简介可以参考正点原子的STM32F4开发手册里有,摘抄一下:在多任务操作系统中,常常需要在任务与任务之间通过传递一个数据(这种数据叫做“消息”)的方式来进行通信。为了达到这个目的,可以在内存中创建一个存储空间作为该数据的缓冲区。如果把这个缓冲区称之为消息缓冲区,这样在任务间传递数据(消息)的最简单办法就是传递消息缓冲区的指针。我们把用来传递消息缓冲区指针的数据结构叫做邮箱(消息邮箱)。
简单描述一下:使用邮箱可以在任务之间传递数据,例如传递一个变量的值。一方面可以做任务同步另一方面可以使用到这个数据来进行逻辑控制。
OSMboxPost
这个函数只有两个参数,分别是要发送的邮箱名和要传递的数据,有几个作用和细节:
细节注意:使用这个函数后会往指定的邮箱中发布消息,同时函数内部会检查有无正在等待这个邮箱消息的任务,如果有正在等待消息的任务则判断当前使用OSMboxPost这个任务的优先级和等待着的任务优先级哪个高,如果等待着的优先级高则马上引起任务切换,若等待着任务的优先级低则不会引起任务切换。
OSMboxPend
这个函数是请求邮箱中的消息,如果设置第二个参数为0则表示会死等直到有消息了才会往下执行,在做数据接收时有几个细节:
OSMboxPostOpt
这个函数有三个参数,相比于OSMboxPost而言多了最后一个参数,是用于广播的,意思是OSMboxPost只能往等待邮箱发一条消息,只有一个正在等待中的并且优先级最高的任务能获取到,其他等待该邮箱的任务不能获取到任然处于挂起状态,而OSMboxPostOpt可以使得所有正在等待这个邮箱消息的任务都能获取到消息。
为什么需要消息队列
主要原因是由于在高并发环境下,由于来不及同步处理,请求往往会发生堵塞,比如说,大量的insert,update之类的请求同时到达MySQL,直接导致无数的行锁表锁,甚至最后请求会堆积过多,从而触发too many connections错误。通过使用消息队列,我们可以异步处理请求,从而缓解系统的压力。
在ucos中吗,当存在多任务想要获取消息队列中的信息时,每个消息队列中都有一个任务链表,表示等待该消息队列中的任务。当任务向如果消息队列中没有任何消息则将任务挂起;当有消息时则让优先级最高的任务进入就绪状态(非广播模式)。
消息队列在ucosii系统中是一个典型的环形缓冲区。
typedef struct os_q { /* QUEUE CONTROL BLOCK */
struct os_q *OSQPtr; /* Link to next queue control block in list of free blocks */
void **OSQStart; /* Ptr to start of queue data */
void **OSQEnd; /* Ptr to end of queue data */
void **OSQIn; /* Ptr to where next message will be inserted in the Q */
void **OSQOut; /* Ptr to where next message will be extracted from the Q */
INT16U OSQSize; /* Size of queue (maximum number of entries) */
INT16U OSQEntries; /* Current number of entries in the queue */
} OS_Q;
OSQCreate()
两个参数,一个被当做消息队列的数组,以及这个数组的大小。返回一个OS_EVENT指针。
OSQDel()
删除一个消息队列之前应该删除所有需要这个消息队列的任务。
OSQPend()
当timeout被设置为0的时候表示任务将永远等待(?linux中如果是NULL表示永远等待,而0表示不加等待直接返回,类似于轮询的模式,可能是ucosii不允许这个参数设置为null所以设定0表示永远等待?)
当消息队列中有消息可用的时候,获取其位于OSQOut指针处的消息,否则将任务挂起,等待消息到来。
OSQPost()与OSQPostFront()与OSQPostOpt()
三都是想消息队列中发送消息。OSQPost属于FIFO模式,将传入的msg存放在读指针OSQOut处,实现先进先出;OSQPostFront的属于LIFO模式,将传入的msg存放在读指针OSQIn处,实现后进先出;OSQPostOpt属于可选择模式,支持FIFO、LIFO、广播模式(让所有队列中等待的任务都处于就绪状态,否则仅仅使得)。
其他函数略,和mutex信号量的都差不多。
任务和中断服务程序都可以调用Post、PostFront、PostOpt函数,但是只有任务才能调用Del、Pend、Query。
一个简单的应用—使用消息队列读取模拟量的值
裸机中实现对模拟量的采集一般是通过定时器,在中断服务程序中采集开启ADC,期间要关中断,要等待ADC转化完成。
利用OSQPend函数的timeout参数,在没有任何任务发送msg的情况下,可以实现一个采样周期。通过向消息队列发送消息还可以修改采样周期。通过msg还可以修改采样通道。
作为一个RTOS操作系统,内存管理是必备的功能,因此UCOSII也具有内存管理能力。通常应用程序可以调用ANSIC编译器的malloc()和free()函数来动态的分配和释放内存,但是嵌入式实时操作系统中最好不要这么做,多次这样的操作会把原来很大的一块连续存储区逐渐分割成许多非常小并且彼此不相邻的存储区域,这就是存储碎片。
UCOSII中将存储空间分成区和块,每个存储区有数量不等大小相同的存储块,在一个系统中可以有多个存储区。
操作系统以分区为单位来管理动态内存,而任务以内存块为单位来获得和释放动态内存。内存分区及内存块的使用情况则由内存控制块来记录。
在内存中定义一个内存分区及其内存块的方法非常简单,只需定义一个二维数组即可。例如:
INT8U IntMemBuf[10][10]; //定义一个内存区,包含10个内存块,每个内存由10个u8数据组成。
#if (OS_MEM_EN > 0u) && (OS_MAX_MEM_PART > 0u)
typedef struct os_mem { /* MEMORY CONTROL BLOCK */
void *OSMemAddr; /* 内存分区的指针 */
void *OSMemFreeList; /* 内存控制块链表指针 */
INT32U OSMemBlkSize; /* 内存块的长度 */
INT32U OSMemNBlks; /* 分区内内存块的数目 */
INT32U OSMemNFree; /* 分区内当前可分配的内存块的数目 */
#if OS_MEM_NAME_EN > 0u
INT8U *OSMemName; /* Memory partition name */
#endif
} OS_MEM;
创建动态内存分区函数OSMemCreate()
OS_MEM *OSMemCreate (void *addr, //内存分区的起始地址
INT32U nblks, //分区中内存块的数目
INT32U blksize, //每个内存块的字节数
INT8U *perr) //错误信息
请求获得内存块函数OSMemGet();
void *OSMemGet (OS_MEM *pmem, //内存分区的指针
INT8U *perr) //错误信息
释放获得内存块函数OSMemGet();
/**********UCOSII任务堆栈设置*****************/
//1.START 任务
//设置任务优先级
#define START_TASK_PRIO 10 //开始任务的优先级设置为最低
//设置任务堆栈大小
#define START_STK_SIZE 128
//创建任务堆栈空间
OS_STK START_TASK_STK[START_STK_SIZE];
//任务函数接口
void start_task(void *pdata);
//2.main_task主任务
//任务优先级
#define MAIN_TASK_PRIO 4
//任务堆栈大小
#define MAIN_STK_SIZE 128
//任务堆栈
OS_STK MAIN_TASK_STK[MAIN_STK_SIZE];
//任务函数
void main_task(void *pdata);
//3.memmanage_task内存管理任务
//任务优先级
#define MEMMANAGE_TASK_PRIO 5
//任务堆栈大小
#define MEMMANAGE_STK_SIZE 128
//任务堆栈
OS_STK MEMMANAGE_TASK_STK[MEMMANAGE_STK_SIZE];
//任务函数
void memmanage_task(void *pdata);
/********************定义存储区**********************/
//定义一个存储区(内部)
OS_MEM *INTERNAL_MEM; //定义内存控制块指针
//存储区中存储块数量
#define INTERNAL_MEM_NUM 5
//每个存储块大小
//由于一个指针变量占用4字节所以块的大小一定要为4的倍数
//而且必须大于一个指针变量(4字节)占用的空间,否则的话存储块创建不成功
#define INTERNAL_MEMBLOCK_SIZE 100
//存储区的内存池,使用内部RAM
u8 Internal_RamMemp[INTERNAL_MEM_NUM][INTERNAL_MEMBLOCK_SIZE]; //划分分区及内存块
//定义一个存储区(外部)
OS_MEM *EXTERNAL_MEM; //定义内存控制块指针
//存储区中存储块数量
#define EXTRENNAL_MEM_NUM 5
//每个存储块大小
//由于一个指针变量占用4字节所以块的大小一定要为4的倍数
//而且必须大于一个指针变量(4字节)占用的空间,否则的话存储块创建不成功
#define EXTERNAL_MEMBLOCK_SIZE 100
//存储区的内存池,使用外部SRAM(位置:0X68000000)
u8 External_RamMemp[EXTRENNAL_MEM_NUM][EXTERNAL_MEMBLOCK_SIZE] __attribute__((at(0X68000000)));//划分分区及内存块
/********************main主函数**********************/
int main(void)
{
delay_init(168); //延时初始化
uart_init(115200); //串口初始化波特率为115200
LED_Init(); //初始化与LED连接的硬件接口
KEY_Init(); //key初始化
LCD_Init(); //LCD初始化
FSMC_SRAM_Init(); //初始化SRAM
ucos_load_main_ui(); //加载主UI1
OSInit(); //初始化UCOSII
OSTaskCreate(start_task,(void *)0,(OS_STK *)&START_TASK_STK[START_STK_SIZE-1],START_TASK_PRIO );//创建起始任务
OSStart(); //开始执行UCOS程序
}
//开始任务
void start_task(void *pdata)
{
u8 err;
OS_CPU_SR cpu_sr=0;
pdata = pdata;
OSStatInit(); //初始化统计任务.这里会延时1秒钟左右
OS_ENTER_CRITICAL(); //进入临界区(无法被中断打断)
//创建一个内部存储分区
INTERNAL_MEM=OSMemCreate(Internal_RamMemp,INTERNAL_MEM_NUM,INTERNAL_MEMBLOCK_SIZE,&err);
//创建一个外部存储分区,使用外部SRAM(位置:0X68000000)
EXTERNAL_MEM=OSMemCreate(External_RamMemp,EXTRENNAL_MEM_NUM,EXTERNAL_MEMBLOCK_SIZE,&err);
OSTaskCreate(main_task,(void *)0,(OS_STK*)&MAIN_TASK_STK[MAIN_STK_SIZE-1],MAIN_TASK_PRIO); //在开始任务中创建main任务
OSTaskCreate(memmanage_task,(void *)0,(OS_STK*)&MEMMANAGE_TASK_STK[MEMMANAGE_STK_SIZE-1],MEMMANAGE_TASK_PRIO); //在开始任务中创建memmanage_task任务
OSTaskSuspend(START_TASK_PRIO); //挂起起始任务.
OS_EXIT_CRITICAL(); //退出临界区(可以被中断打断)
}
//主任务的任务函数(完成内存的申请及释放)
void main_task(void *p_arg)
{
u8 key,num,i=0;
static u8 external_memget_num;
u8 *internal_buf[5];
u8 *external_buf;
u8 err;
while(1)
{
key = KEY_Scan(0); //扫描按键
switch(key)
{
case WKUP_PRES: //按下KEY_UP键:申请内部内存
for(i=0;i<5;i++)
{
internal_buf[i]=OSMemGet(INTERNAL_MEM,&err); //循环申请五次内存并放在internal_buf[i]缓存区内
if(err == OS_ERR_NONE) //内存申请成功
{
sprintf((char*)internal_buf[i],"internal_buf[%d]=%d\r\n",i,i);//在申请到的内存区域内放置数据
printf("%s",internal_buf[i]); //显示出来
printf("internal_buf[%d]内存申请之后的地址为:%#x\r\n",i,(u32)(internal_buf[i]));
printf("internal_buf[%d]内存申请成功!\r\n",i);
}
else if(err == OS_ERR_MEM_NO_FREE_BLKS) //内存块不足
{
LCD_ShowString(30,180,200,16,16,"INTERNAL_MEM Empty! ");
}
delay_ms(500); //延时500ms,也就是每500ms申请一次内存,共五次
}
break;
case KEY1_PRES:
for(i=5;i>0;i--)
{
if(internal_buf[i-1] != NULL) //internal_buf不为空就释放内存
{
OSMemPut(INTERNAL_MEM,internal_buf[i-1]);//循环5次释放内部内存
printf("internal_buf[%d]内存释放之后的地址为:%#x\r\n",i-1,(u32)(internal_buf[i-1]));
printf("internal_buf[%d]内存释放成功!\r\n",i-1);
internal_buf[i-1]=NULL; //释放后的缓存区指向NULL!
}
else if(internal_buf[i-1] == NULL) //内存已释放
{
LCD_ShowString(30,180,200,16,16,"INTERNAL_MEM released! ");
}
delay_ms(500); //延时500ms,也就是每500ms申请一次内存,共五次
}
break;
case KEY2_PRES: //按下KEY2键:申请外部内存
external_buf=OSMemGet(EXTERNAL_MEM,&err);
if(err == OS_ERR_NONE) //内存申请成功
{
printf("external_buf内存申请之后的地址为:%#x\r\n",(u32)(external_buf));
LCD_ShowString(30,260,200,16,16,"Memory Get success! ");
external_memget_num++;
POINT_COLOR = BLUE;
sprintf((char*)external_buf,"EXTERNAL_MEM Use %d times",external_memget_num);
LCD_ShowString(30,276,200,16,16,external_buf);
POINT_COLOR = RED;
}
if(err == OS_ERR_MEM_NO_FREE_BLKS) //内存块不足
{
LCD_ShowString(30,260,200,16,16,"EXTERNAL_MEM Empty! ");
}
break;
case KEY0_PRES:
if(external_buf != NULL) //external_buf不为空就释放内存
{
OSMemPut(EXTERNAL_MEM,external_buf);//释放外部内存
printf("external_buf内存释放之后的地址为:%#x\r\n",(u32)(external_buf));
LCD_ShowString(30,260,200,16,16,"Memory Put success! ");
}
break;
}
num++;
if(num==50)
{
num=0;
LED0 = ~LED0;
}
delay_ms(10); //延时10ms
}
}
//内存管理任务(显示总的内存缓存块数以及剩余缓存块的数)
void memmanage_task(void *p_arg)
{
LCD_ShowString(5,164,200,16,16,"Total: Remain:");
LCD_ShowString(5,244,200,16,16,"Total: Remain:");
while(1)
{
POINT_COLOR = BLUE;
LCD_ShowxNum(53,164,INTERNAL_MEM->OSMemNBlks,1,16,0);
LCD_ShowxNum(125,164,INTERNAL_MEM->OSMemNFree,1,16,0);
LCD_ShowxNum(53,244,EXTERNAL_MEM->OSMemNBlks,1,16,0);
LCD_ShowxNum(125,244,EXTERNAL_MEM->OSMemNFree,1,16,0);
POINT_COLOR = RED;
delay_ms(100); //延时100ms
}
}
1.通过定义一个二维数组在内存中划分一个内存分区,其中的所有内存块应大小相等。
2.系统通过与内存分区相关联的内存控制块来对内存分区进行管理。
3.划分及创建内存分区根据需要由应用程序负责,而系统只提供了可供任务调用的相关函数。
4.在UCOSII中,在使用和释放动态内存的安全性方便,要由应用程序全权负责。
使用STM32的官方HAL库进行开发
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
HAL_Delay(500);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
HAL_Delay(500);
UCOSII 从 V2.83 版本以后,加入了软件定时器,这使得UCOSII的功能更加完善,在其上的应用程序开发与移植也更加方便。在实时操作系统中一个好的软件定时器实现要求有较高的精度、较小的处理器开销,且占用较少的存储器资源。
UCOSII 通过 OSTimTick函数对时钟节拍进行加1操作,同时遍历任务控制块,以判断任务延时是否到时。软件定时器同样由OSTimTick提供时钟,但是软件定时器的时钟还受OS_TMR_CFG_TICKS_PER_SEC 设置的控制,也就是在UCOSII的时钟节拍上面再做了一次“分频”,软件定时器的最快时钟节拍就等于 UCOSII 的系统时钟节拍。这也决定了软件定时器的精度。
软件定时器定义了一个单独的计数器OSTmrTime,用于软件定时器的计时,UCOSII并不在OSTimTick 中进行软件定时器的到时判断与处理,而是创建了一个高于应用程序中所有其他任务优先级的定时器管理任务 OSTmr_Task,在这个任务中进行定时器的到时判断和处理。时钟节拍函数通过信号量给这个高优先级任务发信号。这种方法缩短了中断服务程序的执行时间,但也使得定时器到时处理函数的响应受到中断退出时恢复现场和任务切换的影响。软件定时器功能实现代码存放在 tmr.c文件中,移植时需只需在os_cfg.h文件中使能定时器和设定定时器的相关参数。
S_TMR 为定时器控制块,定时器控制块是软件定时器管理的基本单元,包含软件定时器的名称、定时时间、在链表中的位置、使用状态、使用方式,以及到时回调函数及其参数等基本信息
OSTmrTbl[OS_TMR_CFG_MAX];:以数组的形式静态分配定时器控制块所需的RAM空间,并存储所有已建立的定时器控制块, OS_TMR_CFG_MAX 为最大软件定时器的个数。
OSTmrFreeLiSt:为空闲定时器控制块链表头指针。空闲态的定时器控制块(OS_TMR)中,OSTmrnext 和 OSTmrPrev 两个指针分别指向空闲控制块的前一个和后一个,组织了空闲控制块双向链表。建立定时器时,从这个链表中搜索空闲定时器控制块。
OSTmrWheelTbl[OS_TMR_CFG_WHEEL_SIZE]:该数组的每个元素都是已开启定时器的一个分组,元素中记录了指向该分组中第一个定时器控制块的指针,以及定时器控制块的个数。运行态的定时器控制块(OS_TMR)中,OSTmrnext和OSTmrPrev两个指针同样也组织了所在分组中定时器控制块的双向链表
OSTimeDly
将一个任务延时若干个时钟,无函数返回值。延时时间的长度可从0到65535个时钟节拍,延时时间0表示不进行延时,函数将立即返回调用者,延时的具体时间依赖于系统每秒种有多少时钟节拍
void OSTimeDly(INT16U ticks)
OSTimeDlyHMSM
任务延时函数OSTimeDly用于将任务阻塞一段时间,这个时间是以时间片为单位的。如果想以小时、分、秒、毫秒为单位进行任务延时,需要调用以分秒作为单位的任务延时函数OSTimeDlyHMSM
INT8U OSTimeDlyHMSM(INT8U hours, INT8U minutes, INT8U seconds, INT16U milli);
三、 软件定时器和任务延时的区别
调用OSTimeDly或者OSTimeDlyHMSM,意味着该任务CPU使用权会被没收,然而开启一个定时器之后,该任务还可以使用CPU
举例子:如下情景,可以使用软件定时器作超时处理,设备A管理设备B、C、E,设备A向设备BCE设备发送某一消息搜索,如果在T时间内,设备BCE没有回应,设备A将重起并初始化BCE;那么可以在一个任务中,依次向BCE发送消息,并且启动软件动定时器TMRa,TMRb,TMRc,定时器时间到时调用各自的重起并初始化函数;另一方面,如果接收到BCE的消息则停止定时器TMRa,TMRb,TMRc
然而如果用OSTimeDly或者OSTimeDlyHMSM处理上面的场景,可能要多开几个任务管理BCE并增加信号量通知OSTimeDly或者OSTimeDlyHMSM之后,到底是“重起并初始化BCE”还是什么都不做
软件定时器和延时都是基于“系统的节拍”来计时/定时的,虽然软件定时器是在一个高优先级的任务中管理,这个任务也是由“系统节拍中断“中向其发送信号量,因此还是基于“系统的节拍”
没必要去对它们的区别做出一个定义,关键还是去理解它们的应用场合,它们都能解决什么问题
四、 stm32上使用UCOSII的软件定时器和任务延时
led0通过软件定时器,每500ms执行一次回掉函数,来实现闪烁
led1通过任务延时,每500ms执行一次led1任务,来实现闪烁
/UCOSII任务设置///
//START 任务
//设置任务优先级
#define START_TASK_PRIO 10 //开始任务的优先级设置为最低
//设置任务堆栈大小
#define START_STK_SIZE 64
//任务堆栈
OS_STK START_TASK_STK[START_STK_SIZE];
//任务函数
void start_task(void *pdata);
//定时器设置任务
//设置任务优先级
#define TIMER_TASK_PRIO 5
//设置任务堆栈大小
#define TIMER_STK_SIZE 64
//任务堆栈
OS_STK TIMER_TASK_STK[TIMER_STK_SIZE];
//任务函数
void timer_task(void *pdata);
//LED1任务
//设置任务优先级
#define LED1_TASK_PRIO 4
//设置任务堆栈大小
#define LED1_STK_SIZE 64
//任务堆栈
OS_STK LED1_TASK_STK[LED1_STK_SIZE];
//任务函数
void led1_task(void *pdata);
///
OS_TMR * tmr1; //软件定时器1
int main(void)
{
delay_init(); //延时函数初始化
NVIC_Configuration();
LED_Init(); //初始化与LED连接的硬件接口
OSInit();
OSTaskCreate(start_task,(void *)0,(OS_STK *)&START_TASK_STK[START_STK_SIZE-1],START_TASK_PRIO);//创建起始任务
OSStart();
}
//开始任务
void start_task(void *pdata)
{
u8 err;
OS_CPU_SR cpu_sr=0;
pdata = pdata;
OS_ENTER_CRITICAL(); //进入临界区(无法被中断打断)
OSTaskCreate(timer_task,(void *)0,(OS_STK*)&TIMER_TASK_STK[TIMER_STK_SIZE-1],TIMER_TASK_PRIO);
OSTaskCreate(led1_task,(void *)0,(OS_STK*)&LED1_TASK_STK[LED1_STK_SIZE-1],LED1_TASK_PRIO);
OSTaskSuspend(START_TASK_PRIO); //挂起起始任务.
OS_EXIT_CRITICAL(); //退出临界区(可以被中断打断)
}
//软件定时器1的回调函数
//每500ms执行一次,用于LED0偏转
void tmr1_callback(OS_TMR *ptmr,void *p_arg)
{
LED0=!LED0;
}
//定时器设置任务
void timer_task(void *pdata)
{
u8 err;
tmr1=OSTmrCreate(0,50,OS_TMR_OPT_PERIODIC,(OS_TMR_CALLBACK)tmr1_callback,0,"tmr1",&err);//500ms执行一次
OSTmrStart(tmr1,&err);//启动软件定时器1
while(1)
{
delay_ms(500);
}
}
void led1_task(void *pdata)
{
while(1)
{
//OSTimeDly(100);//500ms执行一次
OSTimeDlyHMSM(0,0,0,500);//500ms执行一次
LED1=!LED1;
}
}
1、准备资源包
可以使用标准库搭建或者CubeMX生成
2、建立IAR工程
在工程文件夹下建立文件夹UCOSII,并建立子文件夹Core、Config、Port分别存放资源包中
Source、Template、IAR文件夹下的内容
3、增加分组UCOSII,并增加子分组Core、Config、Port
在子分组中添加对应文件夹下的文件,这些是与处理器无关的操作系统内核代码。需要注意
ucos_ii.c和os_dbg_r.c不添加,否则会报重复定义错误。
4、添加路径
5、取消钩子函数的使能
6、增加两个宏定义
7、处理系统中断函数
8、处理系统滴答,增加系统滴答处理函数
9、在main.h中包含os.h就可以包含系统相关的所有头文件
10、编译通过后就可以开始配置任务了
11、首先定义任务优先级、堆栈、接口函数等任务基础配置信息,创建任务堆栈空间。之后就可以建立
启动任务,在启动任务函数中建立任务A,在任务A函数中书写一个LED翻转功能
12. 在main函数中,在各初始化函数运行完后,创建启动任务并开启任务调度
ucos实现点灯
任务调度:
UCOSII任务调度思想:“近似地每时每刻让优先级最高的就绪任务处于运行状态”。具体上,采用系统或用户任务调用系统函数及执行中断服务程序结束时来调用调度器,以确定应该运行的任务并运行它。
在多任务系统中,令CPU中止当前正在运行的任务转而去运行另一个任务的工作叫任务切换,而按照某种规则进行任务切换的工作叫任务的调度。
UCOSII中调度器有两种,一是任务级的调度器,二是中断级的调度器。任务调度器主要是完成任务调度,主要工作是寻找最高优先级就绪任务和实现任务切换,由函数OSSched()来实现。中断级的调度器由OSIntExt()实现。主要介绍任务级的调度器OSSched()。
调度器把任务切换的工作分成两个步骤:
1、获得待运行任务的TCB指针;2、进行断点数据的切换。
操作系统通过任务的任务控制块TCB来管理任务,因此调度器真正试试任务切换之前的主要工作就是获得待运行任务的任务控制块指针和当前任务的任务控制块指针。
因为被中止的任务控制块指针存在在全局变量OSTCBCur中,因此调度器这部分主要任务就是获取待运行任务的任务控制指针。
UCOSII运行应用程序调用函数OSSchedLock()和OSSchedUnlock()给调度器上锁和解锁,并用OSLockNesting变量来记录上锁(+1)和解锁(-1)。因此可以通过OSLockNesting来确定嵌套次数。
调度器OSSched()在确认未被上锁并不是中断服务函数时,首先从任务就绪表中查得最高优先级别就绪的优先级别OSPrioHighRdy;然后在确实就绪任务不是当前任务时,OSPrioHighRdy作为下标去访问数组OSTCBPrioTbl[],把数组元素OSTCBPrioTbl[OSPrioHighRdy]的值赋值给指针变量OSTCBHighRdy。依照上述函数获取到的指向待运行任务控制块和当前任务块的指OSTCBHighRdy和OSTCBCur,并在宏OS_TASK_SW()中实施任务切换。
切换的工作主要是靠OSCtxSw()来完成。
任务切换就是中止正在运行的任务,转去运行另外一个任务的操作。这个任务应该是就绪任务中优先级别最高的任务。
为了了解如何切换任务,首先看一下中止任务,将来恢复运行需要什么条件。
如果把任务中止运行的位置叫做断点,当时存放在CPU的PC和PSW和通用寄存器等各种寄存器的数据叫做断点数据。当任务恢复必须以断点数据作为初始数据接着运行。因此在任务中止时我们需要把该任务的断点数据保存在堆栈中。
所以,一个被中止的任务能否正确的在断点处恢复运行,关键在于是否能正确的在CPU各寄存器中恢复断点,而能够正确恢复断点数据的关键在于CPU的堆栈指针SP是否有正确的指向。因此,系统的多任务,如果恢复断点数据时用另一个任务堆栈指针来改变CPU的堆栈SP,那么CPU运行就不是刚刚中止任务,而是另外一个任务,也就实现了任务切换。
综上,任务切换就是断点数据切换,断点数据切换就是CPU堆栈指针的切换。被中止运行任务的任务堆栈指针要保护到该任务的任务控制块中,待运行任务的任务堆栈置要由该任务控制块转存到CPU的SP中。
为了完成上述操作,OSCtxSw()要依次完成7个工作
1、把被中止任务的断点指针保存到任务堆栈中
2、把CPU通用寄存器内容保存到任务堆栈中
3、把被中止任务的任务堆栈指针当前值保存到该任务的任务控制块的OSTCBStkPtr中
4、获得待运行任务的任务控制块、
5、使CPU通过任务控制块获得待运行任务的任务堆栈指针
6、待运行任务堆栈通用寄存器内容恢复到CPU通用寄存器
7、CPU获得待运行任务的蹲点指针(上一次被调度器中止运行保留在任务堆栈中的)
在主程序里面做i++(i=10),在某个中断服务函数里面做i–,会出现主程序刚取出i的值得时候被中断打断,在中断里面做了i—(写回去的值为9),返回到断点后,主程会对i(已经取出的值)做加1操作,然后写回去(最终i=11;).------在主程序里面在对i操作之前先把中断关了,等操作完后再打开中断。
出现以上情况的原因是—打断。在Ucos中不可避免的会出现以上打断问题。因为ucos是以优先级作为调度原则,所以也存在打断问题。所以任务间间交换信息如果用全局变量,就存在以上问题。所以ucos任务间交换信息尽量的不要用全局变量。
Ucosii的任务间通信机制:信号量、互斥信号量、消息邮箱、消息队列、事件标志组
任务控制块:存放当前任务的相关信息(任务函数地址、任务优先级、任务栈、任务状态)
事件控制块:当成功创建一个事件(信号量、互斥信号量、消息邮箱、消息队列、事件标志组)后,系统就分配一段内存空间,这段空间就是事件控制块,存放这该事件的相关信息。
信号量
可以把信号量看成是一个计数器,表示当前资源的占用情况,当释放一个资源时信号量+1,如果占用一个资源,信号量-1。
要想使用信号量,必须先创建一个信号量,并且可以对这个信号量赋予一个初始值。
释放信号量,信号量就会+1
要想得到一个信号量,要先查看信号量是否为0,如果大于0表示当前可以去占用一个信号量,如果为 0表示当前信号量被用光了,可以死等其他任务释放信号量,也可以不等。
函数原型:OS_EVENT *OSSemCreate(INT16U value);
函数作用:建立并初始化一个信号量
参数说明:建立的信号量的初始值,可以取 0 到 65535 之间的任何值。
返回值:指向成功创建的信号量的事件控制块地址
函数原型:void OSSemPend (OS_EVENT *pevent,
INT32U timeout,
INT8U *perr);
函数作用:申请一个信号量(挂起任务等待信号量)。
函数原型:INT16U OSSemAccept(OS_EVENT *pevent);
函数作用:无等待查看信号量是否为0。
函数原型:INT8U OSSemPost(OS_EVENT *pevent);
函数作用:释放一个信号量,把信号值加 1
函数原型:void OSSemSet(OS_EVENT *pevent,
INT16U cnt,
INT8U *perr);
函数作用:改变当前信号量的计数值
消息邮箱
顾名思义,可以往邮箱里存放消息,这个消息的内容比信号量要更加丰富。
这个邮箱只能存放一则消息。
如果这个邮箱里存在消息没有被读取,然后再往里面存放消息,就会失败。
在消息邮箱里面,如果发送方能力比较强(发送速度快),接受方能力比较弱(接受得慢),那么就会丢失新的消息。
消息队列
消息邮箱只能存放一则消息,而消息队列则可以存放一队(多则)消息,相当于增强了接受方的能力
消息队列存放消息的方式是先进先出
创建一个消息队列:
OS_EVENT *OSQCreate (void **start,INT16U size)
start:指向用户创建一个存储区(数组),这个存储空间(数组)存放的是消息的地址。成功创建了消息队列后,创建的这个存储区(数组)就交由UCOSII去管理。
size:消息内存区的大小
发送一则消息
INT8U OSQPost (OS_EVENT *pevent,void *pmsg);
pmsg:消息的地址
发送多则不同消息需要发送不同的消息地址。
互斥信号量
对于互斥信号量: 二值,要么为0,要么为1
Pend/Accept: 想取得互斥信号量的控制权
Post: 释放互斥信号量的控制权。只有取得互斥信号量的任务post才有效
优先级翻转问题:
低优先级的任务取得了互斥信号量后,高优先的任务也想取得互斥信号量的控制权。这个时候高优先级的任务得不到互斥信号量,并且还会把低优先级的任务的优先级提升到一个设定的优先级(提升到的优先级在创建互斥信号量的时候已经指定,一般设置的优先级比想取得此互斥信号量的任务中最高的优先级还高)。
低优先级的任务和高优先级的任务同时都想要获取同一个互斥信号量,假如低优先级的任务先得到了这个互斥信号量,然后高优先级的任务就需要等待低优先级的任务释放该信号量才能得到。如果在低优先级释放信号量之前被其他不参与竞争这个互斥信号量的中等优先级任务抢走了CPU,就有可能发生低优先级的任务释放不了这个互斥信号量,使得在等待该信号量的高优先级任务真的等到死了。
所以,为了避免这种事情的发生,在低优先级的任务得到了互斥信号量后,高优先级的任务也想去获取而获取不到时,就会把得到该互斥信号量的低优先级任务的优先级提升到一个预定的比较高的优先级。当低优先级任务释放了互斥信号量后,就会把优先级回复到原来的级别。
如果高优先级的任务先的到了互斥信号量,而这时候低优先级的任务也想去获取,只能死等。