您的当前位置:首页uCOS-II内核学习总结

uCOS-II内核学习总结

2021-09-02 来源:乌哈旅游


一、 主函数基本结构

void main (void) {

OSInit();

PC_DOSSaveReturn();

PC_VectSet(uCOS, OSCtxSw);

OSTaskCreate(TaskStart, (void *)0, &TaskStartStk[TASK_STK_SIZE - 1], 0);

OSStart(); }

OSInit():初始化函数,包括建立空闲任务OS_TASKIdle()和统计任务OS_TaskStat(),优先级分别为63(OS_LOWEST_PRIO)与62。还包括变量和数据结构的初始化。

空闲任务的优先级不会改变,主要对计数器OSIdleCtr加1,该计数可以用于cpu使用率的计算。另外在空闲任务中的OSTaskIdleHook()写入代码,可以执行其他操作,包括让cpu进入低耗模式。但是由于空闲任务始终在就绪状态,因此不可以调用使任务进入等待或挂起状态的函数。

当统计任务在对应常值配置的情况下,会在进入的第一个执行任务(这里为TaskStart)调用OSStatInit()函数,该函数主要目的是将1s时间空闲任务计数最大值保存至统计任务的OSIdleCtrMax,为了确保最大值,需要控制空闲任务与时钟节拍同步,解决办法是使任务TaskStart等待2个节拍,进入统计任务,等待2s,这样统计任务不会抢占空闲任务,两个节拍后,TaskStart仅执行空闲计数值=0,休眠1s,立刻进入空闲任务,基本达到空闲任务与时钟节拍同步。1s后将计数值赋予OSIdleCtrMax。在后面调用统计函数中,将空闲计数值保存,然后清零。计算公式为:100-OSIdleCtr/(OSIdleCtrMax/100);因为当处理器性能快时,计数值会超出范围,因此计算时除以100,转换方式。

初始化模块主要有任务控制块缓冲区,事件控制块缓冲区,队列缓冲区,事件标记组缓冲区,内存控制块缓冲区。所有的缓冲区都是以单向链表结构组织,表头为**FreeList。

PC_DOSSaveReturn():在程序开始保存寄存器,保证返回到DOS。

PC_VectSet(uCOS, OSCtxSw):设置任务切换函数为0X80号中断向量。当在任务级进行任务切换时,会调用INT指令引发中断,进入切换函数的入口地址。中断的触发与具体处理器相关。 任务的切换分为两种:在任务级的切换和在中断中的切换。 任务级的切换是调用了OS_Sched()函数,在该函数中使用宏调用OS_TASK_SW(),该宏调用含有处理器的软中断,进入中断服务子程序OSCtxSw,进行cpu的保存与栈顶指针的改变,及cpu恢复。

中断级的调用,在中断服务子程序的入口与出口处分别调用函数OSIntEnter()和OSIntExit(),进中断主要是进行OSIntNesting(进入中断内的标志)的加1,出中断进行中断标志的减1,然后判断优先级是否在中断内发生改变,需要切换任务时,调用OSIntCtrSw()函数,恢复cpu寄存器。这个过程类似于OS_Sched(),区别在于中断级的切换已经在中断入口保存了寄存器信息,在任务切换时仅需要恢复高优先级的信息。具体保存与恢复需要根据具体的处理器类型进行移植。

OSTaskCreate(TaskStart, (void *)0, &TaskStartStk[TASK_STK_SIZE - 1], 0):创建任务;

每个优先级都对应一个控制块优先级列表OSTCBPrioTbl[],当该优先级没有使用时,该单元存放NULL,通过判断该指针是否为0得知优先级是否被占用。在该系统中,每个优先级只对应一个任务,任务的优先级可能发生改变。若优先级列表指向为0,将非0赋予该指针(指针类型为OS_TCB*),这样该优先级不会被其他任务占用,因此可以在临界区外执行。 创建任务主要包括两个操作,初始化任务栈和初始化任务控制块。

OSTaskStkInit:每个任务对应一个栈,栈的分配可以通过静态或者动态(malloc),动态分配会引起内存碎片问题,这个可以通过内存控制块解决,这将在后面介绍。栈的初始化主要是模拟cpu的各个寄存器状态按照对应顺序放入栈中,因此对不同的处理器会有不同的初始化结果,这些在系统移植时需要解决的。

另外,由于处理器不同,每次入栈出栈时,实际的地址可从高地址到低地址,或相反。以上述函数为例,入栈时,栈顶地址由高到低,则初始化时,栈顶地址为最大值。 OS_TCBInit:任务控制块的初始化。 首先对任务控制块TCB介绍:TCB用来管理一个任务。 TCB数据结构 OSTCBStrPtr 扩展功能 OSTCBExtPtr OSTCBStrBottom OSTCBStrSize OSTCBOpt OSTCBId OSTCBNext OSTCBPrev 事件相关 OSTCBEventPtr OSTCBMsg OSTCBFlagNode OSTCBFlagRdy 栈顶指针,指向堆栈的栈顶地址, TCB扩展指针,用该指针指向数据块,添加额外信息 栈底指针,指向堆栈的栈底地址,和通常的栈底有区别 栈的指针元数目,不是以字节为单位的实际存储容量。 选择项,决定是否检验栈或使栈值清零 保留用于后期扩展 创建成功的任务控制块是以双向链表的形式出现,表头为OS_TCB,该变量实现前后链接 指向事件控制块 在邮箱管理中,指针是传递给需求任务的OSTCBMsg指针 事件标记时,若等待队列,建立节点,并指向该节点的指针 Post事件时,会用到该变量 任务延时节拍数,每个时钟节拍该变量减1 任务状态,有就绪、挂起、事件相关的状态 优先级数值,该值越小优先级越大 OSTCBDly OSTCBStat OSTCBPrio OSTCBX,OSTCBY 任务优先级对应的四个值,在初始化TCB时,通过公式得出,OSTCBBITX,OSTCBBITY 方便高优先级的选择,调度 OSTCBDelReq 通过赋值,请求删除任务

