STM32 Tutorial: Bootloader Memory Manipulation and Jump Execution
Abstract
Every embedded developer eventually faces the challenge of implementing Over-The-Air (OTA) updates or building custom bootloaders. While creating a bootloader might sound daunting, the core mechanism—jumping from the boot code to the application code—is a straightforward sequence of memory manipulation and function pointers. In this article, we dive into the anatomy of a custom bootloader on an ARM Cortex-M microcontroller. We will explore how to configure the linker scripts to divide the Flash memory, prepare the MCU for a safe transition, and write the C code necessary to execute the jump to the main application.
1. Introduction
A bootloader is simply a piece of code that runs before your main application. It has its own memory space, its own execution context, and its own responsibilities (like verifying firmware integrity, decrypting payloads, or downloading new binaries). However, the most critical part of any custom bootloader is the handover—the exact moment it relinquishes control to the main firmware.
To achieve this safely on an STM32 architecture, we cannot simply use a standard goto statement. The MCU relies on a Vector Table, and we must respect the Initial Main Stack Pointer (MSP) and the Reset Handler of the target application to ensure a successful boot.
2. Prerequisites
Before diving into the code, ensure you understand the following concepts:
Memory Mapping: Knowing where your Flash and SRAM reside in the MCU’s address space.
Linker Scripts (
.ldfiles): The configuration file that tells the compiler where to place the generated binary in the MCU’s memory.C Function Pointers: The low-level mechanism we will use to force the CPU to execute code at a specific memory address.
3. Linker Script Configuration
By default, an STM32 linker script assumes the firmware has access to the entire Flash memory, typically starting at 0x08000000. To coexist, the bootloader and the application need their own isolated regions.
The Bootloader Linker Script
First, we must restrict the bootloader to the beginning of the Flash memory. Let’s assume our MCU has 128KB of Flash, and we want to allocate 32KB (0x8000) exclusively for the bootloader.
/* Bootloader Linker Script */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 36K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K /* Restricted to 32KB */
}
The Application Linker Script
Next, the application must be told to compile its code with an offset. It will reside immediately after the bootloader, starting at 0x08008000.
/* Application Linker Script */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 36K
FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 96K /* Starts after the bootloader */
}
Pro Tip: Remember to also update the Vector Table Offset Register (VTOR) in your application’s system_stm32xx.c file to point to 0x08008000. Without this, the application won’t know where its hardware interrupts are located!
Notice> If using Cortex M0 and not M0+, relocating the interrupt vector in FLASH is not possible, so move it to RAM and be prepared to share it between Boot and App.
4. The Anatomy of the Jump
When an ARM Cortex-M microcontroller boots up, it expects two specific values at the very beginning of its binary:
Word 0 (Offset
0x0): The Initial Main Stack Pointer (MSP).Word 1 (Offset
0x4): The Reset Vector (the address of the first instruction, typically the application’s startup routine).
To execute the application, our bootloader must read these two values from the application’s starting address (0x08008000), update the hardware Stack Pointer, and trigger a jump to the Reset Vector.
5. Pre-Flight Cleanup
Before we execute the jump, the MCU must be returned to a pristine state. If the bootloader initialized peripherals, enabled global interrupts, or started the SysTick timer, those will still be running when the application starts. If a bootloader UART interrupt fires before the application has configured its own handlers, the CPU will hard-fault.
Always follow this checklist before jumping:
Disable global interrupts.
Disable the SysTick timer.
Clear pending interrupt flags.
De-initialize peripherals (like UART, SPI, or USB) using
HAL_DeInit().
6. Hands-On Lab: Writing the Jump Code
In this experiment, we will use the STM32G071RB. For this particular example, just the jump will be performed, no FLASH programming, no firmware update, just the jump.
In order to make this work, make sure you have a simple application, such as the blink LED with the linker script and VTOR offset changes and the Boot, with the code explained below.
So, let’s put it all together. Here is the standard C function to safely execute the jump from the bootloader to the application.
#include
#include
#include "stm32g0xx.h" /* Replace with your specific MCU CMSIS header */
#define APP_ADDRESS 0x08008000 /* The Flash offset where the application lives */
/* Create a typedef for a function pointer that takes void and returns void */
typedef void (*pFunction)(void);
void go_to_app(void) {
printf("Checking for application presence...\r\n");
/* 1. Read the Stack Pointer value from the application's base address.
An erased Flash reads as 0xFFFFFFFF. We check if the SP points to a valid RAM address. */
uint32_t app_stack_pointer = *(__IO uint32_t*)APP_ADDRESS;
if (app_stack_pointer != 0xFFFFFFFF) {
printf("Application found! Preparing to jump...\r\n");
/* 2. Pre-flight cleanup: Disable all interrupts and SysTick */
__disable_irq();
SysTick->CTRL = 0;
/* 3. Fetch the application's Reset Handler address (Base Address + 4 bytes) */
uint32_t app_reset_handler = *(__IO uint32_t*)(APP_ADDRESS + 4);
/* 4. Cast the reset handler address to our function pointer */
pFunction JumpToApplication = (pFunction)app_reset_handler;
/* 5. Initialize the MCU's Main Stack Pointer (MSP) to the application's SP */
__set_MSP(app_stack_pointer);
/* 6. Execute the jump! */
JumpToApplication();
} else {
/* No valid application found at the destination address */
printf("No application found! Halting.\r\n");
while(1) {
/* Error trap: Stay in the bootloader loop, await commands, or blink an LED */
}
}
}
Inside your bootloader’s main() function, you simply call go_to_app() once you are ready to pass execution over.
In case you want to know how to add the printf in the STM32, please refer to this article> https://hackerembedded.com/how-to-implement-printf-with-stm32-setup-for-stm32cubeide-and-vs-code/
Conclusion
Creating a custom bootloader is a major milestone for any embedded software engineer. By properly carving up your Flash memory with linker scripts, respecting the Cortex-M boot sequence, and safely resetting the MCU state before passing execution via function pointers, you can build a highly reliable update mechanism. Mastering these low-level concepts empowers you to take complete control over your microcontroller’s lifecycle.
Happy Coding =)


