只有延时服务的协作式的内核(DIY实时操作系统中)总结

来源:互联网 发布:什么软件赚钱 编辑:程序博客网 时间:2024/05/22 12:27

/*
   第四篇:只有延时服务的协作式的内核 

                    Cooperative Multitasking
   前后台系统,协作式内核系统,与占先式内核系统,有什么不同呢?
   记得在21IC上看过这样的比喻,“你(小工)在用厕所,经理在外面排第一,
   老板在外面排第二。如果是前后台,不管是谁,都必须按排队的次序使用
   厕所;如果是协作式,那么可以等你用完厕所,老板就要比经理先进入;
   如果是占先式,只要有更高级的人在外面等,那么厕所里无论是谁,都要
   第一时间让出来,让最高级别的人先用。”
*/

 

#include <avr/io.h> 
#include 
<avr/Interrupt.h> 
#include 
<avr/signal.h> 
unsigned 
char Stack[200];    //人工堆栈

register unsigned 
char OSRdyTbl           asm("r2");   //任务运行就绪表 
register unsigned char OSTaskRunningPrio  asm("r3");   //正在运行的任务 

#define OS_TASKS 3        //设定运行任务的数量 
struct TaskCtrBlock        //任务控制块 

  unsigned 
int OSTaskStackTop;  //保存任务的堆栈顶 
  unsigned int OSWaitTick;        //任务延时时钟 
}
 TCB[OS_TASKS+1]; 


//防止被编译器占用 
register unsigned char tempR4  asm("r4"); 
register unsigned 
char tempR5  asm("r5"); 
register unsigned 
char tempR6  asm("r6"); 
register unsigned 
char tempR7  asm("r7"); 
register unsigned 
char tempR8  asm("r8"); 
register unsigned 
char tempR9  asm("r9"); 
register unsigned 
char tempR10 asm("r10"); 
register unsigned 
char tempR11 asm("r11"); 
register unsigned 
char tempR12 asm("r12"); 
register unsigned 
char tempR13 asm("r13"); 
register unsigned 
char tempR14 asm("r14"); 
register unsigned 
char tempR15 asm("r15"); 
register unsigned 
char tempR16 asm("r16"); 
register unsigned 
char tempR17 asm("r17"); 



//建立任务(每个任务在其本身的人工堆栈里压了19个字节)
void OSTaskCreate(void (*Task)(void),unsigned char *Stack,unsigned char TaskID) 

  unsigned 
char i;  
  
*Stack--=(unsigned int)Task;        //将任务的地址低位压入堆栈
  *Stack--=(unsigned int)Task>>8;    //将任务的地址高位压入堆栈
     
  
*Stack--=0x00;            //R1 __zero_reg__             
  *Stack--=0x00;            //R0 __tmp_reg__ 
  *Stack--=0x80;            //SREG 在任务中,开启全局中断         
  for(i=0;i<14;i++)            //在 avr-libc 中的 FAQ中的 What registers are used by the C compiler? 
    *Stack--=i;                //描述了寄存器的作用     
  TCB[TaskID].OSTaskStackTop=(unsigned int)Stack;   
  
//将人工堆栈的栈顶,保存到TCB结构体数组中 
  OSRdyTbl|=0x01<<TaskID;    //任务就绪表相应位置1,表明处于就绪态
}
 

//开始任务调度,从最低优先级的任务TaskScheduler开始 
void OSStartTask()         

  OSTaskRunningPrio
=OS_TASKS;            //首先运行TaskScheduler任务
  SP=TCB[OS_TASKS].OSTaskStackTop+17;    //等号右边得到任务TaskScheduler的入口地址
  __asm__ __volatile__("reti" " ");    //ret和reti,它们都可以将堆栈栈顶SP的两个字节弹出来送入程序计数器PC中
}
 

