ucos在s3c2410上运行过程整体剖析-从main函数到UCOS初始化完毕

 

下面要讲解的内容基本上都是c语言编写的了,还有一部分代码是用汇编写的。以下我就以这个系统移植的实验源码为例做详细讲解。讲解的方式是根据程序执行的顺序进行,如果感觉有什么难理解的地方,会做一个宏观讲解和分析。

好了废话不多说,进入main()函数之后做的事情有:

对硬件平台做进一步的初始化操作

对ucos操作系统做初始化操作

 

对硬件的初始化主要包括:

初始化时钟总线

初始化串口

初始化中断结构体的相关内容

初始化定时器

 

关于以上的时钟的,串口的,还有定时器的等硬件的具体初始化和使用我不想一一详细阐述,这些你都可以通过查看datasheet自己弄明白的。毕竟我想详细讲解的是UCOS这个操作系统,只不过有些硬件知识是不得不讲的。我这里只是做一个你查资料的一个索引。

关于时钟频率初始化的相关知识:一,你可以查看datasheet的第七章CLOCK & POWER MANAGEMENT的内容,也可以参考S3C-2410--2440完全开发流程的第十二个实验的相关知识和内容。

关于串口的你查看datasheet的第十一章uart的相关内容。

关于定时器的你查看datasheet的第十章PWM Timer。

 

关于中断结构体,这是ucos响应中断的统一接口。就是定义了一个结构体,里面包含了中断服务函数的指针,屏蔽和开启相应中断请求的函数指针等。整个中断的详细机制,我想在ARM平台的中断机制和ucos如何挂接的时候详细讲解。

现把main()函数的代码贴出来:

int main(void)

{

 

       ARMTargetInit();        // do target (uHAL based ARM system) initialisation //

//初始化硬件平台

       OSInit();               // needed by uC/OS-II //

//对ucos进行初始化

       OSTaskCreate(Task1,  (void *)0,  (OS_STK *)&task1_Stack[STACKSIZE-1],  Task1_Prio);

       OSTaskCreate(Task2,  (void *)0,  (OS_STK *)&task2_Stack[STACKSIZE-1],  Task2_Prio);

       OSAddTask_Init(0);

       OSStart();              // start the OS //

       // never reached //

       return 0;

}//main

 

 

//

void Task1(void *Id)

{

      

 

       for(;;){

              printf("run task1\n");

              OSTimeDly(1000);

       }

}

void Task2(void *Id)

{

      

 

       for(;;){

              printf("run task2\n");

              OSTimeDly(3000);

       }

}

 

 

关于初始化硬件平台的部分前面已经说明了查看哪些资料,就不多说了。

讲解关于ucos的初始化前,我要说一下这个ucos操作系统的相关知识,以及我讲解这个操作系统的基本方法。

我们都知道操作系统的目标就是充分利用计算机提供的硬件资源,合理进行调度并为用户提供友好的借口。这些资源包括处理器(cpu),内存,外围设备等,而其中ucos是一个嵌入式的实时操作系统,它主要管理和调度的资源就是cpu。我们将剖析它是怎么实现cpu的较高的利用率的,以及是如何实现实时性的。分析一下我们平时用电脑那种既能听歌又能同时打游戏这样的功能是怎么做到的。分析我们平时看到的现象和真实的实际情况有什么不同点。嘻嘻

我们都知道cpu的核心是一个叫晶振的东西,它能产生固有的脉冲频率,cpu就在这种信号下有规律的工作,那操作系统是谁给提供这种时序性那,答案是定时器,定时器以固定的时间向cpu发出中断请求,而ucos的一些功能都是在定时器的中断服务程序里实现的。所以定时器是操作系统必须的硬件,有了定时器操作系统才有了生命。才能实现实时性!!!

因此,我将通过两条线路来讲解ucos操作系统的执行流程:

一:正常的执行流程

二:被定时器中断后的中断服务程序

需要注意的是这两条线路是交叉执行的,因为定时器中断是周期性的,所以不论正常程序执行到什么地方定时器中断都有可能发生。其实,就是因为这种打断性才使操作系统的多任务调度有了基础。这个当你充分从整体的角度理解了ucos操作系统,你就知道了。

 

前面说了,ucos主要管理的是处理机的调度,那调度什么那?其实一个程序可能因为某些原因不能一直占有处理机,比如说他在等待一个资源,它再占用处理机那就相当于占着茅坑不拉屎。所以在有操作系统的时候,就可以有多个程序在不同的时间里占用cpu,这样有执行环境的程序,我们给他取个名字叫任务或者进程。而ucos就是决定什么时候该那个任务运行的。

