Loading header...

Startup, Reset Handler, and main()

Many beginners imagine a microcontroller starts by "calling main()." It does not. After reset, the CPU starts from a defined address, executes startup code, initializes runtime state, and only then reaches main().

If startup is broken, main() may never run at all.

Learning Objectives

By the end of this lesson, you should be able to:

  • describe the sequence from reset to main();
  • explain the roles of the vector table, reset handler, stack pointer, .data, and .bss;
  • understand why global initializers cost startup time and memory;
  • recognize typical startup bugs during bring-up.

High-Level Reset Flow

flowchart TD A["Reset event\npower-on, external reset, watchdog, brownout"] --> B["CPU fetches reset vector"] B --> C["Reset handler / startup code"] C --> D["Initialize stack pointer"] D --> E["Copy initialized data from Flash to RAM"] E --> F["Zero .bss"] F --> G["Optional C library init / constructors"] G --> H["Call main()"] H --> I["Program loop or scheduler"]

That sequence is part of the firmware image, even if the IDE hides it.

The Vector Table

Most MCUs store interrupt and exception entry points in a table at a defined location in memory. One entry corresponds to reset.

On Cortex-M devices, the vector table usually begins with:

  1. initial stack pointer value;
  2. reset handler address;
  3. interrupt handler addresses.

On AVR devices, reset and interrupt vectors occupy fixed instruction locations near the start of program memory.

What the Reset Handler Must Do

Before ordinary C code can run correctly, startup code typically must:

  • set the stack pointer;
  • configure memory segments;
  • clear uninitialized globals;
  • prepare library state if required.

.data

Variables with initial values, such as:

uint16_t timeout_ms = 250;

need RAM at runtime but their initial values are stored in Flash. Startup copies them from Flash to RAM.

.bss

Variables with no explicit initializer, such as:

uint8_t rx_buffer[64];

are placed in .bss and zeroed during startup.

Why This Matters

If .bss is not cleared:

  • state machines may start in random states;
  • counters may begin nonzero;
  • pointer variables may contain garbage.

If .data is not copied:

  • initialized configuration values are wrong;
  • lookup tables in RAM start corrupted;
  • code behaves differently from what the source implies.

Typical AVR Perspective

On AVR, startup is often provided by the runtime library and linker script. Conceptually, it:

  • sets SP to the top of RAM;
  • jumps through initialization routines;
  • clears .bss;
  • copies .data;
  • calls main().

You may never write that code yourself at first, but you should know it exists.

Typical Cortex-M Perspective

On Cortex-M, the reset vector usually points to a function often named Reset_Handler. That handler calls support routines such as SystemInit(), copies sections, clears .bss, and then enters main().

void Reset_Handler(void) {
    copy_data_section();
    zero_bss_section();
    SystemInit();
    main();
    while (1) {
    }
}

The exact implementation varies by vendor, but the responsibility is the same.

What Happens If main() Returns?

In hosted desktop C, returning from main() exits the program. In bare-metal firmware there is usually nowhere meaningful to return to. Good startup code often traps forever if main() returns unexpectedly.

int main(void) {
    for (;;) {
        run_tasks();
    }
}

That infinite loop is normal in embedded systems.

Startup Cost and Design Tradeoffs

Every initialized global variable adds startup work or memory cost. This matters when:

  • boot time must be very short;
  • RAM is scarce;
  • startup occurs after watchdog reset;
  • safety logic must respond immediately.

Large tables should often stay in Flash if the architecture allows it, rather than being copied into RAM unnecessarily.

Bring-Up Symptoms of Startup Problems

  • CPU keeps resetting before main().
  • debugger breaks in a default fault handler.
  • globals have impossible values.
  • interrupts fire before required initialization is complete.
  • code works in debug build but fails in release because of layout differences.

Common Mistakes

  • Believing main() is the first code executed.
  • Performing complex work in a global initializer without considering startup order.
  • Using too much RAM in global buffers and only noticing at link time.
  • Enabling interrupts before the system state is ready.
  • Forgetting that startup files and linker scripts are part of the firmware, not background magic.

Summary

main() is the destination of startup, not the beginning of startup. After reset, the MCU enters through a reset vector, startup code prepares the stack and memory, runtime sections are initialized, and only then does main() run. Understanding that path is essential for debugging boot issues and controlling firmware memory layout.

Further Reading

Mind Map

mindmap root((Reset to main)) Core sequence Reset event Fetch vector Run handler Set stack Init sections Call main Memory sections text in Flash data copied to RAM bss zeroed rodata constants stack grows in RAM Applications Board bring up Boot debugging Low boot time Watchdog recovery Custom startup Design rules Vector table correct Stack pointer valid Copy data before globals Zero bss before use Trap if main returns Practical checks Break at Reset_Handler Inspect map layout Check initial SP Verify globals Disable early interrupts Mistakes Assuming main is first Oversized globals Bad linker script Early ISR enable Hidden init work