(1)任务控制块 空任务控制块链表OS_init()/任务控制块链表OSTaskCreat() OS_TCB类型的变量OSTCBCur,用来存放正在运行任务的任务控制块指针。 OSTCBPrioTbl[]任务控制块优先级表(任务调度中使用) (2)任务就绪表OSRdyTbl[ ]和OSRdyGrp(记录OSRdyTbl中哪个任务组有任务就绪,最多64个任务可以管理) 负责任务调度。这个位置的状态(1或0)来表示任务是否处于就绪状态,1表示就绪;0表示非就绪。 (3)调度器(Scheduler) 任务级的调度是由函数OSSched()完成的。 中断级的调度是由另一个函数OSIntExt()完成的. 由于被中止的任务的指针保存在OS_TCB类型中的OSTCBCur变量中,所以调度器的主要任务就是找到就绪态任务的指针。 OSLockNesting来记录上锁的次数,上锁+1 解锁-1 需要由宏OS_TASK_SW( ) 来引发一次中断或者一次调用来使OSCtxSw( )执行任务切换工作 (4)中断 需要由宏OS_TASK_SW( ) 来引发一次中断或者一次调用来使OSCtxSw( )执行任务切换工作 (4)事件控制块 (5)等待任务列表 OSEventTbl[] 和 OSEventGrp OSEventTbl[] 和 OSEventGrp 很像前面讲到的OSRdyTbl[]和OSRdyGrp,只不过前两者包含的是等待某事件的任务,而后两者包含的是系统中处于就绪状态的任务。 typedef struct { void *OSEventPtr; /* 指向消息或者消息队列的指针 */ INT8U OSEventTbl[OS_EVENT_TBL_SIZE]; /* 等待任务列表*/ INT16U OSEventCnt; /* 计数器(当事件是信号量时) */ INT8U OSEventType; /* 事件类型 */ INT8U OSEventGrp; /* 等待任务所在的组 */ } OS_EVENT; (5)OS_FLAG_GRP来描述信号量集 任务控制块 功能:记录任务的堆栈指针、任务的当前状态、任务的优先级别等一些与任务管理有关的属性。同时OS_TCB负责把代码和任务堆栈进行关联,从而使任务控制块、任务代码和任务堆栈成一个整体。 结构 任务控制块是一个结构类型数据。当用户应程调用OSTaskCreate()函数创建一个用户任务时,这个函数会对任务控制块中的所有成员赋予与该任务相关的数据,并驻留在RAM中。(OSTCBInit()) typedef struct os_tcb { OS_STK *OSTCBStkPtr;//任务控制块指针 #if OS_TASK_CREATE_EXT_EN //一个任务创建使能 void *OSTCBExtPtr; OS_STK *OSTCBStkBottom; INT32U OSTCBStkSize; INT16U OSTCBOpt; INT16U OSTCBId; #endif //OS_MAX_QS 系统中最大消息队列数 struct os_tcb *OSTCBNext; //指向后一个任务控制块的指针 struct os_tcb *OSTCBPrev; //指向前一个任务控制块的指针 #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; //指向传递给任务消息的指针 //OSTCBEventPtr和OSTCBMsg分别表示事件控制块的指针和传给任务的消息的指针。 #endif INT16U OSTCBDly; INT8U OSTCBStat; INT8U OSTCBPrio; INT8U OSTCBX; //用于快速访问就绪表的数据 INT8U OSTCBY; INT8U OSTCBBitX; INT8U OSTCBBitY; #if OS_TASK_DEL_EN BOOLEAN OSTCBDelReq; //请求删除任务时用到的标志 #endif } OS_TCB;
OSTCBStat用来存放任务的当前状态: OS_STAT_RDY 就绪态 OS_STAT_SEM 等待信号量的状态 OS_STAT_MBOX 等待消息邮箱状态 OS_STAT_Q 等待消息队列状态 OS_STAT_SUSPEND 挂起状态 OS_STAT_MUTEX 等待互斥型信号量状态 任务控制块链表(两条) 空任务块链表(其中所有任务控制块还没有分配给任务);OS_init()初始化建立。 任务块链表(已经分配)OSTaskCreat()初始化建立 (1)空任务块链表 调用OSinit即初始化空链表 此链表是在应用程序调用函数OSInit()对UC/OS-II系统进行初始化时建立的。 系统在调用函数OSInit()函数OSInit()时,先在RAM中建立一个OS_TCB结构类型的数组OSTCBTbl[],这样每个数组元素就是一个任务控制块,然后把这些控制块链接成一个链表,此链表中的任务控制块还没有与具体任务相关联。
上图可以看到UCOS初始化时建立的空任务链表的元素一共是(OS_MAX_TASKS+OS_N_SYS_TASKS)个,也就是有这么多个任务块。 OS_MAX_TASKS(OS_CFG.H中) 表示用户任务的最大数目; OS_N_SYS_TASKS(UCOS_II.H中)表示系统任务的数目,一般为2,也就是两个系统任务(空闲和统计)。
(2)任务控制块链表 任务块链表是在调用OSTaskCreate()创建任务时建立的。建立任务控制块链表的具体做法是:从空任务控制块链表摘取一个空任务控制块,然后填充上任务属性后,再形成新的链表。 每当应用程序调用OSTaskCreate()和OSTaskCreateExt()创建一个新任务时,系统就将空任务控制块链表头指针OSTCBFreeList指向的任务控制块分配给该任务。在给任务控制块中的个成员赋值后,就按任务控制块链表的头指针OSTCBList将其加入到任务控制块链表中。
其他说明: 为了UC/OS能随时访问正在运行任务的任务控制块,定义了一个 OS_TCB类型的变量OSTCBCur,用来存放正在运行任务的任务控制块指针。 用函数OSTaskDel()删除一个任务,其实质就是把该任务的任务控制块从链表中删掉,并把它归还给空任务控制块链表
任务控制块的初始化: 当应用程序调用OSTaskCreate()创建一个任务时,这个函数会调用系统函数OSTCBInit()来为任务控制块进行初始化。这个函数首先被创建任务从空任务控制块链表获取一个任务控制块;然后用任务的属性对任务控制块各个成员进行赋值;最后把这个任务控制块链入到任务控制块链表的头部。 从空任务控制块链表中取得任务控制块,在加入到任务控制块链表中 OSTCBInit()的原型: INT8U OSTCBInit( INT8U prio, //优先级别 OS_STK *pros, //堆栈栈顶指针 OS_STK *pbos, //堆栈栈低指针 INT16U id, //表示符 INT16U stk_size, //堆栈的长度 void *pext, //任务控制块的扩展指针 INT16U opt //任务控制块的选择项 ) 四、任务就绪表 任务调度的依据是任务就绪表!(重要性) 定义: UC/OS-II在RAM中设立了一个记录表,系统中的每个任务都在这个表中占据一个位置,并用这个位置的状态(1或0)来表示任务是否处于就绪状态。这个表就叫任务就绪状态表。 任务就绪表 用一个类型为INT8U的数组OSRdyTbl[ ]来充当任务就绪表。由于每个任务的就绪状态只占据一位, OSRdyTbl[ ]数组的一个元素就可以表达8个任务的就绪状态(1表示就绪;0表示非就绪)。 为了便于对就绪表的查找,UC/OS-II又定义了一个数据类型为INT8U的变量OSRdyGrp,并使该变量的每一个位都对应OSRdyTbl[ ]的一个任务组,即数组的一个元素。如果某任务组中有任务就绪,就在变量OSRdyGrp里把该任务组所对应的位置1;否则置0。 Eg: OSRdyGrp=11100101 表示OSRdyTbl[0]、[2]、[5]、[6]、[7]任务组 中有任务就绪。 由于变量OSRdyGrp有8个二进制位,每位对应OSRdyTbl[]数组的一个元素,而每个元素又可以记录8个任务的就绪状态,因此最多可以管理64个任务 为加快访问任务就绪表的速度,系统定义了一个变量OSRdyGrp来表明就绪表,每行中是否存在就绪任务 如何根据任务的优先级来查找任务在就绪表中的位置 把优先级别看成一个6位的二进制数,用它的高3位指明变量OSRdyGrp的具体数据,确定数组元素;用它的低三位指明该数组元素中的具体数据位。 举例: 优先级为30的一个就绪任务,判断它在就绪表的那个位置置1. 对任务就绪表的操作 找出进入就绪态的优先级最高的任务 y = OSUnMapTbl[ OSRdyGrp ]; x = OSUnMapTbl[ OSRdyTbl[y] ]; prio = (y << 3) + x; 例如,如果OSRdyGrp的值为二进制01101000,查OSUnMapTbl[OSRdyGrp]得到的值是3,它相应于OSRdyGrp中的第3位bit3,这里假设最右边的一位是第0位bit0。类似地,如果OSRdyTbl[3]的值是二进制11100100,则OSUnMapTbl[OSRdyTbl[3]]的值是2,即第2位。于是任务的优先级Prio就等于26(3*8+2)。利用这个优先级的值。查任务控制块优先级表OSTCBPrioTbl[],得到指向相应任务的任务控制块OS_TCB的工作就完成了。 五、任务的调度 μC/OS-Ⅱ总是运行进入就绪态任务中优先级最高的那一个。确定哪个任务优先级最高,下面该哪个任务运行了的工作是由调度器(Scheduler)完成的。 任务调度器的主要工作: (1)在就绪表中查找具有最高优先级别的就绪任务。 (2)实现任务的切换。 任务级的调度是由函数OSSched()完成的。 中断级的调度是由另一个函数OSIntExt()完成的. 任务切换步骤: 获得待运行就绪任务控制块的指针 调度器实施任务切换之前的主要工作时:获得待运行任务的TCB指针和当前任务的TCB指针。因为被中止任务的任务控制块指针存放在全局变量OSTCBCur(存放正在运行的任务的任务卡的指针)中。此变量里存储的就是当前运行任务的任务控制块指针。所以调度器这块的工作主要就是获得待运行任务的TCB指针。 下面是任务级调度器函数OSSched()源代码 void OSSched (void) { INT8U y; OS_ENTER_CRITICAL(); if ((OSLockNesting | OSIntNesting) == 0)//没有上锁或者中断嵌套 { y = OSUnMapTbl[OSRdyGrp]; OSPrioHighRdy=(INT8U)((y<<3)+OSUnMapTbl[OSRdyTbl[y]]); //获取当前最优先级的指针 if (OSPrioHighRdy != OSPrioCur) //不是当前的任务 { OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; OSCtxSwCtr++; //用OSPrioHighRdy 作为下标去访问数组OSTCBPrioTbl[ ](该数组中存的是任务控制块的指针)。把数组元素OSTCBPrioTbl [OSPrioHighRdy ]的值赋给指针变量 OSTCBHighRdy。
OS_TASK_SW(); //任务切换宏 } } OS_EXIT_CRITICAL(); } 对任务级调度器OSSched()的解释 补充:应用程序调用函数OSSchedLock()和 OSSchedUnLock()给调度器上锁和解锁。为了记录调度器被锁和解锁的情况,系统定义了一个变量OSLockNesting来记录,调度器每被上锁一次,它就加1;反之,调度器每被解锁一次,它就减1。 第一个IF条件:如果在中断服务子程序中调用OSSched(),此时中断嵌套层数OSIntNesting>0,或者由于用户至少调用了一次给任务调度上锁函数OSSchedLock(),使OSLockNesting>0。 如果不是在中断服务子程序调用OSSched(),并且任务调度是允许的,即没有上锁,则任务调度函数将找出那个进入就绪态且优先级最高的任务(if内部代码) 具体过程详述: 调度器OSSched()在确定未被上锁并且不是中断服务程序调用调度器的情况下,首先从任务就绪表中查得的最高优先级别就绪任务的优先级别OSPrioHighRdy ;然后在确认这个就绪任务不是当前正在运行的任务(和OSPrioCur做比较)的条件下,用OSPrioHighRdy 作为下标去访问数组OSTCBPrioTbl[ ](该数组中存的是任务控制块的指针)。把数组元素OSTCBPrioTbl [OSPrioHighRdy ]的值赋给指针变量 OSTCBHighRdy。 任务切换宏OS_TASK_SW() 需要由宏OS_TASK_SW( ) 来引发一次中断或者一次调用来使OSCtxSw( )执行任务切换工作 为了了解调度器是如何进行任务切换的,先来探讨一下一个被中止运行的 任务,将来又要“无缝”的回复运行应该满足的条件。
如果把任务被中止运行时的位置叫做断点,把当时存放在CPU的PC、PSW和通用寄存器等各寄存器中的数据叫做断点数据。那么当任务恢复运行时,必须在断点处以断点数据作为初始数据接着运行,才能实现“无缝”的恢复运行。因此,要实现这种无缝的接续运行,则必须在任务被中止时就把该任务的断点数据保存到堆栈中;而在被重新运行时,则要把堆栈中的这些断点数据再恢复到CPU的各寄存器中,只有这样才能使被中止运行的任务在恢复运行时实现无缝的接续运行。 一个被中止的任务能否正确地在断点处恢复运行,其关键在于能否正确地在CPU各寄存器中恢复断点数据;而能正确恢复断点数据的关键是CPU的堆栈指针SP是否有正确的指向。由此可推出在系统中存在多个任务时,如果在恢复断点数据时用另一个任务的任务堆栈指针来改变CPU的堆栈指针SP,那么CPU运行的就不是刚才被中止运行的任务,而是另一个任务了,也就是实现了任务切换。当然,为了防止被中止的任务堆栈指针的丢失,被中止任务在保存断点时,要把当时CPU的SP值保存到该任务控制块的成员OSTCBStkPtr(任务控制块指针)中。
综上所述 任务的切换就是断点数据的切换,断点数据的切换也就是CPU堆栈指针的切换,被中止运行任务的任务堆栈指针要保存到该任务的任务控制块中。待运行任务的任务堆栈指针要由该任务控制块转存到CPU的SP中。 Void OSCtxSw(void)//任务切换函数 { 将R1,R2,R3及R4推入当前堆栈 //正在运行的任务块指针-》任务块指针 OSTCBCur->OSTCBStkPtr = SP;//把SP保存在中止任务控制块中 当前任务的任务控制块指针用来保存SP OSTCBCur = OSTCBHighRdy;//系统获得待运行控制块 SP=OSTCBHighRdy->OSTCBStkPtr; //待运行任务堆栈指针给SP 将R4,R3,R2及R1从新堆栈中弹出 执行中断返回指令 } 总结 过程: 任务切换OSCtxSw( )必定是一个中断服务程序。 1、OSShed()调用了宏OS_TASK_SW() 2、OS_TASK_SW()通过调用int 128产生了中断 3、中断服务程序OSCTxSw()完成当前任务的断点保存,同时完成新的任务的断电恢复,最后利用IRET指令来切换到新的任务去运行。 六、任务的操作 任务的创建 任务的挂起 任务的恢复 任务的删除 任务优先级别的修改 查询任务的信息 (1)创建任务 建立任务函数: OSTaskCreate() 或 OSTaskCreateExt() OSTaskCreate()与µC/OS是向下兼容的 OSTaskCreateExt()是OSTaskCreate()的扩展版本。 创建任务的工作实质上是创建一个任务控制块,并通过任务控制块把任务代码和任务堆栈关联起来形成一个完整的任务。当然还要使刚建立的任务进入就绪态,并接着引发一次任务调度。 下面来分析一下:OSTaskCreate()函数的 源代码 创建任务的一般方法: 在调用函数OSStart()之前先创建一个任务,并赋予它最高的优先级别,从而使它成为起始任务;然后在这个起始任务中,再创建其他各任务。 函数原型: INT8U OSTaskCreate ( void (*task)(void *pd), void *pdata, OS_STK *ptos, INT8U prio ); OSTaskCreateExt的原型 INT8U OSTaskCreateExt ( void (*task)(void *pd), void *pdata, OS_STK *ptos, INT8U prio, INT16U id, OS_STK *pbos, INT32U stk_size, void *pext, INT16U opt ); (2)任务的挂起 挂起任务可通过调用OSTaskSuspend()函数来完成。(自挂) 被挂起的任务只能通过调用OSTaskResume()函数来恢复。(不可自己恢复) 函数原型: INT8U OSTaskSuspend (INT8U prio) INT8U OSTaskResume (INT8U prio) (3)任务的删除 删除任务就是把该任务置于休眠状态,并不是说任务的代码被删除了,只是任务的代码不再被µC/OS调用。通过调用OSTaskDel()就可以完成删除任务的功能 。 具体做法: 把被删除任务的任务控制块从任务控制块链表中删除,并归还给空任务控制块链表,然后在任务就绪表中把该任务的就绪状态置0,于是该任务就不能再被调度器调用了。 有时候,如果任务A拥有内存缓冲区或信号量之类的资源,而任务B想删除该任务,这些资源就可能由于没被释放而丢失。在这种情况下,用户可以想法子让拥有这些资源的任务在使用完资源后,先释放资源,再删除自己。用户可以通过OSTaskDelReq()函数来完成该功能 //拥有这些资源的任务在使用完资源后,先释放资源,再删除自己 请求删除其它任务的任务(任务B) void RequestorTask (void *pdata) { INT8U err; pdata = pdata; for (;;) { /* 应用程序代码 */ if ('TaskToBeDeleted()' 需要被删除) { while(OSTaskDelReq(TASK_TO_DEL_PRIO) !=OS_TASK_NOT_EXIST) { OSTimeDly(1); } } /*应用程序代码*/ } } 解释 发出删除任务请求的任务(任务B)和要删除的任务(任务A)都需要调用OSTaskDelReq()函数。任务B需要决定在怎样的情况下请求删除任务。换句话说,用户的应用程序需要决定在什么样的情况下删除任务。如果任务需要被删除,可以通过传递被删除任务的优先级来调用OSTaskDelReq()。如果要被删除的任务不存在(即任务已被删除或是还没被建立),OSTaskDelReq()返回OS_TASK_NOT_EXIST。如果OSTaskDelReq()的返回值为OS_NO_ERR,则表明请求已被接受但任务还没被删除。用户可能希望任务B等到任务A删除了自己以后才继续进行下面的工作,这时用户可以象笔者一样,通过让任务B延时一定时间来达到这个目的,延时了一个时钟节拍。如果需要,用户可以延时得更长一些。当任务A完全删除自己后,返回值成为0S_TASK_NOT_EXIST,此时循环结束。 需要删除自己的任务(任务A) void TaskToBeDeleted (void *pdata) { INT8U err; pdata = pdata; for (;;) { /*应用程序代码*/ If (OSTaskDelReq(OS_PRIO_SELF) == OS_TASK_DEL_REQ) { 释放所有占用的资源; 释放所有动态内存; OSTaskDel(OS_PRIO_SELF); } else { /*应用程序代码*/ } } } (4)任务优先级别的修改 原型: INT8U OSTaskChangePrio ( INT8U oldprio, INT8U newprio ); 调用成功后,返回OS_NO_ERR (5)查询任务的信息 用户的应用程序可以通过调用OSTaskQuery()来获得自身或其它应用任务的信息。OSTaskQuery()获得的是对应任务的OS_TCB中内容的拷贝。 原型: INT8U OSTaskQuery ( INT8U prio, OS_TCB *pdata //存储任务信息的结构 ); 若调用函数OSTaskQuery查询成功,则函数将返回OS_NO_ERR,并把查询得到的任务信息存放在结构OS_TCB类型的变量中。
中断与时钟 中断是计算机系统处理异常事件的重要机制! uC/OS-II的时钟是通过硬件定时器产生定时中断实现的。 任务在运行过程中,应内部或外部异常事件的请求中止当前任务,而去处理异常事件所要求的任务的过程叫做中断。应中断请求而运行的程序叫做中断服务子程序(ISR);中断服务子程序的入口地址叫做中断向量。 μC/OS中,中断服务子程序要用汇编语言来写。然而,如果用户使用的C语言编译器支持在线汇编语言的话,用户可以直接将中断服务子程序代码放在C语言的程序文件中。 响应中断过程: 系统接收到中断请求后,如果这时CPU处于中断允许状态,系统就会中止正在运行的任务,而按照中断向量的指向转而去运行中断服务子程序;而中断服务子程序的运行结束后,系统将会根据情况返回到被中止的任务继续执行,或者转向运行另一个具有更高优先级别的就绪任务。(任务的调度) 断是一种硬件机制,用于通知CPU有个异常事件发生了。中断一旦被识别,CPU保存部分(或全部)现场(Context)即部分或全部寄存器的值,跳转到专门的子程序,称为中断服务子程序(ISR)。中断服务子程序做事件处理,处理完成后,程序回到: (1)在前后台系统中,程序回到后台程序 (2)对不可剥夺型内核而言,程序回到被中断了的任务 (3)对可剥夺型内核而言,让进入就绪态的优先级最高的任务开始运行 UC/OS-II系统运行中断嵌套,即:高优先级别的中断源的中断请求可以中断低优先级别的中断服务程序的运行。 为了记录中断嵌套的层数,定义了一个全局变量OSIntNesting。 1)进入中断函数: void OSIntEnter (void) { if(OSRunning==TRUE) { if(OSIntNesting<255) { OSIntNesting++;} //中断嵌套层数计数器加1 } } 调用情况:此函数在中断服务程序保护断点数据后,运行用户中断服务代码之前来调用,所以通常把它叫做进入中断服务函数。 (2)退出中断服务函数
说明:这个函数在中断嵌套层数计数器为0、调度器未被锁定、从任务就绪表中查找到的最高级就绪任务不是被中断的任务的条件下将要进行任务切换,否则就返回被中断的服务子程序 void OSIntExit (void) { OS_ENTER_CRITICAL(); if ((--OSIntNesting && OSLockNesting) = = 0) { OSIntExitY = OSUnMapTbl[OSRdyGrp]; OSPrioHighRdy = (INT8U)((OSIntExitY << 3) + OSUnMapTbl[OSRdyTbl[OSIntExitY]]); if (OSPrioHighRdy != OSPrioCur) { OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; OSCtxSwCtr++; OSIntCtxSw(); } } OS_EXIT_CRITICAL(); } 用户中断服务子程序流程: 1、保存全部CPU寄存器; 2、调用OSIntEnter或OSIntNesting直接加1 3、If(OSIntNesting == 1){ OSTCBCur->OSTCBStkPtr = SP}//V2.04以上新增 4、清中断源 5、重新开中断 6、执行用户代码做中断服务; 7、调用OSIntExit(); 8、恢复所有CPU寄存器; 9、执行中断返回指令; 中断级任务切换函数 uC/OS-II在运行完中断服务程序之后,并不一定返回到被中断的任务上去,而是要进行一次任务调度来决定是返回被中断的任务还是运行一个具有更高优先级别的任务,因此系统需要一个中断级任务调度器。 由函数OSIntExit()的源代码可以知道,通过调用函数OSIntCtxSw()来进行中断级的任务切换。在中断服务程序中调用的负责任务切换的函数OSIntCtxSw()叫做中断级任务切换函数 时钟 时钟:任何操作系统都要提供一个周期性的信号源,以供系统处理与时间相关的事件,这个周期性的信号源叫做时钟。 时钟节拍:UC/OS-II用硬件定时器产生一个周期为毫秒级的周期性中断来实现系统时钟。最小的时钟就是两次中断之间间隔的时间,这个最小时钟单位叫做时钟节拍。 两个中断之间的间隔时间:时钟节拍。 OSTickISR() :硬件定时器以时钟节拍为周期定时地产生中断,该中断的中断服务程序就是它。中断服务程序OSTickISR()通过调用函数OSTimeTick()来完成系统在每个时钟节拍时需要做的工作。 (1)时钟节拍的中断服务程序OSTickISR() 用汇编语言来编写: Void OSTickISR(void) { 保存CPU寄存器; 调用OSIntEnter(); //记录中断嵌套层数 if(OSIntNesting == 1) { OSTCBCur->OSTCBStkPtr=SP; //在任务TCB中保存SP } 调用OSTimeTick(); //时钟节拍服务函数目的是在时钟节拍到来时,检查每个任务的任务控制块中的.OSTCBDly-1后是否为0,如果是,那么表明这个任务刚才是挂起的状态,此时应改变为就绪态 清除中断; 开中断; 调用OSIntExit(); //中断嵌套层数减1 恢复CPU寄存器; 中断返回; } (2)OSTimeTick()用C编写 在时钟中断服务程序中调用OSTimeTick() UC/OS-II在每次响应定时中断时调用OSTimeTick()做了两件事情:一是给计数器OSTime加1;二是遍历任务控制块链表中的所有任务控制块,把各个任务控制块中用来存放任务延时时限的OSTCBDly变量减1,并使该项为0,同时又不使被挂起的任务进入就绪状态。简单地说,函数OSTimeTick的任务就是在每个时钟节拍了解每个任务的延时状态,使其中已经到了延时时限的非挂起任务进入就绪状态。 HOOK() OSTimeTick()是系统调用的函数,为了方便应用程序设计人员能在系统调用的函数中插入一些自己的工作,OS提供了时钟节拍服务函数的钩子函数OSTimeTickHook()。 此外,OS还提供了OSStkInitHook()、OSInitHookBegin()、OSInitHookEnd()、OSTaskCreateHook()、OSTaskDelHook()、OSTaskSwHook()、OSTaskStatHook()、OSTCBInitHook()、OSTaskIdleHook()等与OSTimeTickHook()一起共10个钩子函数,以供用户在系统调用函数中书写自己的代码。 3、时间管理 任务的延时: 由于嵌入式系统的任务是一个无限循环,并且UC/OS-II还是一个抢占式内核,所以为了使高优先级别的任务不至于独占CPU,可以给其他优先级别较低的任务获得CPU使用权的机会。UC/OS-II规定: 除了空闲任务之外的所有任务必须在任务中合适的位置调用系统提供的函数OSTimeDly(),使当前任务的运行暂停一段时间并进行一次任务调度,以让出CPU的使用权。 OSTimeDly()原型: void OSTimeDly(INT16U ticks) 函数的参数ticks是以时钟节拍数为单位的延时时间的。 OSTimeDlyHMSM()时、分、秒为参数 INT8U OSTimeDlyHMSM ( INT8U hours, // 时 INT8U minutes, // 分 INT8U seconds, // 秒 INT16U milli // 毫秒 ) 调用了以上这两个函数的任务,当规定的延时时间期满时,或者有其他任务通过调用函数OSTimeDlyResume()取消了延时时,它会立即进入就绪状态。 取消任务的延时OSTimeDlyResume() 延时的任务可以通过在其他任务中调用函数OSTimeDlyResume()取消延时并进入就绪状态,如果任务比正在运行的任务优先级别高,则立即引发一次任务调度。 原型: INT8U OSTimeDlyResume(INT8U prio) 获取和设置系统时间 系统定义了一个INT32U类型的全局变量OSTime来记录系统发生的时钟节拍数。 OSTime在应用程序调用OSStart()时被初始化为0,以后每发生一个时钟节拍, OSTime的值就被加1。 在应用程序中调用函数OSTimeGet()可获得OSTime的值。原型: INT32U OSTimeGet( void ); 函数的返回值即为OSTime的值 OSTimeSet()设置OSTime的值。 void OSTimeSet( INT32U ticks); 函数的参数ticks为OSTime的设置值(节拍数)。 举例:设计一个应用程序,在任务中调用函数OSTimeGet()获得并显示系统的时间节拍数OSTime。当任务运行10s时,调用函数OSTimeSet()将OSTime设置为10. 任务间的同步 事件和事件控制块——描述事件的数据结构 信号量及其使用 消息邮箱及其使用 消息队列及其使用 任务间的同步 制约关系:为了实现各任务之间的合作和无冲突的运行,在各任务之间必须建立一些制约关系,分为直接制约和间接制约关系。 直接制约关系:源于任务之间的合作。 间接制约关系:源于对资源的共享。 总结: 在多任务合作工作的过程中,OS应该解决两个问题: ① 各任务之间应该具有一种互斥关系,即对于某个共享资源,如果一个任务正在使用,则其他任务只能等待,等到该任务释放资源后,等待的任务之一才能使用它。 ② 相关的任务在执行上要有先后次序,一个任务要等其伙伴任务发来通知,或建立了某个条件后,才能继续执行,否则只能等待。 任务之间这种制约性的合作运行机制叫做任务间的同步。而任务的同步又是依靠任务与任务之间相互发送消息来保证同步的。 二、事件 任务间的同步依赖于任务间的通信。在UC/OS-II中,使用信号量、消息邮箱、消息队列 这些被称为事件的中间环节来实现任务之间的通信。 1、信号量 为了共享资源设立一个表示该共享资源被占用情况的标志。这样,就可使任务在访问共享资源之前,先对这个标志进行查询,在了解资源被占用的情况之后,再来决定自己的行为。 分类:互斥型信号量和信号量 互斥型信号量:二值信号实现共享资源的独占式占用。 信号量:计数式的信号 2、消息邮箱 在多任务操作系统中,常常需要在任务与任务之间通过传递一个数据(消息)的方式来进行通信。为了达到这个目的,可以在内存中创建一个存储空间作为该数据的缓冲区。如果把这个缓冲区叫做消息缓冲区,那么在任务间传递数据(消息)的一个最简单的方式就是传递消息缓冲区的指针。 3、消息队列 消息邮箱不仅可用来传递一个消息,而且也可定义一个数组。让数组的每个元素都存放一个消息缓冲区指针,那么任务就可通过传递这个数组的指针的方式来传递多个消息了。这种可以传送多个消息的数组结构叫做消息队列。 4、等待任务列表 在多任务系统中,当一个事件被占用时,其他请求该事件的任务在暂时得不到事件的服务时应该处于等待状态。因此,作为功能完善的事件,应该有对这些等待任务具有一定的管理功能。这个管理功能包括:一是要对等待事件的所有任务进行记录并排序;二是应该允许任务有一定的等待时限。 (1)等待时间任务的记录 采用了与任务就绪表类似的方法,使用一个INT8U类型的数组OSEventTbl[ ]作为记录等待事件任务的记录表,这个表叫做等待任务表。 在这个等待任务表中仍然是以任务的优先级别为顺序,令系统中的每个任务都在表中占据一位,并且当该位为1时表示这一位对应的任务为事件的等待任务,否则不是等待任务。 同样为了加快对该表的访问速度,也定义了一个INT8U类型的变量OSEventGrp来表示等待任务表中的任务组。等待任务表OSEventTbl[ ]和变量OSEventGrp的示意图如下:
(2)等待时限 等待任务的等待时限记录在等待任务的任务控制块TCB的成员OSTCBDly中,并在每个时钟节拍中断服务程序中对该数据进行维护。每当有任务的等待时限已到时,将该任务从等待任务表中删除,并使它进入就绪状态。 三 事件控制块 uC/os-ii使用叫做事件控制块ECB的数据结构来描述:信号量、消息邮箱、消息队列这些事件。 事件控制块中包含等待任务表在内的所有有关事件的数据。ECB结构如下: 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是用于信号量的计数器。 OSEventType定义了事件的具体类型。它可以是 信号量(OS_EVENT_SEM) 邮箱(OS_EVENT_TYPE_MBOX) 消息队列(OS_EVENT_TYPE_Q) 中的一种。用户要根据该域的具体值来调用相应的系统函数,以保证对其进行的操作的正确性。 在等待任务列表中查找最高优先级的任务 y= OSUnMapTbl[pevent->OSEventGrp]; x= OSUnMapTbl[pevent->OSEventTbl[y]]; prio = (y << 3) + x; 1、空事件控制块链表 在µC/OS-II中,事件控制块的总数由用户所需要的信号量、邮箱和消息队列的总数决定。该值由OS_CFG.H 中的#define OS_MAX_EVENTS定义。在调用OSInit()时,所有事件控制块被链接成一个单向链表——空闲事件控制块链表。 每当建立一个信号量、邮箱或者消息队列时,就从该链表中取出一个空闲事件控制块,并对它进行初始化。而当应用程序删除一个事件时,就会将该事件的控制块归还给空事件控制块链表。
2、对事件控制块进行的通用操作 包括: 初始化一个事件控制块 使一个正在等待任务进入就绪态的函数 使一个任务进入等待事件的状态 使一个等待超时的任务进入就绪态的函数 为了避免代码重复和减短程代码长度,µC/OS-II将上面的操作用4个系统函数实现,它们是:OSEventWaitListInit(),OSEventTaskRdy(),OSEventWait()和OSEventTO()。 (1)初始化一个事件控制块 当建立一个信号量、邮箱或者消息队列时,相应的建立 函数OSSemCreate()\OSMutexCreate()\OSMboxCreate()\OSQCreate() 通过调用OSEventWaitListInit()对事件控制块中的等待任 务列表进行初始化。该函数初始化一个空的等待任务列表,即令事件 的任务等待表中不含有任何等待任务。该函数的调用参数只有一个, 就是指向需要初始化的事件控制块的指针pevent。 初始化ECB块的等待任务列表 void OSEventWaitListInit (OS_EVENT *pevent) { INT8U i; pevent->OSEventGrp = 0x00; for (i = 0; i < OS_EVENT_TBL_SIZE; i++) { pevent->OSEventTbl[i] = 0x00; } } (2)使一个任务进入就绪OSEventTaskRdy() 如果一个正在等待的任务具备了可以运行的条件,那么就要使它进入就绪状态。这时候要调用函数OSEventTaskRdy()。 该函数的作用:把调用这个函数的任务在任务等待表(1表示等待任务)中的位置清0后,再把任务就绪表中对于的位置1,然后引发一次任务调度。函数OSEventTaskRdy()将在任务调用函数OSXXXPost()发送一个事件时,(发送事件表示释放事件)被调用 (3)使一个任务进入等待事件状态OSEventTaskWait() 当某个任务要等待一个事件的发生时,相应事件的OSSemPend(),OSMboxPend()或者OSQPend()函数会调用该函数将当前任务从就绪任务表中删除,并放到相应事件的事件控制块的等待任务表中。 (4)等待超时而将任务置为就绪态 OSEventTO() 当在预先指定的时间内任务等待的事件没有发生时,OSTimeTick()函数会因为等待超时而将任务的状态置为就绪。在这种情况下,事件的OSSemPend(),OSMboxPend()或者OSQPend()函数会调用OSEventTO()来完成这项工作。该函数负责从事件控制块中的等待任务列表里将任务删除,并把它置成就绪状态。最后,从任务控制块中将指向事件控制块的指针删除。用户应当注意,调用OSEventTO()也应当先关中断。 四、信号量及其操作 当事件控制块成员OSEventType的值被设置为OS_EVENT_TYPE_SEM (信号量(OS_EVENT_SEM)邮箱(OS_EVENT_TYPE_MBOX)消息队列(OS_EVENT_TYPE_Q))时,这个事件控制块描述的就是一个信号量。 信号量由信号量计数器和任务等待表组成。 原理:每当有任务申请信号量时,如果信号量计数器OSEventCnt的值大于0,则把OSEventCnt减1并使任务继续运行;如果OSEventCnt的值为0,则会将任务列入任务等待表OSEventTbl[ ],而使任务处于等待状态。 如果正在使用信号量的任务释放了该信号量,则会在任务等待表中找出优先级别最高的等待任务,并在使它就绪后调用调度器引发一次调度;如果在任务等待表中已经没有等待任务,则信号量计数器就只简单地加1. (1)创建信号量 调用函数OSSemCreate() 原型: OS_EVENT *OSSemCreate (INT16U cnt) 返回值:创建的信号量指针。 (2)请求信号量 函数:OSSemPend() 原型:void OSSemPend ( OS_EVENT *pevent,//信号量指针 INT16U timeout, //等待时限 INT8U *err //错误信息 ) 返回值:err函数返回的错误代码,有四种可能: OS_NO_ERR 成功等到该信号量的时候 OS_TIMEOUT 等待超时 OS_ERR_PEND_ISR 在中断服务ISR中调用 OS_ERR_EVENT_TYPE prevent参数有误 (3)发送信号量 任务获得信号量,并在访问共享资源结束后,必须释放信号量。释放信号量也叫做发送信号量。 函数:OSSemPost() 函数OSSemPost() 在对信号量的计数器操作之前,首先要检查是否还有等待信号量的任务:如果没有,就把信号量计数器OSSemCnt加1;如果有,则调用调度器OS_Sched()去运行等待任务中优先级别最高的任务。 原型:INT8U OSSemPost (OS_EVENT *pevent) 返回值:调用函数成功则返回值为OS_ON_ERR; 否则据具体错误返回OS_ERR_EVENT_TYPE、 OS_SEM_OVF (4)无等待地请求一个信号量OSSemAccept() 当一个任务请求一个信号量时,如果该信号量暂时无效,也可以让该任务简单地返回,而不是进入睡眠等待状态。这种情况下的操作是由OSSemAccept()函数完成的 。 中断服务子程序要请求信号量时,只能用OSSemAccept()而不能用OSSemPend(),因为中断服务子程序是不允许等待的。 INT16U OSSemAccept (OS_EVENT *pevent) (5)查询一个信号量的当前状态, OSSemQuery() 在应用程序中,用户随时可以调用函数OSSemQuery()来查询一个信号量的当前状态。该函数有两个参数:一个是指向信号量对应事件控制块的指针pevent。该指针是在生产信号量时,由OSSemCreate()函数返回的;另一个是指向用于记录信号量信息的数据结构OS_SEM_DATA的指针pdata。因此,调用该函数前,用户必须先定义该结构变量,用于存储信号量的有关信息。 原型: INT8U OSSemQuery (OS_EVENT *pevent, OS_SEM_DATA *pdata) OS_SEM_DATA结构如下: Typedef struct{ INT16U OSCnt; INT16U OSEventTbl[OS_EVENT_TBL_SIZE]; INT8U OSEventGrp; }OS_SEM_DATA; 五、邮箱 如果任务与任务之间要传递一个数据,那么为了适应不同数据的需要最好在存储器中建立一个数据缓冲区,把要传递的数据放在这个缓冲区,就可以实现任务间的数据通信。 如果把数据缓冲区的指针赋给一个事件控制块的成员OSEventPtr,同时使事件控制块的成员OSEventType为常数OS_EVENT_TYPE_MBOX ,则该事件控制块就叫做消息邮箱。 消息邮箱是在两个需要通信的任务直接通过传递数据缓冲区指针的方法来通信的。 (1)创建消息邮箱 函数原型: OS_EVENT *OSMboxCreate (void *msg) 参数Msg是消息指针。 返回值是一个指向事件控制块的指针 这个指针在调用函数OSMboxPend(),OSMboxPost(),OSMboxAccept()和OSMboxQuery()时使用。 调用函数须先定义msg的初始值。一般情况下,这个初始值为NULL;但也可以事先定义一个邮箱,然后把这个邮箱的指针作为参数传递给函数中,使其一开始就指向一个邮箱。 (2)向消息邮箱发送消息 函数:OSMboxPost() 原型:INT8U OSMboxPost (OS_EVENT *pevent, void *msg) 参数: *pevent消息邮箱指针(事件控制块指针); *msg消息指针。 (3)请求消息邮箱 函数:OSMboxPend() 作用:查看邮箱指针OSEventPtr 是否为NULL。如果不是,则把邮箱中的消息指针返回给调用函数的任务;如果是,则使任务进入等待状态,并引发一次任务调度。 原型:void *OSMboxPend (OS_EVENT *pevent, INT16U timeout, INT8U *err) (4)查询邮箱的状态 原型:INT8U OSMboxQuery (OS_EVENT *pevent, OS_MBOX_DATA *pdata) OSMboxQuery()函数使应用程序可以随时查询一个邮箱的当前状态。 两个参数:一个是指向邮箱的指针pevent。该指针是在建立该邮箱时,由OSMboxCreate()函数返回的;另一个是指向用来保存有关邮箱的信息的OS_MBOX_DATA(见uCOS_II.H)数据结构的指针pdata。在调用OSMboxCreate()函数之前,必须先定义该结构变量,用来保存有关邮箱的信息。 (5)无等待地从邮箱中得到一个消息 函数:OSMboxAccept() 任务在请求邮箱失败时也可以不进行等待而继续运行。 函数原型: Void *OSMBOXACCEPT (OS_EVENT *PEVENT) (6)删除邮箱 任务可以调用函数OSMBoxDel()来删除一个邮箱 函数原型: OS_EVENT OSMBoxDel( OS_EVENT *pevent,//邮箱指针 INT8U opt, //删除选项 INT8U *err ); 六、消息队列 使用消息队列可以在任务直接传递多条消息。消息队列由3部分组成: 事件控制块、消息队列、消息 当把事件控制块成员OSEVENTTYPE设置成OS_EVENT_TYPE_Q 时,该事件控制块描述的就是一个消息队列。 消息队列相当于一个共用一个任务等待列表的消息邮箱数组,OSEventPtr指向一个叫做队列控制块OS_Q的结构,该结构管理一个数组MsgTbl[ ],该数组的元素都是一些指向消息的指针。
(1)消息指针数组 消息队列的核心就是消息指针数组。 OSQStart是指向消息队列的指针数组的起始地址的指针。 OSQEnd是指向消息队列结束单元的下一个地址的指针。该指针使得消息队列构成一个循环的缓冲区。 OSQIn是指向消息队列中插入下一条消息的位置的指针。当.OSQIn和.OSQEnd相等时,.OSQIn被调整指向消息队列的起始单元。 OSQOut是指向消息队列中下一个取出消息的位置的指针。当.OSQOut和.OSQEnd相等时, OSQOut被调整指向消息队列的起始单元。 OSQSize是消息队列中总的单元数。该值是在建立消息队列时由用户应用程序决定的。在µC/OS-II中,该值最大可以是65,535。 OSQEntries是消息队列中当前的消息数量。当消息队列是空的时,该值为0。当消息队列满了以后,该值和OSQSize值一样。 在消息队列刚刚建立时,该值为0。 向指针数组中插入消息指针的方式: 先进先出(FIFO)方式 后进先出(LIFO)方式 (1)当采用先进先出方式时,消息队列将在指针OSQIn指向的位置插入消息指针,而把指针OSQOut指向消息指针作为输出。 (2)当采用后进先出方式时,则只使用指针OSQOut,当向队列插入消息指针时,指针OSQOUT将先移动到它的下一个地址,再按指针指向的位置插入消息指针,输出时则指针无须进行移动,就把指针指向的消息指针输出。 消息队列最根本的部分是一个循环缓冲区。其中的每个单元包含一个指针。队列未满时,.OSQIn 指向下一个存放消息的地址单元。如果队列已满OSQIn则与.OSQOut指向同一单元。如果在.OSQIn指向的单元插入新的指向消息的指针,就构成FIFO(First-In-First-Out)队列。相反,如果在.OSQOut指向的单元的下一个单元插入新的指针,就构成LIFO队列(Last-In-First-Out)。当.OSQEntries和.OSQSize相等时,说明队列已满。消息指针总是从.OSQOut 指向的单元取出。指针.OSQStart和.OSQEnd 定义了消息指针数组的头尾,以便在.OSQIn和.OSQOut到达队列的边缘时,进行边界检查和必要的指针调整,实现循环功能。
队列控制块: 消息指针数组的基本参数都记录在一个叫做队列控制块的结构中。 typedef struct os_q { struct os_q *OSQPtr; void * *OSQStart; void * *OSQEnd; void * *OSQIn; void * *OSQOut; void * *OSQSize; void * *OSQEntries; }OS_Q; 在UC/OS-II初始化时,系统将按文件OS_CFG.H中的配置常数OS_MAX_QS定义OS_MAX_QS个队列控制块,并用队列控制块中的指针OSQPtr将所以队列控制块链接为链表。由于这时还没有使用它们,因此这个链表叫做空队列控制块链表。
每当任务创建一个消息队列时,就会在空队列控制块链表中摘取一个控制块供消息队列来使用,并令该消息队列事件控制块中的指针OSEventPtr指向这个队列控制块。而当任务释放一个消息队列是,就会将该消息队列使用的队列控制块归还空队列控制块链表。 (二)消息队列的操作 OSQCreate()创建消息队列 OSQPend()请求消息队列 OSQAccept()无等待地请求消息队列 OSQPost() 以FIFO方式向消息队列发送消息 OSQPostFront() 以LIFO向消息队列发送消息 OSQFlush()清空消息队列 OSQQuery()查询消息队列 OSQCreate() 创建一个消息队列,首先需要定义一个指针数组,然后把各个消息数据缓冲区的首地址存入这个数组中,最后再调用函数OSQCreate()来创建消息队列! 函数原型: OS_EVENT *OSQCreate ( void **start, //指针数组的地址 INT16U size )//数组长度 函数中的参数start为存放消息缓冲区指针数组的地址;参数size为该数组的大小。函数的返回值是消息队列的指针。 函数首先从空闲队列控制块链表中摘取有一个控制块并按参数填写逐项,然后把消息队列初始化为空。 OSQPend() 目的:请求消息队列是为了从消息队列中获取信息。 函数原型: void *OSQPend (OS_EVENT *pevent, //所请求消息队列的指针 INT16U timeout, INT8U *err) 参数:pevent是要访问的消息队列事件控制块的指针;timeout是任务等待的时限。 返回值:消息指针 函数要通过访问事件控制块的成员OSEventPtr指向的队列控制块OS_Q的成员OSQEntries来判断是否有消息可用。如果有消息可用,则返回OS_Q成员OSQOut指向的消息,同时调整指针OSQOut ,使之指向下一条消息并把有效消息数的变量OSQEntries减1;如果没有消息可用,则使调用函数OSQPend()的任务挂起,使之处于等待状态并引发一次任务调度。 如果希望任务无等待地请求一个消息队列,则需要调用函数OSQAccept()。 OSQPost()以FIFO先进先出的方式 OSQPostFront()以LIFO后进先出的方式 函数原型: INT8U OSQPost ( OS_EVENT *pevent,//消息队列的指针 void *msg) //消息指针 和 INT8U OSQPostFront ( OS_EVENT *pevent, void *msg) 如果任务希望以广播的方式通过消息队列发送消息,则需要调用函数OSQPostOpt()。 函数原型: INT8U OSQPostFront ( OS_EVENT *pevent,//消息队列指针 void *msg, //消息指针 INT8U opt //广播选项 ) 调用这个函数发送消息时,如果参数opt的值为OS_POST_OPT_BROADCAST,则凡是等待该消息队列的所有任务都对收到消息。 清空消息队列OSQFlush() 原型: INT8U OSQFlush ( OS_EVENT *pevent);//消息队列指针 查询消息队列OSQQuery() INT8U OSQQuery ( OS_EVENT *pevent, OS_Q_DATA *pdata) 参数pdata是OS_Q_DATA *pdata类型的指针,他的结构是: typedef struct{ void *OSMsg; INT16U OSNMsgs; INT16U OSQSize; INT8U OSEventTbl[OS_EVENT_TBL_SIZE]; INT8U OSEventGrp; }OS_Q_DATA; 信号量集 在实际应用中,任务常常需要与多个事件同步,即要根据多个信号量组合作用的结果来决定任务的运行方式! Uc/os-ii为了实现多个信号量组合的功能定义了一个特殊的数组结构——信号量集 信号量集所能管理的信号都是一些二值信号。所以信号量集实质上是一种可以对多个输入的逻辑信号进行基本逻辑运算的组合逻辑。 一、信号量集的结构
标志组:存放信号量集中的所有信号。 等待任务链表:链表中的每个节点都对应一个正在等待信号量集的等待任务,信号量 集根据这个链表来管理等待任务。 1、信号量集的标志组 利用标志组OS_FLAG_GRP来描述信号量集;而不是用事件控制块来描述。 OS_FLAG_GRP的结构: typedef struct{ INT8U OSFlagType; //信号量集的标识 void *OSFlagWaitList;//指向等待任务链表的指针 OS_FLAGS OSFlagFlags; //所有信号列表 }OS_FLAG_GRP; 各变量的含义: OSFlagType:信号量集的标识,该成员变量的值应该固定为OS_EVENT_TYPE_FLAG。 OSFlagFlags:OS_FLAGS类型的变量,该变量用来存放信号量集所有信号的状态,每个信号占据一个二进制位。信号量集中可以存放多少个信号,取决于OSFlagFlags的长度。这个长度可以根据应用程序需要信号的数目定义为8位、16位、32位。 OSFlagWaitList:当一个信号量集被创建后,这个指针指向了这个信号量集的等待任务链表。
在UC/OS-II初始化时,系统会根据在文件OS_CFG.H中定义的常数0S_MAX_FLAGS,来创建0S_MAX_FLAGS个标志组(信号量集),并借用成员OSFlagWaitList作为指针把这些标志组链接成一个单向链表。由于这个链表中的各个标志组还未被真正创建,因此叫做空标志组链表!//初始化标志组为空标志组链表 空标志组链表的头指针存放在系统全局变量OSFlagList中,每当应用程序创建一个信号量集时,就从这个链表中取一个标志组,并移动头指针OSFlagList,使之指向下一个空标志组。 2、等待任务链表 信号量集用一个双向链表来组织等待任务,每一个等待任务都是该链表的一个节点(Node)。标志组OS_FLAG_GRP的成员OSFlagWaitList就指向了信号量集的这个等待任务链表。 等待任务链表节点OS_FLAG_NODE的结构: typedef struct { void *OSFlagNodeNext; //指向下一个节点的指针 void *OSFlagNodePrev; //指向前一个节点的指针 void *OSFlagNodeTCB; //指向对应任务的任务控制块 void *OSFlagNodeFlagGrp; //反向指向信号量集指针 OS_FLAGS OSFlagNodeFlags; //信号过滤器 INT8U OSFlagNodeWaitType; //定义逻辑运算关系的数据 } OSFlagNodeFlagGrp:反向指向信号量集标志组的指针,是在等待任务链表中删除一个节点或添加一个节点时用到的指针。 OSFlagNodeTCB:指向等待任务TCB的指针,信号量集的等待任务链表通过这个指针把链表节点与等待任务关联起来。 OSFlagNodeFlags:利用它可在标志组成员OSFlagNodeFlags的信号中只把请求任务需要的信号筛选出来,而把其它的信号屏蔽掉。也就是说,一个请求信号量集的任务可以需要信号集的所有信号,也可以只需要其中的部分信号,它究竟需要那些信号,要通过在成员OSFlagNodeFlags中与所等待信号对应的二进制位进行置1来指定,而任务不需要的信号的位置0. OSFlagNodeWaitType:指定逻辑关系,它有四个常数可以选择。它们既指定了逻辑关系,也指定了信号的有效状态。 定义信号的有效状态及等待任务与信号之间的逻辑关系常数
OSFlagNodeFlags中的值表明等待任务所等待的信号是标志组成员OSFlagFlags中的第0、4、7个信号。 OSFlagNodeWaitType的值表明:信号的有效状态是0,即当任务所等待的第0、4、7个信号的状态都为0时,任务才可以结束等待状态而进入就绪状态。
信号用OSFlagFlags来记录信号,用OSFlagNodeFlags来筛选信号,用OSFlagNodeWaitType来控制信号的有效状态和信号量集有效之间的逻辑关系。 把等待任务链表的节点链接起来就形成了等待任务链表。在等待任务链表的基础上,再加上标志组和各个节点对应的任务控制块就形成了整个信号量集。 3、对等待任务链表的操作 添加节点和删除节点 添加节点:给等待任务链表添加节点的函数OS_FlagBlock() static void OS_FlagBlock( OS_FLAG_GRP *pgrp, //信号量集指针 OS_FLAG_NODE *pnode, //待添加等待任务节点指针 OS_FLAGS flags, //指定等待信号的数据 INT8U wait_type, //信号与等待任务之间的逻辑 INT16U timeout //等待时限 ); 这个函数将在请求信号量集函数OSFlagPend()中被调用。 删除节点:OS_FlagUnlink() Void OS_FlagUnlink ( OS_FLAG_NODE *pnode ); 二、信号量集的操作 创建信号量集OSFlagCreate() 请求信号量集OSFlagPend() 向信号量集发信号OSFlagPost() 查询信号量集的状态OSFlagQuery() 删除信号量集OSFlagDel() 1、创建信号量集 OS_FLAG_GRP *OSFlagCreate( OS_FLAGS flags,//信号的初始值 INT8U *err //错误信息 ) 主要的工作: (1)从空标志组链表中取下一个标志组,并同时给成员OSFlagType和OSFlagFlags赋初值; (2)令指向等待任务链表的指针OSFlagWaitList为空指针 创建一个信号量集分两步:首先定义一个全局的OS_FLAG_GRP类型的指针,然后再应用程序需要创建信号量集的位置调用函数OSFlagCreate()。 调用创建信号量集OSFlagCreate()成功后,该函数返回的是这个信号量集的标志组的指针,应用程序可以用这个指针对信号量集进行相应的操作。 2、请求信号量集 函数: OSFlagPend() 原型: OS_FLAGS OSFlagPend( OS_FLAG_GRP *pgrp, // 请求信号量集指针 OS_FLAGS flags , // 滤波器 INT8U wait_type, // 逻辑运算类型 INT16U timeout, // 等待时限 INT8U *err // 错误信息 );
返回值:调用成功后返回标志组成员OSFlagFlags的值。也就是信号的状态 任务也可以通过调用函数OSFlagAccept()无等待地请求一个信号量集。 原型: OS_FLAGS OSFlagPend( OS_FLAG_GRP *pgrp, //请求信号量集指针 OS_FLAGS flags, //滤波器 INT8U wait_type, //逻辑运算类型 INT v *err //错误信息 ); 3、向信号量集发信号 任务可以通过调用函数OSFlagPost()向信号量集发信号。 函数原型: OS_FLAGS OSFlagPost( OS_FLAG_GRP *pgrp, // 请求信号量集指针 OS_FLAGS flags, // 选择所要发送的信号 INT8U opt, // 信号有效的选项 INT v *err // 错误信息 ); 说明:所谓任务向信号量集发信号,就是对信号量集标志组中的信号进行置1或者置0的操作。至于对信号量集中的那些信号进行操作,由函数的参数flags决定;对指定的信号是置1还是置0,有参数opt决定(opt = OS_FLAG_SET为置1;opt = OS_FLAG_CLR为置0操作)。 4、查询信号量集的状态 函数OSFlagQuery()查询一个信号量集的状态。 原型: OS_FLAGS OSFlagQuery ( OS_FLAG_GRP *pgrp, //待查询的信号量集的指针 INT v *err //错误信息 ); 返回值:被查询信号量集标志组的成员OSFlagFlags,应用程序可以用它来完成一些更为复杂的操作。 5、删除信号量集 函数:OSFlagDel() 原型: OS_FLAGS OSFlagDel ( OS_FLAG_GRP *pgrp, //待删除的信号量集的指针 INT8U opt, INT8U *err //错误信息 );
内存的动态分配 内存空间 . 内存分区(OS以它为单位来管理动态内存) . 内存块(任务以它为单位获得释放动态内存) . 关系:连续的内存空间分成了若干个分区, 每个分区又分成了若干个大小相等的内存块。 内存分区及内存块的使用情况由内存控制块来记录。 注意:同一个分区中内存块的字节数必须相同;每个分区与其内存块的数据类型必须相同。
|
|
来自: 新用户3699NmrV > 《待分类》