如何编写一个简单的嵌入式操作系统 (2)时间片轮转

转载于http://blog.csdn.net/zds9204/article/details/18994853

上篇日志最后给出了一个最简单的人工调度系统。在实际应用中,人工的调度很常见,但更为普遍的是操作系统自动的任务调度。这篇日志介绍一种最常见的自动调度,即时间片轮转法,在上一节的程序的基础上,添加一些函数,用C语言实现。

1.时间片轮转调度

时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。算法的模型如右图所示。

但是现在为了简单起见,我们可以把模型设置的尽量简单:没有任务优先级,不考虑任务的等待,阻塞等等形式。所有的任务一旦创建,就是就绪的,等待系统的调度。调度时,简单的按照任务号的顺序依次调度。


2.时钟粒度与定时器中断

我们在使用单片机时,一般是用单片机的时钟周期(或者是指令周期)作为程序中最小的时间单位。在使用操作系统后,这种方法无疑太细致了些。如果把这个最小的时间单位划分的更大一点的话,管理起来会更方便。我们可以用定时器T0来做这种划分:每10000个时钟周期,定时器T0溢出一次。如果用12M晶振的话,这个时间正好是10ms。操作系统就把这个时间作为它的最小时间单位。换句话说,操作系统中,所有任务的执行时间都是它的整数倍。

这种定时程序相信只要是用过单片机的人都不陌生。

先做一下宏定义,求出定时器要赋的值。

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. #define  INT_CLOCK          10000    // 每个定时中断的时钟数 (如果12M的话10000即10ms)  
  2. #define  OS_CLOCK          (0 - INT_CLOCK)  
主程序中添加启动定时器的代码:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. TMOD|=0X01;       
  2. TH0=(OS_CLOCK)/256;  
  3. TL0=(OS_CLOCK)%256;  
  4.   
  5. ET0=1;  
  6. TR0=1;  
  7. EA=1;  
再写一个定时器中断函数,给定时器寄存器不断装数就可以了。

但是,我们还要往定时器中断函数中添加大量的内容,这容易使中断函数变得过长,难以维护,并且容易产生一些意向不到的后果。所以,我们把中断函数设置的尽量简短。把本应该在中断函数中执行的程序放在另一个函数中,然后,在中断函数中,要做的仅仅是利用上一节讲过的间接改变PC值方法,让程序切换到那个函数中继续执行。我们的中断函数写作如下形式:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. void timer0_int() interrupt 1 using OS_REGISTERBANK //使用第RTX_REGISTERBANK组寄存器(不写的话默认为using 0,一部分R还要压栈)  
  2. {  
  3.     union  
  4.     {  
  5.         uint8  tmp[2];  
  6.         uint16 temp;  
  7.     }  
  8.     OS_PROCE;       <span style="white-space: pre;">    </span>//union分拆字节  
  9.   
  10.     idata uint8     OS_SAVEPSW;  
  11.     idata uint8     OS_SAVEACC;  
  12.   
  13.     EA=0;  
  14.   
  15.     OS_SAVEACC=ACC;  
  16.     OS_SAVEPSW=DBYTE[SP];  
  17.   
  18.     OS_PROCE.temp=timer0_comm;  //timer0事件入栈,执行timer0_comm  
  19.   
  20.      //注意在ACC和PSW之前,还有进入中断时的PC地址,这个一直存在堆栈里直到下次执行这个任务时弹出来  
  21.     SP--; //ACC和PSW已经存储在RTX_SAVE里,这里即把它们抹掉了。(注意,由于不是push指令,需要的是先SP++,再对地址单元赋值)  
  22.     DBYTE[SP]=OS_PROCE.tmp[1];  
  23.     SP++;  
  24.     DBYTE[SP]=OS_PROCE.tmp[0];  
  25.   
  26.     SP++;  
  27.     DBYTE[SP]=OS_SAVEACC;<span style="white-space: pre;">       </span>//原则就是,压什么,出什么。除了PC,还压了ACC和PSW,所以人工也要压ACC和PSW  
  28.   
  29.     SP++;  
  30.     DBYTE[SP]=OS_SAVEPSW;  
  31.   
  32.     EA=1;  
  33. }  

 

其中DBYTE指令需要头文件 #include <absacc.h>

这里有几点特殊解释一下。

