新闻中心

EEPW首页 > 嵌入式系统 > 牛人业话 > 51单片机多任务操作系统的原理与实现

51单片机多任务操作系统的原理与实现

作者: 时间:2017-01-06 来源:网络 收藏

 

本文引用地址:https://www.eepw.com.cn/article/201701/342566.htm

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

  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会做一个"覆盖优化",即没有直接调用关系的函数的变量共用空间.由于不是同时使用,所以不会冲突,这对内存小的来说,是好事.

  但现在我们进入多任务的世界了,这就意味着两个没有直接调用关系的函数其实是并列执行的,空间不能共用了.怎么办呢?一种笨办法是关掉覆盖优化功能.呵呵,的确很笨.

  比较简单易行一个解决办法是,不关闭覆盖优化,但将那些在作用域内需要跨越任务(换句话说就是在变量用完前会调用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


上一页 1 2 3 下一页

关键词: 51 操作系统

评论


相关推荐

技术专区

关闭