×
单片机 > 单片机程序设计 > 详情

ucos系统的精华提炼

发布时间:2020-05-23 发布时间:
|
前言:线程不是什么神秘的东西,当你理解后你会有一种茅塞顿开的感觉,其实它本身就很简单。

  第一节:程序代码运行条件
  回想一下:
  1.一个链接过的程序由以下组成:代码段,只读数据段,可读写数据段。
  2.单片机上常使用的两个资源:Flash(只读),RAM(可读写)
  3.对于单片机,我们习惯于一种模式,代码段和只读数据放在FLASH上,可读写数据放在RAM的起始地址,栈从RAM中最高地址向下开始运行。
  4.处理器从代码段提取代码,有三种方式,顺序,跳转,调用。所有代码段必须放在正确的位置上。
  5.处理器上数据段是通过地址来处理数据,所以数据也必须放在正确的位置上。
  6.处理上临时变量通过栈指针的向下偏移来提取变量,所以临时变量的地址是不固定的。
  7.至于堆,那是C语言的技巧,不列入条件范围内。
  总结:1.程序运行需要代码段,数据段和栈区,而代码段和数据段都必须放在编辑时对应的地址上,只有栈区是可以设置的。那么在不使用临时变量地址作为计算因数的情况下,就算改变栈顶的位置,程序的运行结果相同。

  第二节:多程序运行原理
  多线程的原理其实很简单,系统为每个线程提供一片内存作为栈区。然后选取FLASH中一点作为线程代码的起始地址,最后调转到线程代码的起始地址开始执行。线程代码可以随意操作被分配的栈中的临时变量而不会干扰到任何其他线程。除非你的临时变量过大超过了分配的栈区,这个就要你使用线程的经验和感觉有关了,一般人都不会去仔细的计算使用多大临时变量空间。线程也有个缺点,就是线程在访问栈区以外的地址时,包括数据区,都会存在这样一种可能,多个线程同时读取和修改一个数据区中的数据时,就会发生边界现象,即多线程共管的数据。当然同时是相对的,相对我们的感觉,现在举例说明:A线程在从Addr地址处读取出数据data后,恰巧被另外一线程B交接,而B线程也读取了Addr地址处的data数据并修改了data的一位,然后...,当再次运行到A线程时,A线程也修改了data并写回到Addr地址处,这样就存在了一个bug,B线程修改的位就被A线程的写回覆盖了。这点其实我们不担心,因为系统一般都会提供很多避免这种现象的机制。
  如果你对多线程还是不了解的话,我估计你应该就是对栈的认识不够准确,可参考相关资料。


  第三节:ucos系统介绍
  关于ucos的广告部分我已经屏蔽,我直接进入正题,ucos是一个抢占式系统,抢占式是指任务以抢占式的方式来运行。把Ucos中的任务当成进程来理解是不恰当的,这会影响我们对Windows进程和Linux进程的理解。Ucos中的任务只能相当于线程的角色。Ucos内容包括两大部分,一个是系统部分:包括任务操作,时间操作,事件操作,内存操作,这些不随处理器的不同而不同。另外一个是接口部分:由汇编和C语言组成。提供任务切换函数,定时器接口,CPU寄存器保存和读取,及中断处理等硬件相关操作函数。

  第四节:创建ucos任务
  使用下面函数创建一个任务:
  INT8UOSTaskCreate(void(*task)(void*p_arg),void*p_arg,OS_STK*ptos,INT8Uprio);

  创建函数设置任务函数(任务代码首地址)和任务参数,分配栈顶(ptos),和优先级。Ptos的传入做法在可读写数据区分配一个数据OS_STKStk【size】.然后把stk最高地址传送给ptos。而stk数组就是默认的分配给任务的栈区,任务task运行后使用stk存储临时变量。
  另外,stk还有个缩水就是系统需要从stk最高位减去一部分空间用来存储寄存器信息,对于ARM是16个unsignedlong长度,用来存储该任务的r0-r15,CPSR.
  系统也会为每一个创建的任务分配一个任务控制块(TCB)。TCB管理者任务的状态和信息。另外还有一个TCB指针指向当前正在运行的任务。TCB控制着当前进程是否在运行,如果不是在运行是否是因为事件阻塞,当任务运行时,从什么地方找到上次运行时保存的信息等等。
  对于每个创建后或运行的任务,都有两个重要的部分,一个是TCB,一个是栈头(栈区顶上保留的空间)。任务开始调度的第一步就是找到该任务的TCB,然后从TCB中找到栈头地址,然后使用栈头保存的数据复制到CPU寄存器上和CPSR上,最后跳转到栈头上上次运行保存的地址处开始执行。当运行的任务被调度时,一样是首先找到TCB所指向的栈头,然后把CPU所有寄存器内容和CPSR及当前地址全部复制过去,再去找到另外一个被任务是应该运行的进程,然后调度那个进程。
  多任务的背景来自一点,其实我们写的大部分程序其实都有太多的延迟,对于没有系统的程序,真正执行效率(即不做无效循环)的时间可能只占到处理器运行的5%都不到。插入一句,如果你善于处理器编程的话,你看代码不应该只看到代码的长度,而是这段代码运行占用了多长时间,和占用哪些资源和多大空间。系统的引入会让我们重新认识任务运行时间,我们不希望程序长时间做无效循环,我们要利用这段时间去做其他的事,从而提高处理器的效率。所以不让认为系统会占用你的资源,系统会帮助你努力收回那95%以上效率。实际上收回全部资源是不可能的。这就看你如何使用架构,和你的任务级别了。每个项目可能都会不同。


  第四节:抢占式调度(ucos的经典)
  调度的意思就是从所有的任务队列中找到最应该运行的任务,然后运行该任务。而调度的方式决定了系统的性能。Ucos的经典就来自于它只用了一个数组采取了最单纯的行为来进行任务调度,同时也占用了最小的资源。所以,即使是8位处理器,很多人也会使用ucos系统。
  上面说过,ucos是抢占式调度。抢占式调度的概念就是,只有一个CPU,所有线程以抢占的方式占有CPU,然后运行任务,除非他主动让出,或他被其他任务抢占,否则,他会一直占用CPU.UCOS的抢占方式是比较优先级,每个任务都需要分配一个且唯一的优先级。每次调度就是比较所有任务的优先级,找到优先级最高的任务(这点其实不复杂,下段介绍),然后调度该任务并运行,最高优先级的任务需要自己主动退出,否则,永远是这一个在运行。当这个任务运行到延迟或等待事件时,系统函数就会把这个任务从运行队列屏蔽掉,然后重新调度,再次搜索最高优先级的任务,这样就找到了另外一个优先级的任务,然后运行该任务,到这个任务睡眠或等待事件时,也会睡眠,然后再次调度,这时,如果前面睡眠的最高优先级的任务被唤醒,那么他将也会被放到优先级队列中。否则,再进入下一个优先级。
  关于任务的队列,睡眠,运行等概念都是一种理解概念,实际上ucos在这点是很简单的,也是很经典的。现在说明下ucos的调度队列。首先,我们需要知道下面个数组是干什么用的。
  INT8UconstOSUnMapTbl[256]={
  0,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
  4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
  5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
  4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
  6,0,1,0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
  4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
  5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
  4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
  7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
  4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
  5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
  4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
  6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
  4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
  5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
  4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0
  };

  随意给一个8位数data,这个数组的作用就是以查表的方式最快的速度找到data数据中从低位到高位中为第一个为1的数的位置(bit0为0)。即代替下面的函数的作用。使用方法:prto= OSUnMapTbl[data]
  for (i=0; i<8; i++)
  {
   If ( data & (1<    Break;
  }
  理解了那个数组后我们再引入两个变量,
  OS_EXTINT8U OSRdyGrp;
  OS_EXTINT8U OSRdyTbl[OS_RDY_TBL_SIZE];
  每个任务的优先级对应于OSRdyTbl数组中的一个位。对应关系是OSRdyTbl[prio/8]中的的第(prio%8)位,(prio/8)和(prio%8)使用任务控制块TCB中的->OSTCBX和->OSTCBY表示。将OSRdyTbl数组中任务对应的位置置一表示该任务准备妥当,可参加抢占运行, OSRdyTbl数组中任务对应的位置为0表示该任务不存在,或该任务当前被阻塞无法运行。OSRdyGrp变量的作用是使用8个位依次对应OSRdyTbl数组的前8个字节,对第n位为0,代表 OSRdyTbl[n]全部为0,如果第n位为1,代表对应的OSRdyTbl[n]至少有一个为1;
  然后调度工具就开始使用下面机制来得到最高优先级的任务,即优先级号最低的那个任务
  y = OSUnMapTbl[OSRdyGrp]; 
  OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]);
  第一行代码,通过查表找到OSRdyTbl数组中不为0的最低的一个数组。然后通过第二条代码的OSUnMapTbl[OSRdyTbl[y]]);找到对应的OSRdyTbl[y]中的最低的一个是1的位置。然后与(y << 3)相加,就得到了OSRdyTbl数组中从低到高的最低的为1的1位的位置。即最高优先级任务的优先级,然后根据优先级找到对应的TCB进行调度。
  每次调度时都是关闭了前一个进程,因此ucos需要队列中至少有一个可运行的程序,为此UCOS制作了一个IDLE任务,这个任务优先级最低,在最高位,目的是所有任务进程都睡眠时,让系统仍然有任务可调,不至于崩溃。另外一个用作是统计CPU使用率。如果IDLE任务从没调用过,那就说明的任务抢占度过高,优先级高的任务有需要释放一些空间让优先级低的任务运行。

  第五节:UCOS的实时性能
  按我理解,UCOS的实时性能是一种设想,让所有的任务等处于等待信号阶段,当有中断触发时,执行中断处理函数,通过信号唤醒进程,来完成任务,完成后可以继续睡眠。即使有多个中断响应,只要中断函数能及时响应,那么任务就排着队来完成后续工作。这就是我的UCOS设想。
  所以,我们需要在两个地方调度,一个是中断,每次进入中断后关闭调度,但是允许信号唤醒任务,然后在最后一个中断嵌套完成后退出时进行调度,检测有没有被唤醒的可执行任务。另外一个就是定时中断。每次tick完成后都进行一次重调度。目的是当低优先级执行时没有释放资源,而高优先级的任务已被唤醒。特别是IDLE任务,除非你问它要,否则他不会给你释放资源的。

  第六节:事件处理
  UCOS的事件主要包括SEMAPHORE,Mutex,和Mbox,Q,使用起来都很简单,一般都只适用三个函数创建,挂起等待,释放。
  SEMAPHORE作用是当某个任务运行到必须得到某种资源时进行挂起等待资源满足,其他的任务或中断发送SEMAPHORE表示资源已经建立,你可以运行了,如果是任务在发送SEMAPHORE时发现有任务因此被挂起,会唤醒并调度到该任务上执行。
   Mutex有一种锁的概念,当得到一个东西后,就立马对其上锁,其他的任务就只能等待该任务完成后打开锁才能运行。这里有一个问题就是一旦低优先级的任务占用锁后而高优先级的就必须等待,而恰巧低优先级的又被中优先级的任务抢去执行就会发生,高优先级等低优先级,低优先级等中优先级的现象称为优先级翻转,所有Mutex有一个机制就是高优先级想得到锁的话就临时提高低优先级的优先级,使低优先级尽快完成完成释放锁。
  邮箱MBox基本和SEMPAPHORE相同,只是SEMPAPHORE被当做一个信号标志来传送,Mbox也可以被当做SEMPAPHORE使用,但是会返回一个地址指针。
  Q消息队列没有用过,看样子是首先初始化一个数组,然后对数组使用FIFO的方式发送和接受信件。
  我一般还会再加上一些原子读写函数atom_read/wirte,主要针对边界变量。其实很简单就是读取前关中断,读取后开中断而已。

  第七节:tick
   Tick是系统时间,他和定时的概念是不同的,如OSTimeDly (OS_TICKS_PER_SEC/100),实际上不是严格的延迟了OS_TICKS_PER_SEC/100秒,存在0-1/OS_TICKS_PER_SEC之间的误差。Tick相当于钟表在不停的跑,秒表变化的瞬间被称为tick,而我们是不可能从tick那一瞬间开始计时的。所以这是一个概念是要分清的。
   
  第八节:ucos的缺陷
  UCOS毕竟是一个小系统,甚至可以在8位处理器上运行,所以对于我们完成更复杂的任务和对系统效率更高的要求的话,它是存在一定的局限性的。如:
  1. 系统和应用,中断等关系密切,开发人员需要熟悉系统特性,如。任务被创建后是不能直接退出的,必须使用API函数销毁它。
  2. 调度方式过于单一,任务较少时可以达到平衡,任务较多时,高优先级的和低优先级的运行时间就会存在严重不平衡,并且会增加考虑调度问题。
  3. 缺少异步读取机制,如我想向串口发送数据,而此时串口缓存已满,我们就需要放弃资源调度其他任务。串口可以通过多开缓存来弥补,但是对于TCP,退出就需要至少等待下一个Tick,时间就显得有些长久了,这个机制其实我一直在考虑。 

  第九节:写后
  不喜欢LPC21xx和周立功的UCOS系统还有个原因就是LPC21xx的中断机制看起来不错,但实际上已经能够影响了我们代码的发挥。也可能我自己懒惰的原因,没有来及在LPC上改造ucos。周立功的中断函数使用__irq声明,这一点已经和上面第五节所说内容想违背。
  两外,周立功的关中断函数和开中断函数使用swi中断,我觉得是不如原版的较好。原版的函数是保存寄存器关中断函数和恢复寄存器内容。我本来考虑着周立功可能是考虑软中断可直接进入中断来避免中断干扰,而原版的在关中断函数中间仍有可能被中断,如下 
  MRSR0, CPSR;//复制CPSR,执行后可能被中断
  ORR R1, R0, #0xC0;//计算,也有可能被中断
  MSRCPSR_c, R1;//这个代码完成才真正关闭中断
  但后来相通之后,觉得周立功是多此一举,即使关中断前被中断也没有什么的,因为中断后它会原模原样的返回给你。还是不喜欢周立功的UCOS和LPC
  后来在三星的s3c2440上也架构了一个ucos,并且搭配了TFTP传输和TCP对话,感觉用起来要比LPC的好用很多。
  当然,这只是个人用法和感觉,每个芯片只要写好了软件应该也是不错的。下面稍微提下个人用法,我一般如下定义main函数
  int main(void)
  {
   OSInit();
   OSTaskCreate(MainTask,(void *)1,&MainTaskStk[MainTaskStkLengh-1], MainTaskPrio);
   OSStart(); 
   return 0;
  }
  直接创建一个MainTask任务,然后在MainTask中进行初始化硬件和创建任务,事件
  void MainTask(void *pdata)
  {
   u8 err,iLed=0;
   TargetInit(); 
   env_init();
   PrintMutux=OSMutexCreate(MutexPrintPrior,&err);
   OSTaskCreate (Consoler,(void *)0, &ConsolerStk[ConsolerStkLengh - 1], ConsolerPrio);
   OSTaskCreate(NetConsole,(void *)0, 
   &NetConsoleStk[NetConsoleStkLengh - 1],
   NetConsolePrio);
   while(1){
   iLed++;
   Led_Display(iLed);
   OSTimeDly (400);
   rtcDisplayTime();
   }

  }

关键字:ucos系统  线程 

『本文转载自网络,版权归原作者所有,如有侵权请联系删除』

热门文章 更多
C51 特殊功能寄存器SFR的名称和地址