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
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:
- initial stack pointer value;
- reset handler address;
- 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
- ARM: Cortex-M Startup Files and Vector Tables
- Microchip AVR Libc User Manual: Startup Code
- GNU LD Linker Documentation