Skip to main content

Welcome to a Grasp lesson

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

Lesson 91

Configuring Vector Tables and Memory Maps for Bootloader Compatibility

Recommended resources

Introduction

Welcome to the second-to-last lesson of the course! In our previous session, you successfully implemented a robust, system-wide error handling strategy, transforming your capstone project into a resilient application that can manage faults gracefully. You now have a complete, multi-tasked automotive sensor node that is feature-complete and reliable.

But what happens when you need to update the firmware in a device that's already deployed, perhaps sealed inside a vehicle's dashboard? You can't just plug in a debugger. This is where a bootloader becomes essential. A bootloader is a small, protected program that runs on startup and can load new application firmware into memory, typically received over a communication bus like CAN or UART.

Today's lesson addresses a critical piece of that puzzle. We won't be building the bootloader itself, but we will configure our existing capstone application to be compatible with one. This directly targets the learning outcome: Configure the application's vector table and memory map to be compatible with a bootloader.

You will learn how to shift your application away from the default starting address in memory, a fundamental skill for creating updatable, professional embedded systems.


1. Understanding the Bootloader-Application Memory Map

On an STM32 microcontroller, program execution typically begins at the start of flash memory, address 0x08000000. This is where the processor expects to find the vector table, which contains the initial stack pointer and the address of the first code to run (the reset handler).

When a bootloader is introduced, it occupies this initial memory region. The main application must be stored at a higher address. This requires a clear partitioning of the flash memory.

STM32F103C8T6 Flash Memory Partitioning for Bootloader
This diagram shows the 64KB flash memory of an STM32F103C8T6. On the right, it's partitioned into a 16KB bootloader region and a 47KB application region (with a 1KB reserved space in between). Our application must be configured to reside in this upper region.

We will adopt a similar memory layout for our project:

  • Bootloader Region: 0x08000000 - 0x08003FFF (16 KB)
  • Application Region: 0x08004000 - 0x0800FFFF (48 KB)

Our goal is to modify the capstone project so that all its code (.text), initialized data (.data), and zero-initialized data (.bss) are correctly placed and executed from this new 0x08004000 starting address.

Achieving this involves two key modifications:

  1. Modifying the Linker Script: To instruct the linker to place the application at the new address.
  2. Relocating the Vector Table: To inform the CPU where to find the application's interrupt vectors at runtime.

2. Modifying the Linker Script

The linker script is a configuration file with a .ld extension that tells the GNU linker how to organize the output from the compiler into a final executable file. Most importantly for us, it defines the memory map of the target device.

Bare metal embedded lecture-4: Writing linker scripts and section placement

To understand the fundamentals of what a linker script does, this video from Fastbit Embedded Brain Academy is an excellent resource. You don't need to watch the whole thing, but the first few minutes explain the key commands we are about to use.

Watch the video from the beginning to 3:02 to understand the purpose of a linker script and the ENTRY and MEMORY commands. Then, watch from 3:50 to 9:05 to see a detailed explanation of the MEMORY command, where ORIGIN and LENGTH are defined for flash and SRAM. This is the exact section we will be modifying.

Now, let's apply this to our project.

  1. In the STM32CubeIDE Project Explorer, find the linker script for your project. It will be named something like STM32F103C8TX_FLASH.ld. Double-click to open it.

  2. At the top of the file, you will see a MEMORY block that looks like this:

    /* Specify the memory areas */
    MEMORY
    {
      RAM    (xrw)    : ORIGIN = 0x20000000, LENGTH = 20K
      FLASH  (rx)     : ORIGIN = 0x08000000, LENGTH = 64K
    }
    
  3. This tells the linker that the flash memory starts at 0x08000000 and is 64 KB long. We need to change this to reflect our new memory map. Modify the FLASH line as follows:

    • ORIGIN: Change this to 0x08004000, the new starting address for our application.
    • LENGTH: We've reserved the first 16 KB for the bootloader, so the remaining length is 64K - 16K = 48K.

    The modified MEMORY block should now look like this:

    /* Specify the memory areas */
    MEMORY
    {
      RAM    (xrw)    : ORIGIN = 0x20000000, LENGTH = 20K
      FLASH  (rx)     : ORIGIN = 0x08004000, LENGTH = 48K
    }
    
  4. Save the file.

By making this change, you have instructed the linker to generate all addresses for your application's code and constant data relative to 0x08004000. If you build the project now, the resulting .bin file will be structured to run from this new location.


3. Relocating the Vector Table with VTOR

