首页 > 技术文章 > stm32内存知识点(转)

linxw-blog 2019-08-09 14:20 原文

一、STM32 堆栈大小详解 以及变量存储位置

栈增长和大端/小端问题是和CPU相关的两个问题.

1,首先来看:栈(STACK)的问题.

函数的局部变量,都是存放在"栈"里面,栈的英文是:STACK.STACK的大小,我们可以在stm32的启动文件里面设置,以战舰stm32开发板为例,在startup_stm32f10x_hd.s里面,开头就有:

Stack_Size      EQU     0x00000800

表示栈大小是0X800,也就是2048字节.这样,CPU处理任务的时候,函数局部变量做多可占用的大小就是:2048字节,注意:是所有在处理的函数,包括函数嵌套,递归,等等,都是从这个"栈"里面,来分配的.
所以,如果一个函数的局部变量过多,比如在函数里面定义一个u8 buf[512],这一下就占了1/4的栈大小了,再在其他函数里面来搞两下,程序崩溃是很容易的事情,这时候,一般你会进入到hardfault....
这是初学者非常容易犯的一个错误.切记不要在函数里面放N多局部变量,尤其有大数组的时候!

对于栈区,一般栈顶,也就是MSP,在程序刚运行的时候,指向程序所占用内存的最高地址.比如附件里面的这个程序序,内存占用如下图:


图中,我们可以看到,程序总共占用内存:20+2348字节=2368=0X940
那么程序刚开始运行的时候:MSP=0X2000 0000+0X940=0X2000 0940.
事实上,也是如此,如图:

 


图中,MSP就是:0X2000 0940.
程序运行后,MSP就是从这个地址开始,往下给函数的局部变量分配地址.

再说说栈的增长方向,我们可以用如下代码测试: 

//保存栈增长方向
//0,向下增长;1,向上增长.
static u8 stack_dir;

//查找栈增长方向,结果保存在stack_dir里面.
void find_stack_direction(void)
{
    static u8 *addr=NULL; //用于存放第一个dummy的地址。
    u8 dummy;               //用于获取栈地址 
    if(addr==NULL)    //第一次进入
    {                          
        addr=&dummy;     //保存dummy的地址
        find_stack_direction ();  //递归 
    }else                //第二次进入 
 {  
        if(&dummy>addr)stack_dir=1; //第二次dummy的地址大于第一次dummy,那么说明栈增长方向是向上的. 
        else stack_dir=0;           //第二次dummy的地址小于第一次dummy,那么说明栈增长方向是向下的.  
 }


这个代码不是我写的,网上抄来的,思路很巧妙,利用递归,判断两次分配给dummy的地址,来比较栈是向下生长,还是向上生长.
如果你在STM32测试这个函数,你会发现,STM32的栈,是向下生长的.事实上,一般CPU的栈增长方向,都是向下的.

2,再来说说,堆(HEAP)的问题.

全局变量,静态变量,以及内存管理所用的内存,都是属于"堆"区,英文名:"HEAP"
与栈区不同,堆区,则从内存区域的起始地址,开始分配给各个全局变量和静态变量.
堆的生长方向,都是向上的.在程序里面,所有的内存分为:堆+栈. 只是他们各自的起始地址和增长方向不同,他们没有一个固定的界限,所以一旦堆栈冲突,系统就到了崩溃的时候了.
同样,我们用附件里面的例程测试:



stack_dir的地址是0X20000004,也就是STM32的内存起始端的地址.
这里本来应该是从0X2000 0000开始分配的,但是,我仿真发现0X2000 0000总是存放:0X2000 0398,这个值,貌似是MSP,但是又不变化,还请高手帮忙解释下.
其他的,全局变量,则依次递增,地址肯定大于0X20000004,比如cpu_endian的地址就是0X20000005.
这就是STM32内部堆的分配规则.

3,再说说,大小端的问题.
大端模式:低位字节存在高地址上,高位字节存在低地址上 
小端模式:高位字节存在高地址上,低位字节存在低地址上

STM32属于小端模式,简单的说,比如u32 temp=0X12345678;
假设temp地址在0X2000 0010.
那么在内存里面,存放就变成了:
地址              |            HEX         |
0X2000 0010  |  78   56   43  12  |

CPU到底是大端还是小端,可以通过如下代码测试:
//CPU大小端
//0,小端模式;1,大端模式.
static u8 cpu_endian;

//获取CPU大小端模式,结果保存在cpu_endian里面
void find_cpu_endian(void)

 int x=1;
 if(*(char*)&x==1)cpu_endian=0; //小端模式 
 else cpu_endian=1;    //大端模式  
}
以上测试,在STM32上,你会得到cpu_endian=0,也就是小端模式.


3,最后说说,STM32内存的问题.
    还是以附件工程为例,在前面第一个图,程序总共占用内存:20+2348字节,这么多内存,到底是怎么得来的呢?
我们可以双击Project侧边栏的:Targt1,会弹出test.map,在这个里面,我们就可以清楚的知道这些内存到底是怎么来的了.在这个test.map最后,Image 部分有:
==============================================================================

Image component sizes

 


      Code (inc. data)   RO Data    RW Data    ZI Data      Debug   Object Name

       172         10          0          4          0        995   delay.o//delay.c里面,fac_us和fac_ms,共占用4字节
       112         12          0          0          0        427   led.o
        72         26        304          0       2048        828   startup_stm32f10x_hd.o  //启动文件,里面定义了Stack_Size为0X800,所以这里是2048.
       712         52          0          0          0       2715   sys.o
       348        154          0          6          0     208720   test.o//test.c里面,stack_dir和cpu_endian 以及*addr  ,占用6字节.
       384         24          0          8        200       3050   usart.o//usart.c定义了一个串口接收数组buffer,占用200字节.

    ----------------------------------------------------------------------
      1800        278        336         20       2248     216735   Object Totals //总共2248+20字节
         0          0         32          0          0          0   (incl. Generated)
         0          0          0          2          0          0   (incl. Padding)//2字节用于对其

    ----------------------------------------------------------------------

      Code (inc. data)   RO Data    RW Data    ZI Data      Debug   Library Member Name

         8          0          0          0          0         68   __main.o
       104          0          0          0          0         84   __printf.o
        52          8          0          0          0          0   __scatter.o
        26          0          0          0          0          0   __scatter_copy.o
        28          0          0          0          0          0   __scatter_zi.o
        48          6          0          0          0         96   _printf_char_common.o
        36          4          0          0          0         80   _printf_char_file.o
        92          4         40          0          0         88   _printf_hex_int.o
       184          0          0          0          0         88   _printf_intcommon.o
         0          0          0          0          0          0   _printf_percent.o
         4          0          0          0          0          0   _printf_percent_end.o
         6          0          0          0          0          0   _printf_x.o
        12          0          0          0          0         72   exit.o
         8          0          0          0          0         68   ferror.o
         6          0          0          0          0        152   heapauxi.o
         2          0          0          0          0          0   libinit.o
         2          0          0          0          0          0   libinit2.o
         2          0          0          0          0          0   libshutdown.o
         2          0          0          0          0          0   libshutdown2.o
         8          4          0          0         96         68   libspace.o          //库文件(printf使用),占用了96字节
        24          4          0          0          0         84   noretval__2printf.o
         0          0          0          0          0          0   rtentry.o
        12          0          0          0          0          0   rtentry2.o
         6          0          0          0          0          0   rtentry4.o
         2          0          0          0          0          0   rtexit.o
        10          0          0          0          0          0   rtexit2.o
        74          0          0          0          0         80   sys_stackheap_outer.o
         2          0          0          0          0         68   use_no_semi.o
         2          0          0          0          0         68   use_no_semi_2.o
       450          8          0          0          0        236   faddsub_clz.o
       388         76          0          0          0         96   fdiv.o
        62          4          0          0          0         84   ffixu.o
        38          0          0          0          0         68   fflt_clz.o
       258          4          0          0          0         84   fmul.o
       140          4          0          0          0         84   fnaninf.o
        10          0          0          0          0         68   fretinf.o
         0          0          0          0          0          0   usenofp.o

    ----------------------------------------------------------------------
      2118        126         42          0        100       1884   Library Totals  //调用的库用了100字节.
        10          0          2          0          4          0   (incl. Padding)   //用于对其多占用了4个字节

    ----------------------------------------------------------------------

      Code (inc. data)   RO Data    RW Data    ZI Data      Debug   Library Name

       762         30         40          0         96       1164   c_w.l
      1346         96          0          0          0        720   fz_ws.l

    ----------------------------------------------------------------------
      2118        126         42          0        100       1884   Library Totals

    ----------------------------------------------------------------------

==============================================================================


      Code (inc. data)   RO Data    RW Data    ZI Data      Debug  

      3918        404        378         20       2348     217111   Grand Totals
      3918        404        378         20       2348     217111   ELF Image Totals
      3918        404        378         20          0          0   ROM Totals

==============================================================================

    Total RO  Size (Code + RO Data)                 4296 (   4.20kB)
    Total RW  Size (RW Data + ZI Data)              2368 (   2.31kB)   //总共占用:2248+20+100=2368.
    Total ROM Size (Code + RO Data + RW Data)       4316 (   4.21kB)

==============================================================================

通过这个文件,我们就可以分析整个内存,是怎么被占用的,具体到每个文件,占用多少.一目了然了.

4,最后,看看整个测试代码:
main.c代码如下,工程见附件.
#include "sys.h"
#include "usart.h"  
#include "delay.h" 
#include "led.h" 
#include "beep.h"    
#include "key.h"    
//ALIENTEK战舰STM32开发板堆栈增长方向以及CPU大小端测试

//保存栈增长方向
//0,向下增长;1,向上增长.
static u8 stack_dir;

//CPU大小端
//0,小端模式;1,大端模式.
static u8 cpu_endian;

 


//查找栈增长方向,结果保存在stack_dir里面.
void find_stack_direction(void)
{
    static u8 *addr=NULL; //用于存放第一个dummy的地址。
    u8 dummy;               //用于获取栈地址 
    if(addr==NULL)    //第一次进入
    {                          
        addr=&dummy;     //保存dummy的地址
        find_stack_direction ();  //递归 
    }else                //第二次进入 
 {  
        if(&dummy>addr)stack_dir=1; //第二次dummy的地址大于第一次dummy,那么说明栈增长方向是向上的. 
        else stack_dir=0;           //第二次dummy的地址小于第一次dummy,那么说明栈增长方向是向下的.  
 }

//获取CPU大小端模式,结果保存在cpu_endian里面
void find_cpu_endian(void)

 int x=1;
 if(*(char*)&x==1)cpu_endian=0; //小端模式 
 else cpu_endian=1;    //大端模式  

int main(void)
{    
 Stm32_Clock_Init(9); //系统时钟设置
 uart_init(72,9600);   //串口初始化为9600
 delay_init(72);       //延时初始化 
 LED_Init();      //初始化与LED连接的硬件接口  
    printf("stack_dir:%x\r\n",&stack_dir);
    printf("cpu_endian:%x\r\n",&cpu_endian);
 
 find_stack_direction(); //获取栈增长方式
 find_cpu_endian();  //获取CPU大小端模式
  while(1)
 {
  if(stack_dir)printf("STACK DIRCTION:向上生长\r\n\r\n");
  else printf("STACK DIRCTION:向下生长\r\n\r\n");
  if(cpu_endian)printf("CPU ENDIAN:大端模式\r\n\r\n");
  else printf("CPU ENDIAN:小端模式\r\n\r\n"); 
  delay_ms(500);
  LED0=!LED0;  
 }  
}
测试结果如图:

二、.bss段,data段、text段、堆(heap)和栈(stack)

bss段:

  bss段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。

  bss是英文Block Started by Symbol的简称。

  bss段属于静态内存分配。

data段:

  数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。

  数据段属于静态内存分配。 

text段:

  代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。

  这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架构也允许代码段为可写,即允许修改程序)。

  在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

堆(heap):

  堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。

  当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);

  当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。

栈(stack):

   栈又称堆栈,是用户存放程序临时创建的局部变量,

  也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。

  除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。

  由于栈的先进先出(FIFO)特点,所以栈特别方便用来保存/恢复调用现场。

  从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。 


一个程序本质上都是由 bss段、data段、text段三个组成的。

  这样的概念,不知道最初来源于哪里的规定,但在当前的计算机程序设计中是很重要的一个基本概念。

  而且在嵌入式系统的设计中也非常重要,牵涉到嵌入式系统运行时的内存大小分配,存储单元占用空间大小的问题。

    在采用段式内存管理的架构中(比如intel的80x86系统),bss段通常是指用来存放程序中未初始化的全局变量的一块内存区域,

  一般在初始化时bss 段部分将会清零。bss段属于静态内存分配,即程序一开始就将其清零了。

    比如,在C语言之类的程序编译完成之后,已初始化的全局变量保存在.data 段中,未初始化的全局变量保存在.bss 段中。

  text和data段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中),由系统从可执行文件中加载;

  而bss段不在可执行文件中,由系统初始化。

例:

两个小程序如下:

程序1:
int ar[30000];
void main()
{
    ......
}

 


程序2:

int ar[300000] = {1, 2, 3, 4, 5, 6 };
void main()
{
    ......
}
发现程序2编译之后所得的.exe文件比程序1的要大得多。当下甚为不解,于是手工编译了一下,并使用了/FAs编译选项来查看了一下其各自的.asm,
发现在程序1.asm中ar的定义如下:
_BSS SEGMENT
     ?ar@@3PAHA DD 0493e0H DUP (?)  ; ar
_BSS ENDS
而在程序2.asm中,ar被定义为:
_DATA SEGMENT
     ?ar@@3PAHA DD 01H  ; ar
                DD 02H
                DD 03H
                ORG $+1199988
_DATA ENDS
区别很明显,一个位于.bss段,而另一个位于.data段,两者的区别在于:
全局的未初始化变量存在于.bss段中,具体体现为一个占位符;
全局的已初始化变量存于.data段中;
而函数内的自动变量都在栈上分配空间;
.bss是不占用.exe文件空间的,其内容由操作系统初始化(清零);
.data却需要占用,其内容由程序初始化。因此造成了上述情况。
bss段(未手动初始化的数据)并不给该段的数据分配空间,只是记录数据所需空间的大小;
bss段的大小从可执行文件中得到 ,然后链接器得到这个大小的内存块,紧跟在数据段后面。
data段(已手动初始化的数据)则为数据分配空间,数据保存在目标文件中;
data段包含经过初始化的全局变量以及它们的值。当这个内存区进入程序的地址空间后全部清零。
 
包含data段和bss段的整个区段此时通常称为数据区。
 
STM32的内存管理和堆栈相关的认知
 

今天仔细读了一下内存管理的代码,然后还有看了堆栈的相关知识,把以前不太明白的一些东西想通了,写下来,方便以后查看,也想大家看了能指出哪里不对,然后修改。    

 

 

 首先,先看一下stm32的存储器结构。

​Flash,SRAM寄存器和输入输出端口被组织在同一个4GB的线性地址空间内。可访问的存储器空间被分成8个主要块,每个块为512MB。

FLASH存储下载的程序。

SRAM是存储运行程序中的数据。

所以,只要你不外扩存储器,写完的程序中的所有东西也就会出现在这两个存储器中。

这是一个前提!

堆栈的认知

1.     STM32中的堆栈。

这个我产生过混淆,导致了很多逻辑上的混乱。首先要说明的是单片机是一种集成电路芯片,集成CPU、RAM、ROM、多种I/O口和中断系统、定时器/计数器等功能。CPU中包括了各种总线电路,计算电路,逻辑电路,还有各种寄存器。Stm32有通用寄存器 R0‐ R15 以及一些特殊功能寄存器,其中包括了堆栈指针寄存器。当stm32正常运行程序的时候,来了一个中断,CPU就需要将寄存器中的值压栈到RAM里,然后将数据所在的地址存放在堆栈寄存器中。等中断处理完成退出时,再将数据出栈到之前的寄存器中,这个在C语言里是自动完成的。

2.     编程中的堆栈。

在编程中很多时候会提到堆栈这个东西,准确的说这个就是RAM中的一个区域。我们先来了解几个说明:

(1) 程序中的所有内容最终只会出现在flash,ram里(不外扩)。

(2) 段的划分,是将类似数据种类存储在一个区域里,方便管理,但正如上面所说,不管什么段的数据,都是最终在flash和ram里面。

C语言上分为栈、堆、bss、data、code段。具体每个段具体是存储什么数据的,直接百度吧。重点分析一下STM32以及在MDK里面段的划分。

MDK下Code, RO-data,RW-data,ZI-data这几个段:

Code是存储程序代码的。

​RO-data是存储const常量和指令。

​RW-data是存储初始化值不为0的全局变量。

​ZI-data是存储未初始化的全局变量或初始化值为0的全局变量。

Flash=Code + RO Data + RW Data;

RAM= RW-data+ZI-data;

这个是MDK编译之后能够得到的每个段的大小,也就能得到占用相应的FLASH和RAM的大小,但是还有两个数据段也会占用RAM,但是是在程序运行的时候,才会占用,那就是堆和栈。在stm32的启动文件.s文件里面,就有堆栈的设置,其实这个堆栈的内存占用就是在上面RAM分配给RW-data+ZI-data之后的地址开始分配的。

堆:是编译器调用动态内存分配的内存区域。

栈:是程序运行的时候局部变量的地方,所以局部变量用数组太大了都有可能造成栈溢出。

堆栈的大小在编译器编译之后是不知道的,只有运行的时候才知道,所以需要注意一点,就是别造成堆栈溢出了。。。不然就等着hardfault找你吧。

3.     OS中的堆栈及其内存管理。

嵌入式系统的堆栈,不管是用什么方法来得到内存,感觉他的方式都和编程中的堆差不多。目前我知道两种获得内存情况:

(1)用庞大的全局变量数组来圈住一块内存,然后将这个内存拿来进行内存管理和分配。这种情况下,堆栈占用的内存就是上面说的:如果没有初始化数组,或者数组的初始化值为0,堆栈就是占用的RAM的ZI-data部分;如果数组初始化值不为0,堆栈就占用的RAM的RW-data部分。这种方式的好处是容易从逻辑上知道数据的来由和去向。

(2)​就是把编译器没有用掉的RAM部分拿来做内存分配,也就是除掉RW-data+ZI-data+编译器堆+编译器栈后剩下的RAM内存中的一部分或者全部进行内存管理和分配。这样的情况下就只需要知道内存剩下部分的首地址和内存的尾地址,然后要用多少内存,就用首地址开始挖,做一个链表,把内存获取和释放相关信息链接起来,就能及时的对内存进行管理了。内存管理的算法多种多样,不详说,这样的情况下:OS的内存分配和自身局部变量或者全局变量不冲突,之前我就在这上面纠结了很久,以为函数里面的变量也是从系统的动态内存中得来的。这种方式感觉更加能够明白自己地址的开始和结束。

这两种方法我感觉没有谁更高明,因为只是一个内存的获取方式,高明的在于内存的管理和分配。​

keil编译后code,RO-data,RW-data,ZI-data含义及mcu的flash实际存储数据

keil编译后会有一行:Program Size:Code=xxxRO-data=xxx RW-data=xxx ZI-data=xxx

Code 代表执行的代码,程序中所有的函数都位于此处。

RO-data 代表只读数据,程序中所定义的全局常量数据和字符串都位于此处。

RW-data 代表已初始化的读写数据,程序中定义并且初始化的全局变量和静态变量位于此处。

ZI-data 代表未初始化的读写数据,程序中定义了但没有初始化的全局变量和静态变量位于此处。ZI英语是zero initial,就是程序中用到的变量并且被系统初始化为0的变量的字节数,keil编译器默认是把你没有初始化的变量都赋值一个0,这些变量在程序运行时是保存在RAM中的。

2.如果你查看.map文件,如下例子:

 

==============================================================================

    Total RO  Size (Code + RO Data)                 2980 (   2.91kB)
    Total RW  Size (RW Data + ZI Data)               104 (   0.10kB)
    Total ROM Size (Code + RO Data + RW Data)       2988 (   2.92kB)

==============================================================================

Total ROM Size (Code + RO Data + RW Data)这样所写的程序占用的ROM的字节总数,也就是说程序所下载到ROM flash 中的大小。为什么Rom中还要存RW,因为掉电后RAM中所有数据都丢失了,每次上电RAM中的数据是被重新赋值的,每次这些固定的值就是存储在Rom中的,为什么不包含ZI段呢,是因为ZI数据都是0,没必要包含,只要程序运行之前将ZI数据所在的区域一律清零即可,包含进去反而浪费存储空间。

实际上,ROM中的指令至少应该有这样的功能:
       1. 将RW从ROM中搬到RAM中,因为RW是变量,变量不能存在ROM中。
       2. 将ZI所在的RAM区域全部清零,因为ZI区域并不在Image中,所以需要程序根据编译器给出的ZI地址及大小来将相应得RAM区域清零。ZI中也是变量,同理:变量不能存在ROM中。
       在程序运行的最初阶段,RO中的指令完成了这两项工作后C程序才能正常访问变量。否则只能运行不含变量的代码。

推荐阅读