Featured image of post The Simplest Bootloader

The Simplest Bootloader

Comprehensive guide on writing a simple bootloader from scratch for the STM32F4 microcontroller.

Introduction

A bootloader is a critical piece of software responsible for initializing the hardware and loading the main application. Whether you’re working on a custom project or exploring how embedded systems boot up, understanding bootloaders is essential. In this blog post, I’ll guide you through writing a simple bootloader from scratch for the STM32F4 microcontroller, a popular choice among developers.

You’ll learn the basics of what a bootloader does, how to set up your development environment, and how to write and test your first bootloader. You can find the complete code on my GitHub repo.


Understanding Bootloaders

A bootloader is the first code that runs when a microcontroller is powered on or reset. Its primary responsibilities are:

  • Initializing the hardware (e.g., setting up the clock, configuring GPIOs).
  • Loading the main application from a specific memory location.
  • Jumping to the main application’s entry point to start execution.

 Booting process

In more complex systems, bootloaders may also handle tasks like firmware updates, security checks, and communication with external peripherals. However, for this tutorial, we’ll focus on the simplest form of a bootloader to get you started.


Setting Up the Development Environment

Before diving into the code, let’s set up the necessary tools:

Tools Required:

  • IDE: An integrated development environment from STMicroelectronics, I use vscode but it’s easier to go with STM32IDE.
  • GCC Toolchain: not needed if you are using STM32IDE, if you are using vscode you should install the toolchain from here.
  • Ceedling: for building the binary .

Hardware Required:

  • STM32F4 Discovery Board: This tutorial targets the STM32F4 series, specifically the STM32F407VGT6.

Setup Instructions:

  1. Install STM32CubeIDE: Download and install it from STMicroelectronics’ official website.
  2. Install GCC Toolchain: Ensure you have the ARM GCC toolchain installed. This usually comes with STM32CubeIDE.
  3. Connect the STM32F4 Board: Use a USB cable to connect the board to your development machine, or if you don’t have the board like me you can use renode.

Writing the Bootloader Code

Memory Layout and Vector Table

The STM32F4 has a memory layout that includes various regions for different purposes (e.g., code, data, peripherals). The vector table, which contains the addresses of the exception handlers and the reset vector, is usually located at the start of the flash memory, we will go back to talk about the vector table later.

Let’s start by setting up the reset handler:

1
2
3
4
5
6
7
8
9
#include "stm32f4xx.h"

// Forward declaration of the main application entry point
extern void main_app(void);

void Reset_Handler(void) {
    // Call the bootloader initialization function
    Bootloader_Init();
}

Minimal Hardware Initialization

The bootloader needs to initialize the hardware minimally. This includes setting up the system clock and configuring any necessary GPIOs:

1
2
3
4
5
6
7
8
void Bootloader_Init(void) {
    // Set up system clock (assuming default clock settings)
    SystemInit();

    // Optionally configure GPIOs, LEDs, etc.
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;  // Enable GPIOA clock
    GPIOA->MODER |= GPIO_MODER_MODER5_0;  // Set PA5 as output (e.g., onboard LED)
}

Loading the Application

The core functionality of a bootloader is to load and execute the main application. For simplicity, we’ll assume the application is already flashed at a specific memory address:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define APP_ADDRESS 0x08004000  // Address where the main application is located

void Bootloader_JumpToApp(void) {
    // Get the application's reset vector (second word in the vector table)
    uint32_t app_reset_vector = *((volatile uint32_t *)(APP_ADDRESS + 4));
    
    // Set the application's stack pointer (first word in the vector table)
    __set_MSP(*((volatile uint32_t *)APP_ADDRESS));
    
    // Jump to the application's reset vector
    ((void (*)(void))app_reset_vector)();
}

int main(void) {
    Bootloader_Init();

    // Perform any additional bootloader tasks...

    // Jump to the main application
    Bootloader_JumpToApp();

    // The bootloader should never return here
    while (1);
}

Testing the Bootloader

Now that we’ve written our simple bootloader, it’s time to test it:

  1. Compile the Bootloader:

    • Use STM32CubeIDE to compile the bootloader code.
  2. Flash the Bootloader:

    • Flash the compiled bootloader onto the STM32F4 microcontroller using OpenOCD or the built-in tools in STM32CubeIDE.
  3. Load a Simple Application:

    • Create a simple application (e.g., a program that blinks an LED) and flash it to the address defined as APP_ADDRESS.
  4. Test:

    • Reset the board. If everything is set up correctly, the bootloader will initialize the hardware, load the main application, and the LED will start blinking.

Adding Basic Features

LED Blinking

To make the bootloader visually indicate its operation, you can add a simple LED blink before jumping to the main application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void Bootloader_BlinkLED(void) {
    for (int i = 0; i < 5; i++) {
        GPIOA->ODR ^= GPIO_ODR_OD5;  // Toggle LED
        for (volatile int j = 0; j < 1000000; j++);  // Delay
    }
}

int main(void) {
    Bootloader_Init();
    Bootloader_BlinkLED();
    Bootloader_JumpToApp();
    while (1);
}

Basic Error Handling

You can also add basic error handling to check if a valid application is present before attempting to jump:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
bool Bootloader_CheckApp(void) {
    uint32_t app_reset_vector = *((volatile uint32_t *)(APP_ADDRESS + 4));
    return (app_reset_vector != 0xFFFFFFFF);  // Check if reset vector is valid
}

int main(void) {
    Bootloader_Init();
    
    if (Bootloader_CheckApp()) {
        Bootloader_BlinkLED();
        Bootloader_JumpToApp();
    } else {
        // Handle the error (e.g., keep blinking LED)
        while (1) {
            GPIOA->ODR ^= GPIO_ODR_OD5;
            for (volatile int j = 0; j < 1000000; j++);
        }
    }
}

Conclusion

In this blog, we’ve walked through creating the simplest bootloader for the STM32F4 from scratch. We’ve covered the basics of what a bootloader is, how to set up the development environment, and how to write and test a minimal bootloader. We’ve also explored adding basic features like LED blinking and error handling.

There are many ways to expand this bootloader, such as adding UART support for debugging, handling firmware updates, or implementing security features. I encourage you to experiment and build on this foundation to suit your project needs.


References and Further Reading


This guide should help you create a simple bootloader from scratch for your STM32F4 platform. Happy coding!

Note to Readers:

This blog post is just the beginning of a series where we’ll build and refine our custom bootloader for the STM32F4. While we’ve covered the basics here, there are still many pieces to explore, such as advanced debugging techniques, handling firmware updates, and adding communication protocols like UART. These topics, along with more complex features, will be detailed in the upcoming parts. Stay tuned for more!

Built with Hugo
Theme Stack designed by Jimmy