TCB的初始化首先从TCB缓冲区中拿出一个空的控制块,然后根据参数值给控制块各变量赋值,将控制块的指针赋予控制块优先级列表OSTCBPrioTbl[prio],再将该控制块添入双向链表的表头,最后将任务加入就绪表,等待调度。 与OSTaskCreate创建函数类似的有一个OSTaskCreateExt函数可以更灵活的创建函数,上述TCB表中的扩展部分只有在该函数下才能得到使能。利用该函数建立的任务可以进行堆栈检验任务。

堆栈检验函数OSTaskStrChk()能够确定出任务堆栈中已使用的部分与未使用的部分大小,知道这些信息可以避免过多的分配栈空间。当然这种检验应尽量持续较长时间,经历随坏的情况,而且应多出栈容量的10%到甚至100%。

介绍堆栈检验的方法首先要对栈的结构进行说明,在TCB中含有栈顶,栈底,栈大小,及栈选项几个变量。栈顶指针与栈底指针随着栈增长方向的不同在初始的指向不同,上面有所阐述。通常栈的结构中,初始时栈底和栈顶指向相同地址,而任务栈的栈底与栈顶指向栈的两端,指向地址差值为栈的大小。其次检验前要对栈选项进行配置,目的是栈检验的使能以及栈空间统一初始化为(OS_STK*)0。堆栈检验操作就是从栈底开始检查栈值,直到发现不为0的指针项。该计数值就是空闲的栈大小。 OSStart():启动多任务: 由于在进入函数时首先要判断OSRunning,这样确保该函数只能运行一次。该函数主要操作是找到就绪表中优先级最高的任务,然后调用OSStartHighRdy(),这是一个与处理器相关的函数,功能是让任务栈中寄存器信息弹入cpu,然后执行一条中断返回,另外要使变量OSRunning置为True。

当OSStart执行完之后,系统会选择优先级最高的任务,这里必然是自己创建的任务,跳转到该任务后,系统不会再回到OSStart中。

二、 TaskStart函数基本结构

void TaskStart (void *pdata) OS_EXIT_CRITICAL(); {

#if OS_CRITICAL_METHOD == 3 OSStatInit(); OS_CPU_SR cpu_sr; #endif TaskStartCreateTasks();

pdata = pdata; OS_ENTER_CRITICAL();

PC_VectSet(0x08, OSTickISR);

PC_SetTickRate(OS_TICKS_PER_SEC);

for (;;) {

if (PC_GetKey(&key) == TRUE) { if (key == 0x1B) { OSTimeDly (2); PC_DOSReturn(); } } } } OSCtxSwCtr = 0; uC/OS-II系统要求必须首先建立个任务,然后在该任务中建立其他任务,当建立完需要的任务后,可以选择删除TaskStart,也可以保留任务留作他用。这里将该任务用于系统的返回,任务必须是一个无限的循环。

1. 临界段,OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL() 为了避免同时有多个任务或中断进入临界段,需要进行关中断与开中断操作,该指令与具体的处理器类型相关。应当注意,由于关闭中断,系统不会发生调度,如果在临界区内再调用使任务等待或挂起的函数,系统便会无法继续运行。因此,该系统设计的一个普遍原则是,所有的系统函数都是在中断打开的情况下调用的。

实现进出临界区的方法:

1) 利用处理器指令开关中断,

2) 在堆栈中保存中断状态,即将PSW入栈;

3) 将PSW保存在局部变量中,因此这里需要定义一个局部变量,如上述中的

OS_CPU_SR cpu_sr

2. 为时钟节拍函数选择中断向量

这里选择0X08号中断向量,该中断是计算机提供的时钟中断,其频率为18.2048Hz,该频率是定时器自由运行到65536时产生的。通过函数PC_SetTickRate(),可以更改频率值,一般的频率范围为10~100Hz,这里的OS_TICKS_PER_SEC已经在头文件中定义,该值为200。

时钟节拍的重要性,相当于人的心脏,它提供周期性的信号源,频率越高,中断的到达越快,负载越重。当该中断向量到达时,中断服务子程序会调用时钟节拍函数OSTimeTick(),时钟节拍函数主要功能是操作所有的等待任务的TimeDly值,将该值减1,若值等于0则放入就绪表中,在出中断时再调用OSIntExit()便实现了任务的调度。这一操作是系统的最基础操作,而OSTimeTick()函数也因此是最基本最重要的函数之一。

