11
$\begingroup$

I have been working on and off on small embedded systems project on and off. Some of these projects used an ARM Cortex-M4 base processor. In the project folder there is a startup.s file. Inside that file I noted the following two command lines.

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

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

How does one define the size of the heap and stack for a microcontroller? Is there any specific information in the datasheet to guide to arrive at the correct value? If so, what should one look for in the datasheet?


References:

$\endgroup$

migrated from embedded.stackexchange.com Aug 14 '15 at 19:04

11
$\begingroup$

Stack and heap are software concepts, not hardware concepts. What the hardware provides is memory. Defining zones of memory, one of which is called “stack” and one of which is called “heap”, is a choice of your program.

The hardware does help with stacks. Most architectures have a dedicated register called the stack pointer. Its intended use is that when the program makes a function call, the function parameters and the return address are pushed to the stack, and they are popped when the function terminates and returns to its caller. Pushing onto the stack means writing at the address given by the stack pointer and decrementing the stack pointer accordingly (or incrementing, depending in which direction the stack grows). Popping means incrementing (or decrementing) the stack pointer; the return address is read from the address given by the stack pointer.

Some architectures (not ARM though) have a subroutine call instruction that combines a jump with writing to the address given by the stack pointer, and a subroutine return instruction that combines reading from the address given by the stack pointer and jumping to this address. On ARM, the address saving and restoring is done in the LR register, the call and return instructions do not use the stack pointer. There are however instructions to facilitate writing or reading multiple registers to the address given by the stack pointer, to push and pop function arguments.

To choose the heap and stack size, the only relevant information from the hardware is how much total memory you have. You then make your choice depending on what you want to store in memory (allowing for code, static data, and other programs).

A program would typically uses these constants to initialize some data in memory that will be used by the rest of the code, such as the address of the top of the stack, maybe a value somewhere to check for stack overflows, bounds for the heap allocator, etc.

In the code you're looking at, the Stack_Size constant is used to reserve a block of memory in the code area (via a SPACE directive in ARM assembly). The upper address of this block is given the label __initial_sp, and it is stored in the vector table (the processor uses this entry to set SP after a software reset) as well as exported for use in other source files. The Heap_Size constant is similarly used to reserve a block of memory and labels to its boundaries (__heap_base and __heap_limit) are exported for use in other source files.

; 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
$\endgroup$
  • $\begingroup$ Do you know how those values 0x00200 and 0x000400 are determined $\endgroup$ – Mahendra Gunawardena Aug 1 '15 at 19:26
  • $\begingroup$ @MahendraGunawardena It's up to you to determine them, based on what your program needs. Niall's answer gives a few tips. $\endgroup$ – Gilles 'SO- stop being evil' Aug 1 '15 at 19:31
7
$\begingroup$

The sizes of the stack and heap are defined by your application, not anywhere in the microcontroller's datasheet.

The stack

The stack is used to store the values of local variables inside of functions, the previous values of CPU registers used for local variables (so they can be restored on exit from the function), the program address to return to when leaving those functions, plus some overhead for management of the stack itself.

When developing an embedded system, you estimate the maximum call depth you expect to have, add up the sizes of all the local variables in the functions in that hierarchy, then add some padding to allow for the overhead mentioned above, then add some more for any interrupts that might occur during execution of your program.

