Introduction
Welcome back! In our previous lesson, you learned the powerful technique of placing specific variables and functions at precise memory locations by modifying the linker script. This gave you fine-grained control over individual elements of your firmware.
In this lesson, we will scale that concept up to a full system level. You will learn how to design a memory map for a system with a separate bootloader and application. This is a cornerstone of professional embedded systems design, as it's the fundamental prerequisite for enabling field firmware updates—a feature present in countless products, from IoT devices to modern cars.
We will cover:
- The concept of partitioning the MCU's flash memory.
- How to configure the linker scripts for both a bootloader project and an application project.
- The critical step of relocating the application's vector table to ensure it runs correctly.
By the end of this lesson, you will have the complete blueprint for setting up a two-part firmware system, paving the way for implementing a full bootloader in the lessons to come.
1. Partitioning the Flash Memory
Our STM32F103C8T6 has 64 KB of flash memory, which appears as a single, contiguous block starting at address 0x08000000. To host both a bootloader and an application, we must conceptually divide this space into distinct regions.
A typical and robust memory map includes three main parts:
- Bootloader Region: This is where the bootloader code resides. It always starts at the beginning of flash (
0x08000000) because this is where the CPU begins execution after a reset. The bootloader should be as small and reliable as possible, as it is difficult to update once the product is in the field. - Application Region: This is where the main application firmware lives. Since the bootloader occupies the start of flash, the application will be located at an offset. This is the region that our bootloader will eventually learn to erase and rewrite.
- Metadata Region (Optional but Recommended): A small, separate region, often just one flash page (1 KB on the STM32F103), can be reserved between the bootloader and the application. This space is invaluable for storing information about the application, such as its size, a version number, or a CRC checksum for integrity verification.
The image below shows a practical memory map for our 64 KB MCU.

Let's adopt this design for our system:
- Flash Start:
0x08000000 - Bootloader (16 KB): Starts at
0x08000000 - Metadata (1 KB): Starts at
0x08004000(16 KB = 0x4000 bytes) - Application (47 KB): Starts at
0x08004400(16 KB + 1 KB = 17 KB = 0x4400 bytes)
With this map defined, our next task is to configure our software projects to respect these boundaries.
2. Configuring the Projects
We need two separate projects in STM32CubeIDE: one for the bootloader and one for the application. The key is to modify their respective linker scripts to enforce our memory map.
The following video from ControllersTech provides a clear, step-by-step walkthrough of this entire process. We will follow its structure, breaking down the essential steps for each project.
STM32 Custom Bootloader (Part 1): Flash Layout and Application Jump
This video, 'STM32 Custom Bootloader (Part 1)', is an excellent guide for setting up the memory layout. We will use it to walk through the configuration of both the bootloader and application projects.
Watch from 07:13 to 18:59. The video will guide you through: Understanding the flash memory organization. Defining the memory map with specific addresses. Creating a shared header file for these addresses. Modifying the linker scripts for both the bootloader and application. Relocating the application's vector table. I will summarize the key actions below.
If you prefer a text-based guide, the article "STM32 Custom Bootloader Tutorial" from ControllersTech (resource ID LINK) covers the same steps in written form.
2.1. The Bootloader Project Configuration
The bootloader project is simpler. Since it resides at the default starting address 0x08000000, we only need to ensure it doesn't grow beyond its allocated 16 KB size.
-
Create a Shared Header (
flash_layout.h): As shown in the video (00:11:35), it's best practice to define our memory map addresses in a header file. This avoids "magic numbers" and ensures both projects use the exact same values.// flash_layout.h #define BL_START_ADDR 0x08000000 // Bootloader start address #define APP_HEADER_ADDR 0x08004000 // Application metadata start address #define APP_START_ADDR 0x08004400 // Application start address -
Modify the Linker Script: Open the bootloader's linker script (
.ldfile). Find theMEMORYdefinition block. TheORIGINforFLASHis correct at0x08000000. You must change itsLENGTHto16Kto restrict the bootloader to its allocated space. If the bootloader code ever exceeds this size, the linker will generate an error, preventing you from accidentally overwriting the application area./* Original */ MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K /* <-- Change this */ } /* Modified for Bootloader */ MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 16K /* <-- Now restricted */ }This step is demonstrated in the video at timestamp 00:15:09.
2.2. The Application Project Configuration
The application project requires more changes because it's being relocated away from the default starting address.
-
Copy the Shared Header: Copy the
flash_layout.hfile into your application project to maintain consistency. -
Modify the Linker Script: Open the application's linker script. Here, you must change both the
ORIGINand theLENGTHof theFLASHmemory region to match the application's slot in our memory map./* Modified for Application */ MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K FLASH (rx) : ORIGIN = 0x08004400, LENGTH = 47K /* <-- Both are changed */ }This step is demonstrated in the video at timestamp 00:17:50.
-
Relocate the Vector Table (Crucial Step): The ARM Cortex-M CPU expects the vector table—which contains the initial Stack Pointer (SP) and the address of the
Reset_Handler—to be at the beginning of the program image. When a program starts at0x08000000, the CPU looks there by default. Since our application now starts at0x08004400, we must explicitly tell the CPU where to find its new vector table.This is done using the Vector Table Offset Register (VTOR), part of the System Control Block (SCB).
In your application's
main.cfile, add the following line right at the beginning of themainfunction, afterHAL_Init()but before any other peripheral or clock configurations.#include "flash_layout.h" // Make sure to include your header int main(void) { /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ // Relocate the vector table to the application's starting address SCB->VTOR = APP_START_ADDR; __enable_irq(); // Re-enable interrupts, as the bootloader might disable them /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); // ... rest of your application code }This code, shown at timestamp 00:18:27 in the video, ensures that when an interrupt occurs, the CPU looks for the correct Interrupt Service Routine (ISR) address in the application's vector table, not the bootloader's. Failing to do this is a common source of bugs in bootloader systems, where the application crashes as soon as the first interrupt (like the SysTick timer) fires.
Conclusion
You have now successfully designed a complete memory map for a bootloader-application system and learned how to configure the toolchain to enforce it. This is a significant step beyond simply placing a single variable; you are now architecting the memory layout of an entire system.
Key Takeaways:
- A bootloader system requires a well-defined memory map that partitions the flash into at least a bootloader region and an application region.
- The bootloader's linker script must have its flash
LENGTHrestricted to its allocated size. - The application's linker script must be modified to set the flash
ORIGINto its new starting address and itsLENGTHto its allocated size. - The most critical step for the application is to relocate its vector table by writing its starting address to the
SCB->VTORregister at the beginning ofmain().
In our next lesson, we will build on this foundation. With the memory map in place, we will dive into the specifics of the flash controller and learn how to implement functions to erase and write to the internal flash memory. This will give our bootloader the ability to actually install a new application.