Quadcopter Flight Controller from scratch

Robotics course inĀ coding a Drone Flight Controller from scratch using STM32: IMU, PID Controller, Motor Control, Rc Joystick Integration

Check it out

STM32 Attitude Estimation: IMU, Euler Angles, Filters

Ā IMU sensor interfacing, Euler angles, Quaternions, and Kalman Filter. Learn practical STM32 programming for SPI, UART, and Timer interrupts.

Check it out

STM32 Bootlader P2: Linker Script, Vector Table, and Jumping to the Application

Jan 14, 2026
STM32 Bootloader

 

In the first part of the tutorial, we laid the groundwork for developing our bootloader. At this point, we have a fully functional UART interface and a Python script for sending/receiving serial data. In addition, the project includes a library to store the data within flash memory. If you miss the first part, check out the link below

https://www.steppeschool.com/blog/stm32-bootloader-part1-flash-uart

 In the second part, we finalize and address all gaps in the bootloader design project.

STM32 Bootloader: Flash memory organization

The figure below illustrates how we arrange the bootloader and application within a flash memory. In this tutorial, I will devote 25 pages of the flash memory to the bootloader and the rest to the application. Since each page is 2K (2048 bytes on STM32L4 MCUs) and the flash memory start address is always 0x80000000, the application start address will be 0x8000000 + 2048 * 25 = 0x800c800. We will need this number when configuring the application project. 

Note that the page size on your microcontroller may differ. Refer to the reference manual to obtain this information.

 

STM32 Bootloader: Updating the Linker Script and redefining VTOR

In this part of the tutorial, we create an application project. We will build the binary of the project, so we can send it to the bootloader using the Python script

There is nothing special about this project. For testing, I will send an "Application code" message via the UART Interface every second. 

As we discussed earlier, we denote the first 25 pages to the bootloader, while the rest to the application file. When building the project, we must specify the project memory start address as 0x800C800 (0x8000000 + 0x800 x 25 = 0x800C800). Open the linker script file, and enter the start address in a flash field. Also do not forget to change the Length field

 

VTOR (Vector Table Offset Register) is a register within the ARM Cortex-M core that specifies the memory address of the interrupt vector table.

Normally, in a simple project without a bootloader:

  • The vector table is placed at address 0x08000000 (start of Flash)

  • On reset, the CPU:

    1. Reads the initial stack pointer from the address 0x08000000

    2. Reads the Reset_Handler address from 0x08000004

  • All interrupts (SysTick, EXTI, timers, etc.) use this same table

In this case, VTOR is either left at its default value or explicitly set to 0x08000000

When we design a bootloader, Flash memory is split into two regions:

  • 0x8000000 - Bootloader
  • 0x800c800 - Application

This means the VTOR value must be updated in the application project as the application memory onset. 

We open system_stm32l4xx.c (the file name varies by MCU used) and uncomment the USER_VECT_TAB_ADDRESS. Then we define the offset according to the application onset memory address. 

These changes are enough to set the right VTOR value. During System Initialization, the MCU updates the VTOR register, as shown in the screenshot below. 

 

Next, we need to generate the binary file during the build. Open project settings (press the right button of the mouse on the project name). Then C/C++ Build, Settings, and Post-build outputs. Tick the box as shown in the screenshot below. Finally, press "Apply and Close ."

If everything is done correctly, we will see the .bin file in the Debug folder. This means that our application binary file is ready.

 

 

STM32 Bootloader: Data Packet Format and Sending the Binary File

 Our next step is to transmit this file over the UART and request that the microcontroller store it in the flash memory. Then, if the MCU runs the application, it will execute the logic defined in the application project. To handle the complexities of data transactions and ensure reliable data transfer, I created a simple protocol for data packets. The figure below shows the packet format. The first byte of the packet is always a message token with value 0x23. Then, the command byte follows. Currently, there are three commands:

  • Application Data = 0x42: We send this command when transmitting a chunk of the binary file
  • Run application =  0x43: This command is necessary to run the application once the binary file transfer is over 

Later, we can add more commands, such as firmware version requests, protections, and application erasure.

I created macros and a struct to define the command types and easily handle the data transactions

#define BOOT_RUN_APPLICATION 0x43 /*!< Run application, */

#define BOOT_UPDATE_REQUEST 0x42 /*!< BOOT UPDATE request, */

 

#define MESSAGE_TOKEN 0x23 /*!< # symbol to define the message onset*/

 

#define BOOT_NACK 0xaa

#define BOOT_ACK 0x5f

 

typedef enum

{

 SERIAL_OK = 0x00,

 SERIAL_MESSAGE_ERROR = 0x01,

 SERIAL_SIZE_ERROR = 0x02,

 SERIAL_CRC_ERROR = 0x03

} Serial_StatusTypeDef;

 

 

