Skip to content

Instantly share code, notes, and snippets.

@gonzabrusco
Last active October 9, 2025 22:03
Show Gist options
  • Select an option

  • Save gonzabrusco/fd47e89e4c6fb302fc54b83637a3a101 to your computer and use it in GitHub Desktop.

Select an option

Save gonzabrusco/fd47e89e4c6fb302fc54b83637a3a101 to your computer and use it in GitHub Desktop.
How to Jump to the STM32 Bootloader and Return to the Application.

How to Jump to the STM32 Bootloader and Return to the Application

Disclaimer: This guide was tested on an STM32G070CB, but with minor adjustments, it should work for most STM32 models.

Entering the Bootloader

To jump to the STM32 Bootloader from your running application, simply call the following function:

#define BOOTLOADER_ADDR 0x1FFF0000 // Bootloader start address (refer to AN2606). STM32 family-dependent.

struct bootloader_vectable__t {
    uint32_t stack_pointer;
    void (*reset_handler)(void);
};
#define BOOTLOADER_VECTOR_TABLE	((struct bootloader_vectable__t *)BOOTLOADER_ADDR)

void JumpToBootloader(void) {
    // Deinit HAL and Clocks
    HAL_DeInit();
    HAL_RCC_DeInit();
    
    // Disable all interrupts
    __disable_irq();

    // Disable Systick
    SysTick->CTRL = 0;
    SysTick->LOAD = 0;
    SysTick->VAL = 0;

    // Disable interrupts and clear pending ones
    for (size_t i = 0; i < sizeof(NVIC->ICER)/sizeof(NVIC->ICER[0]); i++) {
        NVIC->ICER[i]=0xFFFFFFFF;
        NVIC->ICPR[i]=0xFFFFFFFF;
    }

    // Re-enable interrupts
    __enable_irq();

    // Map Bootloader (system flash) memory to 0x00000000. This is STM32 family dependant.
    __HAL_SYSCFG_REMAPMEMORY_SYSTEMFLASH();
    
    // Set embedded bootloader vector table base offset
    WRITE_REG(SCB->VTOR, SCB_VTOR_TBLOFF_Msk & 0x00000000);

    // Switch to Main Stack Pointer (in case it was using the Process Stack Pointer)
    __set_CONTROL(0);
    
    // Instruction synchronization barrier
    __ISB();

    // Set Main Stack Pointer to the Bootloader defined value.
    __set_MSP(BOOTLOADER_VECTOR_TABLE->stack_pointer);

    __DSB(); // Data synchronization barrier
    __ISB(); // Instruction synchronization barrier

    // Jump to Bootloader Reset Handler
    BOOTLOADER_VECTOR_TABLE->reset_handler();
    
    // The next instructions will not be reached
    while (1){}
}

After executing this function, the Bootloader will be active and ready to receive your commands. For details on how to send commands to the Bootloader, please refer to the application notes listed in the references.

Returning to the Application

During my tests, when I attempted to return to the application by sending the GO COMMAND to the address of my application's vector table (0x08000000), the microcontroller would hang. It seemed as though the bootloader was passing control to the application in an unstable state, causing the application to freeze. The only solution was to perform a power cycle, which allowed the application to boot normally. However, for the project I was developing, this wasn’t an option. I needed a mechanism to return to the application without powering down the device.

Then I had a simple thought: if returning to the application required forcing a reboot of the microcontroller, why not make the bootloader jump to a function that triggers a reboot?

And that's exactly what I did, and what I’m presenting in this guide. We will create a new 'fake' vector table, where its 'reset handler' simply triggers a reboot of the microcontroller.

First, create a file named secondary_vtable.c with the following contents and add it to your project. Alternatively, you can simply paste this code wherever it fits within your project.

#include "stm32g0xx.h" // STM32 family-dependent.

void Force_Reset_Handler(void) {
    while(1) {
        NVIC_SystemReset(); // Force a reboot
    }
}

extern uint32_t _estack;

void* __attribute__ ((section(".force_reset_vtable")))
force_reset_vtable[] = {
    [0] = (void*)&_estack,
    [1] = Force_Reset_Handler
};

This is the new 'fake' vector table that the Bootloader will jump to. Please note that the extern variable _estack should already be defined in the Linker Script, as it represents the address of the stack pointer in the original vector table.

Next, we need to place this new vector table at a fixed address in Flash. To do this, we will need to modify the Linker Script. In this guide, we will place the new vector table at 0x08000200.

Edit your Linker Script and add this after the isr_vector (the original vector table). If your original vector table is larger than 0x200 bytes (512 bytes), you should place the new vector table at a later address, such as 0x08000300 or 0x08000400. The exact address will depend on the STM32 family.

.secondary_isr_vector 0x08000200 (READONLY):
{
    . = ALIGN(4);
    KEEP(*(.force_reset_vtable)) /* Startup code */
    . = ALIGN(4);
} >FLASH