所以,在分析源码之前还是要说明一个重要的概念,那就是任务(进程)。

相信大家对计算机能同时干这么多活这一现象可能存在疑问,那是什么机制让我们的电脑做到了真正的三心二意那,那当然属操作系统了。它管理着我们的电脑到底该干什么工作了?上一个工作干到什么程度了。我们也知道程序是一段有顺序的解决问题的指令。我们的机器里面只有一个CPU,但可能有很多解决问题的程序,他们肯定要争夺CPU这个资源,怎么管理他们,让他们有条不紊的运行下去那。这是我们今天要讨论的问题,怎么样,你有什么好的想法吗?

其实,这个问题也很简单,比如咱们现在先不说什么程序代码。我们先说一个类似的问题。

让你带着5个小孩玩,你的任务就是看护这5个小孩,让他们不打架,不受伤害,玩的开心。

而你现在只有一个游戏机,而你现在就面临着这样一种情况,怎么利用这一个游戏机让所有的孩子都能玩好又不产生矛盾。

要是我解决这个问题的话,我可能会这样去做,跟孩子们说:因为只有一个游戏机,你们5个只能轮流着玩这一个游戏机,没人玩半小时,那至于谁先玩的问题,我们这样,今天谁先来的这里谁就先玩(即谁今天来的早谁就先玩)。好了,这个问题到现在为止解决了。不,到真正实施的时候可能会遇到一些问题,比如一个孩子玩够了半个小时,是让他自己自愿提出来我玩的时间到了还是有你在旁边计时并强制让其给另一个孩子玩。还有每个孩子玩的游戏进度都不一样,你可能还要保存每一个孩子玩的进度问题,因为每一个孩子都只关心自己玩的游戏而不关心别的事情。当然,还有一些其他的问题,一个孩子特别不懂事,非要提前玩游戏机,你还要考虑这个问题。当然,在实施过程中还有很多问题,你能想一想还有其他什么问题可能在这个过程中遇到吗?

呵呵,看似一件很简单的事情,做起来还真有难度。其实很多事情都是这样,在一件事情还没有真正做之前做一些计划和方案是很难的,因为你不知道会遇到什么临时情况。但我们一般会做计划,并且要考虑所有可能遇到的情况以及遇到这些情况的处理方案。

好了,闲言少叙,回归到我们要处理很多程序争夺一个CPU的问题。显然,程序没有小孩那么智能,它不能听懂一些规则。因此,我们必须把这些规则具体化,即用程序代码去实现这些规则。因为计算机只认识程序。

好了,上面一直说程序,这个很好理解,也就是一个算法。那这个算法怎么才能体现出它的具体价值那,恩,这是一个事。比如,你写了一个程序计算两个10以内的两个数的加法。那你的目的很明确就是输入两个10以内的数,而得到这两个数的结果。你可以通过计算机的输入设备来进行输入,可以利用输出设备来进行输出结果。而程序在计算机运行一遍的这个过程所产生的效果是我们想要的。那把程序中的代码一一运行产生效果的这个过程,原来那一段静止的代码就一步步动起来了。当然它运行起来了肯定会需要一些环境,这些环境当然是计算机硬件提供的了,关于程序运行到底需要哪些具体的环境,我们下面具体探讨。那我们给这个正在在计算机内部执行的这个程序起个名字。叫什么那?

别人已经起好了,叫进程了,或者任务。

现在大家明白什么叫进程了吧,其实就是一段代码加上这段代码在计算机内部运行时所需要的所有环境。不管现在它运行没运行,一段程序加上可在计算机内部运行的能力,我们给他起了个名字叫进程(可能大概的意思是正在运行的程序吧)。

这是我对进程(任务)的理解。这是在计算机操作系统中对底层最基本的一个抽象。在上面说的带小孩玩的例子中,每一个小孩就是一个进程(任务)。我会在下一个章节中具体以UCOS为例,具体讲解怎么高效的管理这些小家伙(进程或者任务)。

好了,下面说一下进程中所要包含的具体硬件环境,咱们就以arm920t为实例,说明在这上面运行的程序到底需要哪些环境,他们这些叫进程的小家伙生活在什么环境下那?我想你一定很想知道的。

我们都知道的,arm920t有运行模式,我们就选其中一个模式来说。