The linker script change solves the placement problem, but it doesn't solve the execution problem. The ARM Cortex-M3 processor core still assumes the vector table is at 0x08000000. We must explicitly tell the CPU about the new location at the very beginning of our application's runtime.

This is done using the Vector Table Offset Register (VTOR), which is part of the System Control Block (SCB).

STM32 Custom Bootloader Tutorial - ControllersTech

The following article is the text-based version of a detailed video tutorial. It provides a clear, step-by-step guide on how to prepare an application for a bootloader, covering both the linker script and VTOR modifications.

Please read the section 'Modifying Application Linker Script and Vector Table'. Pay close attention to the sub-sections 'Setting Application Flash Start Address' (which confirms what we just did) and 'Relocating Vector Table (VTOR)'. This second part shows the exact code we need to add to our main.c file.

As the article explains, we need to add code to main.c to update the VTOR.

  1. Open your project's main.c file.

  2. Navigate to the main() function.

  3. Find the section immediately after the HAL_Init(); call and before SystemClock_Config();. It is marked with /* USER CODE BEGIN Init */.

  4. Add the following two lines of code inside this user code block:

    /* USER CODE BEGIN Init */
    
    SCB->VTOR = 0x08004000; // Relocate the vector table
    __enable_irq();          // Re-enable all interrupts
    
    /* USER CODE END Init */
    

Why are both lines necessary?

  • SCB->VTOR = 0x08004000;: This line writes our application's starting address to the VTOR. From this point forward, whenever an interrupt or exception occurs, the CPU will look for the corresponding handler address in the vector table located at 0x08004000. This must be done before any interrupt-generating peripherals are initialized, which is why we place it before SystemClock_Config() and the MX_..._Init() functions.
  • __enable_irq();: A well-behaved bootloader will disable all global interrupts before jumping to the application to ensure a clean state transition. Therefore, the application is responsible for re-enabling them. This CMSIS intrinsic function handles that for us.

With both the linker script and VTOR configured, your application is now fully prepared to be launched by a bootloader.


4. Verification

How can we be sure our changes worked without a bootloader to actually launch the application? We can simulate the last step of the bootloader process: flashing the application binary to the correct offset.

STM32 Custom Bootloader (Part 1): Flash Layout and Application Jump

This video from ControllersTech walks through the entire process of creating and flashing a bootloader and application. We will focus on the final step, where both binaries are programmed.

Watch the section from 19:57 to 21:55. Observe how STM32CubeProgrammer is used to flash the two separate binary files. Notice that for the application binary, the 'Start address' is explicitly changed to the application's starting location. This is the procedure we will follow to test our modified application.

Follow these steps to verify your work:

  1. Build your project in STM32CubeIDE. This will generate a new binary file (.bin) based on your modified linker script.
  2. Open STM32CubeProgrammer and connect to your ST-Link.
  3. Click the "Erasing & Programming" tab (or the download icon).
  4. Browse for your project's binary file. It will be located in the Debug (or Release) folder of your project workspace.
  5. Crucially, set the "Start address" field to 0x08004000.
  6. Click "Start Programming".

Once flashing is complete, disconnect from the programmer and reset your board. If your application (blinking LEDs, CAN messages, etc.) runs exactly as it did before, you have successfully made it bootloader-compatible! The MCU woke up, found nothing at 0x08000000, but your code running from 0x08004000 successfully updated the VTOR, allowing the SysTick timer and other peripherals to initialize and trigger their interrupts correctly.


Conclusion

In this lesson, you took a critical step toward creating professional, field-updatable firmware. By treating your application as a relocatable unit rather than a fixed program, you've adopted a mindset essential for modern embedded systems development.

Key Takeaways:

  • Memory Partitioning: You learned that in a system with a bootloader, the flash memory is divided into separate regions for the bootloader and the application.
  • Linker Script Modification: You modified the MEMORY block in the .ld file, changing the ORIGIN and LENGTH to compel the linker to place your application at a non-default address.
  • Vector Table Relocation: You updated the VTOR register at the start of your application's runtime to ensure the CPU knows where to find the interrupt handlers. This is a mandatory step for any relocated ARM Cortex-M application.
  • Verification: You learned how to test a bootloader-compatible application by flashing it directly to its target offset address.

Preview of the Final Lesson:

You have now completed the entire technical journey, from blinking an LED with bare-metal code to building a complex, robust, bootloader-compatible RTOS application. In our final lesson, we will focus on packaging and presenting your work. You will structure your capstone project in a public Git repository, write a professional README.md, and plan a demonstration video—the final steps to creating a powerful portfolio piece that showcases your new skills to potential employers.