Post

Bare Metal STM32 - compilation

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.

This post is licensed under CC BY 4.0 by the author.

Trending Tags