The Cortex-M0 is one of ARM’s most basic and widespread microcontroller cores. As an ultra low power 32-bit chip optimized for cost-sensitive and power-constrained embedded applications, Cortex-M0 can be found in a vast range of IoT and wearable devices. To develop software for Cortex-M0, understanding how to build the correct startup code and initialization routines is essential.
When using GCC as the toolchain, the crt0.o object file implements the standardized C runtime initialization and must be customized for each specific Cortex-M0 target. This involves configuring the vector table, stack pointer, hardware initialization, and calling the main() function. With the right startup code in place, you can then leverage the full capabilities of GCC to build efficient C/C++ applications for your Cortex-M0 chip.
Understanding the Role of crt0.o
The crt0.o object performs several key tasks during startup:
- Sets up the initial stack pointer value
- Initializes static C++ objects
- Calls constructors for global C++ objects
- Calls the main() function
- Implements the default interrupt vector table
- Performs any target-specific hardware initialization
Without a properly configured crt0.o, your Cortex-M0 application will fail to start correctly. The good news is that once you understand the basic initialization steps, creating a custom crt0.o is straightforward.
Stack Pointer Initialization
One of the first things crt0.o must do is set the initial stack pointer value on startup. On Cortex-M0, this is done by populating the value into the Main Stack Pointer (MSP) register which points to the master stack used by Thread mode code.
In GCC, the stack pointer location is indicated using the following syntax: .word _estack
Where _estack is a symbol that points to the end of the stack memory region reserved for your Cortex-M0 application.
Configuring the Interrupt Vector Table
Cortex-M0 supports Nested Vectored Interrupt Controller (NVIC) for managing hardware interrupts. This is enabled by populating a vector table with addresses pointing to the Interrupt Service Routines (ISRs).
The vector table should be placed in a dedicated .vector_table section. Each entry corresponds to a specific interrupt or exception and indicates the location of the associated ISR function.
For example: .section .vector_table .word _estack /* 0x0000 */ .word Reset_Handler /* 0x0004 */ .word NMI_Handler /* 0x0008 */ .word HardFault_Handler /* 0x000C */
The Reset_Handler label points to the startup initialization code within crt0.o that will call main(). Each subsequent entry points to interrupt handler functions defined elsewhere in your application code.
Calling the main() Function
After configuring the stack pointer and interrupt vector table, the next step is to call main(). This is done inside the Reset_Handler code: Reset_Handler: bl main
The main() function serves as the entry point for your C/C++ application code. Any target-specific hardware initialization steps should be performed before calling main().
Target Hardware Initialization
Depending on the specific Cortex-M0 chip and board being used, some amount of hardware initialization may be required before starting the main application. This includes things like:
- Clock initialization
- GPIO pin configuration
- Watchdog timer setup
- Chip-specific errata workarounds
These initialization steps can be implemented in a systemic_init() function called before main(): Reset_Handler: bl system_init bl main
The hardware initialization requirements will vary across Cortex-M0 targets and may require consulting the chip vendor’s documentation.
Building the Default crt0.o
With an understanding of the key startup steps, we can now look at building the default crt0.o object file for GCC. This can be done by creating a cortexm0.S assembly source file with the following contents: .section .stack .word _estack /* Set stack pointer */ .section .vector_table .word _estack /* Initial stack pointer value */ .word Reset_Handler /* Reset vector */ /* Other interrupt vectors… */ /* Reset handler */ Reset_Handler: bl system_init bl main system_init: /* Target hardware initialization */ /* Default interrupt handlers */ Default_Handler: Infinite_Loop: b Infinite_Loop
This implements stack and vector table initialization, calls target-specific init code, runs main(), and provides default interrupt handler stubs. The file is assembled into an object file: arm-none-eabi-as cortexm0.S -o cortexm0.o
And then passed to the linker when building your Cortex-M0 application: arm-none-eabi-gcc test.c cortexm0.o -o test.elf
With this approach, you now have a proper crt0.o default startup file to build atop for your GCC-based Cortex-M0 projects.
Customizing crt0.o
For most projects, additional customization of crt0.o will be needed beyond the bare basics shown above. Common extensions include:
- Adding chip errata workarounds in system_init()
- Implementing custom interrupt handlers
- Setting up the C library newlib
- Init code for peripheral libraries
- Enabling semihosting for debug logging
Consult your Cortex-M0 reference manual and data sheet to determine what additional initialization and configuration must be performed for your specific chip implementation.
Linker File Configuration
To complement the crt0.o startup code, you will need to provide a linker script file that defines the memory regions and output sections for your chip. This ensures code and variables are placed into the correct Flash and RAM locations.
Key linker file settings needed for Cortex-M0 GCC projects include:
- Flash and RAM memory region addresses and sizes
- Placement of vector table section
- Linker script symbols for stack and heap
- .bss and .data section configuration
Consult the GCC linker documentation for details on how to properly configure memory regions, output sections, and other linker script directives.
Debugging Startup Code
Debugging issues with crt0.o and early startup code can be challenging. Some techniques to help include:
- Enable semihosting in crt0.o for early debug printfs
- Step through initialization code line-by-line in a debugger
- Add temporary LED toggles in startup code to indicate progress
- Print stack pointer address after initialization to verify location
- Check linker map file to ensure sections are placed as expected
With careful use of debugging aids like these, you can identify and resolve bootup crashes, memory errors, stack overflows, and configuration problems caused by your Cortex-M0 crt0.o and startup code.
Conclusion
While creating a properly configured crt0.o requires some upfront effort, the benefits are substantial for building robust and efficient Cortex-M0 applications with GCC. Following the steps outlined here will give you a solid foundation to start your projects and customize further for your specific chip and use case needs.
With a customized crt0.o, linker script, and startup code tuned for your hardware, you can leverage the full capabilities of GCC to maximize the performance and capabilities of your Cortex-M0 microcontroller.