A detailed explanation of the STM32 memory mapping and bootloading process.
While all the examples below are for STM32F446, the basic principles apply to most MCUs.
STM32CubeMX produces STM32F446RETx_FLASH.ld which we'll be looking at as a reference for all our explorations.
The linker takes the object files produced by the compiler and generates the final compilation output, which in our case is the .elf binary. The linker always uses a linker script file; even if you don't specify one, a default script is used.
Important to note, that the linker script only describes the memory based on the MCU specifications and does not alter any hardware memory adressing.
We can't talk about bootloader process without understanding the memory structure. Actually it's the purpose of the bootloader to have the memory in a state ready to execute our application's main()
method.
Program memory, data memory, registers and I/O ports in STM32F4 are organized within the same linear address space.
...
+---- 0x2001FFFF ----+
| |
| RAM |
| |
+---- 0x20000000 ----+
| ... |
+---- 0x1FFF7A0F ----+
| |
| System |
| |
+---- 0x1FFF0000 ----+
| ... |
+---- 0x081FFFFF ----+
| |
| Flash |
| |
+---- 0x08000000 ----+
| ... |
+---- 0x001FFFFF ----+
| |
| Alias |
| |
+---- 0x00000000 ----+
Where Alias memory is pointing to Flash, System or RAM memory depending on the BOOT0
pin. By default it's FLASH.
Two spaces are of the most interest for us for now:
- RAM stores data produced by the running programm, utilizing heap or stack abstractions for memory management. The data isn't persisted.
- FLASH keeps the program binary, constants and initial variables values, used by bootloader to initialize RAM on startup. The data is persisted.
However, in practice, the functional division could vary. For instance, you may need to load program binary data into RAM or vice versa.
Memory structure is reflected in the linker file. FLASH starts at ORIGIN = 0x8000000
and RAM at ORIGIN = 0x20000000
.
// linker file
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 512K
}
Note that this is just a default implementation, you could easily split FLASH and RAM and add your own blocks or sections as soon as they adhere to the MCU memory specification.
Memory is then split in the linker file into sections, each having its address and destination (for instance, FLASH or RAM).
// linker file
SECTIONS
{
...
.text :
{
...
} >FLASH
...
}
To explore .elf symbol table we'll be using arm-none-eabi-objdump command and sort all entries by their addresses:
arm-none-eabi-objdump -t stm32-boot-explained.elf | sort
The program produces output as described in details in the manual:
Address Flag_Bits Section Size Name
Compiled programm code goes into this section in FLASH memory.
The section starts from the flash's ORIGIN
address, and _etext
points to the last address of the section.
08000000 l d .text 00000000 .text
08000c00 g .text 00000000 _etext
We can see the location of the functions, that we use in our bootloader file:
080005c8 g F .text 00000048 __libc_init_array
08000610 g F .text 000000d8 main
080006e8 g F .text 00000064 Reset_Handler
08000bc4 g F .text 00000024 SystemInit
The section resides in RAM memory and holds all variables with defined values. The address range spans from _sdata
to _edata
.
20000000 g .data 00000000 _sdata
20000010 g .data 00000000 _edata
Let's check what variables from main.c reside in this section:
20000000 l O .data 00000004 static_data_int
20000008 g O .data 00000008 data_double
Actual values must be filled by a bootloader script by copying data from FLASH memory at address _sidata
:
08000c08 g *ABS* 00000000 _sidata
Block Starting Symbol RAM data section for all variables without assigned value. The bootloader takes care of setting this block's data to 0
.
Memory range spans from _sbss
to _ebss
.
20000010 g .bss 00000000 _sbss
20000044 g .bss 00000000 _ebss
All variables from main.c without explicit value reside in this section:
2000002c l O .bss 00000004 static_bss_int
20000030 g O .bss 00000008 bss_double
20000038 g O .bss 00000008 bss_my_struct
20000040 g O .bss 00000004 bss_my_union
Let's check with gdb:
(gdb) p bss_double
$1 = 0
(gdb) p &bss_double
$2 = (double *) 0x20000030 <bss_double>
Once you assign some value to the variable, its address doesn't change and it still stays in .bss
:
(gdb) p bss_double
$3 = 86
(gdb) p &bss_double
$4 = (double *) 0x20000030 <bss_double>
All RAM memory above _end
and until _estack
is dedicated to heap and stack memory.
20000048 g ._user_heap_stack 00000000 _end
20020000 g .isr_vector 00000000 _estack
_estack
address is calculated as ORIGIN(RAM) + LENGTH(RAM)
.
So that for 128Kb RAM:
_estack = 0x20000000 + 128 * 1024 # dec
= 0x20000000 + 0x20000 # hex
= 0x20020000
Stack is a LIFO structure that starts at _estack
and grows downwards. The minimum stack size is defined in the linker file as _Min_Stack_Size
. Stack memory is automatically freed.
+---- 0x20020000 ----+ <-- _estack
| |
| Stack |
| |
+ - - 0x2001FC00 - - + <-- -_Min_Stack_Size
| |
+---- 0x200sssss ----+ <-- $msp register
| |
| |
| Free space |
| |
| |
+---- 0x200hhhhh ----+ <-- Actual heap end
| |
| Heap |
| |
+ - - 0x20000248 - - + <-- +_Min_Heap_Size
| |
+---- 0x20000048 ----+ <-- _end
Heap in turn starts from _end
and grows upwards up to _estack - _Min_Stack_Size
when requested by malloc
.
We have a stack_int
variable defined in main.c. Let's check the address with gdb after it's been initialized:
(gdb) p &stack_int
$1 = (unsigned short *) 0x2001ffd6
(gdb) p $msp
$2 = (void *) 0x2001ffd0
Which matches with our expectations, when the variable is above the $msp
.
It's worth mentioning that the standard C library implementation commonly used in embedded C applications is Newlib. This library requires the implementation of certain system-specific functions. STM32CubeMX generates the syscalls.c file with the necessary default implementations.
It's also the case for sbrk
call that increases program data space. malloc
is using this function to allocate more heap memory. You may find an implementation generated by STM32CubeMX in sysmem.c. It simply allows the heap to grow from _end
up to _estack - _Min_Stack_Size
.
Now when we understand the MCUs memory, let's connect to our programm with gdb, here's what we see as the first output:
...
Reading symbols from ./stm32-boot-explained.elf...
Remote debugging using localhost:61234
Reset_Handler () at %PATH%/bootloader.c:25
25 void Reset_Handler() {
(gdb) info registers
...
pc 0x80006e8 0x80006e8 <Reset_Handler>
...
Reset_Handler
was somehow identified as a boot point for our application.
You may have noticed that there's an ENTRY
instruction in the linker file:
// linker file
ENTRY(Reset_Handler)
Actually, it saves a reference to the Reset_Handler
function address in the .elf file header:
arm-none-eabi-objdump -t -f stm32-boot-explained.elf | grep "start address"
start address 0x080006e9
Looking at our symbol table, Reset_Handler
is present in the FLASH .text
section, just like all other functions:
080006e8 g F .text 00000064 Reset_Handler
Addresses are page aligned, which is why there's a possibility for a mismatch between the .elf header and the actual address.
Though this information is mostly used by the linker to verify the existence of the entry point symbol within the code, it has no practical meaning for the MCU.
According to the STM32 specification, the CPU fetches the top-of-stack _estack
value from address 0x00000000
, then begins code execution from the boot memory starting at 0x00000004
.
+---- 0x001FFFFF ----+
| |
| Alias |
| |
+---- 0x00000000 ----+
This is exactly where the Alias memory mentioned above is defined. With the default configuration, when pin value BOOT0 = 0
, it aliases to the FLASH memory block starting at 0x8000000
.
Other options based on the BOOT0
and BOOT1
include System memory with an embedded bootloader or RAM memory.
The embedded bootloader is programmed by ST during production and out of scope of this manual.
But Reset_Handler
has an address 0x08000524
which is not exactly the beginning of the FLASH memory, how does the MCU find the bootstrap method then?
Here's where Vector Table comes into play.
08000000 g O .isr_vector 000001c4 Vector_Table
MCU treats the beginning of memory as a Vector Table, that contains pointers to various interrupt service routines and essential startup functions, including the Reset_Handler
. Consult the spec to see exact table structure that MCU expects to load from 0x00000000
. Actual table must be filled by the bootloader.
Address | Name |
---|---|
0x00000000 | Reserved |
0x00000004 | Reset Handler |
0x00000008 | Non Maskable Interrupt |
0x00000012 | Hard Fault |
... | Other Interrupts |
This Reset_Handler
is a bootloader function that can be used for many applications, from security-specific tasks to firmware auto-updating. Here, we'll explore the basic default implementation to understand its interaction with the MCU's memory.
By default Reset_Handler
method is defined in the startup_stm32f436xx.s ASM file provided by STM23CubeMX. Actual bootloader.c implementation in this project is written in C for clarity.
Note that variables defined in the linker script can be accessed in C code:
extern uint32_t _estack;
So that it's easily possible to replicate the ASM version.
The minimal loading process then could be split into the following steps:
- Setup the microcontroller system, initialize the FPU setting, vector table location and External memory configuration (
SystemInit()
function) - Copy the
.data
segment initializers from FLASH to RAM - Zero fill the
.bss
segment - Call static constructors (
__libc_init_array()
function) - Call the application's entry point (
main()
function)
For steps #1 and #4, STM32CubeMX provides function implementations, you can check details in system_stm32f4xx.c.
The project has a minimal set of files required to boot up the STM32. You may want to try it yourself to check the output of arm-none-eabi-objdump and step through with gdb.
ARM GNU Toolchain is required to build the project.
It's recommended to install an all-in-one STM32CubeCLT command tools package with arm-none-eabi-gcc, STM32_Programmer_CLI and ST-LINK_gdbserver tools included.
Build the project using CMake:
mkdir build; cd build
cmake ../ -DPROGRAMMER_CLI=/opt/ST/STM32CubeCLT_1.15.1/STM32CubeProgrammer/bin \
-DGDB_SERVER=/opt/ST/STM32CubeCLT_1.15.1/STLink-gdb-server/bin
make VERBOSE=1
Note that the last command is the linking stage. If we strip all other compiler flags, the command could look like the following. This is where the linker is instructed to use our linker script.
arm-none-eabi-gcc
...
-T "%SCRIPT_DIR%/STM32F446RETx_FLASH.ld"
...
"%OBJ_DIR%/%OBJECT_NAME%.c.obj"
...
-o stm32-boot-explained.elf
STM32_Programmer_CLI is preconfigured for SWD procotol, just run:
make flash
Take a note on the programmer output, which is using 0x08000000
address as a starting point:
...
Memory Programming ...
Opening and parsing file: stm32-boot-explained.elf
File : stm32-boot-explained.elf
Size : 1,46 KB
Address : 0x08000000
...
It's actually the starting address of the FLASH memory, which we already know.
There's a custom target pre-configured to run ST-LINK_gdbserver:
# start ST-Link gdb server
make gdb-server
# connect with gdb debugger
gdb -ex 'target remote localhost:61234' ./stm32-boot-explained.elf