微波EDA网,见证研发工程师的成长!
首页 > 研发问答 > 嵌入式设计讨论 > MCU和单片机设计讨论 > ICC编译下 AVR单片机堆栈结构

ICC编译下 AVR单片机堆栈结构

时间:10-02 整理:3721RD 点击:

                             AVR单片机堆栈结构
本文章由"WS"写作,如果你觉得还行请点一个赞.

本文章所有程序都是在ICCAVR上测试,在AVR STUDIO仿真查看结果。


背景:为什么我要在此研究AVR的堆栈机构,有何目的。一是为了完成UCOS在AVR单片机上移植;二是,学习单片机编程到一定的层次必要对单片机的堆栈结构有一定的了解,避免一些潜在的BUG产生。对汇编要能看懂,在某些时候我们需要对我们的C程序对应生成的汇编代码进行分析,查找隐藏很深的BUG。

一、     AVR有两个堆栈,一个为硬件堆栈(指针SP),一个软件堆栈(Y指针)。

首先了解一下AVR数据存储空间结构(仅片内) AVR单片机的数据存储器是线形的,从低地址到高地址依次是CPU寄存器区(32个通用寄存器),I/O寄存器区,数据存储区。  ICC编译器又将数据存储区划分为全局变量,软件堆栈区和硬件堆栈区三个空间。以16为例,32个通用寄存器+64个IO寄存器+1024字节RAM构成整个数据存储区,如下:
硬件堆栈区                        高地址
软件堆栈区                        
全局变量                          60H开始
I/O寄存器区                       (20H ~ 05FH)
CPU寄存器区                     低地址(R0(00H) ~ R31(1FH))

硬件堆栈:也就是大家所熟知的SP,在ICC编译器中,在连接用户主函数之前,会自动添加一段启动代码,在启动代码中SP被初始化指到RAM最高地址,如Atmega16就是SP = 0x45F。数据入栈时,指针减一,堆栈结构为向下生长模式。硬件堆栈用于保存函数调用及中断返回的地址。堆栈长度可以在ICC软件的Project->Options->Taiget->ReturnStack Siae设置,默认为30。函数每调用一层使用两个字节,保存返回地址。第一次调用_main函数使用了两个字节,加上要给中断预留2个字节,30个字节硬件堆栈你的函数嵌套最多只能有(30-2-2)/2 = 13 层。如果没有递归函数差不多能满足需求了。这个结构跟我们所学习的51单片机堆栈结构很相似,知识51是向上增长模式。我们可以想象一下,堆栈里面保存的是函数返回的地址,一旦这个地址被某种不可预料的操作修改,函数回来的路被修改,后果很严重,轻则跑到其他函数里去了,重则死机。
ICC自动连接的启动代码中,把SP指向了内存的最高处,完成一系列初始化工作以后,调用用户的主函数(main()),此时main函数的返回地址会入栈,如下:
这里只讨论了函数返回地址,我们似乎忽略了一些重要的东西,在微处理器运行中,AVR是如何处理局部变量的,以及中断的时候如何保存现场。
再来讨论一下我们所熟悉的两条指令:POP、PUSH。出栈与进栈指令,他们以SP为指针将数据压入堆栈与弹出堆栈,AVR支持这两条指令,通常用在中断程序做现场保护,32个通用寄存器+状态寄存器。我们先来理论计算一下,在ICC编译器里面,堆栈默认大小30个字节,若产生中断,有32个通用寄存器+状态寄存器需要保护,早已溢出我们的堆栈空间,为此ICC编译器并非用POP、PUSH来保护现场,硬件堆栈(SP)只用来保存函数返回地址。其他的数据都是用软件堆栈保存,用Y(R28,R29)指针指向栈顶。

软件堆栈:软件堆栈用Y指向栈顶,在启动代码中被初始化为 SP - (硬件堆栈大小+1)。
ICC编译器在中断函数中将32个通用寄存器+状态寄存器全部保存到软件堆栈去。而对于函数调用过程中对于局部变量。经过对汇编分析,得出以下结论(不一定全对):有两种方式:一是用通用寄存器做局部变量,在函数正式执行之前,将本函数所要用到的通用寄存器先保存到软件堆栈区,通过Y指针寻址,例如:“ST  -Y,R20 ”,将R20保存到软件堆栈区,在函数执行完成后,从软件堆栈区恢复自己用过的通用寄存器的原来值。注意这个地方不要理解错了,是被调函数需要用哪个寄存器就先保存哪个寄存器的原有值,然后再恢复寄存器值,不破坏调用者的数据;而不是调用者用了哪些寄存器,当它调用其他函数时就保存自己用过的寄存器。这样做可以节省资源与提高运行效率;二是直接在软件堆栈区分一块区域用作你的局部变量,通过修改Y指针分配地址空间,获取变量存储空间。一下几个小程序供大家参考:
C源程序:
int fun1(int dat1)
{
   int dat2;
   dat1 ++;
   dat2 = fun2(dat1);
   return dat2;
}
对应的汇编代码:
_fun1:
  dat2                 --> R10                    // 局部变量dat2分配到R10,R11
  dat1                 --> R20                    // 局部变量dat1分配到R20,R21
    00071 940E 010C CALL  push_xgset300C //  本函数使用了R10,R11,R20,R21,
                                                        //   现将数据入栈保存
    00073 01A8        MOVW  R20,R16
(0021) }
(0022) int fun1(int dat1)
(0023) {
(0024)    int dat2;
(0025)    dat1 ++;
    00074 5F4F      SUBI   R20,0xFF
    00075 4F5F      SBCI   R21,0xFF
(0026)    dat2 = fun2(dat1);
    00076 018A      MOVW   R16,R20
    00077 D003      RCALL  _fun2
    00078 0158      MOVW   R10,R16
(0027)    return dat2;
    00079 940C 0111 JMP   pop_xgset300C // 将R10,R11,R20,R21,取出

push_xgset300C:    //  R10,R11,R20,R21 存
    0010C 935A      ST  -Y,R21
    0010D 934A      ST  -Y,R20
    0010E 92BA      ST  -Y,R11
    0010F 92AA      ST  -Y,R10
    00110 9508      RET
pop_xgset300C:       //   R10,R11,R20,R21 取
    00111 90A9      LD  R10,Y+
    00112 90B9      LD  R11,Y+
    00113 9149      LD  R20,Y+
    00114 9159      LD  R21,Y+
    00115 9508      RET
   
再来看另一段代码:
C源程序:
int fun5(int dat1)
{
   int buf[20],i;
   float f = 12.3454;
   dat1 ++    ;
   for(i = 0;i<20;i++) buf++;
  
f += 1;
   return dat1;
}
对应的部分汇编代码:
_fun5:
  f                    --> Y,+40    //  局部变量f分配到Y指针+40后的四个字节   
  buf                  --> Y,+0     //  局部数组buf分配到Y指针后的四十个字节
  i                    --> R22      // 局部变量i分配到R22,R23
  dat1                 --> R20      // 局部变量dat1分配到R20,R21
    00099 940E 0116 CALL  push_xgsetF000//保存自己将使用的R20,R21,R22,R23
    0009B 01A8      MOVW   R20,R16
    0009C 97AC      SBIW   R28,0x2C
push_xgsetF000: //  R20,R21,R22,R23 入软件堆栈                           
    00116 937A      ST  -Y,R23
    00117 936A      ST  -Y,R22
    00118 935A      ST  -Y,R21
    00119 934A      ST  -Y,R20
    0011A 9508      RET

通过这两段程序分析,我们可以对ICC编译器对局部变量的处理有一定的了解。通用寄存器的读写速度比RAM读写要快,对于局部变量诺分配到寄存器对程序的执行效率将提高,但寄存器数量有限,当局部变量很多时,寄存器无法满足要求,只能分配到软件堆栈,ICC在分配局部变量是有限使用寄存器,在寄存器不能满足的情况下分配到软件堆栈区。在C语言里有个关键字“register”,告诉编译器这个变量会频繁使用,被修饰过的字符将优先分配寄存器。在这里强调一点,对于局部变量要尽可能的少定义,要避免定义数组局部变量,特别是在UCOS系统编程里面,局部变量越多你所要分配的堆栈空间就越多。

看了ICC对函数的局部变量处理,接下来看看在中断程序中,ICC是如何保护现场的。
先来看另一段代码:
C源程序:
#pragma interrupt_handler T2:4
void T2(void)
{      
   Delayms(100);
}

对应的汇编代码:
_T2:
    000DD 920A      ST  -Y,R0        // 首先通过Y指针将R0~R31(不包括R28,R29)
    000DE 921A      ST  -Y,R1            保存到软件堆栈区,保护现场。
    000DF 922A      ST  -Y,R2
    000E0 923A      ST  -Y,R3
    000E1 924A      ST  -Y,R4
    000E2 925A      ST  -Y,R5
    000E3 926A      ST  -Y,R6
    000E4 927A      ST  -Y,R7
   000E5 928A      ST  -Y,R8
    000E6 929A      ST  -Y,R9
    000E7 930A      ST  -Y,R16
    000E8 931A      ST  -Y,R17
    000E9 932A      ST  -Y,R18
    000EA 933A      ST  -Y,R19
    000EB 938A      ST  -Y,R24
    000EC 939A      ST  -Y,R25
    000ED 93AA      ST  -Y,R26
    000EE 93BA      ST  -Y,R27
    000EF 93EA      ST  -Y,R30
    000F0 93FA      ST  -Y,R31
    000F1 B60F      IN  R0,0x3F        // 保存状态寄存器
    000F2 920A      ST  -Y,R0
(0066)  }
(0067) }
(0068) #pragma  interrupt_handler T2:4
(0069) void T2(void)
(0070) {      
(0071)    Delayms(100);
FILE: <library>
    000F3 E604      LDI R16,0x64
    000F4 E010      LDI R17,0
    000F5 DF5B      RCALL  _Delayms
    000F6 9009      LD  R0,Y+             // 取出状态寄存器值
    000F7 BE0F      OUT 0x3F,R0
    000F8 91F9      LD  R31,Y+           // 将R0~R31全部取出,恢复现场
    000F9 91E9      LD  R30,Y+
    000FA 91B9      LD  R27,Y+
    000FB 91A9      LD  R26,Y+
    000FC 9199      LD  R25,Y+
    000FD 9189      LD  R24,Y+
    000FE 9139      LD  R19,Y+
    000FF 9129      LD  R18,Y+
    00100 9119      LD  R17,Y+
    00101 9109      LD  R16,Y+
    00102 9099      LD  R9,Y+
    00103 9089      LD  R8,Y+
    00104 9079      LD  R7,Y+
    00105 9069      LD  R6,Y+
    00106 9059      LD  R5,Y+
    00107 9049      LD  R4,Y+
    00108 9039      LD  R3,Y+
    00109 9029      LD  R2,Y+
    0010A 9019      LD  R1,Y+
    0010B 9009      LD  R0,Y+
    0010C 9518      RETI
通过上面的代码,ICC编译器并不是通过POP、PUSH指令来保护和恢复中断现场,而是通过Y指针把数据保存到软件堆栈区。Y(R28,R29)并不需要保存, 在整个程序中它都是指向软件堆栈的底端。在AVR中有着双堆栈指针,SP指向硬件堆栈,保存函数及中断返回地址。Y指向软件堆栈区,保存局部变量、寄存器和传递的参数。
    掌握这两个指针的意义,对于我们移植UCOS有非常重大的意义,才能理解任务堆栈结构的设置,才能调试程序。

Copyright © 2017-2020 微波EDA网 版权所有

网站地图

Top