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
@m4l490n
Copy link

m4l490n commented Feb 13, 2025

When I saw this, I thought; What if I just jump to the already existing Reset_Handler instead of creating a new fake vector table? So, I looked into the .map file and saw that the Reset_Handler address is 0x0800d560. Then I tried with the following command:

STM32_Programmer_CLI -c port=USB1 -g 0x0800d560

And BOOM! It works just fine.

@gonzabrusco
Copy link
Author

gonzabrusco commented Feb 13, 2025

Yes, I considered that as well. However, using the GO COMMAND not only jumps to the specified address but also initializes the Main Stack Pointer (MSP) with the value at the jump address. According to AN4286 (page 19), the GO COMMAND:

  • Initializes the registers of the peripherals used by the bootloader to their default reset
    values.
  • Initializes the user application main stack pointer.
  • Jumps to the memory location programmed in the received address + 4 (which
    corresponds to the address of the application reset handler). For example, if the
    received address is 0x08000000, the bootloader jumps to the memory location
    programmed at address 0x08000004.

If you jump directly to the Reset_Handler, the bootloader sets the MSP to the value at 0x0800D560, which could lead to unpredictable results. I found it cleaner to establish a new vector table at a fixed location, ensuring predictable behavior. My goal was for the Reset_Handler to force a software reboot, providing the application with a consistently clean start.

P.S.: I was reviewing AN3156, and I couldn't find any mention of the GO COMMAND there. Are we referring to the same command? My tests were conducted using SPI communication with the bootloader.

@m4l490n
Copy link

m4l490n commented Feb 13, 2025

Yeah, maybe we are not referring to the same GO command. I tried to use the dfu protocol but It was very hard to put together so I just switched to use STM32_Programmer_CLI which is installed along with STM32CubeProgrammer.

STM32_Programmer_CLI has very simple to use and understand commands and -g or --go is one of them. With STM32_Programmer_CLI, my fw update process becomes very simple.

To perform an update I just execute the following after having transitioned to USB DFU mode:

STM32_Programmer_CLI -c port=USB1 -d my_firmware.elf 0x08000000
STM32_Programmer_CLI -c port=USB1 -g 0x0800d560

And that's it.

My application works over USB so I'm using that to perform the updates. I execute this on the regular CLI with my device attached to the computer via USB.

@gonzabrusco
Copy link
Author

Great! I'm glad I could help, even indirectly, in getting your application up and running!

P.S.: Be aware that the position of your Reset_Handler can change. I recommend fixing it at a specific address using the linker script to avoid any issues.

@patzf
Copy link

patzf commented Feb 22, 2025

@gonzabrusco : I am wondering whether the passage

    // Deinit clocks and HAL
    HAL_RCC_DeInit();
    HAL_DeInit();

is sufficient in any case or do I need to DeInit every single Peripheral I have initialised?

As a second question:
Why is

__enable_irq();

not the very last command just before the jump? Is enabling the interrupts safe before the remapping of the stack pointer, etc.?

Thanks!

@gonzabrusco
Copy link
Author

Hi @patzf

is sufficient in any case or do I need to DeInit every single Peripheral I have initialised?

Yes, it should be enough. I don't deinit anything in my code before calling JumpToBootloader().

Why is __enable_irq(); not the very last command just before the jump? Is enabling the interrupts safe before the remapping of the stack pointer, etc.?

There's no issue with enabling the IRQ because the function first disables all interrupts (using ICER) and clears all pending interrupts (using ICPR) before enabling them. This ensures that no interrupt will be triggered during that moment. Just make sure you're correctly setting the MCU_ICER_QTY define for your microcontroller. This value should match the number of ICER registers your microcontroller has, which you can find in the HAL (Hardware Abstraction Layer) or in the reference manual.

@patzf
Copy link

patzf commented Mar 1, 2025

@gonzabrusco : Thanks a lot for the reply. I would like to ask you a short second question which is fundamental for my understanding. So the goal is to switch from user application to internal STM32 Bootloader. I am using an STM32G0B1CBT6 with a single memory page.

The thing I did not find an answer yet ist very simple: Is it possible (I suppose yes) to trigger this action of "updating firmware through ROM Bootloader by before jumping there from user application" really possible without a boot button oe other HW interaction by the user?

Case 1: The Device is empty (fresh, unprogrammed device) -> By default ROM Bootloader is started and USB DFU programming works fine
Case 2: There is already the User Application running and I would like to reprogram the user app by again using USB DFU
- The code you were so nice to provide can manage the jumping to ROM bootloader from user app
- What I am missing is - how does that work together with dfu-util?

For Case 2: I have included ST`s HAL Lib of USB DFU and the Device (besides running the user code now ALSO shows up as USB DFU on the host side). So dfu-util can access it. But what I am missing is the following: when I start programming with dfu-util, the tool immediately wants to "program" the device, but before that, I have to trigger the jump to ROM Bootloader. So I miss the portion between the first taslking from dfu-util to the DFU device and the actual program command from dfu-util. I have set breakpoints to see where I can add user code as soon as dfu-util is statrting to taslk with my DFU Device Code but I think If i there add your jumping code. dfu-util in the meantime already wants to start programming or with other words: I would need a dfu-util command that just triggers the jump on the device and THEN starts the programming. I have not seen such a "wait" command that could facilitate the necessary "2-step" procedure (let the device jump first and then do the programming). I experiemnted with the E command line optin of dfu-util, but I think that is something else.

Would you be so kind to shed some light because I am not sure what specifically I am missing and how others do what I try to accomplish =)
Thanks!

@gonzabrusco
Copy link
Author

Hi @patzf,

Sorry for the delay.

I haven't personally used dfu-util, but from what I've gathered, it expects the device to already be in DFU mode when its programming sequence starts. That means you must trigger the jump to the ROM bootloader, and allow the device to re-enumerate as a DFU device, before running dfu-util.

In my setup, I program the device over SPI and have implemented a special command that tells the application firmware to jump to the bootloader. This approach lets me perform the entire update remotely without any physical intervention. You might consider a similar strategy for your device. For example, you could implement a menu option, a specific key combination, or even use a command sent over MQTT (or another protocol) to trigger the jump.

If your device uses USB, you could try sending a custom command over USB (using a protocol that your application firmware understands) to initiate the jump, and then run dfu-util once the device re-enumerates in DFU mode.

Hope that helps!

@by-gnome
Copy link

by-gnome commented Apr 10, 2025

The JumpToBootloader() is not bad, but:

  1. Disable all interrupts and disable Systick should probably 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);
  1. 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);
  1. 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();
  1. It is not necessary to use the bootloader address 0x1FFF0000 after remap System Flash memory at address 0x00000000.
  2. 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){}

@by-gnome
Copy link

by-gnome commented Apr 10, 2025

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 */

@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