另外OSTimeTick函数还会对变量OSTime函数进行加1,根据设置的频率值,便可计算出系统运行的时间。该变量为32位,对变量的操作涉及两个函数:OStimeGet得到时间和OStimeSet(tick)设置时间信息。

3. 如果启用了统计函数,则要在第一个任务内调用初始化函数;

4. TaskStartCreateTasks为创建函数,在第一个任务内创建需要的任务

5. PC_DOSReturn(),对应main中的PC_DOSSaveReturn(),在退出该系统时,能回到该

点去。

6. OSCtxSwCtr任务切换计数变量,在任务调度时计数值加1,具体实现在OS_Sched()

和OSIntExit中进行。

7. OSTimeDly()延迟函数,也是系统最基本的函数之一,是使任务进入等待的一种方式,

也是最常用的方式,任务调用它将主动的让出cpu,进行等待,只有等到延时结束或者别的任务调用OSTimeDlyResume()时,任务才会回到就绪状态。 该函数会根据参数值进行延时,先将任务在就绪表中对应位清零,然后将延时的信息赋与该任务的控制块的延迟变量。最后进行任务级的调度,即OS_Sched函数。

延迟信息的操作只有通过OSTimeTick函数和OSTimeDlyResume才能进行,前者在每次中断到达时减1,后者直接将对应优先级的任务的延迟清0,在就绪表中置1。

值得注意的是,上述函数参数值是时钟节拍数,不是具体的时间,具体时间的计算要用到频率信息。与该函数类似的函数是OSTimeDlyHMSM(),该函数的参数是现实生活中的时间信息,从左到右对应着时,分,秒,毫秒,函数主要任务是将信息转换为时钟节拍数,进行延时。这里的问题在于时钟节拍最大只能为65535,而转换的时间必然可能会超过该值。解决办法是,将最终的节拍值除以65536,得到余数与商,先延迟余数个节拍,再根据商值循环延迟两个32768个节拍,这里不用65536的值原因是计数值最多为65535。

三、 创建多个任务

在TaskStart中,根据需求可以创建多个任务,任务的创建类似于TaskStart的结构。创建多任务时遇到一个很困难的问题就是如何分配优先级。因为对于一个多任务实时系统,特别是硬实时系统,任务必须要尽快的并且要准时的完成,优先级的不合理分配可能会导致重要任务得不到执行。简单的优先级分配原则可以参照单调速率调度算法RMS(Rate Monotonic Scheduling),RMS定理证明了在静态调度下,该算法具有最有效的分配方法,具体是以任务执行周期为依据,周期越小,或者执行频率越高,则优先级越高。

可参考文章:单调速率调度算法RMS.doc。

任务被创建后, 总是在几种状态间转换,任务的状态有睡眠态,就绪态,运行态,等待态和中断态。各个状态间的关系如下图所示。

睡眠态的任务代码被保留在程序空间中,只有通过任务创建才可以将任务交给系统去管

理。当调用删除函数OSTaskDel()[1]后,系统返回到睡眠态,值得注意的是,删除任务并不删除任务的源代码,任务还可以再次被创建使用。

就绪态的任务已经获得了除cpu资源外的所有资源,等待被调度到运行状态。就绪态的任务都会在就绪表[2]中对应位标记,当发生调度时找到优先级最高的任务运行。任务的调度途径有OSStart,OSched,中断调用。而具体使用的情况有很多,比如创建、删除任务后,延迟恢复,删除信号量,投递信息等都有可能会发生调度。

运行态的任务得到cpu的资源,进行任务处理。对于不可剥夺内核来说,除非任务进入等待态,否则必须在执行完任务后才会有新的任务进入就绪态;而对于可剥夺内核[3],当发生调度或中断时,如果有更高优先级的任务,则当前任务回到就绪态,高优先级任务运行。当中断到达时,运行态进入中断服务态,中断返回时会发生调度,可能会改变当前任务的状态。运行态到等待态的转换发生在任务调用延迟函数[4]、挂起函数或者待决函数时发生的,任务分别等待时间的到达和资源的可用。才外,当运行的任务删除自己时,会使任务回到睡眠态。

等待态的任务在等待时间或事件的到达,等待时间时当延迟结束或取消后,任务回到就绪态,等待事件的任务当通过post投递了事件后或者超时时,会回到就绪态。

中断服务态是指中断[5]发生后且未被屏蔽时,任务进入ISR。 下面将针对上述论述中的个别细节进行介绍,这也相当于对uCOS-II系统内核遗留知识点的补充。

1) 删除任务OSTaskDel,与创建任务相对应,它的操作首先是清除就绪表中对应的位,再