//进行任务调度 
void OSSched(void
{  
   
//  根据中断时保存寄存器的次序入栈(压栈不是在人工堆栈里面,而是在avr芯片本来分配的堆栈里),模拟一次中断后,入栈的情况   
  __asm__ __volatile__("PUSH __zero_reg__ ");  //R1 
  __asm__ __volatile__("PUSH __tmp_reg__ ");    //R0  
  __asm__ __volatile__("IN   __tmp_reg__,__SREG__  ");  //保存状态寄存器SREG 
  __asm__ __volatile__("PUSH __tmp_reg__ "); 
  __asm__ __volatile__(
"CLR  __zero_reg__ ");  //R0重新清零 
  __asm__ __volatile__("PUSH R18 "); 
  __asm__ __volatile__(
"PUSH R19 "); 
  __asm__ __volatile__(
"PUSH R20 "); 
  __asm__ __volatile__(
"PUSH R21 "); 
  __asm__ __volatile__(
"PUSH R22 "); 
  __asm__ __volatile__(
"PUSH R23 "); 
  __asm__ __volatile__(
"PUSH R24 "); 
  __asm__ __volatile__(
"PUSH R25 "); 
  __asm__ __volatile__(
"PUSH R26 "); 
  __asm__ __volatile__(
"PUSH R27 "); 
  __asm__ __volatile__(
"PUSH R30 ");     
  __asm__ __volatile__(
"PUSH R31 "); 
  __asm__ __volatile__(
"PUSH R28 ");  //R28与R29用于建立在堆栈上的指针 
  __asm__ __volatile__("PUSH R29 ");  //入栈完成 
     
  TCB[OSTaskRunningPrio].OSTaskStackTop
=SP;            //将正在运行的任务的堆栈顶保存 

  unsigned 
char OSNextTaskID;                        //在现有堆栈上开设新的空间  
  for (OSNextTaskID = 0;                            //进行任务调度 
    OSNextTaskID < OS_TASKS && !(OSRdyTbl & (0x01<<OSNextTaskID));  //有已就绪任务就退出循环
    OSNextTaskID++);                                //注意分号位置,这里循环体只是那个For语句
    OSTaskRunningPrio = OSNextTaskID ;                //得到一个新任务,调度到就执行态

  cli();  
//关中断,保护堆栈转换
  SP=TCB[OSTaskRunningPrio].OSTaskStackTop;            //OSSched函数一执行完,就可以运行这个新任务
  sei(); 
     
    
//根据中断时的出栈次序     
  __asm__ __volatile__("POP  R29 ");     
  __asm__ __volatile__(
"POP  R28 ");         
  __asm__ __volatile__(
"POP  R31 ");     
  __asm__ __volatile__(
"POP  R30 ");     
  __asm__ __volatile__(
"POP  R27 ");     
  __asm__ __volatile__(
"POP  R26 ");     
  __asm__ __volatile__(
"POP  R25 ");     
  __asm__ __volatile__(
"POP  R24 ");     
  __asm__ __volatile__(
"POP  R23 ");     
  __asm__ __volatile__(
"POP  R22  ");     
  __asm__ __volatile__(
"POP  R21 ");     
  __asm__ __volatile__(
"POP  R20 ");     
  __asm__ __volatile__(
"POP  R19 "); 
  __asm__ __volatile__(
"POP  R18 "); 
  __asm__ __volatile__(
"POP  __tmp_reg__ ");        //SERG 出栈并恢复 
  __asm__ __volatile__("OUT  __SREG__,__tmp_reg__ ");
  __asm__ __volatile__(
"POP  __tmp_reg__ ");        //R0 出栈 
  __asm__ __volatile__("POP  __zero_reg__ ");      //R1 出栈 
  
//中断时出栈完成 
}
 

//使正在运行的任务自动放弃CPU,但不是进入就绪态
void OSTimeDly(unsigned int ticks) 

  
if(ticks)                                    //当延时有效
  
    OSRdyTbl 
&= ~(0x01<<OSTaskRunningPrio);    //任务运行就绪表相应位清0       
    TCB[OSTaskRunningPrio].OSWaitTick=ticks;//设置任务延时时钟
    OSSched();                                //从新调度 
  }
 
}
 


void TCN0Init(void)    // 计时器0 

  TCCR0 
= 0
  TCCR0 
|= (1<<CS02);  // 256预分频 
  TIMSK |= (1<<TOIE0); // T0溢出中断允许                   
  TCNT0 = 100;         // 置计数起始值 (递增,往255计数)   
}
 


SIGNAL(SIG_OVERFLOW0) 

  unsigned 
char i; 
  
for(i=0;i<OS_TASKS;i++)            //任务时钟 
  
    
if(TCB[i].OSWaitTick)  
    

      TCB[i].OSWaitTick
--
      
