您的位置:首页 > 编程语言 > C语言/C++

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

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

1.时间片轮转调度

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

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

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

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

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

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

#define  INT_CLOCK          10000    // 每个定时中断的时钟数 (如果12M的话10000即10ms)
#define  OS_CLOCK          (0 - INT_CLOCK)
主程序中添加启动定时器的代码:

TMOD|=0X01;
TH0=(OS_CLOCK)/256;
TL0=(OS_CLOCK)%256;

ET0=1;
TR0=1;
EA=1;
再写一个定时器中断函数,给定时器寄存器不断装数就可以了。

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

void timer0_int() interrupt 1 using OS_REGISTERBANK	//使用第RTX_REGISTERBANK组寄存器(不写的话默认为using 0,一部分R还要压栈)
{
union
{
uint8  tmp[2];
uint16 temp;
}
OS_PROCE;			//union分拆字节

idata uint8		OS_SAVEPSW;
idata uint8		OS_SAVEACC;

EA=0;

OS_SAVEACC=ACC;
OS_SAVEPSW=DBYTE[SP];

OS_PROCE.temp=timer0_comm; 	//timer0事件入栈,执行timer0_comm

//注意在ACC和PSW之前,还有进入中断时的PC地址,这个一直存在堆栈里直到下次执行这个任务时弹出来
SP--; //ACC和PSW已经存储在RTX_SAVE里,这里即把它们抹掉了。(注意,由于不是push指令,需要的是先SP++,再对地址单元赋值)
DBYTE[SP]=OS_PROCE.tmp[1];
SP++;
DBYTE[SP]=OS_PROCE.tmp[0];

SP++;
DBYTE[SP]=OS_SAVEACC;		//原则就是,压什么,出什么。除了PC,还压了ACC和PSW,所以人工也要压ACC和PSW

SP++;
DBYTE[SP]=OS_SAVEPSW;

EA=1;
}



其中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器默认为大端模式,即高位在低地址,所以得到上述结果)

当然,上一篇日志中的

Task_Stack1[1] = (uint16) Task_1;
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.如果时间片用完,执行任务调度。

void timer0_comm()
{
static uint8 i = 0;

TR0=0;
TL0+=(OS_CLOCK+9)%256;   	//+9为TR0=0到TR0=1要用的时间
if(CY)TH0++;			//+9造成的进位
TH0+=(OS_CLOCK+9)/256;
TR0=1;

if(OS_TIMESHARING==0)    		 // 任务占用多少时间片。如果为0,不切换,一直执行一个任务
{
return;
}

if(++OS_TIME==TIMESHARING)
{
OS_TIME = 0;
if(++i>=3)i=0;
Task_Scheduling(i);	  //每个时间片结束后就是任务调度
}
}
为了定时更精确,计算了装载定时器所用的时间,即TR0=0到TR0=1要用的时间。并进行了补偿。

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

但是,这种任务切换其实有着巨大的问题,大家发现了吗?我们将在下一节中仔细分析。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