typedef struct

{

 uint8_t rw_request;

 uint8_t *data;

 uint16_t data_size;

 uint32_t crc_value;

}__attribute__((__packed__)) CommandTypeDef;

 

And the function parse_request parses the data packet and returns the error code in case of faulty data transactions:

uint8_t parse_request(CommandTypeDef* command, uint8_t *data, uint16_t size)
{
    if((uint8_t)(*data) == MESSAGE_TOKEN)
    {
        if( (uint8_t)(*(data + 1)) == (BOOT_UPDATE_REQUEST) ||
          (uint8_t)(*(data + 1)) == (BOOT_RUN_APPLICATION))
        {
            command->rw_request = *(data + 1);
            command->data_size = *((uint16_t *)&data[2]);
            // data + 2 (message token and command code) + 2 (data size) + 4 (crc)
            if (size != command->data_size + 2 + 2 + 4)
            {
                return SERIAL_SIZE_ERROR;
            }
            command->data = &data[4];
            command->crc_value = *((uint32_t *)&data[4 + command->data_size]);
            if(boot_verify_crc(data, size - 4, command->crc_value))
            {
                return SERIAL_CRC_ERROR;
            }
        }
       else
       {
           // Wrong data format
           return SERIAL_MESSAGE_ERROR;
       }
    }
    else
    {
         return SERIAL_MESSAGE_ERROR;
    }
    return SERIAL_OK;
}

STM32 Bootloader: Python function for sending the binary file

In the Python script, I defined a function to send the binary file. Of course, it is impossible to send the whole file as a single packet. Instead, we divide it into small chunks (WINDOW_SIZE).

def update_flash_mem(serial_com, file_name):
    WINDOW_SIZE  = 128
    try:
        flash_bin_file = open(file_name, 'rb')
    except:
        Exception('cannot open the bin file')
    size = os.path.getsize(file_name)
    # data token and command
    data_token = [MESSAGE_TOKEN, BOOT_UPDATE_REQUEST]
    data_token_bytes = bytes()
    data_token_bytes = data_token_bytes.join((struct.pack('<'+format, val) for format,val in zip('BB',data_token)))
    size_copy = size
    while size > 0:
        # actual data
        data_sent = flash_bin_file.read(WINDOW_SIZE)
        size = size - len(data_sent)
        data_sent = data_token_bytes + struct.pack('<H',len(data_sent)) + data_sent
        send_data_serial(serial_com, data_sent)
        print('Firmware update ' + str(int(100 * (size_copy - size)/size_copy)) +' %' +': ' + int(50 * (size_copy - size)/size_copy) * '#', end = '\r')
        if read_boot_reply(serial_com) != BOOT_ACK:
            print("flash update failed ")
            Exception("flash update failed")
            break

    if size == 0:
        print('Firmware update ' + str(int(100 * (size_copy - size)/size_copy)) +' %' +': ' + \
            int(20 * (size_copy - size)/size_copy) * '#')
        print('Firmware update is over')
           
 

STM32 Bootloader: Switching to the Application

After we handled the transmission of the binary file, the next crucial step is to manage the application's execution. After all, what is the point of designing the bootloader if we cannot run the application? Before switching to the application, we must follow several essential steps. First, we disable interrupts. If an interrupt is triggered just before switching the application, the MCU may crash once we change the Vector Table address, resulting in Hardfault. Then we deinitialize the peripherals, if needed, and redefine the Main Stack Pointer, which is stored at the start of the application data. The next four bytes in the application file store the address of the Reset handler, the first function to run. To sum up, the function below follows all these steps. We extract its address and run it after completing the preceding steps.

void bootloader_jump_to_user_data(void)
{
    void (*app_reset_handler)(void);

    //setting main stack pointer
    // first 4 bytes hold the address of the stack pointer
    uint32_t msp_value = *(volatile uint32_t*)(APPPLICATION_PROGRAM_ADDRESS);
    /* Disable all interrupts */
    __disable_irq();

    /* De-init peripherals if needed */
    HAL_RCC_DeInit();
    HAL_DeInit();

    __set_MSP(msp_value);

    // next four bytes hold the address of the reset handler function
    uint32_t app_reset_handler_address = *(volatile uint32_t*)(APPPLICATION_PROGRAM_ADDRESS + 4);
    app_reset_handler = (void*)app_reset_handler_address;
    //run the reset handler of the application
    app_reset_handler();

}

 

Stuck on your STM32 or robotics project?

Tired of asking ChatGPT?

Ask your question below and getĀ help within 24 hours from a real person