检查是否存在信号量或者事件标记组有待删除,然后对已建立任务的双向链表进行操作,去除该任务项,将该控制块放入TCB缓冲区中。最后进行任务的调度。然而,有时候任务会占用一些内存缓冲区或信号量一类的资源,当删除任务时,占用的资源也将被释放而丢掉。因此在删除任务前有必要清除使用到的资源。uCOS-II系统提供了请求删除任务函数OSTaskDelReq()。实际运行中,该函数需要被调用两次。首先,请求删除任务调用该函数,将要删除的优先级对应的TCB中的OSTaskDelReq变量置为DEL_REQ,然后延迟1个节拍,这个操作循环执行,直到请求的任务被删除后任务才继续向下执行。接下来当要被删除的任务开始执行时,也要调用OSTaskDelReq()函数,返回TCB中对应变量的值,如果发现自己悲剧了,便释放占用的资源、内存,再调用OSTaskDel删除自己。任务删除后,在请求任务的请求删除循环将发生变化,因为在循环调用的OSTaskDelReq 函数中对应的优先级任务已不存在,系统将返回NOT_EXIST,这说明任务已被删除了,而且任务的资源也释放了。任务继续向下执行。由上所述可知,OSTaskDelReq()函数应根据优先级是否是任务本身的而分为两部分。是任务本身的直接返回TCB对应状态,不是本身的,将待删除任务的对应位改变变量,当任务不存在时返回不存在信息。

2) 就绪表:uCOS-II系统对就绪任务的管理是通过就绪表实现的,而这种管理方法可以极

为方面的实现就绪表的写入,删除,以及寻找最高优先级。就绪表有两个变量,一维数组OSRdyGrp[]和二维数组OSRdyTbl[],OSRdyTbl为8*8的数组,每一位代表一个优先级,当有任务就绪时,对应位置位;而当每一行有任何一个任务为1时,OSRdyGrp对应位置1。对应关系如下图所示。考虑任何优先级值的二进制表示,其最低三位即XXX代表在二维数组中对应位的位置,中间三位YYY代表在OSRdyGrp中的位置。在对就绪表进行操作的过程中要随时用到0x00YYYXXX这个特点。

就绪表操作 进入就绪态 脱离就绪态 需找最高优先级 系统的具体实现 OSRdyGrp |= OSMapTbl[prio>>3] OSRdyTbl [prio>>3] |= OSMapTbl[prio&0x07] If((OSRdyTbl[prio>>3] &= ~OSMapTbl[prio&0x07])==0) OSRdyGrp &= ~OSMapTbl[prio>>3] Y = OSUnMapTbl[OSRdyGrp] X= OSUnMapTbl[OSRdyTbl[y]] Prio = y<<3 +x 说明 OSRdyGrp置位 OSRdyTbl置位 OSRdyTbl相应位清零并判断改行是否全0 将OSRdyGrp清0 找OSRdyGrp为1的最低位 找OSRdyTbl为1的最低位 y*8+x得出最高优先级 其中,OSMapTbl[]是为了方便对应的位置位而定义的数组,其每个位对应一个位掩码,0对应着二进制00000001,1对应着00000010…依次类推。根据优先级的XXX与YYY能找到优先级在数组中的位置。在脱离就绪态时,对得到的掩码取反能够使对应位清0。

OSUnMapTbl是为了方便找到最低的优先级数值而设计的表,它实现的功能是对输入的任一的8位二进制数找出最低位为1的位置。该查询表有256项,对应着0x00到0xFF这256项最低位为1的位置,从0开始计数。例如对于0x76,查询值为1。通过对OSRdyGrp查表,可以找到YYY的最低位,通过对YYY所对应行的数值进行查表,可以找到XXX最低位,通过8*y+x得到最高优先级。 3) 内核可以分为可剥夺内核和不可剥夺内核,不可剥夺调度也被称为合作式调度,它要求

任务主动让出cpu,这种调度的缺点是响应时间问题,当高优先级任务就绪后可能需要等很长时间才得以运行。可剥夺内核当优先级高的任务就绪后,cpu使用权将被剥夺。uCOS-II系统支持可剥夺内核,但通过对调度器上锁可以禁止系统的自由调度,上锁和解锁函数分别为OSSchedLock()和OSSchedUnLock(),二者应成对出现,上锁操作是通过对上锁标记OSLockNesting加1,解锁是对应的将变量值减1。由于该函数的使用将使系统无法正常调度任务,所以应谨慎使用,尽量避免使用。 4) 上文已经对延时有所介绍,这里只是对延时节拍进行说明,延迟一个节拍并不意味着延

迟一个节拍的时间,根据情况的不同,延迟的时间可能会大于或者小于设置的时间间隔,如下图所示,时钟节拍设置为20ms,当中断服务时间以及高优先级任务的运行时间较长时,延时的节拍甚至会占到很小的比重,因此如果至少要延迟一个节拍的时间,应当把延迟节拍设为2,以此类推。对于一个好的实时内核,过大的延迟节拍偏差是不希望有的。

5) 与第四点类似,这里将主要对中断的时间进行介绍,而且是对可剥夺内核的。

中断延迟 = 关中断的最长时间 + 开始执行ISR第一条指令的时间;

中断响应 = 中断延迟 + 保存cpu寄存器时间 + 内核进入ISR的执行时间(可剥夺) 中断恢复时间 = 判断是否有优先级高的任务进入就绪态时间 +

恢复高优先级任务cpu寄存器时间 + 执行中断返回指令时间;

6) 内存管理

这一节是最后附加的,本打算放在事件标记组后做论述,但是内存管理在任务间的同步与通讯中的作用并不是必须的,因此把它放在系统的补充中说明。同时间管理,任务管理一起,内存管理也作为一个分支介绍。

