IAR Linker Script for STM32: Complete Guide (Basic to Advanced) 🛠️

Understanding the linker script (.icf file) is crucial for achieving precise control over the memory layout when working with STM32 microcontrollers in IAR Embedded Workbench. Whether you’re a beginner just trying to flash an LED or an advanced embedded developer optimizing bootloaders, this guide walks you through IAR linker scripts—from the basics to the advanced use cases.

So let’s first understand the linker script and its role.

What Is a Linker Script?

A linker script defines how your code and data are placed in memory.

It tells the linker:

  • Where the ROM and RAM regions start and end.
  • What goes into those memory regions (code, variables, stack, heap, etc.).
  • How to manage custom sections, overlays, or bootloaders.

Linker scripts typically have the .icf (IAR Configuration File) extension in IAR. The ICF syntax is declarative and easy to read, allowing developers to clearly describe the desired memory and code layout without writing complex instructions.

Example,

define symbol __ICFEDIT_region_ROM_start__ = 0x08000000;

define symbol __ICFEDIT_region_ROM_end__   = 0x080FFFFF;

These are just symbolic markers used throughout the script to define region boundaries.

 

Commonly used keywords in linker configuration files (.icf):

Sure! In IAR Embedded Workbench, linker configuration files (.icf) use a specific set of keywords and directives to define memory regions, section placements, alignment, etc. Here’s a list of important .icf keywords and what they do:

🔹 1. define memory:

Used to define memory regions (e.g., Flash, RAM).

define memory mem with size = 4G;
define region FLASH = mem:[from 0x08000000 size 0x100000];
define region RAM = mem:[from 0x20000000 size 0x20000];
🔹 2. define block

Defines logical memory blocks or sections.

define block CSTACK with alignment = 8, size = 0x400 { };

🔹 3. Initialize by copy / initialize by zero

Specifies initialization behavior for sections at startup.

initialize by copy { section .data };
initialize by zero { section .bss };
🔹 4. place at / place in

Used to place code/data in a specified memory region.

place at address mem:0x08000000 { readonly };
place in RAM { section .data, block CSTACK };
🔹 5. do not initialize

Tells the linker to leave certain sections uninitialized (helpful for .noinit).

do not initialize { section .noinit };

🔹 6. keep

Prevents the linker from discarding unused sections (e.g., for ISRs).

keep { section .isr_vector };

🔹 7. export symbol / import symbol

Used for sharing symbols across different modules.

export symbol __my_symbol;
import symbol __extern_symbol;
🔹 8. place in { readonly }

Places all readonly sections (like .text) in the specified region.

place in FLASH { readonly };

🔹 9. define symbol

Defines a linker symbol for use in application code or debugging.

define symbol __heap_limit = 0x20020000;

🔹10. heap and stack:

Defines where the heap and stack blocks are located.

define block HEAP with size = 0x800 { };
define block CSTACK with size = 0x800 { };
place in RAM { block CSTACK, block HEAP };

 

🛠️ A Simple Linker Script for STM32H5 in IAR:

Here’s a minimalistic STM32xxx_flash.icf example that configures code to run from internal Flash and variables to sit in RAM:

define symbol __ICFEDIT_region_ROM_start__ = 0x08000000;
define symbol __ICFEDIT_region_ROM_end__   = 0x080FFFFF;

define symbol __ICFEDIT_region_RAM_start__ = 0x20000000;
define symbol __ICFEDIT_region_RAM_end__   = 0x2002FFFF;

define memory mem with size = 4G;
define region ROM_region = mem:[from __ICFEDIT_region_ROM_start__ to __ICFEDIT_region_ROM_end__];
define region RAM_region = mem:[from __ICFEDIT_region_RAM_start__ to __ICFEDIT_region_RAM_end__];

place in ROM_region { readonly };
place in RAM_region { readwrite, block CSTACK, block HEAP };

 

Let’s break it down step by step:

🔹 1. Memory Region Symbols:
define symbol __ICFEDIT_region_ROM_start__ = 0x08000000;
define symbol __ICFEDIT_region_ROM_end__   = 0x080FFFFF;
define symbol __ICFEDIT_region_RAM_start__ = 0x20000000;
define symbol __ICFEDIT_region_RAM_end__   = 0x2002FFFF;

The above lines define symbols that represent the start and end addresses of the memory regions:

Symbol Address Description
__ICFEDIT_region_ROM_start__ 0x08000000 Start of Flash/ROM memory
__ICFEDIT_region_ROM_end__ 0x0803FFFF End of Flash/ROM (256 KB total)
__ICFEDIT_region_RAM_start__ 0x20000000 Start of RAM (SRAM)
__ICFEDIT_region_RAM_end__ 0x2000FFFF End of RAM (64 KB total)

 

🔹 2. Define Memory and Regions:

The below line defines a logical memory space of 4 GB. It’s a container or address space reference, not real memory.

define memory mem with size = 4G;

It is used to describe specific regions within.

define region ROM_region = mem:[from __ICFEDIT_region_ROM_start__ to __ICFEDIT_region_ROM_end__];
define region RAM_region = mem:[from __ICFEDIT_region_RAM_start__ to __ICFEDIT_region_RAM_end__];

These define two named regions within the memory:

  • ROM_region: for code and constants (Flash)
  • RAM_region: for read-write data (SRAM)

 

🔹 3. Placing Sections into Regions:

This below line tells the linker to place all read-only sections like:

place in ROM_region { readonly };

  • .text (code)
  • .rodata (constants)
  • vector table into the ROM region.

place in RAM_region { readwrite, block CSTACK, block HEAP };

This places all read/write sections, like:

  • .data (initialized global variables)
  • .bss (zero-initialized global/static variables)
  • CSTACK (stack for C runtime)
  • HEAP (heap for dynamic allocation)

into the RAM region.

 

Let’s explore some important use cases of linker scripts:

  • Placing Custom Sections: Controlling where specific code or data resides in memory.
  • Splitting RAM: Dividing RAM into separate regions for stack, heap, or different cores/peripherals.
  • Bootloaders and Overlays: Managing memory for bootloaders and implementing overlays for efficient resource usage.

1. Placing Custom Sections:

Case 1: Suppose you want to put a specific function into a custom memory section:

Chang in your code:

__attribute__((section(".your_ram_code"))) void fast_func()
{
   // Fast execution critical code
}

Change in linker script:

place in RAM_region { section .your_ram_code };

This tells the linker to place fast_func() in RAM for faster access or timing-sensitive applications.

 

Case 2: Suppose you want to put a specific variable into a custom memory section:

Change in your code,

__attribute__((section(".your_data")))
const uint32_t your_data[] = {0x12345678, 0x87654321};

Change in your linker script,

place at address mem:0x0803F000 { section .your_data};

 

2. Splitting RAM:

Some STM32 MCUs have multiple SRAM banks. You can define them separately:

define symbol __ICFEDIT_region_RAM1_start__ = 0x20000000;
define symbol __ICFEDIT_region_RAM1_end__   = 0x2000FFFF;

define symbol __ICFEDIT_region_RAM2_start__ = 0x20010000;
define symbol __ICFEDIT_region_RAM2_end__   = 0x2001FFFF;

define region RAM1 = mem:[from __ICFEDIT_region_RAM1_start__ to __ICFEDIT_region_RAM1_end__];
define region RAM2 = mem:[from __ICFEDIT_region_RAM2_start__ to __ICFEDIT_region_RAM2_end__];

place in RAM1 { readwrite };
place in RAM2 { block CSTACK, block HEAP };

The above linker script splits RAM into two regions:

  • RAM1 (0x20000000 – 0x2000FFFF): Used for global and static read-write variables.
  • RAM2 (0x20010000 – 0x2001FFFF): Dedicated to stack (CSTACK) and heap (HEAP).