if(TCB[i].OSWaitTick==0)        //当任务时钟到时,必须是由定时器减时的才行 
      {   
        OSRdyTbl 
|= (0x01<<i);        //使任务在就绪表中置位    
      }
 
    }
 
  }
 
  TCNT0
=100
}
 

void Task0() 

  unsigned 
int j=0
  
while(1
  
{             
    PORTB
=j++
    OSTimeDly(
2); 
  }
 
}
 



void Task1() 

  unsigned 
int j=0
  
while(1
  

    PORTC
=j++
    OSTimeDly(
4); 
  }
 
}
 

void Task2() 

  unsigned 
int j=0
  
while(1
  

    PORTD
=j++;    
    OSTimeDly(
8); 
  }
 
}
 

// 任务调度任务(相当于Task3),一般优先级最低
void TaskScheduler() 
{  
  
while(1
  
{         
     OSSched();      
//反复进行调度 
  }
 
}
 


int main(void
{     
  TCN0Init(); 
  OSRdyTbl
=0
  OSTaskRunningPrio
=0
  OSTaskCreate(Task0,
&Stack[49],0); 
  OSTaskCreate(Task1,
&Stack[99],1); 
  OSTaskCreate(Task2,
&Stack[149],2); 
  OSTaskCreate(TaskScheduler,
&Stack[199],OS_TASKS); 
  
//经过上面四个语句后,建立了四个任务,都处于就绪态
  OSStartTask(); 
}
 

 

/*
   在上面的例子中,一切变得很简单,三个正在运行的主任务,都通过延时服务,主动放弃对CPU的控制权。
   在时间中断中,对各个任务的的延时进行计时,如果某个任务的延时结束,将任务重新在就绪表中置位。
   最低级的系统任务TaskScheduler(),在三个主任务在放弃对CPU的控制权后开始不断地进行调度。如果
   某个任务在就绪表中置位,通过调度,进入最高级别的任务中继续运行。
*/

按函数执行顺序的详细流程文字说明:
主函数main()
 OSTaskCreate()
 函数建立了4个任务,分别是task0,task1,task2和TaskScheduler,在这个函数中,我们做了三件主要的事情
 1、我们先把每个任务的函数地址分别压到它相对应的人工堆栈里面
 2、压完了任务函数地址,我们就把人工堆栈的栈顶指针赋给TCB结构体数组里面的OSTaskStackTop中,
    保存住(其实就是为了后面的寻找相应任务的函数地址)
 3、把每个任务都在任务就绪表的相应位置1,表示任务可以被调用得到CPU的能力之一。但其实还要靠任务的优先级,有
    点需要注意的是:任务就绪表中相应位越低,优先级越高,比如,bit0就比bit1低,bit0的那个任
    务优先级就高。优先级体现在任务调度函数OSSched()里面的任务调度for循环语句中。应该仔细体会。
 OSStartTask()
  准备开始执行任务,因为函数里面“OSTaskRunningPrio=OS_TASKS;”这里(只是这里)根本就没需要考虑优先级的情况,
  所以第一个准备执行(还没开始)的任务是TaskScheduler。
  第二句SP=TCB[OS_TASKS].OSTaskStackTop+17;等号右边得到任务TaskScheduler的入口地址,接着赋给SP。
  给它干吗,当然是有用了,你还记得“2人工堆栈”里面有一句话“ 对于ret和reti,它们都可以将堆栈栈顶的
  两个字节弹出来送入程序计数器PC中,一般用来从子程序或中断中退出。”,显然,那就是等接着的第三句去完
  成他的使命了。呵呵,第三句"reti"一完事,你的avrCPU就可以去程序计数器PC里面取地址,而这个地址呢,
  就正是任务TaskScheduler的函数地址,哈哈,这才是正式开始执行第一个任务TaskScheduler。

现在,你看,我们已经不用再看主函数里面的函数了,因为以后也不会涉及到。以后的任务就是靠任务调度了。我们接着来看吧

刚才不是任务TaskScheduler是执行态嘛,这个任务又去反复不断调用OSSched函数,OSSched是个关键哦,这个函数在
这个“4只有延时服务的协作式的内核.”里面应该是最主要的,操作系统要靠调度,调度就是你妈,吃饭的时候,给
你吃饭你就可以吃,不给你吃你就那里先撑着,还给"TMD"按孩子从大到小(优先级)来分,不过不会相信你妈有那么偏心。
OK,不说废话,我们接着讲。

调度任务里面的调度函数OSSched()
 OSSched()
  一大堆PUSH指令
  这里的一大堆PUSH汇编指令是为了保存一些通用积存器寄存器,也就是保护现场,不懂的,先得看看操作系统里面的书。
  中断就是这样处理的,和这个差不多。刚才我们不是运行了第一个任务TaskScheduler吗,这里保存就是任务TaskScheduler
  的现场,保存了大堆的寄存器。需要注意的是,这里PUSH的压可不是象建立任务OSTaskCreate()里面的把寄存器压到
  每人任务相应的人工堆栈里面,而是压到avr芯片里面专门定义的一片堆栈里面。
  接着TCB[OSTaskRunningPrio].OSTaskStackTop=SP;呵呵,你看了这里的SP是区别于人工堆栈里面的栈顶指针,一旦
  这里的SP赋给了TCB[OSTaskRunningPrio].OSTaskStackTop,那么相应的,此任务的OSTaskStackTop原本指向人工堆
  栈的栈顶指针就再也找不到,看OSTaskCreate()仔细对照一下吧。现在呢,我们OSTaskStackTop保存是正在运行的任务
  的栈顶指针。其实保存SP也应该算是保存现场的一部分工作。
       
  接着
  unsigned char OSNextTaskID;      //在现有堆栈上开设新的空间 
  for (OSNextTaskID = 0;       //进行任务调度
     OSNextTaskID < OS_TASKS && !(OSRdyTbl & (0x01<<OSNextTaskID));  //有已就绪任务就退出循环
     OSNextTaskID++);      //注意分号位置,这里循环体只是那个For语句
  OSTaskRunningPrio = OSNextTaskID ;    //得到一个新任务(有可能是原来的任务),调度到就执行态
  注释也都很清楚了,关键有一点就是“得到一个新任务(有可能是原来的任务)”这句。这个要等我后面就会提到了,这
  里暂时不会。
  for里面的循环查找任务就绪表里面优先级最高的任务,都是从bit0向高位开始查找的。很显然,在这之前,我们从来
  没有运行过task0,task1,task2,况且,他们在建立任务(见OSTaskCreate())的时候的就绪位都还是置1,具体
  OSRdyTbl = 0000_1111。在检查了bit0位后就跳出了循环,所以,我们得到了将可执行的任务task0。相应的,
  我们把此时的TCB[O].OSTaskStackTop赋给SP,OSTaskStackTop是等于指向人工堆栈里面的那个栈顶指针。哈哈,你
  有点明白了吗,我们为什么在OSTaskCreate里面压那么多次的栈,其实有有用的,只可惜,它就只用一次,一夜情,用
  完就走,哈哈。SP=TCB[OSTaskRunningPrio].OSTaskStackTop后接下来我们就开始大规模的弹栈了,这次(也只有这次,
  好好再想想,再想想),我们弹出的是人工堆栈里面的那些东西。哦,还是弹17次哦,呵呵,看看OSTaskCreate(),是
  不是也是压17次,想明白了吧。你看,弹了栈后,OSSched函数准备退出,那肯定也执行了"ret",呵呵,这不,SP不是
  给了程序计数器PC,所以,我们就可以从TaskScheduler跳到task0了
  提醒一下,弹栈的时候,我们只会弹一次人工堆栈里面的东西,也只有当这个任务(这个任务可能是task0,task1,task2,
  之一,但不会是TaskScheduler,看for循环里OSNextTaskID < OS_TASKS=3)第一次准备被执行的时候(搞清楚点吧,多看下)
  也就都弹出去了,以后也不会有了,所以,task0,task1,task2的人工堆栈最后只剩下任务入口地址在那里,它没有被弹出,
  但其实它也没什么用了,一旦运行过它本身的任务后,任务函数入口地址就无意义了(为什么无意义,注意一下任务函数
  里面的那个while(1)无限循环)。用一次后就无意义,不是个套吗,一次性,哈哈。

  说到这里,感觉写得真多,不知道能不能让你明白。呵呵,接着

  我们因为上面的调度,task0就开始执行了,我们看
  void Task0()
  {
   unsigned int j=0;
   while(1)
   {            
    PORTB=j++;
    OSTimeDly(2);
   }
  }
  如果我们把OSTimeDly(2);注释掉,那这个任务就永远在那里运行了,也不会被调度,因为OSTimeDly()函数里面有个OSSched()。
  没有它,就不会让其他任务得到运行的机会。
  OSTimeDly()
  OSTimeDly()函数会让任务自动放弃CPU,并且任务就绪表OSRdyTbl相应为清0,任务就暂时没有执行的权利了。OSRdyTbl = 00001110
  哦,只是暂时哦,什么时候有呢?那要看定时器/计数器的工作,也就是看定时器的那个中断服务程序。
  SIGNAL(SIG_OVERFLOW0)
  当每个任务时钟到时,使各自任务在就绪表中置位,这样任务就重新得到了运行的机会条件(还需要考虑优先级)之一了。

  刚才不是说到任务Task0通过OSTimeDly()-->OSSched()有进行了一次调度。
  这次调度有点象Task0任务被调度到一样。有一点不同的是,OSRdyTbl = 00001110,根据优先级,知道任务Task1会被调度到,
  调度的过程,比如说压栈,弹栈什么的同第一次调用Task0一样。
  依次类推,会接着执行到Task2,直到执行完........

  现在,你仔细想过没有,上面的过程

  TaskScheduler-->OSSched()-->Task0--->OSSched()-->Task1--->OSSched()-->Task2
  有可能不是这样吗?
  答案是有可能,不过是有可能,编写这个操作系统的人肯定考虑到这种状况了,所以没出现那个问题。
  问题出在哪里?就出现在void OSTimeDly(unsigned int ticks)函数里面的那个任务时钟ticks的设定上,期间,会出现很多情况。
  就用我们这个操作系统的的例子说一下吧
  有三个任务(不包括调度任务)执行,任务就绪表OSRdyTbl = 00001111
  Task0任务比较大,4.5秒 才能执行完这个任务,void Task0{ ...OSTimeDly(2秒);}
  Task1任务比较大,5.0秒 才能执行完这个任务,void Task1{ ...OSTimeDly(4秒);}
  Task2任务比较大,0.5秒 就能执行完这个任务,void Task2{ ...OSTimeDly(8秒);}
  现在,我执行了Task0,花掉了4.5秒,期间,我也让它OSTimeDly(2秒),OSRdyTbl = 00001110。当然,调用这个函数肯定不能是
  这样的,这里无非只是打个比方。就让它延时两秒。Task0执行完了,根据优先级,Task1将会被调用,但执行了Task1,它要执行5.0秒,
  在执行Task1的5秒期间,Task0延时的2秒早就完了,它会让任务就绪表中Task0的相应位置1,重新得到执行的可能。也正是它,你看,
  Task1执行完后,又去搜索任务就绪表OSRdyTbl,而此刻OSRdyTbl = 00001101,我们就又能执行Task0了,任务Task2没有执行的份!!
  接着,执行Task0需要4.5秒,但,Task1延时的那个4秒早就过了,它在任务Task0没执行完就已经得到就绪的机会了.......周而复始。
  任务Task2不管大小,永远也轮不到它去执行,死在那里看人家吃饭了。
  所以,这个ticks的设置在这个"只有延时服务的协作式的内核"是有点讲究的。

  调度任务里面的调度函数OSSched() 我曾经说过:
  OSTaskRunningPrio = OSNextTaskID ;  //得到一个新任务(有可能是原来的任务),调度到就执行态
  注释也都很清楚了,关键有一点就是“得到一个新任务(有可能是原来的任务)”这句。这个要等我后面就会提到了,这
  里暂时不会。
  你想到了答案没有?
  比如说
  Task0任务比较大,0.5秒 才能执行完这个任务,void Task0{ ...OSTimeDly(2秒);}
  Task1任务比较大,0.5秒 才能执行完这个任务,void Task1{ ...OSTimeDly(10秒);}
  Task2任务比较大,0.5秒 就能执行完这个任务,void Task2{ ...OSTimeDly(20秒);}
  Task0有一段是将在那里一直执行几次,当然,期间有有点延时的。

  其实,还有其他的一点问题,大家按这种思路去,也不会很难,就可能在有一点上不开窍而已,多看几次吧,如果你会用仿真,那就更好
  了,知道具体程序怎么走。我没用过,只是凭思路的。希望对大家有点用处

原创粉丝点击