Bare Metal STM32 - compilation
There’s no better way to truly understand something than building it from scratch. So I decided to dust off my STM32 Nucleo board and try to bring it up completely “bare metal”. Let’s start with the simplest possible program:
1
2
3
4
5
6
/* main.cpp */
int main()
{
while (1) {}
}
First problem – libc
When trying to compile, we immediately hit the first issue:
1
2
3
4
5
arm-none-eabi-g++ main.cpp -o app.elf
... undefined reference to `_exit'
... undefined reference to `_write'
... undefined reference to `_sbrk'
The linker is trying to pull in functions from libc, which in turn assume the presence of an operating system (e.g. via syscalls like _write, _sbrk, etc.).
The problem is — in bare metal, there is no OS.
So the first attempt to fix this is to disable the standard library:
1
arm-none-eabi-g++ main.cpp -nostdlib -ffreestanding -o app.elf
Missing entry point
This time we get:
1
ld: warning: cannot find entry symbol _start
By default, the linker expects a _start symbol (the “hosted” world), but in embedded systems we define the entry point ourselves — usually Reset_Handler.
So let’s create a minimal startup:
1
2
3
4
5
6
7
8
9
/* startup.cpp */
extern "C" void Reset_Handler()
{
extern int main();
main();
while (1) {}
}
And tell the linker to start from there:
1
-Wl,-e,Reset_Handler
C++ and exceptions
Next attempt:
1
undefined reference to `__aeabi_unwind_cpp_pr1'
This happens because the compiler generated exception handling code.
In bare metal environments:
- exceptions are usually disabled
- RTTI is typically not used
So we disable everything:
1
2
3
4
-fno-exceptions
-fno-unwind-tables
-fno-asynchronous-unwind-tables
-fno-rtti
First successful build
Final command:
1
2
3
4
5
arm-none-eabi-g++ main.cpp startup.cpp \
-nostdlib -ffreestanding \
-fno-exceptions -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-rtti \
-Wl,-e,Reset_Handler \
-o app.elf
An ELF file is generated — so at least we’re moving forward.
Is the code in the right place?
Before flashing anything, it’s worth checking where the linker actually placed our code:
1
arm-none-eabi-objdump -h app.elf
We get:
1
.text 00008000
That looks suspicious.
For STM32, code should start in FLASH at:
1
0x08000000
So clearly our code ended up in the wrong place.
Linker script
We need to tell the linker what the memory layout of the microcontroller looks like.
A minimal linker script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* linker_script.ld */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
}
SECTIONS
{
.text :
{
*(.text*)
*(.rodata*)
} > FLASH
}
Add it to the build:
1
-T linker_script.ld
Verification
1
arm-none-eabi-objdump -h app.elf
Now we get:
1
.text 08000000
That looks much better.
Next step is to actually flash this to the board and see what happens.
And a lot will go wrong — which is exactly what we want.