An alternative estimation method (where RAM isn't constrained) is to allocate far more stack space than you'll ever need, fill the stack with a sentinel value, then monitor how much you actually use during execution. I've seen debug versions of C language runtimes that will do this for you automatically. Then, when you've finished developing, you can reduce the stack size if you want.

The heap

Calculating the size of the heap you need can be a trickier. The heap is used for dynamically allocated variables, so if you use malloc() and free() in a C language program, or new and delete in C++, that's where those variables live.

However, in C++ especially, there can be some hidden dynamic memory allocation going on. For example, if you have statically allocated objects, the language requires that their destructors be called when the program exits. I'm aware of at least one runtime where the addresses of the destructors are stored in a dynamically allocated linked list.

So to estimate the size of the heap you need, look at all the dynamic memory allocation in each path through your call tree, calculate the maximum and add some padding. The language runtime may provide diagnostics that you can use to monitor total heap usage, fragmentation, etc.

$\endgroup$
  • $\begingroup$ Thank you for the response, I like to how to determine the specific number such as 0x00400 and so forth $\endgroup$ – Mahendra Gunawardena Aug 1 '15 at 18:54
5
$\begingroup$

In addition to the other answers, I'd like to add that when carving up RAM between stack and heap space, that you also need to consider the space for static non-constant data (e.g. file globals, function statics, and program-wide globals from a C perspective, and probably others for C++).

How stack/heap allocation works

It is worth noting that the startup assembly file is one way of defining the region; the toolchain (both your build environment and run-time environment) mostly care about the symbols which define the start of stackspace (used to store the initial stack pointer in the Vector Table) and the start and end of heap space (used by the dynamic memory allocator, typically provided by your libc)

In OP's example, only 2 symbols are defined, a size of stack at 1kiB and a size of heap at 0B. These values get used elsewhere to actually produce the stack and heap spaces

In @Gilles example, the sizes are defined and used in the assembly file to set a stack space starting wherever and lasting the size, identified by the symbol Stack_Mem and sets a label __initial_sp at the end. Likewise for the heap, where the space is the symbol Heap_Mem (0.5kiB in size), but with labels at the beginning and end (__heap_base and __heap_limit).

These get processed by the linker, which won't allocate anything within the stack space and heap space because that memory is occupied (by Stack_Mem and Heap_Mem symbols), but it can place those memories and all of the globals wherever it needs. The labels end up being symbols with no length at the given addresses. The __initial_sp is used directly for the vector table at link time, and the __heap_base and __heap_limit by your runtime code. The actual addresses of the symbols are assigned by the linker based on where it placed them.

As I aluded to above, these symbols don't actually have to come from a startup.s file. They can come from your linker configuration (Scatter Load file in Keil, linkerscript in GNU), and in those you can have finer grained control over placement. For example, you can force the stack to be at the beginning or end of RAM, or keep your globals away from the heap, or whatever you want. You can even specify that the HEAP or STACK just occupy whatever RAM is left over after globals are placed. NOTE though that you have to be careful that adding more static variables that your other memory will decrease.

However, each toolchain is different, and how to write the configuration file and what symbols your dynamic memory allocator will use will have to come from the documentation of your particular environment.

Stack Sizing

As to how to determine stack size, many toolchains can give you a maximum stack depth by analyzing the function call trees of your program, IF you don't use recursion or function pointers. If you do use those, estimating a stack size and pre-filling it with cardinal values (perhaps via the entry function before main) and then checking after your program has run for a while where the max depth was (which is where the cardinal values end). If you have fully exercised your program to its limits, you'll know fairly accurately whether you can shrink the stack or, if your program crashes or no cardinal values are left, that you need to increase the stack and try again.

Heap Sizing

Determining heap size is a bit more application dependant. If you only do dynamic allocation during startup, you can just add up the space required in your startup code (plus some overhead for memory management). If you have access to the source of your memory manager, you can know exactly what the overhead is, and possibly even write code to walk the memory to give you usage information. For applications that need dynamic runtime memory (e.g. allocating buffers for inbound ethernet frames) the best I can suggest is to carefully hone your stacksize and give the Heap everything that is left over after stack and statics.

Final note (RTOS)

OP's question was tagged for bare-metal, but I want to add a note for RTOSes. Often (always?) each task/process/thread (I'll just write task here on out for simplicity) will be assigned a stack size when the task is created, an in addition to task stacks, there will likely be a small OS stack (used for interrupts and such)

The task accounting structures and the stacks have to be allocated from somewhere, and this will often be from the overall heap space of your application. In these instances, your initial stack size often won't matter, because the OS will only use it during initialization. I have seen, for example, specifying ALL remaining space during linking be allocated to the HEAP and placing the initial stack pointer at the end of the heap to grow into the heap, knowing that the OS will allocate starting from the beginning of the heap and will allocate the OS stack just before abandoning the initial_sp stack. Then all of the space is used for allocating task stacks and other dynamically allocated memory.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.