一般在对系统的动态分配是调用malloc和free函数,但是由于分配内存的地址和大小是随机的,释放的内存地址也就有很大可能不会连接在一起,这样便会造成内存碎片,如果再有任务申请较大的内存块,就会造成问题。为了解决该问题,系统通过对内存分块的管理,每次申请内存块时,在已建立的内存块中取出一块,使用完放回内存分区中。另外可以建立不同的分区,每个分区中内存块的大小相同,但是不同分区含有不同的大小。

内存的管理也是通过内存控制块,由内存控制块建立一个内存分区,在这个内存分区中将内存分为大小相同,确定个数的块,这一小块就是一个单元,分配内存块时,就是取这一个小的单元。因此这里需要区分,内存控制块,内存分区,内存块。

MCB内存控制块结构 OSMenAddr OSMemFreeList OSMemBlkSize OSMemNBlks OSMemNFree

内存分区起始地址指针 指向链表中下一个空余内存块 MCB控制的内存分区中每个内存块的大小 内存分区的数量 内存分区中可用的空余内存块的数量

内存控制块的管理同任务控制块一样,是一个链表结构,当建立一个内存分区时,会从链表中取出一个块,进行初始化等;内存分区中各个内存块的管理也是一个链表结构,这个链表是通过每个块的第一个地址中的指针起链接作用的。

建立一个内存分区,需要静态的申请一段地址空间,将这段内存空间的首地址作为参数传递,将首地址传递给MCB的MemAddr变量,已该首地址开始建立内存块,内存块的大小是通过参数设定的。在每个块的第一个地址内存放下一个内存块的地址,这段操作可通过下述的程序代码实现。

INT8U *pblk; void **plink; // 给连续的一段内存区间截断成一些内存块,内存块为单向链表,其中plink类型与pnext

plink = (void **)addr; pblk = (INT8U *)addr + blksize; for (i = 0; i < (nblks - 1); i++) { *plink = (void *)pblk; plink = (void **)pblk; pblk = pblk + blksize; }

建立一个内存块是通过OSMemGet()函数实现的,将内存块分配一个出来,将下一个空余的内存块地址传递给MCB的FreeList指针,将未使用的内存块数量减1。

释放一个内存块OSMemPut()将内存块插入到链表的表头位置,这些操作可以参考数据结构中对链表的操作,其中把FreeList作为表头。

查询一个内存分区的状态,是通过OSMemQuery()函数,将MCB的信息传递给变量OS_MEN_DATA,对该数据结构的调用得到最终的各变量值。

四、 多个任务间的协作

上文已经讲述了如何创建多任务,如何实现任务的调度,实现时间的管理等。而多个任务间并不是单一的,他们之间存在着交集,这种相互之间的联系是通过对数据的控制实现的。具体到uCOS-II系统,是利用了信号量来实现任务间的联系,这种联系主要是同步与通信。

两个任务间的同步是靠信号量与互斥信号量实现的,多个任务间的同步则需要用到事件标记组,此外对共享资源的使用也需要信号量这把钥匙。任务间信息的传递主要靠邮箱和队列信号量,或称为消息邮箱和消息队列,二者通过传递指针来传递数据。当然sem信号量也可以用来传递单个数据。

同任务的管理一样,信号量的管理有类似的控制块,即事件控制块ECB。这里信号被看做是一个事件,事件标记组也算是一种事件,但是它不会用到该控制块。

ECB数据结构 OSEventType OSEventCnt OSEventPtr OSEventGrp

信号量的类型 同步时信号量需要使用的变量,作用不一 通信时需要使用的量,指针类型,也作为链表的指针 任务管理的一维数组,同OSRdyGrp

OSEventTbl[] 同OSRdyTbl[] 信号量的类型有四种,分别为着sem信号量,互斥信号量,邮箱信号量和队列信号量。 ECB中对任务的管理功能类似于就绪表,当有等待该事件块的任务时,任务优先级相应位置位,这样当有资源释放时,可以快速的找到等待列表中优先级最高的任务号。在就绪表的描述中已经对如何寻找高优先级的任务做了详细的说明,这里不再阐述。 在系统的初始化时,对ECB建立了空闲事件控制块缓冲区。当建立一个事件时,从链表中拿出一个空闲块,对该块进行初始化。初始化任务是在各个信号量建立的过程中实现的,主要是对几个变量赋初值。对于各个事件控制块的任务表的初始化有专门的函数可用来调用,即OS_EventWaitListInit(),通过调用该函数,等待事件的任务清零。为了减小开销,其中对OSEventTbl[]的操作没有使用for循环清零64位,而是使用了条件编译,每次清零一组8位。 当调用了pend函数时而没有可用的资源时,系统将让该任务进入等待状态,在信号量的使用中通过调用OS_EventTaskWait()函数使任务进入等待状态,该函数的主要操作是使就绪表中该任务的优先级清零,使事件等待列表中该任务置位。另外,要将该事件控制块的指针传递给任务控制块对应的事件控制块指针,建立任务与事件控制块之间的链接。 当调用了post函数时,资源得到释放,如果有任务在等待该资源,系统会调用OS_EventTakRdy(),该函数的操作是从等待表中找出优先级最高的任务,将该任务在等待表中清零,将TCB中事件指针变量清零,解除关系。然后判断该任务的状态值,将状态变量与相对应的信号量取反再做与操作,判断该值是否为OS_STAT_RDY状态,若是,则将就绪表中的位置位。 另外在对ECB的操作中,还存在一种现象,即任务等待超时现象。此时,应强制将任务返回到就绪态,然后发布错误信号。该系统中可以调用OS_EventTO()函数实现超时处理功能。其主要操作是清除等待表,解除二者。因为任务超时是在时钟节拍中断中已延时了指定的节拍,任务已经被放入就绪表了,这里不比重复操作。 信号量的操作有6种,分别为Create,Delete,Pend,Post,Accept(无等待的申请信号量),Query。其中Create,Delete, Pend函数不可由中断调用 下面将对sem信号量进行详细的介绍,再对其他几种信号量的不同点进行阐述。