This separation improves memory management, prevents corruption, and is especially useful in complex embedded applications.

 

Now I believe you are thinking can we combine it??

The answer is, Yes, you can combine different SRAM regions in a linker script, but it depends on how you plan to use them and what your toolchain or linker allows.

define symbol __ICFEDIT_region_RAM_combined_start__ = 0x20000000;
define symbol __ICFEDIT_region_RAM_combined_end__   = 0x2001FFFF;

define region RAM_ALL = mem:[from __ICFEDIT_region_RAM_combined_start__ to __ICFEDIT_region_RAM_combined_end__];

place in RAM_ALL { readwrite, block CSTACK, block HEAP };

This treats both SRAM blocks (RAM1 and RAM2) as one large region of 128 KB. Now, the linker can place variables, heap, and stack anywhere within this combined space.

⚠️ Caution:
  • This works only if your hardware allows access to these regions as if they were a continuous block.
  • Some MCUs may have different access speeds, bus types, or DMA restrictions on separate SRAM blocks—so combining may not always be ideal.
  • Use memory-mapped addresses carefully in drivers or RTOS where specific stack/heap placement is critical.

 

3. Bootloaders:

If you are building a bootloader + application architecture, you will likely split Flash memory:

// Bootloader
define symbol BL_ROM_start = 0x08000000;
define symbol BL_ROM_end   = 0x08007FFF;

// Application
define symbol APP_ROM_start = 0x08008000;
define symbol APP_ROM_end   = 0x0803FFFF;

define region Bootloader_ROM = mem:[from BL_ROM_start to BL_ROM_end];
define region App_ROM = mem:[from APP_ROM_start to APP_ROM_end];

place in Bootloader_ROM { section .boot_code };
place in App_ROM { readonly };

 

4. Overlay Memory:

Overlays allow multiple code or data sections to share the same memory address, but only one is active/loaded at a time. It is useful for sharing RAM between mutually exclusive code blocks. Useful when:

  • RAM is limited.
  • Only one module needs access at a time (e.g., math library, file system, audio codec)
define region OVERLAY_RAM = mem:[from 0x20000000 to 0x20000FFF];

place in OVERLAY_RAM with overlay {
  section .math;
  section .audio;
  section .crypto;
};

Note: Each section occupies the same memory address, but only one is loaded at runtime.

 

5. Startup Symbols and Vectors:

During system startup, the startup file (usually written in assembly or C) needs to know where the RAM and ROM regions begin so it can correctly initialize the system.

To do this, the linker script exports key memory symbols, which the startup code then references.

export symbol __ICFEDIT_region_RAM_start__;
export symbol __ICFEDIT_region_ROM_start__;

These lines export memory region symbols so they are accessible to the startup code:

  • __ICFEDIT_region_RAM_start__:
    • Used by the startup file to know where to initialize the .data section in RAM.
    • Often also used to set up the stack pointer (MSP).
  • __ICFEDIT_region_ROM_start__:
    • Used to locate the initial values of global/static variables stored in Flash.
    • These values are copied to RAM at startup.

 

6. Placing the Interrupt Vector Table:

In most STM32 devices, the interrupt vector table must be placed at the start of Flash (0x08000000). You can ensure this by defining a special symbol and placing the .intvec section explicitly:

define symbol __ICFEDIT_intvec_start__ = 0x08000000;
place at address mem:__ICFEDIT_intvec_start__ { readonly section .intvec };

This is crucial because:

  • The Cortex-M core fetches the initial stack pointer and reset handler from this table.
  • Placing it at the wrong address will cause the MCU to crash or hang after reset.

In your C/C++ code or startup file, make sure the vector table is defined like this: c Copy Edit

extern const uint32_t __vector_table[];

Note: IAR automatically looks for .intvec section for placement, and this placement rule ensures it’s at the right address.