一个与大家分享一下用union拆分一个多字节变量的方法。很多朋友们用过A51汇编,把2字节操作数,拆分成高8位和低8位的形式,有一种很巧妙的方法。那就是把这个数赋给16位的DPTR寄存器,然后DPH和DPL两个8位寄存器的内容就是拆分后的结果。这里要介绍的方法与它异曲同工。先介绍一下union的特性:所有包含的元素共享同一段内存。也就如,DPTR和DPH,DPL本来就是一回事。我们再看上面代码有关union的部分:union 变量OS_PROCE包含了两部分:数组tmp[2],每个元素8位;整形temp,16位。我们把目标函数timer0_comm赋值给16位的OS_PROCE.temp时,8位数组tmp储存的值也发生了变化。其中tmp[0]存储的是temp的高位,tmp[1]存储的是temp的低位。这就是分拆后的结果。大家可以自己调试验证,对变量进行查看。(这里特别指出的是,51单片机指令本身是不区分大端小端的,但是Keil器默认为大端模式,即高位在低地址,所以得到上述结果)

当然,上一篇日志中的

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <span style="white-space: pre;">    </span>Task_Stack1[1] = (uint16) Task_1;  
  2.     Task_Stack1[2] = (uint16) Task_1 >> 8;  
也是一种拆分的办法。大家可能认为上一篇日志中的方法比较好,因为不需要定义变量。其实不然,这种用union拆分一个多字节变量的方法编译后用到的汇编指令更少,无疑是更迅速的。大家可以打开汇编文件进行验证。

另一个就是堆栈压栈的问题。查看编译的汇编文件,可以发现除了自动压栈的PC指针以外,ACC和PSW也进行了压栈。这是因为中断程序改变了ACC和PSW造成的。(无论是函数还是中断,压栈都是很灵活的,只有被改变且需要保存的寄存器才会压栈)所以,在函数返回时,出栈的不仅是PC,还有ACC和PSW。这在程序更改PC值的时候得到了体现。我们用目标PC替换原有的PC时,还要注意把ACC和PSC要填回原来的位置。总之一句话就是:什么压栈了,什么就会出栈。知道什么要出栈,就可以确定如何手动压栈。

using OS_REGISTERBANK 也是从出入栈角度考虑的。OS_REGISTERBANK宏定义为1,即使用单片机中的寄存器组1(当然也可以改为其他值,使用其他寄存器组)。如果不指明的话,默认为using 0,但是寄存器组0正在使用,在中断中还要使用的话,就要把其中一部分寄存器压栈。这就增加了响应时间。切换一组寄存器,就免除了这种问题,需要压栈的,除了PC,只有ACC和PSW了。(在更高级的处理器中,为了快速相应中断,寄存器都是分为两组的,一组在中断中使用,一组在平时使用,这样,中断时压栈的只有PC了,相应速度更快。比如ARM7的快中断模式就是这种思路)

3.每个时钟粒度都要执行的程序

上面 程序中的timer0_comm就是定时器中断里面要转跳到的程序的入口。这段程序主要有以下功能:1.重装定时器初值 2.检测任务剩余的时间 3.如果时间片用完,执行任务调度。

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. void timer0_comm()  
  2. {  
  3.     static uint8 i = 0;  
  4.   
  5.     TR0=0;  
  6.     TL0+=(OS_CLOCK+9)%256;      //+9为TR0=0到TR0=1要用的时间  
  7.     if(CY)TH0++;            //+9造成的进位  
  8.     TH0+=(OS_CLOCK+9)/256;  
  9.     TR0=1;  
  10.   
  11.       if(OS_TIMESHARING==0)    <span style="white-space: pre;">     </span> // 任务占用多少时间片。如果为0,不切换,一直执行一个任务  
  12.         {  
  13.             return;  
  14.         }  
  15.   
  16.      if(++OS_TIME==TIMESHARING)  
  17.      {  
  18.         OS_TIME = 0;  
  19.         if(++i>=3)i=0;  
  20.         <span style="white-space: pre;">    </span>Task_Scheduling(i);    //每个时间片结束后就是任务调度  
  21.      }  
  22. }  
为了定时更精确,计算了装载定时器所用的时间,即TR0=0到TR0=1要用的时间。并进行了补偿。

将这些函数加到上一节的程序中,编译运行程序,可以看到每隔50ms,任务切换一次,循环执行。


但是,这种任务切换其实有着巨大的问题,大家发现了吗?我们将在下一节中仔细分析。


  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值