Understanding the Role of Start-Up Code in Microcontrollers

The start-up code is the first piece of code executed by a microcontroller (MCU) immediately after a Power-On Reset (POR). Its main responsibility is to prepare the MCU’s hardware so that the actual application can run smoothly.

The main purpose of start-up code initializes critical components such as the stack, system memory, and configuration settings. It also includes the reset handler, which is triggered after a reset to perform essential hardware setup before the main application begins.

Generally, start-up files are written in assembly language or a mix of C and assembly to keep them small and fast. When start-up code is written in assembly, developers gain more control and can optimize for speed by choosing specific instructions. This helps improve performance, especially in resource-limited systems.

In short, the start-up code ensures proper MCU initialization for reliable application execution.

 

Let’s first understand the start-up code. This post explains the start-up code for STM32H5 using IAR, but the concepts are similar for most microcontrollers.

 

What is Startup Code and Why Does it Matter?

The startup code is the first set of instructions that run after a reset or power-up. It sets up the stack, initializes memory, and then jumps to main(). Without it, your application wouldn’t even start.

In the IAR environment for STM32H5xxx, the startup code is usually written in assembly (.s file) or a mix of assembly and C. It works closely with:

  • Linker script (.icf). It is IAR-specific linker file defining memory layout.
  • System initialization code (system_stm32h5xx.c). It initializes clocks, PLLs, and low-level system settings.
  •  Interrupt handlers file (stm32h5xx_it.c). it has Interrupt handlers (weak declarations).
  • CMSIS (Cortex Microcontroller Software Interface Standard).

 

Key Responsibilities of the Startup File:

Here are some of the key tasks performs by startup code:

  • Reset Handling – Defines the reset handler, which runs first after power-on or reset.
  • Stack Initialization – Sets up the stack pointer for proper function call handling.
  • Memory Initialization – Initializes data and clears uninitialized variables in RAM.
  • System Configuration – Sets up basic system settings like clock configuration (if applicable).
  • Vector Table Setup – Defines interrupt vector table with pointers to handlers.
  • Calling main() – Finally, it hands control over to the main() function of the user application.

 

🧑‍💻 Step-by-Step: What Happens During Startup?

When a microcontroller powers on or resets, it doesn’t immediately run your main() function. Instead, it goes through a sequence of low-level startup steps to prepare the system.

👉 In this blog post, I will walk through the startup process using STM32 as an example, but the overall flow is similar across most ARM Cortex-M based microcontrollers.

Let’s break it down step by step:

1. Power-On or Reset Occurs:

The microcontroller powers up due to a power-on event or resets due to an event (e.g., external reset pin, watchdog, etc.).

2. Vector Table is Accessed:

The MCU looks at a fixed memory location (usually 0x00000000) for the vector table. Vector table defines the initial stack pointer and addresses of interrupt and exception handlers.​

  • The first entry in this table is the initial stack pointer.
  • The second entry is the reset handler address—this is the function the MCU jumps to right after reset.

3. Stack Pointer is Set:

The MCU loads the initial value of the stack pointer from the first vector table entry.

4. Reset Handler is Called:

The MCU jumps to the reset handler (usually defined in the start-up file). This function begins the startup sequence.

5. Memory Initialization:

  • Copies initialized variables from flash to RAM
  • Zeros out uninitialized variables (.bss section)

6. System Configurations (Usually)

  • Setup system clocks
  • Configure low-level hardware if needed (done here or in SystemInit())

7. Call main():

Once all the necessary setup is complete, the start-up code calls the main() function and transfers control to it. — the entry point of your application.

 

Structure of the ARM Cortex-M4 Startup File:

Let’s walk through this Startup file’s structure.

1. Vector Table Declaration:

This array of function pointers defines where the MCU should jump for each interrupt. The first entry is the initial stack pointer, and the second is the Reset_Handler.

    EXPORT  __vector_table
    EXPORT  Reset_Handler

    AREA    RESET, DATA, READONLY
    ALIGN

__vector_table
    DCD     __initial_sp
    DCD     Reset_Handler
    DCD     NMI_Handler
    DCD     HardFault_Handler
    ; ... (rest of the interrupt vectors)

 

Note: IAR places the vector table in flash at address 0x08000000 unless changed via linker.

 

2. Stack and Heap Configuration:

The actual stack size is defined in the IAR linker configuration file (.icf), not directly in the startup assembly file. The startup file declares symbols for the initial stack pointer, often like this:

EXTERN __initial_sp

 

3. Default Exception and Interrupt Handlers:

All exception and IRQ handlers are declared as weak by default. If you do not provide your own implementations, they will default to a dummy handler that runs an infinite loop. You can override any of these handlers simply by defining a function with the same name in your C code.

NMI_Handler
        B       .

HardFault_Handler
        B       .

 

4. The Reset Handler:

This is the first function called after a reset. In IAR, the Reset Handler typically looks like:

Reset_Handler: 
        LDR     R0, =SystemInit
        BLX     R0

        LDR     R0, =__iar_program_start
        BX      R0

Here, __iar_program_start is an internal C library function provided by provided by the IAR runtime library. It’s called by the Reset_Handler in the startup file.

  • SystemInit() is a function defined in system_stm32h5xx.c. It typically sets up:
    • Clock configuration
    • PLL setup
    • FPU configuration
    • Possibly watchdog disabling, etc.
  • __iar_program_start() is the IAR-specific C runtime entry point:
    • Zero-initialization of .bss
    • Copying initialized .data from flash to RAM
    • Calling global/static C++ constructors
    • Calling main()

📝 Note: If you don’t see SystemInit being called explicitly, then you should add it yourself just before __iar_program_start.

 

⚙️ Customizing the Startup File in IAR:

You code stuck before main()? Here’s what to check:

  • Verify the vector table.
    Make sure SCB->VTOR points to the correct base address and the table is properly aligned.
  • Check the stack pointer.
    The first word in the vector table must contain a valid initial SP value—usually the top of SRAM.
  • Review SystemInit().
    Look for any incorrect clock or peripheral configurations that could cause a lock-up.
  • Initialize .data and .bss sections properly.
    If the runtime doesn’t zero or copy them correctly, uninitialized variables can cause undefined behavior.

✅ Use IAR C-SPY to debug early startup code.
Place breakpoints in Reset_Handler and step through line-by-line to find where the system fails.