磁动力电子网-雕刻机DIY论坛,单片机论坛,CNCDIY,DIYCNC

 找回密码
 加入磁动力

QQ登录

只需一步,快速开始

扫一扫,访问微社区

查看: 52972|回复: 191

教你自制51片上操作系统(更新到3)

    [复制链接]
发表于 2008-8-14 01:49:00 | 显示全部楼层 |阅读模式

这两天不知怎么的,开磁力的页面超慢,发不了帖.

相信很多人都为多个流程并行执行的问题搞得很头痛吧?很多人都是用状态机来解决,少数人则用操作系统来解决.操作系统可以使代码跟单任务时一样直观简洁,但附带的开销有点大,51这样的片子有点吃不消.我自已做了好几个操作系统,都是超轻量级的,其中最简单的一个,整个"操作系统"只有十几行代码,几十个字节,却能很好的支持多个任务并行.想知道是怎么做的吗?跟我来,玩玩改改.

有一点我必须申明:这篇贴子的目的,是教会大家如何现场写一个OS,而不是给大家提供一个OS版本.提供的所有代码,也都是示例代码,所以不要因为它没什么功能就说LAJI之类的话.如果把功能写全了,一来估计你也不想看了,二来也失去灵活性没有价值了


两个附件:

让初学者加更容易破除多任务并行机制的神秘感. cd8tJKri.rar (114.97 KB, 下载次数: 5540)
回复

使用道具 举报

发表于 2008-8-14 08:17:00 | 显示全部楼层
up & uP !

楼主辛苦,更多的期待
--------------------------------------------------------------------------
没位置了,借你的地盘发发贴,希望别介意,呵呵.


                          三.向操作系统迈进

完整代码:
GG2IIl2D.rar (38.77 KB, 下载次数: 1667)

回复 支持 反对

使用道具 举报

 楼主| 发表于 2008-8-14 01:51:00 | 显示全部楼层

                          用KEIL写多任务系统的技巧与注意事项

C51编译器很多,KEIL是其中比较流行的一种.我列出的所有例子都必须在KEIL中使用.为何?不是因为KEIL好所以用它(当然它的确很棒),而是因为这里面用到了KEIL的一些特性,如果换到其它编译器下,通过编译的倒不是问题,但运行起来可能是堆栈错位,上下文丢失等各种要命的错误,因为每种编译器的特性并不相同.所以在这里先说清楚这一点.
但是,我开头已经说了,这套帖子的主要目的是阐述原理,只要你能把这几个例子消化掉,那么也能够自已动手写出适合其它编译器的OS.

好了,说说KEIL的特性吧,先看下面的函数:

sbit sigl = P1^7;
void func1(){
 register char data i;
 i = 5;
 do{
  sigl = !sigl;
 }while(--i);
}

你会说,这个函数没什么特别的嘛!呵呵,别着急,你将它编译了,然后展开汇编代码再看看:

   193: void func1(){
   194:         register char data i;
   195:         i = 5;
C:0x00C3    7F05     MOV      R7,#0x05
   196:         do{
   197:                 sigl = !sigl;
C:0x00C5    B297     CPL      sigl(0x90.7)
   198:         }while(--i);
C:0x00C7    DFFC     DJNZ     R7,C:00C5
   199: }
C:0x00C9    22       RET     

看清楚了没?这个函数里用到了R7,却没有对R7进行保护!
有人会跳起来了:这有什么值得奇怪的,因为上层函数里没用到R7啊.呵呵,你说的没错,但只说对了一半:事实上,KEIL编译器里作了约定,在调子函数前会尽可能释放掉所有寄存器.通常性况下,除了中断函数外,其它函数里都可以任意修改所有寄存器而无需先压栈保护(其实并不是这样,但现在暂时这样认为,饭要一口一口吃嘛,我很快会说到的).
这个特性有什么用呢?有!当我们调用任务切换函数时,要保护的对象里可以把所有的寄存器排除掉了,就是说,只需要保护堆栈即可!

现在我们回过头来看看之前例子里的任务切换函数:

void task_switch(){
 task_sp[task_id] = SP;//保存当前任务的栈指针

 if(++task_id == MAX_TASKS)//任务号切换到下一个任务
  task_id = 0;

 SP = task_sp[task_id];//将系统的栈指针指向下个任务的私栈.
}