R0~R12这些寄存器是他们生活的必需品,还有sp(r13)堆栈指针,lr(r14)the linker register ,还有程序计数器PC(r15),还有就是当前程序运行状态寄存器CPSR,以及SPSR。

这些寄存器就是程序执行时的所有硬件环境。其实也很简单,哈。

你想一想,如果你把一个程序在某一时刻打断了,但是你把这些寄存器都保存到一个地方了,你下次再把这些内容都恢复到各个寄存器里面,那这个程序就按照原来被打断的地方开始执行喽。就相当于把这个小生命给暂定了,静止了,然后在一个合适的时候它又恢复了原来的生活。呵呵呵。

好,现在我们理解了什么是进程以及它生活的环境。那么只要我们能记住每个进程的状态,我们就可以管理他们了。你想管理他们那就要首先了解他们,并且能记忆他们被打断时的状态。这样我们就能恢复这个任务的所有状态了。如果我们有很好的调度算法,那就可以很好的管理他们啦。另外,我们的操作系统还要记录进程的运行状态,在ucos里包括,运行态,就绪态,阻塞态等状态,这样有利于管理这些进程。

下面说说操作系统一般是怎么记忆这些东西的,那就要定义一个数据结构用来记忆这些信息。这个东西起个名字叫任务控制块(TCB,task contorl block)。

我们看看这个ucos的任务控制块的结构:

我们跳过那些宏条件编译,只说明核心的部分。

typedef struct os_tcb {

    OS_STK          *OSTCBStkPtr;  /* Pointer to current top of stack */              

//这个是任务的堆栈的栈顶指针。我说一下,在ucos中每个任务都有自己的堆栈空间,用于保存程序运行中函数调用的情况(这点可以从c语言执行和堆栈这里知道栈的重要性)以及当发生任务切换时保存任务执行的状态。也就是那些寄存器的内容在发生任务切换时就都入到自己任务的堆栈里去。

#if OS_TASK_CREATE_EXT_EN > 0

    void            *OSTCBExtPtr;   /* Pointer to user definable data for TCB extension */

    OS_STK          *OSTCBStkBottom; /* Pointer to bottom of stack*/                                  

    INT32U           OSTCBStkSize; /* Size of task stack (in number of stack elements) */            

    INT16U           OSTCBOpt;      /* Task options as passed by OSTaskCreateExt()*/                 

    INT16U           OSTCBId;       /* Task ID (0..65535) */                                         

#endif

 

    struct os_tcb   *OSTCBNext;  /* Pointer to next     TCB in the TCB list*/                     

    struct os_tcb   *OSTCBPrev;  /* Pointer to previous TCB in the TCB list*/                     

//这两个是任务控制块指针,最后这些任务的控制块是链接成双向链表的,以便于查找和修改任务控制块。

#if OS_EVENT_EN

    OS_EVENT        *OSTCBEventPtr;    /* Pointer to event control block*/                              

#endif

 

#if ((OS_Q_EN > 0) && (OS_MAX_QS > 0)) || (OS_MBOX_EN > 0)

    void            *OSTCBMsg; /* Message received from OSMboxPost() or OSQPost()*/             

#endif

 

#if (OS_VERSION >= 251) && (OS_FLAG_EN > 0) && (OS_MAX_FLAGS > 0)

#if OS_TASK_DEL_EN > 0

    OS_FLAG_NODE    *OSTCBFlagNode;    /* Pointer to event flag node*/                                  

#endif   

    OS_FLAGS         OSTCBFlagsRdy;    /* Event flags that made task ready to run*/                      

#endif

 

INT16U           OSTCBDly;  /* Nbr ticks to delay task or, timeout waiting for event*/

//这个是延时的ticks数量,ticks是每秒定时器产生的中断数量,一般是每秒1000个ticks。也就是每秒产生1000个定时器中断。这个变量是用来记忆一个任务延时的ticks的,比如一个任务自己想延时1s钟,那这个变量就要赋值1000.

INT8U            OSTCBStat;        /* Task status */  

//任务的状态,用于记忆任务是运行态,就绪态,还是……                                             

    BOOLEAN    OSTCBPendTO; /* Flag indicating PEND timed out (TRUE == timed out)*/

INT8U            OSTCBPrio;        /* Task priority (0 == highest, 63 == lowest) */

//这个是任务优先级,这个ucos是根据优先级进行调度的,优先级是我们创建任务时赋给任务的一个优先顺序值,在ucos中这个数值越小,优先级越高,比如优先级为3的任务和优先级为8的任务都就绪,那ucos首先运行任务3.  关于ucos高效的任务调度算法我们将在以下文章中详细讲解。            

    INT8U         OSTCBX; /* Bit position in group  corresponding to task priority (0..7) */

    INT8U            OSTCBY;/* Index into ready table corresponding to task priority */     

    INT8U            OSTCBBitX;      /* Bit mask to access bit position in ready table */  

    INT8U            OSTCBBitY;/* Bit mask to access bit position in ready group*/ 

//上述四个变量是和高效率调度任务有关的。

#if OS_TASK_DEL_EN > 0

    INT8U            OSTCBDelReq;/* Indicates whether a task needs to delete itself*/ 

#endif

 

#if OS_TASK_PROFILE_EN > 0

    INT32U       OSTCBCtxSwCtr;    /* Number of time the task was switched in */       

    INT32U  OSTCBCyclesTot;   /* Total number of clock cycles the task has been running*/   

    INT32U   OSTCBCyclesStart; /* Snapshot of cycle counter at start of task resumption*/      

    OS_STK  *OSTCBStkBase;     /* Pointer to the beginning of the task stack */          

    INT32U    OSTCBStkUsed;     /* Number of bytes used from the stack */

#endif

#if OS_TASK_NAME_SIZE > 1

    char             OSTCBTaskName[OS_TASK_NAME_SIZE];

#endif

} OS_TCB;