1. Sem信号量

在使用信号量时,首先要创建一个信号量,这步操作基本上在主函数中,需要使用到OSSemCreate()函数,该函数的主要操作是先从空余ECB中取出一个ECB,然后根据参数对ECB初始化,包括初始化Type和Cnt变量,Type指定为TYPE_SEM。当信号量用于同步作用时,Cnt的值设为1,当用于共享资源的使用时,Cnt的值设为资源的个数。然后调用ECB初始化函数,将任务等待表初始化。 当一个任务需要得到同步信息或者需要使用资源时,便会请求信号,该过程是通过调用OSSemPend()函数实现的。如果Cnt>0,表示有资源,将资源个数减1,返回到调用的函数;如果Cnt=0表示没有资源,此时该任务将被挂起等待资源,挂起操作是标记任务热Stat为SEM型,设置延迟变量值为指定的参数,调用任务进入等待函数EventTaskWait(),任务挂起后,自然要调用OS_Sched()函数进行调度,该任务将停留在此位置,知道有资源到来或者延时超时。当该任务重新就绪后,通过对Stat变量判断,得出是否超时,是则调用EventTO()函数,否则表示成功使用了资源,解除任务与事件的关系。继续执行该任务。 有任务接收信号,就必然有任务给它发送信号,发送信号的函数为OSSemPost(),该函数的操作也分为两种情况。当有任务在等待资源时,即OSEventGrp不为0,通过事件就绪函数EventTaskRdy(),将资源给优先级最高的任务。为了避免高优先级任务就绪等待,调用一次Sched函数调度。当没有资源使用时,简单的将Cnt变量加1。值得说明的是,post函

数可以在中断中调用,由于中断的调度发生在最后一层出中断的OSInitExit()函数。 Pend函数无法在中断中调用,因为该函数会发生等待。该系统还提供一个无等待的信号量请求,调用该函数时,如果有资源,使用资源,如果没有资源则返回0。 查询函数Query用于对当前信号量使用状况的查询,该函数会将ECB的状态复制到一个SEM_DATA型变量,通过对该变量即可。 当信号量使用完,不再使用时,需要删除信号量。因为删除信号量可能影响到调用该资源的任务,因此最好在检查没有任务调用时再删除。函数OSSemDel分两种情况删除信号量,一种是等待删除,即当无任务等待时删除,有任务等待时就等待,发出任务等待错误信号。一种是直接删除,不管有无任务等待,删除该事件。

2. 互斥信号量

假设有三个任务ABC,任务优先级依次降低,任务A与C共享资源,此时任务C在使用资源。由于A与B的优先级高,当发生调度时,A与B会依次进入运行态,等A,B运行结束C才可以运行。但是由于A要使用的资源被C占用,则A需要等待C释放资源。这样就造成了A等待C,C等待B,间接的A要等待B,这样便造成了优先级反转问题。

高优先级等待低优先级是不希望发生的,uCOS-II提供互斥信号量(mutex,二值信号量)来解决该问题。它的主要思想是在建立信号量时提供了一个未使用的较高优先级,如果有一个高优先级任务需要使用资源,而资源被低优先级占用时,就提高低优先级任务的优先级,先执行任务释放资源。 互斥信号量与sem不同的是,它不需要记录资源可用个数,只需要记录申请的高优先级与在使用资源的任务的优先级。优先级记录在16位变量Cnt中,该变量前8位为申请的优先级,后8位为使用资源的优先级,如果低8位为0,则表示资源可用。 创建任务时,与Sem不同的是要申请一个优先级,将该优先级标记为已使用。在变量赋值时,将Cnt高8为置位优先级值。 任务pend时,检查cnt低8位,没有被优先级占用则将该任务的优先级赋值,return继续执行任务。当低8位被任务占用时,检查当前任务优先级与占用任务优先级的大小,只有申请资源的任务优先级高才会改变占用资源的任务优先级。过程是先清除就绪表中对应位标志,在对应高优先级的位置置位,将TCB传递给新优先级的PrioTbl,建立优先级与TCB之间的联系。再将等待任务的状态和延迟变量赋值,等待列表置位。进行任务调度即回执行高优先级的任务。当资源释放后的操作同Sem信号量。 任务post时,通过检查当前任务优先级与申请占用的高优先级值对比。若想等,即提高了任务优先级,这时将进行与pend相反的操作,高优先级值清零,低优先级置位,TCB传递给低优先级的PrioTbl。然后判断是否有等待任务,若没有,将Cnt低8位清零;若有,将任务的信息传递给事件标记组。 其他的函数操作类似于Sem信号量,不做论述。

