Memory Layout in Embedded Systems: Designing Compact Apartments in Microcontrollers

Memory Layout in Embedded Systems: Designing Compact Apartments in Microcontrollers

As an embedded engineer, have you ever wondered how your code settles into the limited memory of a microcontroller after you upload it? Today, we will delve into the layout of embedded programs in microcontroller memory, akin to designing a compact apartment in a limited space, where every corner must be meticulously planned.

The Uniqueness of Embedded Memory

Unlike general-purpose computers, embedded systems do not have a virtual memory mechanism or a hard drive as backup storage. The memory layout of microcontrollers is more direct and compact, just like extreme planning within a limited space of tens of KB to hundreds of KB.

The Four Core Areas of Microcontroller Memory

1. Flash Storage: The Permanent Residence of Programs

Characteristics: Non-volatile, Read-only, Stores all code and constants

In microcontrollers, Flash is equivalent to the program’s “permanent residence”, similar to the building blueprint and fixed decoration of an apartment.

Contents Included:

  • Program code (.text section)

  • Read-only data (const constants)

  • Interrupt vector table

  • Initialized data (needs to be copied to RAM at startup)

const uint32_t SERIAL_BAUD_RATE = 115200;  // Stored in Flashvoid system_init(void) { /* Code is also in Flash */ }

Embedded Characteristics:

  • Slower read speed (compared to RAM)

  • Limited write cycles (usually around 100,000 times)

  • May require wait states when executing code

2. RAM Data Area: The Workshop for Program Execution

Characteristics: Volatile, Read-write, fast but space is precious

RAM is the workshop for program execution, where all dynamically changing data is processed. For embedded systems, RAM is the most valuable resource.

2.1 Data Section (.data): Initialized Global Variables

int32_t sensor_value = 0;  // Initial value copied from Flash to RAM at startupstatic uint8_t device_status = INITIALIZED;

2.2 BSS Section (.bss): Uninitialized Global Variables

float temperature_buffer[100];  // Initialized to 0 at startupchar uart_tx_buffer[256];

Embedded Challenges: Limited RAM space requires careful planning of each variable’s usage

Data Section (.data + .bss) – “Fixed Resident Area”

This part stores the global and static variables:

// .data section: Initialized global variablesint32_t sensor_value = 100;        // Copied from Flash to RAM at startupstatic char device_name[] = "STM32";// .bss section: Uninitialized global variablesfloat temperature_buffer[100];     // Initialized to 0 at startupuint8_t comm_buffer[256];

Characteristics:

  • Fixed location, size determined at compile time

  • Lifetime is the same as the program

2.3 Heap Area: The “Elastic Space” for Dynamic Memory

Characteristics: Usually used cautiously in embedded systems

Unlike general-purpose computers, the use of the heap in embedded systems requires extra caution:

// Use cautiously in resource-constrained embedded systemsvoid *dynamic_buffer = malloc(512);  // May fail or cause fragmentation

Embedded Practice Recommendations:

  • Small embedded systems typically avoid dynamic memory allocation

  • Use static allocation or memory pool techniques instead

  • If used, the heap size must be explicitly defined in the linker script

Heap Area – “Free Development Zone”

The heap area is used for dynamic memory allocation, which needs to be used cautiously in embedded systems:

// Example of heap allocationvoid* dynamic_buffer = malloc(512);    // Allocate from heapif (dynamic_buffer != NULL) {    // Use memory...    free(dynamic_buffer);              // Must be manually freed!}

Heap Characteristics:

  • Grows upwards: Allocated from low addresses to high addresses

  • Manual management: Requires explicit allocation and deallocation

  • Prone to fragmentation: Frequent allocation and deallocation of different sizes can lead to fragmentation

2.4 Stack Area: The “Temporary Workbench” for Function Calls

Characteristics: Automatically managed, last in first out, limited depth

The stack area is one of the most critical memory regions in embedded systems:

void process_sensor_data() {    int32_t raw_value = read_adc();     // Local variable on the stack    float converted = calibrate(raw_value);  // Function call uses stack space    // Automatically released when function returns}

Embedded Key Issues:

  • Stack overflow is the most common cause of crashes in embedded systems

  • Need to reasonably estimate the maximum depth of function call chains and local variable sizes

  • Each task in a multitasking system requires independent stack space

Stack Area – “Temporary Workspace”

The stack area is used for function calls and local variables:

void process_data(void) {    int32_t local_var = 10;           // Allocated on the stack    float temp_array[50];             // Array on the stack    function_call(local_var);         // Function call uses stack space    // Automatically releases all local variables when function returns}

Stack Characteristics:

  • Grows downwards: Uses from high addresses to low addresses

  • Automatically managed: Allocated when entering the function, automatically released when returning

  • Last in first out: The last allocated is the first released

Example of Actual Embedded Memory Layout

A typical memory map example based on the STM32F103 series:

Memory Layout in Embedded Systems: Designing Compact Apartments in Microcontrollers

Key Issue: Risk of Heap-Stack Collision

Since the heap grows upwards and the stack grows downwards, there is a collision risk:

Memory Layout in Embedded Systems: Designing Compact Apartments in Microcontrollers

When the heap and stack meet, the system will crash!

The Secrets of the Embedded Startup Process

Key memory operations performed during microcontroller startup:

  1. Initialize the stack pointer: Set the starting position of the stack

  2. Copy the .data section: Copy initialized data from Flash to RAM

  3. Zero the .bss section: Set uninitialized global variables to zero

  4. Call the main function: The program officially runs

Typical startup file snippetReset_Handler:    ldr sp, =_estack          ; Set stack pointer    ldr r0, =_sdata           ; Start address of .data in Flash    ldr r1, =_edata           ; End address of .data in RAM    ldr r2, =_sidata          ; Start address of .data in RAM    bl  Memory_Copy           ; Copy initialized data    ldr r0, =_sbss            ; Start of BSS section    ldr r1, =_ebss            ; End of BSS section    bl  Memory_Zero           ; Zero the BSS section    bl  main                  ; Jump to main function

Practical Memory Management in Embedded Development

1. The Importance of Linker Scripts

Linker scripts (.ld files) are the “architectural blueprints” of embedded memory layout,linker scripts (.ld files) clearly delineate each area:

MEMORY{    RAM (xrw)  : ORIGIN = 0x20000000, LENGTH = 64K}SECTIONS{    /* .data section: Initialized data */    .data :     {        _sdata = .;        /* Start address of .data section */        *(.data)           /* Contents of .data section */        _edata = .;        /* End address of .data section */    } >RAM    /* .bss section: Uninitialized data */    .bss :    {        _sbss = .;         /* Start address of .bss section */        *(.bss)            /* Contents of .bss section */        _ebss = .;         /* End address of .bss section */    } >RAM    /* Start of heap */    . = ALIGN(8);    _heap_start = .;       /* Start address of heap */    /* Stack definition */    _stack_size = 0x1000;  /* Stack size 4KB */    _stack_end = ORIGIN(RAM) + LENGTH(RAM);    /* End address of stack (end of RAM) */    _stack_start = _stack_end - _stack_size;   /* Start address of stack */    /* End of heap (before stack starts) */    _heap_end = _stack_start;                  /* End address of heap */}

2. Initialization in Startup Files

The startup file is responsible for setting the stack pointer:

Reset_Handler:    /* Set stack pointer */    ldr sp, =_stack_end    /* Initialize .data section (copy from Flash to RAM) */    ldr r0, =_sdata    ldr r1, =_edata      ldr r2, =_sidata    bl memory_copy    /* Zero the .bss section */    ldr r0, =_sbss    ldr r1, =_ebss    bl memory_zero    /* Jump to main function */    bl main

3. Implementation of the Heap

In embedded systems, the <span>_sbrk</span> function needs to be implemented to support heap operations:

// Heap memory management functionextern char _heap_start;   /* Start of heap defined in linker script */extern char _heap_end;     /* End of heap defined in linker script */char *_current_heap_pointer = &_heap_start;void *_sbrk(int incr) {    char *prev_heap_pointer;    /* Save current heap pointer */    prev_heap_pointer = _current_heap_pointer;    /* Check if exceeds heap range */    if (_current_heap_pointer + incr > &_heap_end) {        return (void*)-1;  /* Out of memory */    }    /* Update heap pointer */    _current_heap_pointer += incr;    return (void*)prev_heap_pointer;}
4. Stack Size Estimation Method
// Check stack usage by filling pattern#define STACK_CANARY 0xDEADBEEFvoid stack_usage_check(void) {    // Fill stack space at startup    fill_stack_with_canary();    // Check maximum usage at runtime    check_stack_watermark();}

5. Embedded Memory Optimization Techniques

// Technique 1: Use bit fields to save spacestruct {    uint8_t sensor_ready : 1;    uint8_t data_valid : 1;    uint8_t transmission_active : 1;} status_flags;// Technique 2: Use const and static wiselystatic const char LOG_PREFIX[] = "SYS:";  // Save RAM// Technique 3: Memory pool instead of dynamic allocation#define BUFFER_SIZE 1024static uint8_t memory_pool[BUFFER_SIZE];static size_t pool_index = 0;void* pool_alloc(size_t size) {    if (pool_index + size > BUFFER_SIZE) return NULL;    void* ptr = &memory_pool[pool_index];    pool_index += size;    return ptr;}

Common Issues and Debugging Techniques

  1. Stack Overflow: Manifesting as random crashes, can be detected by filling with canary values

  2. Heap Conflicts: Dynamic memory allocation leads to system instability

  3. Memory Alignment: Certain architectures require specific alignment

  4. DMA Conflicts: Memory area conflicts between DMA access and CPU access

Conclusion

For embedded engineers, understanding memory layout is not just theoretical knowledge, but the foundation of stability and reliability. In resource-constrained microcontroller environments, every byte of memory needs to be meticulously planned. Mastering the principles and practical techniques of memory layout can help you write more efficient and stable embedded programs.

I hope this analysis of memory layout in embedded systems is helpful for your development work. Feel free to share your embedded memory management experiences in the comments!

Leave a Comment