关于这个任务控制块,这里只是做一下简单的说明,如果有什么不理解的地方没有关系,只要知道定义这个TCB结构体的目的就行。我们会在下面的程序分析中详细地说明的。

前面老是说任务切换和任务调度,我说一下这两个概念的区别,任务调度就是决定要执行那个任务。比如现在有优先级为2,15,34的任务就绪,那任务调度要找出优先级最高的任务,即任务2,这个任务将要运行。而任务切换就是在合适的时间里将当前正在运行的任务保存进度,转而去执行调度程序得到的最高优先级的任务2。这也就是切换的真正含义。

 

下面我们到OSInit()这个函数内部去看看它具体怎么初始化的,

下面贴代码:

void  OSInit (void)

{

#if OS_VERSION >= 204

    OSInitHookBegin();  /* Call port specific initialization code   */

#endif

    OS_InitMisc();  /* Initialize miscellaneous variables       */

 

    OS_InitRdyList();   /* Initialize the Ready List                */

 

    OS_InitTCBList();      /* Initialize the free list of OS_TCBs      */

 

OS_InitEventList();    /* Initialize the free list of OS_EVENTs    */

 

……………………

  OS_InitTaskIdle(); 

 

看代码就知道这个文件实现了对全局杂项变量的初始化,就绪表的初始化,任务控制块的初始化,初始化空闲任务等工作,当然还有其他的初始化操作,这些和我们的操作系统核心内容关系就不是很多了,我们尽量精简这个ucos,只展现其核心内容,让大家明白ucos操作系统是怎么实现对处理机调度的。即多任务实时操作系统到底是怎么实现的。

 

所谓ucos的初始化就是对那些核心的全局变量进行清零工作,使其处于可知的状态。

OSIntNesting  = 0;           /* Clear the interrupt nesting counter      */

//这个全局变量是用于记录中断嵌套层数的

OSLockNesting = 0;       /* Clear the scheduling lock counter          */

//这个全局变量是给任务上锁的标志

OSTaskCtr     = 0;       /* Clear the number of tasks                */

//这个变量用于记录系统中的任务数量

OSRunning     = FALSE;   /* Indicate that multitasking not started     */

//这个变量用于标志ucos的运行状态的

OSCtxSwCtr    = 0;       /* Clear the context switch counter         */

//这个变量用于记录系统进行任务切换的次数的

OSIdleCtr     = 0L;       /* Clear the 32-bit idle counter            */

//这个是空闲任务用于计数的变量。空闲任务是这样一种任务,在没有任何可执行的用户任务的时候,就要执行空闲任务,它什么也不干,只是让OSIdleCtr这个变量加一。因为cpu在加电后就不能停止执行指令,即是用户没有需要执行的任务,那也要执行一个空闲任务,只要cpu是按照我们的意愿去执行程序,那什么就都在我们的掌握之中,是吧。

下面开始初始化就绪表的相关变量。

 

static  void  OS_InitRdyList (void)

{

    INT8U    i;

    INT8U   *prdytbl;

 

    OSRdyGrp      = 0x00;       /* Clear the ready list                     */

    prdytbl       = &OSRdyTbl[0];

    for (i = 0; i < OS_RDY_TBL_SIZE; i++) {

        *prdytbl++ = 0x00;

    }

//上述代码是把就绪表的相关变量清零。

    OSPrioCur     = 0;

//这个变量是当前任务的优先级

    OSPrioHighRdy = 0;

//这个是当前最高的优先级

    OSTCBHighRdy  = (OS_TCB *)0; 

//最高优先级的任务控制块                             

    OSTCBCur      = (OS_TCB *)0;

//当前优先级的任务控制块

}

 

// 设计就绪表有两个目的:一,记录那些任务就绪。二,从这些就绪的任务中找出优先级最高的。就绪表是和任务调度有关系的数据结构,关于它的详细情况,在下面进行详细讲解。

static  void  OS_InitTCBList (void)

{

    INT8U    i;

    OS_TCB  *ptcb1;

    OS_TCB  *ptcb2;

 

 

OS_MemClr((INT8U *)&OSTCBTbl[0],sizeof(OSTCBTbl));/* Clear all the TCBs */ 

//对任务控制块进行清零操作

OS_MemClr((INT8U *)&OSTCBPrioTbl[0], sizeof(OSTCBPrioTbl)); /* Clear the priority table */

// 对任务控制块优先级指针索引进行清零操作

    ptcb1 = &OSTCBTbl[0];

    ptcb2 = &OSTCBTbl[1];

    for (i = 0; i < (OS_MAX_TASKS + OS_N_SYS_TASKS - 1); i++) {  /* Init. list of free TCBs            */

        ptcb1->OSTCBNext = ptcb2;

#if OS_TASK_NAME_SIZE > 1

        ptcb1->OSTCBTaskName[0] = '?'; /* Unknown name    */

        ptcb1->OSTCBTaskName[1] = OS_ASCII_NUL;

#endif

        ptcb1++;

        ptcb2++;

}

//以上代码是把原来申请的TCB控制块的数组,链接成一个单向链表,这样有利于对TCB的操作。

ptcb1->OSTCBNext = (OS_TCB *)0;   /* Last OS_TCB */

//最后一个TCB的next指针指向空

#if OS_TASK_NAME_SIZE > 1

    ptcb1->OSTCBTaskName[0] = '?';  /* Unknown name */

    ptcb1->OSTCBTaskName[1] = OS_ASCII_NUL;

#endif

OSTCBList  = (OS_TCB *)0;     /* TCB lists initializations          */

// OSTCBList 指向空,OSTCBList是创建后任务的任务控制块的头

OSTCBFreeList           = &OSTCBTbl[0];

//空闲任务控制块的链表头

 

}

 

我们对ucos初始化就是对ucos中的全局变量进行清零操作。在我们平时所写的程序里,可以动态分配内存,那是有操作系统的支持。现在我们写的是操作系统,因此不能用动态方式分配内存,ucos是这么做的,分配一些全局变量,然后为了好管理这些全局变量,我们把这些变量做成链表。当然我们申请的全局变量是结构体,结构体里有指向下一个结构体的指针,我们初始化只是把一些变量链接成链表的形式。

 

Ucos中对全局变量的初始化就这些,不好呀,突然出现这么多变量,都不知道他们是干什么的?他们之间有什么关系?这没事,我下面对这些变量以及他们的作用,和他们以后的用途,还有他们之间的关系做一个详细的说明。

主要分为以下几个核心部分:

1,  关于TCB的来龙去脉,一开始的逻辑结构以及到最后的逻辑结构。怎么说这个TCB是整个ucos的核心数据,用于控制任务的唯一数据。也是任务在ucos中存在的唯一标示。

2,  关于就绪表和任务调度

3,  其他几个核心变量的相关内容。

 

有了以上对这些数据的逻辑结构上的认识以及他们的作用,再加上前面咱们讲解的相关内容,我想我们在阅读源代码时就有了一定的思路。按照正常路线和中断路线两条路线去理解。

 

我们对这个TCB应该不陌生了,它是一个记录任务的相关信息的一个结构体。这个TCB空间一开始是程序通过数组的形式申请的全局变量存储空间,然后在ucos的初始化中做成一个单项链表,且它的链表头为OSTCBFreeList,当我们创建一个任务需要TCB时就从这个由TCB组成的线性链表上摘除一个,然后进行TCB的初始化,最后创建的任务的TCB连接成一个双向链表,且有一个以任务优先级为索引的TCB指针,这样设计数据结构的目的是方便通过优先级进行查找相应的TCB。

一开始,空闲TCB的状态如图:

 

当创建了几个任务之后TCB的状态如下:

我们先不看创建的任务是什么,而且这个图是从书上截取的,把这张图放到这个地方的原因是让我们知道OSTCBPrioTbl这个数组,以及TCB之间是怎样一种逻辑结构。OSTCBPrioTbl这个数组是一个索引数组,用于优先级的快速查找TCB。比如你知道一个优先级为3,那OSTCBPrioTbl[3]里的指针指向的就是优先级为3的任务控制块。那么再看这些TCB控制块是什么逻辑结构,这些控制块连接成双向链表,这个双向链表的头是OSTCBList。

对TCB 这个控制块的整体结构要有一个宏观的了解,这样我们看源码时才不会一头雾水,没有头绪。

关于就绪表,我要详细的说明。

就绪表是用于记录哪些任务是就绪状态的,创建就绪表的目的有两个:

1,  记录哪些任务是就绪态

2,  通过就绪表查找优先级最高的任务

 

首先,我们要先介绍一下OSRdyGrp和OSRdyTbl这两个东西,OSRdyGrp这个是一个无符号的八位数,OSRdyTbl是一个数组,是由八个八位无符号数组成的一个数组。这两个东东是干什么的那,前面说了,我们要知道系统中有哪些任务处于就绪态,那就需要记忆,要记忆就需要存储空间。那具体要记忆哪些信息那,如果一个任务被创建,我们既需要知道这个任务是否处于就绪态,还要知道这个就绪的任务优先级是多少。Ucos支持64个优先级,我们可以定义一个数组xx[64]来记录那个任务就绪了,比如优先级为15的任务就绪了,我们可以把xx[15]置一表示优先级为15的任务就绪了,置0表示任务未就绪。当我们有很多任务就绪时,这个数组中就有很多被置一,查找优先级最高的(在ucos中就是数值最小的)任务,那一种方法就是从数组的下标0开始扫描,当第一次遇到1时的数组下标,这个优先级的任务就是当前最高的优先级的任务。这也就实现了上述的两个目的,当然还有其他很多方法,我刚才想的这个方法有两个缺点,一是这样有点浪费空间,二是这样寻找最高优先级的算法复杂度有点高,不太符合实时调度的要求。咱们看看ucos的作者Jean J.Labrosse是怎么解决的这个问题。首先他用的是位变量来表示任务就没就绪,即用变量的每一位的数值是0还是1来表示任务就没就绪的状态。用某一位置一说明这个任务就绪,置零说明这个任务未就绪。关键问题是怎么在记录这两种状态的同时又能记录这个优先级的数值。使用这样一个方案,申请有64个位的数组,如果申请的变量是无符号8位的,那么我们就要申请xx[8],每个变量8位,数组共8个变量,因此有64个二进制位,用每一位的0和1表示就没就绪,同时被置一的位本身也就代表着数值的大小。看看下面的图:

这些是64个位变量,中间的我省略了,然后那,从右边往左边编号(从0编到63),你比如说优先级为15的任务就绪,你就可以把从左边数(当然从零开始),数到第15个位时把这个位置一即可。这64个位的空间怎么来的那,是申请了一个包括8个无符号数的数组,其数组下表是0~7.在c语言中也就是把这64个位空间,分为了8组,每组一个8位无符号变量,共8个变量。我们在c语言中对每一位置位时就可以先看这个优先级按照这种方法应该在第几组里置位,然后再确定在这个组里的具体位置。我们知道优先级变量OSTCBPrio是一个无符号的8位数。咱们就以一个具体事例来说明怎么利用这个方法来实现对相应的位置一。比如现在有一个优先级为12的任务就绪,把这个十进制的数值化为二进制为00001100,你把低三位看成是组里的第几个数的话,那高三位就代表这个优先级的任务所在的组号。(其中低三位是红色颜色标注的,高三位是蓝色颜色标注的),那么我们就知道这个优先级的任务属于001(即第一组),在组内的位置是100(即第四个)。这样我们就很容易用代码实现对某一固定的位置一操作。至于代码的真正实现,我们到分析代码时在具体分析。仅看到这些,Jean J.Labrosse的实现方法也就是节约了内存空间而已,但精华不止这些,还有另外一个很重要的问题就是怎么从这些就绪的任务中迅速找出优先级最高的来。上面介绍了利用OSRdyTbl这个数组如何实现记录任务的优先级和是否就绪的状态。现在来看一下OSRdyGrp这个变量,这个变量用来记忆那组里有就绪的任务。比如第三组里有就绪的任务,那这个变量的第三位的位变量就置一。这两个变量的逻辑结构如下:

那好,我们前面也已经知道了我们用OSRdyTbl这个数组的每一位是0还是1来表示这个任务是否就绪。用这个1处于的位置(从左向右数的位变量的个数)来表示这个优先级的大小。

那怎么得到最高优先级那(在ucos里是数值最小的),当然是利用OSRdyTbl这个数组,前面说的是创建任务时对其进行置位操作,现在就利用已经置位的OSRdyTbl这个数组来找到最小的优先级,方法很简单,就是从左到右扫描当遇到第一个1时的位变量的位置的数值就是当前的最高优先级。当然我们不是直接在OSRdyTbl数组里直接线性扫描的。先看OSRdyGrp这个变量,看看那个组里有就绪的任务,同样的方法遇到第一个一的位的位数即为最高优先级的任务所在的组号。所在的组号乘以8再加上在组里的位置,即得到当前优先级最高的任务的优先级。但现在的问题是我怎么根据OSRdyGrp和OSRdyTbl[]的值得到他们第一个1是第几位哪。只有建立一个索引表,这个表以他们的数值为索引,以他们的值中从左边数第一个1的位置为内容。即   

你比如说你的OSRdyGrp的值最后是00110110的第一个1的位置是1,第零个数是0,第一个数是1.

而根据这个表,数组下标为0x46的内容是1.

 

根据上面讲的这些内容,你就能实现记录任务优先级的值和状态,还有就是快速的实现查找就绪任务中优先级最高的任务。

上面说的有点乱,要真正理解这个机制还需要看源码或者是看《嵌入式实时操作系统ucos-ii》。下面对这个机制做一个总结。

我们的两个目的:一,记录优先级及优先级的状态。二,在就绪的任务中实现快速查找优先级最高的任务。

作者所用方法中的精华:

1,  利用位变量存储任务状态,节约存储空间。同时把优先级的数值用申请的连续空间的位的位置来表示。

2,  在进行置位、清零操作时,利用分组的概念实现。因为64个连续的位空间是由8个8位的无符号数组成的,其下标是0~7,然后任务的优先级也可以看成是第几组的第几个。比如:00011100就可以看成是第三组的第四个优先级。

3,  为了实现快速查找,作者又增加了一个变量OSRdyGrp,某一位置一表示这一组内有就绪的任务,否则没有就绪任务。比如,OSRdyGrp的值为00000011就表示第0组和第一组都有就绪任务,在这种情况下肯定第0组的任务优先级高。也就是说第一个遇到1的位的位置代表的是优先级高的任务所在的组。同样的道理,OSRdyTbl[0]的值假设是00110010这个数第一个位置是1(都是从0开始数)。那就代表第零组的第一个任务就绪了,而且是优先级最高的。现在看来,最主要的问题是找第一个1,因为它的位置代表的优先级最高(也即代表的数值最小,其他的数值肯定比这个数值大)。为了快速的知道OSRdyGrp和OSRdyTbl[OSRdyGrp]第一个1在什么位置,我们建立了一个数组OSUnMapTbl[256],这个数组的下标是无符号8位变量的所有值,即OSRdyGrp或者OSRdyTbl[OSRdyGrp]的所有取值情况,而数组的内容是这个数的第一个1的位的位置(从右边数)。这样根据OSRdyGrp和OSRdyTbl[OSRdyGrp]的值就直接可以通过查表OSUnMapTbl[256]和简单的计算就能的到最高优先级的数值了。计算方法是通过OSRdyGrp这个索引查到的数值乘以8(因为每组八个)加上通过OSRdyTbl[OSRdyGrp]这个索引查表得到的数。这个方法是利用了空间换取时间效率的高效算法,O(1)算法,算法复杂度不变,无论有多少任务就绪(当然现在是64个)。

 

上面就是对ucos任务调度算法的一个分析。

 

下面说一下其他几个全局变量:

OSTaskCtr记录的是操作系统中创建任务的数量。

OSCtxSwCtr是操作系统任务切换的次数

OSIdleCtr是当空闲任务运行时这个变量持续加一。用这个变量可以计算cpu的利用率,具体方法可以参考《嵌入式实时操作系统,ucos-ii》的第三章的统计任务有关章节。

OSIntNesting是记录中断嵌套层数,进入一层中断这个值就加一,推出中断后就减一。

这样,我们就对这个ucos操作系统有了一个宏观的认识,到现在为止,我们的ucos已经初始化完毕,下一步我们就该创建任务了,当创建完任务后,开启ucos的多任务调度后,ucos就开始真正运行。下一部分就主要讲解ucos创建任务到多任务开始调度运行的所有过程。

 

写到后面突然发现一个很重要的问题没有说明,那就是有关开中断和关中断的相关问题。

在ucos内部,当处理一些共享性的资源时我们要实现互斥访问,即对这些共享资源要进行保护。比如说ucos对TCB进行操作时,你要保证你的操作代码要对TCB有完全的读写权限,还要保证只有你这一段代码在操纵TCB。要实现对TCB的互斥访问,以避免因竞争共享资源而产生的错误。要实现对共享资源的互斥访问,方法有很多,在ucos中主要使用的是关中断的方法。在ucos定义了两个宏; OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()。

这两段代码都是用汇编书写的,其实开关中断很简单,就是对CPSR的I、F位置一实现关闭中断。

在这里可能有一个地方挺难理解,那就是这个关中断的代码是可嵌套的。那出现了两个问题:

关中断可嵌套有什么意义那?怎么实现的中断可嵌套那?

先说一下中断嵌套的意义,

其实大家分析一下,所谓保护临界段代码就是关中断,在操作系统内部本身就是关中断、然后开中断。这是没有问题的,但大家想一下,操作系统给用户提供了一些编程接口,当用户希望用同样关中断的方法来保护共享数据时。比如用户写的代码如下:

关中断

调用系统服务

用户其他操作

开中断

大家想一下,本来用户想保存从用户关中断和开中断这些代码的,不过你调用了操作系统的服务,如果你是用的方法是不可嵌套的,那在退出操作系统代码时已经开中断了。违背了用户的愿望。所以我们操作系统提供可嵌套的关 开中断的方法。这种嵌套是针对高层应用程序的。在操作系统内部这个嵌套是没有任何意义的。在操作系统内部一般在使用临界资源之前关中断,在使用完毕后开中断,OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()都是成对使用的。

下面分析中断嵌套是怎么实现的:

先看两个宏的定义:

#define  OS_ENTER_CRITICAL()  { cpu_sr = INTS_OFF(); }

#define  OS_EXIT_CRITICAL()   { if(cpu_sr == 0) INTS_ON(); }

再看INTS_OFF()这个函数的内容:

INTS_OFF

   mrs   r0, cpsr          ; current CSR//读取当前CPSR的值

   mov   r1, r0           ; make a copy for masking//把CPSR的值复制给r1

   orr   r1, r1, #0xC0     ; mask off int bits  //屏蔽中断

   msr   CPSR_cxsf, r1   ; disable ints (IRQ and FIQ)//把屏蔽中断后的内容重新写会CPSR

   and   r0, r0, #0x80     ; return IRQ bit from original CSR//返回CPSR中原来的CPSR中I位的状态,由前面的ATPCS知识我们知道,函数的返回值在R0中

   mov   pc,lr             ; return

这个关中断的函数执行完毕后有两个作用,一:实现了关闭中断。二:记忆CPSR中原来的I位的状态,并将其保存在cpu_sr变量中。

开中断的代码中INTS_ON()这个函数的内容就不多说了,很简单,就是读取CPSR的值改写后重新写回CPSR中。开中断的宏定义的代码中if(cpu_sr == 0) INTS_ON();,首先判断cpu_sr的值,若其值为0,开中断,否则并不开中断。其值为0说明你调用OS_ENTER_CRITICAL()这个关闭中断之前CPSR是开中断的,即你只关闭了一次中断。若你嵌套关闭中断,在里层的开中断并不执行。例如:

你在你的应用程序里定义了一个变量cpu_sr1用来存放你关中断时返回的值。

Ucos内核定义的变量是cpu_sr,用来存放你关中断的返回值。

其代码如下:

cpu_sr1 = INTS_OFF();//你的应用程序要关闭中断,如果这是第一次关闭中断,那cpu_sr1的值是0

OS_ENTER_CRITICAL();//ucos内核又关闭了一次中断,这时cpu_sr的值肯定是1

OS_EXIT_CRITICAL();// 由于cpu_sr的值肯定是1,开中断不执行

其他用户代码

if(cpu_sr1 == 0) INTS_ON();//开中断

 

 

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值