3. 消息邮箱

当进行任务通信过程中,需要将数据在任务间沟通,数据的传递是通过邮箱和队列实现的。邮箱传递一个数据指针,队列可以传递多个指针。

邮箱的操作类似于信号量,只是它无需对Cnt变量进行操作,而是利用EventPtr传递数据指针。这里重点介绍pend与post过程。

Pend时,检查ECB的指针,如果有数据则使用数据,将指针清零,返回任务继续执行。

如果发现指针为0,任务就需要设置状态、延迟变量,任务等待,进行调度。调度后任务在该任务就停留在此,直到该任务重新被运行,才会继续执行。接下来,第一步是检查TCB的存放信息指针的变量,因为信息传递时指针并没有直接给ECB的指针,而是赋给了TCB中的指针。检查该变量的结果,若为0,说明是由于超时才回到了本任务,调用超时函数,返回超时标志。若不为0,则使用资源,将Stat重新Rdy,将该任务的TCB的信息指针项清零,以便下次使用。另外要解除任务与事件的关系,因为已使用完资源。

Post时,若没有任务等待,将信息指针放到ECB的指针,供其他任务使用事件时直接调用。若有任务在等待,就根据等待的优先级将数据指针放到优先级对应的任务的TCB的信息指针上。执行OS_Sched(),让高优先级任务可以调度执行。另外系统还提供了PostOpt函数,这个函数兼容了Post函数,另外还提供了广播选项,如果使能该项,将使所有等待该事件的任务就绪。

4. 消息队列

相对于邮箱用来传递一个数据指针,消息队列可以传递多个指针,这些指针排成一个循环队列,循环队列是通过数据结构体实现的,这个数据结构即为队列控制块。这里先对该控制块进行介绍。

QCB数据结构 OSQPtr OSQStart OSQSize OSQOut OSQIn OSQEnd OSQEntries 空余QCB的链表节点指针,在队列操作不起作用 指向循环队列的实际起始地址 循环队列可容纳的总容量 指向循环队列中将要取出的数据指针的位置 指向循环队列下一个要存储数据指针的位置 指向循环队列结束单元的下一个地址 循环队列中当前存在的消息指针总数 消息队列的创建,队列控制块的使用类似于TCB、ECB,在创建的时候从空余的QCB链表中取出一个块,根据创建时传递的起始位置和队列大小参数对块初始化。初始化过程比较简单,因为块内存没有被占用,所有指针类型都指向起始位置。不过要说明的是变量的类型,由于指针变量指向的地址内存放的仍然是指针,因此所有使用的指针都应是指针型的指针变量。初始化后,对ECB进行初始化,需要将建立好的QCB传递给Ptr指针,建立ECB与QCB间的联系。 Pend时,如果通过OSQEntries变量判断指针数>0,就利用OSQOut取出一个指针,给任务使用,对统计变量减1,out变量指向下一个,若此时判断out==end,则out=start,达到循环效果。如果没有可用资源,将任务进入等待,进行调度。后面的操作同邮箱信号量的pend操作相同。 Post时,基本与邮箱信号量相同,只是多了对循环队列的操作,这个操作体现在没有任务在等待资源的情况下,将Qin指针指向下一个地址,将统计变量加1,判断越界等等。此外,系统还提供了postfront函数,这个函数可以实现队列的先进后出,类似栈的功能。与post不同的是,在给队列增加一个数据指针时,是将指针添加到out所指向的地址前一个,即QOut--。当然还存在一个综合型的OSQPostOpt()函数,不多做阐述。 对队列的操作还有一个函数可以使用,即清空消息队列OSQFlush(),这个函数可以清空指定事件快指向的队列以重新开始使用。为了节省时间,并没有给每一项清零,只是对指针初始化到起始位置。对已使用容量清零。

5. 事件标记组

上述对信号量进行了大致的介绍,与信号量类似的,可还以进行任务同步的事件还有事件标记组。与信号量作为一对一的任务同步不同,事件标记组可以作为一对多或者多对一的同步。当任务进行等待事件时,一个任务post资源可能不会使该任务就绪,但是会保存任务的标记,当几个任务都到来后标记的位满足了等待任务的要求时,就会让等待任务重新运行,这是一个任务等待多任务资源的情况。另外与信号量不同的是,事件标记组当有任务post资源时,它会检查所有等待该事件的任务,会使满足条件的所有任务都进入就绪表,这是一个任务资源使多个任务就绪的情况。这些特点也正是要使用事件标记组的原因吧。

事件标记组的实现不会涉及到事件控制块ECB,它会有自己的事件标记组结构。它的结构简单又复杂,简单是创建一个事件标记组变量,这个变量结构相对简单;复杂是因为当有任务等待该事件时,在实标记组后续的节点结构相对复杂。这里对标记组结构和标记组后续的节点数据结构进行详细说明,因为这些是在学习过程中比较糊涂的地方。

