Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 65

Memory Map Design for Bootloader and Application

Recommended resources

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:

  1. 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.
  2. 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.
  3. 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.

STM32F103C8T6 Flash Memory Partitioning for Bootloader and Application
A common memory map for an STM32F103. The 64KB flash is divided into a 16KB bootloader, a 1KB reserved/metadata region, and a 47KB application region, each with a defined starting address.

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.

  1. 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
    
  2. Modify the Linker Script: Open the bootloader's linker script (.ld file). Find the MEMORY definition block. The ORIGIN for FLASH is correct at 0x08000000. You must change its LENGTH to 16K to 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.

  1. Copy the Shared Header: Copy the flash_layout.h file into your application project to maintain consistency.

  2. Modify the Linker Script: Open the application's linker script. Here, you must change both the ORIGIN and the LENGTH of the FLASH memory 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.

  3. 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 at 0x08000000, the CPU looks there by default. Since our application now starts at 0x08004400, 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.c file, add the following line right at the beginning of the main function, after HAL_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 LENGTH restricted to its allocated size.
  • The application's linker script must be modified to set the flash ORIGIN to its new starting address and its LENGTH to its allocated size.
  • The most critical step for the application is to relocate its vector table by writing its starting address to the SCB->VTOR register at the beginning of main().

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.