The startup file is one of the most important parts of embedded software for ARM Cortex-M microcontrollers, but it is often forgotten. This file runs before your main application starts and gets the microcontroller ready to run your code.
Without it, even the most basic embedded program wouldn’t work right.
If you know how the startup file works, you can learn a lot about the boot process, memory initialisation, interrupt handling, and the overall structure of embedded firmware.
It also helps developers fix low-level problems and change how the system works for more advanced apps.
What Is a Startup File?
A startup file is a low-level piece of code, typically written in assembly or a mix of assembly and C, that runs immediately after the microcontroller resets. It acts as the bridge between the hardware reset state and your application’s main function.
When power is applied or the system is reset, the processor begins executing instructions from a predefined memory location. The startup file resides at this location and takes control of the system before any high-level code is executed.
This file defines the initial state of the system, including setting up the stack pointer, initializing memory sections, and configuring the interrupt vector table.
It ensures that the runtime environment is correctly prepared so that your C or C++ program can execute reliably.
The Reset Sequence and Entry Point
When an ARM Cortex-M microcontroller starts, it does not immediately jump to the main() function. Instead, it follows a well-defined reset sequence. The processor reads the first two entries from memory.
The first entry sets the initial stack pointer, and the second entry provides the address of the reset handler. The reset handler is the first function executed after startup and is defined in the startup file.
The reset handler performs several critical tasks. It initializes system memory, sets up data sections, and prepares the runtime environment.
Only after these steps are completed does it call the main() function. This sequence ensures that all variables and system resources are in a known and stable state before the application begins.
The Vector Table
The interrupt vector table is one of the most important parts of the startup file. This table is basically a list of function pointers that tell the system what to do when an interrupt or exception happens. Each entry is linked to a certain interrupt source, like a timer, an external input, or a fault condition.
The vector table is usually at the beginning of memory and has entries for core system exceptions like reset, non-maskable interrupt (NMI), and hard fault. There are also entries for peripheral interrupts in it. The processor uses this table to figure out which function to run when an interrupt happens.
By creating their own functions with the same names as the default interrupt handlers, developers can change how they work. This flexibility lets you control exactly how the system responds to events, which is very important in real-time embedded applications.
Memory Initialization
Before the application code runs, the startup file is responsible for initializing memory sections. In embedded systems, variables are often stored in different memory regions depending on their type and usage.
For example, initialized global variables are stored in flash memory but need to be copied to RAM before use. Uninitialized variables, on the other hand, must be set to zero.
The startup file handles this process by copying the initialized data section from flash to RAM and clearing the uninitialized data section.
This step is crucial because the C language assumes that global and static variables are properly initialized before main() is executed. Without this initialization, programs may behave unpredictably.
Stack and Heap Setup
The startup file also defines the initial stack pointer, which is essential for function calls, local variables, and interrupt handling.
The stack is typically located in RAM and grows downward as functions are called. Proper stack initialization ensures that the system can manage nested function calls and interrupts without corruption.
In some systems, the startup file may also define the heap, which is used for dynamic memory allocation. While not all embedded applications use dynamic memory, having a properly configured heap allows for more flexible memory management when needed.
System Initialization Function
After basic memory setup, the startup file often calls a system initialization function. This function configures essential hardware components such as clocks, power settings, and peripheral interfaces.
It ensures that the microcontroller operates at the correct frequency and that all required subsystems are ready for use.
This stage is particularly important because many peripherals depend on accurate clock configuration. Incorrect setup can lead to communication failures, timing issues, or unstable system behavior.
By centralizing this configuration, the startup process provides a consistent and reliable foundation for the application.
Transition to the Main Application
Once all initialization steps are complete, the startup file transfers control to the main() function. At this point, the system is fully prepared, and the application code can execute as expected. From the perspective of a high-level programmer, this transition appears seamless, but it is the result of careful preparation carried out by the startup code.
If the main() function ever returns, which is uncommon in embedded systems, the startup file typically enters an infinite loop to prevent the system from executing undefined instructions. This behavior ensures stability even in unexpected situations.
Default Handlers and Weak Symbols
Startup files often have default interrupt handlers that are weak symbols. This means that user-defined functions can change them without having to change the startup file itself. If an interrupt happens and there is no custom handler, the default handler runs. This usually leads to an infinite loop.
This feature is helpful for debugging because it lets developers find interrupts that haven’t been handled. Developers can change how the system responds to certain events by replacing weak handlers with their own implementations.
Why Understanding the Startup File Matters
Many developers don’t look at the startup file very often because development tools make it for them. But to do advanced embedded programming, you need to know how it works and what it’s for. It lets developers fix problems with initialisation, make better use of memory, and set up custom boot sequences.
Changing the startup file in a complicated system can give you more control over how it works and how it behaves. For instance, developers can change how memory is initialised to speed up boot times or add special interrupt handling for real-time apps.
One of the best things about working with low-level embedded systems is that you have this much control.
Common Variations Across Toolchains
Startup files can be different depending on the toolchain and microcontroller family, but the basic ideas stay the same. Some are written only in assembly, while others use both assembly and C. The way things are named and set up may be different, but the main tasks—initializing memory, setting up the vector table, and calling main()—are the same in all implementations.
Developers can work on different platforms and get used to new development environments more easily if they know about these differences.