事件标记组 数据结构 (OS_FLAG_GRP) OSFlagType OSFlagWaitList OSFlagFlags

事件等待节点 数据结构 (OS_FLAG_NODE) OSFlagNodeNext OSFlagNodePrev OSFlagNodeTCB OSFlagNodeFlagGrp OSFlagNodeFlags 双向链表指向下一个结点 双向链表指向前一个结点 结点指向等待的任务控制块 反向指向事件标记组的指针 表明任务在等待标记的位 结构类型,检查是否为flag结构 指向后续的等待该事件的节点,后续为双向链表结构 当前事件标记组的状态位,可以为8,16或32位 OSFlagNodeWaitType 指明等待事件标记的类型 事件标记的等待类型有四种,其中SET和CLEAR型功能实际相同,这里仅以一种类型来说明,SET_ALL是指任务等待的所有事件标记都满足时才可以,SET_ANY是指任务等待的事件标记只要有一个满足情况即可。下面在每个函数的分类中也主要已置位为准。 创建事件标记组,就是从空余的标记组缓冲区中取出一个FCB,再按照参数值给该块附上初值。 Pend时,如果任务需要的事件标记和FCB中的Flag相同,就将事件标记已使用的部分清除,返回FCB修改后的标记值。这部分的实现是通过位操作完成的,对于SET_ALL操作,将需要的事件标记与FCB中的标记相与,若等于需要的事件标记,即表明对应的位均为1,使用完后,再将FCB中标记位与需要的标记取反相与,将使用的1清零。对于SET_ANY位,只是需要判断有一个位满足为1即可,操作类似。 上述是满足情况时,对于不满足情况的时候,就需要任务进行等待,即对任务建立对应的事件等待节点,这一部分是通过函数OS_FlagBlock()完成的。这个函数通过传递的参数,将建立的节点对应的变量赋值,然后将该节点加到事件标记组的后续等待链表中,这个过程涉及到链表操作,不做阐述。最后将等待的任务在就绪表中的位置清零。

设置完等待后,将进行调度,接下来的操作类似于信号量的操作。不同的地方为,在超时操作中,需要调用OS_FlagUnLink()函数将等待节点清除掉。在得到资源时,还要判断是否将该资源已使用的位清零,清零的使能位是在pend函数的操作类型中设置的,如果参数传递加了OS_FLAG_CONSUME项,在后续操作将清零已消费的位。 Post时,函数的操作与信号量的操作不相同,首先根据类型,将事件标记与事件标记组的flag相与或相或。再将这个结果用于接下来的等待任务。后面利用一个while循环,将等待链表的每一块都进行检查,发现是否有满足的任务,如果满足,就调用OS_FlagTaskRdy()函数。这个函数的操作是将任务就绪,设置就绪变量,就绪表置位。再调用OS_FlagUnLink()函数,这个函数的操作实际上是清除双向链表中指定的节点,删除工作分为四种情况,具体在这里不做阐述。 其他的对于删除、无等待的获得事件和查询事件标记的函数同信号量类似,这里不在论述。

五、 系统移植

7.1.1 移植条件移植µC/OS-II到处理器上必须满足以下条件(1)处理器的C编译器能产生可重入代码编译器还得支持,MDK开发环境,可生成可重入代码µC/OS是多任务内核,函数可能会被多个任务调用,代码的重入性是保证完成多任务的基础。可重入代码指的是可被多个体任务同时调用,而不会破坏数据的一段代码,或者说代码具有在执行过程中打断后再次被调用的能力。举例说明:Swap1函数代码:举例说明:Swap2函数代码:Int temp;不可重入void swap1(int *x,int *y){temp=*x;*x=*y;可重入void swap2(int *x,int *y){int temp;temp=*x;*x=*y;*y=temp;}8/17/20101*y=temp;}作者:四川师范大学成都学院屈召贵QQ:35247485

(2)用C语言可打开和关闭中断PRIMASKARM处理器核包含一个CPSR寄存器,该寄存器包括一个全局的中断禁止位,控制它便可打开和关闭中断。(3)处理器支持中断并且能产生定时中断µC/OS-II通过处理器产生的定时器中断来实现多任务之间的调度。ARM Cortex-M3的处理器都支持中断并能产生定时器中断,专门有一个SysTick定时器来实现。(4)处理器支持能够容纳一定量数据的硬件堆栈(通常需要几十KByte字节)比如AT98C51处理器,内部只有128字节的RAM,要运行,需外扩RAM。CM3的芯片,内部可多达128KByte的容量,因此可直接使用。运行TCP、UDP需要的内存会更大,通常要100K左右(5)处理器有将堆栈指针和其他CPU寄存器读出和存储到堆栈(或内存)的指令µC/OS-II进行任务调度时,会把当前任务的CPU寄存器存到此任务的堆栈中,然后,再从另一个任务的堆栈中恢复原来的工作寄存器,继续运行另一个任务。所以,寄存器的入栈和出栈是µC/OS-II多任务调度的基础。8/17/20109作者:四川师范大学成都学院屈召贵QQ:35247485

因篇幅问题不能全部显示,请点此查看更多更全内容