That's it! You're ready to go. And by 'go', I mean send the Bootloader GO COMMAND to the address of your new vector table (0x08000200 in this example). This will cause the bootloader to set the Main Stack Pointer to the value of _estack and then jump to Force_Reset_Handler(), which will trigger a reboot of the microcontroller, ultimately allowing the application to start.

If you found this guide useful and would like to show your appreciation, please consider making a donation. Your support helps me continue to create and share helpful resources.

References:

  • AN2606: STM32 microcontroller system memory boot mode
  • AN3154: CAN protocol used in the STM32 bootloader
  • AN3155: USART protocol used in the STM32 bootloader
  • AN3156: USB DFU protocol used in the STM32 bootloader
  • AN4221: I2C protocol used in the STM32 bootloader
  • AN4286: SPI protocol used in the STM32 bootloader
  • AN5405: FDCAN protocol used in the STM32 bootloader
  • AN5927: I3C protocol used in the STM32 bootloader
@gonzabrusco
Copy link
Author

The JumpToBootloader() is not bad, but:

1. Disable all interrupts and disable Systick should be done after the HAL_RCC_DeInit() since it uses HAL Tick.
HAL_DeInit();
HAL_RCC_DeInit();

__disable_irq();

// Disable & Reset SysTick
CLEAR_REG(SysTick->CTRL);
CLEAR_REG(SysTick->LOAD);
CLEAR_REG(SysTick->VAL);
2. It's also a good idea to set the vector table base offset, since the application can change it.
// Remap System Flash memory at address 0x00000000
__HAL_SYSCFG_REMAPMEMORY_SYSTEMFLASH();

// Set embedded bootloader vector table base offset
WRITE_REG(SCB->VTOR, SCB_VTOR_TBLOFF_Msk & 0x00000000);
3. According to "STM32 Cortex®-M4 MCUs and MPUs programming manual" __ISB() should be used after switching the current stack pointer MSP/PSP. Check me please. In any case it is not critical here.
// Switch to Main Stack Pointer
__set_CONTROL(0);

// Instruction synchronization barrier
__ISB();
4. It is not necessary to use the bootloader address 0x1FFF0000 after remap System Flash memory at address 0x00000000.

5. It is necessary to add an infinite loop at the end of the JumpToBootloader(), since at medium and high optimization levels the stack will be popped before the jump to the application address.
// Set embedded bootloader stack pointer
__set_MSP(*(UINT_TO_PTR(0x00000000)));

// Start embedded bootloader
(*(void (**)())(UINT_TO_PTR(0x00000004)))();

// The next instructions will not be reached
while (1){}

Hi. Thank you for the constructive feedback!

I tested all the suggested changes, and everything worked well—except for the jump instruction. When jumping to 0x00000000, the bootloader doesn't start, even after remapping the memory. I'm not sure why that's happening. However, jumping to 0x1FFF0000 works as expected. So, I’ll keep that part of the code unchanged, since that’s what works fine for me!

@gonzabrusco
Copy link
Author

When Run after programming or Leave DFU mode:

* System Flash memory mapped at 0x00000000 (SYSCFG->MEM_MODE = 1)

* Vector Table base offset point to System Flash memory (SCB->VTOR = 0x1FFF0000)

Therefore, when an interrupt occurs, the embedded bootloader vector table is used. So it's necessary remap Main Flash memory at address 0x00000000 and set the vector table base offset to 0x00000000 or application vector table.

It would be nice to reset of all peripherals, disable Systick, disable and clear pending interrupts in NVIC.

void RunAfterProgramming(void)
{
   // Prevent the activation of all interrupts
  __disable_irq();

  // Reset peripherals
  HAL_DeInit();

  // Disable & Reset SysTick
  CLEAR_REG(SysTick->CTRL);
  CLEAR_REG(SysTick->LOAD);
  CLEAR_REG(SysTick->VAL);

  // Disable & Clear pending interrupts in NVIC
  for (uint32_t irqn = 0; irqn < MAX_IRQN; irqn++)
  {
    NVIC_DisableIRQ((IRQn_Type)irqn);
    NVIC_ClearPendingIRQ((IRQn_Type)irqn);
  }

  // Remap Main Flash memory at address 0x00000000
  __HAL_SYSCFG_REMAPMEMORY_FLASH();

  // Set application vector table base offset
  WRITE_REG(SCB->VTOR, SCB_VTOR_TBLOFF_Msk & 0x00000000);

  // Enable activation of all interrupts
  __enable_irq();
}

Just call this from main()

int main(void)
{

  /* USER CODE BEGIN 1 */
  RunAfterProgramming();

  /* USER CODE END 1 */

For that, I think I’ll stick with my brute-force method, haha. Forcing a reset by jumping to my “fake” reset table does exactly what I need—it ensures everything starts up the way it’s supposed to, with no surprises.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment