定义ARM Cortex-M4微控制器的堆和堆栈大小?


11

我一直在从事小型嵌入式系统项目的开发工作。其中一些项目使用了ARM Cortex-M4基本处理器。在项目文件夹中,有一个startup.s文件。在该文件中,我注意到了以下两个命令行。

;******************************************************************************
;
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
;
;******************************************************************************
Stack   EQU     0x00000400

;******************************************************************************
;
; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
;
;******************************************************************************
Heap    EQU     0x00000000

如何定义微控制器的堆栈的大小?数据表中是否有任何特定信息可指导您得出正确的值?如果是这样,应该在数据表中寻找什么?


参考文献:

Answers:


12

堆栈和堆是软件概念,而不是硬件概念。硬件提供的是内存。定义内存区域是您的程序选择,其中一个区域称为“堆栈”,另一个区域称为“堆”。

硬件确实有助于堆栈。大多数体系结构都有一个专用的寄存器,称为堆栈指针。它的预期用途是,当程序进行函数调用时,函数参数和返回地址被压入堆栈,并在函数终止并返回其调用者时弹出。压入堆栈意味着写入堆栈指针给定的地址,并相应地递减堆栈指针(或递增,具体取决于堆栈增长的方向)。弹出意味着递增(或递减)堆栈指针。从堆栈指针给定的地址中读取返回地址。

某些体系结构(虽然不是ARM)具有子例程调用指令,该子例程调用指令将跳转和写入堆栈指针给定的地址相结合,子例程返回指令将对堆栈指针给定的地址的读取并跳转到该地址。在ARM上,地址的保存和恢复在LR寄存器中完成,调用和返回指令不使用堆栈指针。但是,有一些指令可以帮助将多个寄存器写入或读取到堆栈指针给定的地址,以压入和弹出函数参数。

要选择堆和堆栈大小,硬件中唯一相关的信息是您拥有多少总内存。然后,根据要存储在内存中的内容(允许代码,静态数据和其他程序)做出选择。

程序通常会使用这些常量来初始化内存中的一些数据,这些数据将由其余代码使用,例如堆栈顶部的地址,可能是某个位置的值来检查堆栈溢出,堆分配器的范围等

您正在查看的代码中Stack_Size常量用于在代码区域中保留一块内存(通过SPACEARM汇编中的指令)。该块的高位地址__initial_sp带有标签,并且存储在向量表中(处理器在软件复位后使用此条目来设置SP),并导出以供其他源文件使用。该Heap_Size常数类似地用于保留一块内存,并在其边界(__heap_base__heap_limit)处导出标签以供其他源文件使用。

; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
;   <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Stack_Size      EQU     0x00000400

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp


; <h> Heap Configuration
;   <o>  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Heap_Size       EQU     0x00000200

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

…
__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler

…

                 EXPORT  __initial_sp
                 EXPORT  __heap_base
                 EXPORT  __heap_limit

您知道如何确定这些值0x00200和0x000400
Mahendra Gunawardena

@MahendraGunawardena由您决定是否要根据程序需要确定它们。Niall的答案提供了一些提示。
吉尔斯(Gilles)'所以

7

堆栈和堆的大小由您的应用程序定义,而不是在微控制器的数据表中的任何地方定义。

堆栈

堆栈用于存储函数内部的局部变量的值,用于局部变量的CPU寄存器的先前值(以便可以在退出函数时恢复它们),离开这些函数时要返回的程序地址,以及堆栈本身管理的一些开销。

在开发嵌入式系统时,您需要估计期望的最大调用深度,将层次结构中函数中所有局部变量的大小相加,然后添加一些填充以解决上述开销,然后为在程序执行过程中可能发生的任何中断。

另一种估算方法(不限制RAM)是分配比您需要的堆栈空间更多的空间,用一个定点值填充堆栈,然后监视执行期间实际使用的内存量。我见过C语言运行时的调试版本,它将自动为您完成此操作。然后,完成开发后,可以根据需要减小堆栈大小。

计算所需的堆大小可能会比较麻烦。该堆用于动态分配的变量,因此,如果您使用malloc(),并free()在C语言程序,或newdelete在C ++中,这是这些变量住的地方。

但是,尤其是在C ++中,可能存在一些隐藏的动态内存分配。例如,如果您有静态分配的对象,则该语言要求在程序退出时调用其析构函数。我知道至少有一个运行时,析构函数的地址存储在动态分配的链表中。

因此,要估计所需的堆大小,请查看调用树中每个路径中的所有动态内存分配,计算最大值并添加一些填充。语言运行时可能会提供诊断,您可以使用这些诊断来监视总堆使用情况,碎片情况等。


谢谢您的回应,我想知道如何确定具体的数字,例如0x00400,等等
Mahendra Gunawardena

5

除了其他答案,我还要补充一点,在堆栈和堆空间之间划分RAM时,还需要考虑静态非恒定数据(例如,文件全局,函数静态和程序范围内的数据)的空间。从C的角度来看全局变量,对于C ++则可能是其他观点)。

堆栈/堆分配的工作方式

值得注意的是,启动程序集文件是定义区域的一种方法。工具链(您的构建环境和运行时环境)大多关心定义堆栈空间的开始(用于将初始堆栈指针存储在向量表中)以及堆空间的开始和结束(由动态使用)的符号内存分配器,通常由您的libc提供)

在OP的示例中,仅定义了2个符号,堆栈的大小为1kiB,堆的大小为0B。这些值在其他地方用于实际产生堆栈空间和堆空间

在@Gilles示例中,定义了大小,在汇编文件中使用了大小,以设置从任意位置开始并持续该大小的堆栈空间(由符号Stack_Mem标识),并在末尾设置标签__initial_sp。对于堆,空间也是符号Heap_Mem(大小为0.5kiB),但在开头和结尾都有标签(__heap_base和__heap_limit)。

这些由链接器处理,该链接器不会在堆栈空间和堆空间内分配任何内容,因为该内存已被占用(由Stack_Mem和Heap_Mem符号占用),但是它可以将这些内存和所有全局变量放在需要的位置。标签最终成为在给定地址没有长度的符号。链接时,__ initial_sp直接用于向量表,运行时代码将__heap_base和__heap_limit直接使用。链接器根据放置符号的位置分配符号的实际地址。

正如我上面所提到的,这些符号实际上不必来自startup.s文件。它们可以来自您的链接器配置(Keil中的Scatter Load文件,GNU中的linkerscript),在这些文件中,您可以更好地控制布局。例如,您可以强制堆栈位于RAM的开头或结尾,或者使全局变量远离堆或其他任何对象。您甚至可以指定放置全局变量后,HEAP或STACK仅占据剩余的RAM。注意,尽管如此,但要注意,增加更多静态变量会使其他内存减少。

但是,每个工具链都是不同的,如何编写配置文件以及动态内存分配器将使用哪些符号将取决于特定环境的文档。

堆栈大小

至于如何确定堆栈大小,如果您不使用递归或函数指针,许多工具链可以通过分析程序的函数调用树来为您提供最大的堆栈深度。如果确实使用了这些值,则估计堆栈大小并用基值预先填充(也许通过main之前的输入功能),然后在程序运行了一段时间后,即最大深度为(基值所在)结束)。如果您已充分行使程序的极限,则将相当准确地知道是否可以缩小堆栈,或者,如果程序崩溃或没有基本值,则需要增加堆栈并重试。

堆大小

确定堆大小取决于应用程序。如果仅在启动期间进行动态分配,则可以仅添加启动代码中所需的空间(加上一些内存管理开销)。如果可以访问内存管理器的源代码,则可以确切知道开销是多少,甚至可以编写代码遍历内存以提供使用情况信息。对于需要动态运行时内存的应用程序(例如,为入站以太网帧分配缓冲区),我建议的最佳选择是仔细调整堆栈大小,并将堆和静态数据后剩下的一切都交给Heap。

最后说明(RTOS)

OP的问题被标记为裸机,但我想为RTOS添加注释。通常(总是吗?)每个任务/进程/线程(为简单起见,我将在这里写出任务)在创建任务时都会分配一个堆栈大小,除了任务堆栈外,可能还会有一个小的OS堆栈(用于中断等)

任务计费结构和堆栈必须从某个位置分配,这通常是从应用程序的整个堆空间分配的。在这些情况下,初始堆栈大小通常无关紧要,因为操作系统仅在初始化期间使用它。例如,我知道将链接期间的所有剩余空间分配给HEAP,并将初始堆栈指针放在堆的末尾以增长到堆中,因为我知道OS将从堆的开头开始分配,并且将在放弃initial_sp堆栈之前分配OS堆栈。然后,所有空间都用于分配任务堆栈和其他动态分配的内存。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.