看到没,一个寄存器也没保护,展开汇编看看,的确没保护寄存器.


好了,现在要给大家泼冷水了,看下面两个函数:

void func1(){
 register char data i;
 i = 5;
 do{
  sigl = !sigl;
 }while(--i);
}
void func2(){
 register char data i;
 i = 5;
 do{
  func1();
 }while(--i);
}

父函数fun2()里调用func1(),展开汇编代码看看:
   193: void func1(){
   194:         register char data i;
   195:         i = 5;
C:0x00C3    7F05     MOV      R7,#0x05
   196:         do{
   197:                 sigl = !sigl;
C:0x00C5    B297     CPL      sigl(0x90.7)
   198:         }while(--i);
C:0x00C7    DFFC     DJNZ     R7,C:00C5
   199: }
C:0x00C9    22       RET     
   200: void func2(){
   201:         register char data i;
   202:         i = 5;
C:0x00CA    7E05     MOV      R6,#0x05
   203:         do{
   204:                 func1();
C:0x00CC    11C3     ACALL    func1(C:00C3)
   205:         }while(--i);
C:0x00CE    DEFC     DJNZ     R6,C:00CC
   206: }
C:0x00D0    22       RET     

看清楚没?函数func2()里的变量使用了寄存器R6,而在func1和func2里都没保护.
听到这里,你可能又要跳一跳了:func1()里并没有用到R6,干嘛要保护?没错,但编译器是怎么知道func1()没用到R6的呢?是从调用关系里推测出来的.
一点都没错,KEIL会根据函数间的直接调用关系为各函数分配寄存器,既不用保护,又不会冲突,KEIL好棒哦!!等一下,先别高兴,换到多任务的环境里再试试:

void func1(){
 register char data i;
 i = 5;
 do{
  sigl = !sigl;
 }while(--i);
}
void func2(){
 register char data i;
 i = 5;
 do{
  sigl = !sigl;
 }while(--i);
}

展开汇编代码看看:

   193: void func1(){
   194:         register char data i;
   195:         i = 5;
C:0x00C3    7F05     MOV      R7,#0x05
   196:         do{
   197:                 sigl = !sigl;
C:0x00C5    B297     CPL      sigl(0x90.7)
   198:         }while(--i);
C:0x00C7    DFFC     DJNZ     R7,C:00C5
   199: }
C:0x00C9    22       RET     
   200: void func2(){
   201:         register char data i;
   202:         i = 5;
C:0x00CA    7F05     MOV      R7,#0x05
   203:         do{
   204:                 sigl = !sigl;
C:0x00CC    B297     CPL      sigl(0x90.7)
   205:         }while(--i);
C:0x00CE    DFFC     DJNZ     R7,C:00CC
   206: }
C:0x00D0    22       RET     


看到了吧?哈哈,这回神仙也算不出来了.因为两个函数没有了直接调用的关系,所以编译器认为它们之间不会产生冲突,结果分配了一对互相冲突的寄存器,当任务从func1()切换到func2()时,func1()中的寄存器内容就给破坏掉了.大家可以试着去编译一下下面的程序:

sbit sigl = P1^7;
void func1(){
 register char data i;
 i = 5;
 do{
  sigl = !sigl;
  task_switch();
 }while(--i);
}
void func2(){
 register char data i;
 i = 5;
 do{
  sigl = !sigl;
  task_switch();
 }while(--i);
}

我们这里只是示例,所以仍可以通过手工分配不同的寄存器避免寄存器冲突,但在真实的应用中,由于任务间的切换是非常随机的,我们无法预知某个时刻哪个寄存器不会冲突,所以分配不同寄存器的方法不可取.那么,要怎么办呢?
这样就行了:

sbit sigl = P1^7;
void func1(){
 static char data i;
 while(1){
  i = 5;
  do{
   sigl = !sigl;
   task_switch();
  }while(--i);
 }
}
void func2(){
 static char data i;
 while(1){
  i = 5;
  do{
   sigl = !sigl;
   task_switch();
  }while(--i);
 }
}

将两个函数中的变量通通改成静态就行了.还可以这么做:

sbit sigl = P1^7;
void func1(){
 register char data i;
 while(1){
  i = 5;
  do{
   sigl = !sigl;
  }while(--i);
  task_switch();
 }
}
void func2(){
 register char data i;
 while(1){
  i = 5;
  do{
   sigl = !sigl;
  }while(--i);
  task_switch();
 }
}

即,在变量的作用域内不切换任务,等变量用完了,再切换任务.此时虽然两个任务仍然会互相破坏对方的寄存器内容,但对方已经不关心寄存器里的内容了.

以上所说的,就是"变量覆盖"的问题.现在我们系统地说说关于"变量覆盖".

变量分两种,一种是全局变量,一种是局部变量(在这里,寄存器变量算到局部变量里).
对于全局变量,每个变量都会分配到单独的地址.
而对于局部变量,KEIL会做一个"覆盖优化",即没有直接调用关系的函数的变量共用空间.由于不是同时使用,所以不会冲突,这对内存小的51来说,是好事.
但现在我们进入多任务的世界了,这就意味着两个没有直接调用关系的函数其实是并列执行的,空间不能共用了.怎么办呢?一种笨办法是关掉覆盖优化功能.呵呵,的确很笨.

比较简单易行一个解决办法是,不关闭覆盖优化,但将那些在作用域内需要跨越任务(换句话说就是在变量用完前会调用task_switch()函数的)变量通通改成静态(static)即可.这里要对初学者提一下,"静态"你可以理解为"全局",因为它的地址空间一直保留,但它又不是全局,它只能在定义它的那个花括号对{}里访问.
静态变量有个副作用,就是即使函数退出了,仍会占着内存.所以写任务函数的时候,尽量在变量作用域结束后才切换任务,除非这个变量的作用域很长(时间上长),会影响到其它任务的实时性.只有在这种情况下才考虑在变量作用域内跨越任务,并将变量申明为静态.
事实上,只要编程思路比较清析,很少有变量需要跨越任务的.就是说,静态变量并不多.

说完了"覆盖"我们再说说"重入".
所谓重入,就是一个函数在同一时刻有两个不同的进程复本.对初学者来说可能不好理解,我举个例子吧:
有一个函数在主程序会被调用,在中断里也会被调用,假如正当在主程序里调用时,中断发生了,会发生什么情况?

void func1(){
 static char data i;
 i = 5;
 do{
  sigl = !sigl;
 }while(--i);
}

假定func1()正执行到i=3时,中断发生,一旦中断调用到func1()时,i的值就被破坏了,当中断结束后,i == 0.

以上说的是在传统的单任务系统中,所以重入的机率不是很大.但在多任务系统中,很容易发生重入,看下面的例子:
void func1(){
....
delay();
....
}
void func2(){
....
delay();
....
}
void delay(){
 static unsigned char i;//注意这里是申明为static,不申明static的话会发生覆盖问题.而申明为static会发生重入问题.麻烦啊
 for(i=0;i<10;i++)
  task_switch();
}

两个并行执行的任务都调用了delay(),这就叫重入.问题在于重入后的两个复本都依赖变量i来控制循环,而该变量跨越了任务,这样,两个任务都会修改i值了.
重入只能以防为主,就是说尽量不要让重入发生,比如将代码改成下面的样子:
#define delay() {static unsigned char i; for(i=0;i<10;i++) task_switch();}//i仍定义为static,但实际上已经不是同一个函数了,所以分配的地址不同.
void func1(){
....
delay();
....
}
void func2(){
....
delay();
....
}

用宏来代替函数,就意味着每个调用处都是一个独立的代码复本,那么两个delay实际使用的内存地址也就不同了,重入问题消失.
但这种方法带来的问题是,每调用一次delay(),都会产生一个delay的目标代码,如果delay的代码很多,那就会造成大量的rom空间占用.有其它办法没?

本人所知有限,只有最后一招了:
void delay() reentrant{
 unsigned char i;
 for(i=0;i<10;i++)
  task_switch();
}
加入reentrant申明后,该函数就可以支持重入.但小心使用,申明为重入后,函数效率极低!

 

最后附带说下中断.因为没太多可说的,就不单独开章了.
中断跟普通的写法没什么区别,只不过在目前所示例的多任务系统里因为有堆栈的压力,所以要使用using来减少对堆栈的使用(顺便提下,也不要调用子函数,同样是为了减轻堆栈压力)
用using,必须用#pragma NOAREGS关闭掉绝对寄存器访问,如果中断里非要调用函数,连同函数也要放在#pragma NOAREGS的作用域内.如例所示:

#pragma SAVE
#pragma NOAREGS  //使用using时必须将绝对寄存器访问关闭
void clock_timer(void) interrupt 1 using 1 //使用using是为了减轻堆栈的压力
}
#pragma RESTORE

改成上面的写法后,中断固定占用4个字节堆栈.就是说,如果你在不用中断时任务栈深定为8的话,现在就要定为8+4 = 12了.
另外说句废话,中断里处理的事一定要少,做个标记就行了,剩下的事交给对应的任务去处理.

 

现在小结一下:

切换任务时要保证没有寄存器跨越任务,否则产生任务间寄存器覆盖.        使用静态变量解决
切换任务时要保证没有变量跨越任务,否则产生任务间地址空间(变量)覆盖.  使用静态变量解决
两个不同的任务不要调用同时调用同一个函数,否则产生重入覆盖.          使用重入申明解决

 

[此贴子已经被作者于2008-8-14 19:41:53编辑过]
回复 支持 反对

使用道具 举报

发表于 2008-8-14 10:22:00 | 显示全部楼层
好好的学学。
[此贴子已经被作者于2008-8-14 10:23:26编辑过]
回复 支持 反对

使用道具 举报

发表于 2008-8-14 12:22:00 | 显示全部楼层
学习,等下一篇
回复 支持 反对

使用道具 举报

发表于 2008-8-17 19:55:00 | 显示全部楼层

非常罕见的描述,感触之深,慢慢消化吸收。谢谢楼主。

回复 支持 反对

使用道具 举报

发表于 2008-8-15 10:07:00 | 显示全部楼层

受益非浅啊。谢谢了

回复 支持 反对

使用道具 举报

发表于 2008-8-18 12:14:00 | 显示全部楼层
我都服了   这么好的帖子 

有300多人看   就有这么点人顶

论坛成了潜水员的天下了
回复 支持 反对

使用道具 举报

发表于 2008-8-18 10:34:00 | 显示全部楼层
讲的太好了,谢谢楼主
回复 支持 反对

使用道具 举报

发表于 2008-8-19 09:53:00 | 显示全部楼层
顶,希望LZ再接再厉
回复 支持 反对

使用道具 举报

发表于 2008-8-19 09:53:00 | 显示全部楼层
太深,先做个记号备用。
顶起来。
回复 支持 反对

使用道具 举报

发表于 2008-8-20 09:23:00 | 显示全部楼层
受益非浅,记号
回复 支持 反对

使用道具 举报

发表于 2008-8-19 23:57:00 | 显示全部楼层
受益匪浅
回复 支持 反对

使用道具 举报

发表于 2008-8-20 19:38:00 | 显示全部楼层
很不错,但我基础差,还不能完全看懂,只能收藏了慢慢看!

回复 支持 反对

使用道具 举报

发表于 2008-8-22 21:30:00 | 显示全部楼层

楼主的这个帖子不错。继续!顶你!

回复 支持 反对

使用道具 举报

发表于 2008-8-22 17:59:00 | 显示全部楼层

哎,我懂不起,51上能上多任务系统?

回复 支持 反对

使用道具 举报

发表于 2008-8-23 09:36:00 | 显示全部楼层
好贴,可是我看不懂,好好记着,以后学学再看,
回复 支持 反对

使用道具 举报

发表于 2008-8-27 10:02:00 | 显示全部楼层
好贴一定要顶上去
回复 支持 反对

使用道具 举报

发表于 2008-8-27 08:36:00 | 显示全部楼层
不得不先顶下

谢谢楼主的无私分享!!!

好好看去
回复 支持 反对

使用道具 举报

发表于 2008-9-12 10:49:00 | 显示全部楼层
受益非浅,记号
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 加入磁动力

本版积分规则

QQ|小黑屋|手机版|Archiver|www.cdle.net 磁动力电子网 2001-2017 ( 粤ICP备10098153号

粤公网安备 44040402000001号

GMT+8, 2019-6-18 20:43

Powered by Discuz! X3.2 Licensed

© 2001-2013 Comsenz Inc.