如何编写一个简单的嵌入式操作系统 (2)时间片轮转
2014-02-10 22:52
489 查看
上篇日志最后给出了一个最简单的人工调度系统。在实际应用中,人工的调度很常见,但更为普遍的是操作系统自动的任务调度。这篇日志介绍一种最常见的自动调度,即时间片轮转法,在上一节的程序的基础上,添加一些函数,用C语言实现。
1.时间片轮转调度
时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。算法的模型如右图所示。
但是现在为了简单起见,我们可以把模型设置的尽量简单:没有任务优先级,不考虑任务的等待,阻塞等等形式。所有的任务一旦创建,就是就绪的,等待系统的调度。调度时,简单的按照任务号的顺序依次调度。
这种定时程序相信只要是用过单片机的人都不陌生。
先做一下宏定义,求出定时器要赋的值。
但是,我们还要往定时器中断函数中添加大量的内容,这容易使中断函数变得过长,难以维护,并且容易产生一些意向不到的后果。所以,我们把中断函数设置的尽量简短。把本应该在中断函数中执行的程序放在另一个函数中,然后,在中断函数中,要做的仅仅是利用上一节讲过的间接改变PC值方法,让程序切换到那个函数中继续执行。我们的中断函数写作如下形式:
其中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器默认为大端模式,即高位在低地址,所以得到上述结果)
当然,上一篇日志中的
另一个就是堆栈压栈的问题。查看编译的汇编文件,可以发现除了自动压栈的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的快中断模式就是这种思路)
将这些函数加到上一节的程序中,编译运行程序,可以看到每隔50ms,任务切换一次,循环执行。
但是,这种任务切换其实有着巨大的问题,大家发现了吗?我们将在下一节中仔细分析。
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,任务切换一次,循环执行。
但是,这种任务切换其实有着巨大的问题,大家发现了吗?我们将在下一节中仔细分析。
相关文章推荐
- 如何编写一个简单的嵌入式操作系统 (2)时间片轮转
- 如何编写一个最简单的嵌入式操作系统(1)简单任务调度
- 如何编写一个最简单的嵌入式操作系统(1)简单任务调度
- linux内核分析作业:操作系统是如何工作的进行:完成一个简单的时间片轮转多道程序内核代码
- Linux操作系统的简单指令及如何使用vim编写一个程序,然后使用gcc查看【预处理】、【编译】、【汇编】、【链接】各阶段文件的内容。
- 操作系统是如何工作的-------通过一个简单的时间片轮转多道程序内核代码分析
- Linux内核分析课程--完成一个简单的时间片轮转多道程序内核代码,理解操作系统是如何工作的
- 如何用FFmpeg编写一个简单播放器详细步骤介绍
- 如何使用Createjs来编写HTML5游戏(六)完成一个简单的打飞机游戏(上)
- 通过一个简单的时间片轮转多道程序内核代码,分析linux操作系统系统
- 如何编写并运行你写的简单“操作系统”
- 实现一个最简单的嵌入式操作系统
- 如何编写一个最简单的聊天程序?
- 如何编写一个简单的shell脚本.task3用到的脚本
- 如何使用libgdx编写一个简单的游戏(二)— 完善
- 实现一个最简单的嵌入式操作系统
- 如何用FFmpeg编写一个简单播放器详细步骤介绍(转载)
- Java入门篇(一)——如何编写一个简单的Java程序
- 教你如何使用java语言编写一个简单的SqlHelper类
- 如何写一个最简单的操作系统