Index ¦ Archives  ¦ Atom  ¦ RSS

Jumping to the bootloader of an embedded device

Jumping to the bootloader from application code is highly desirable for many reasons, such as wanting to be able to reprogram devices in the field without any extra hardware (such as a button) to increase complexity, BOM cost, etc.

The code in this post will be tested on the STM32F070xB, but the code should still work for any STM32F0, and the concepts apply to many microcontrollers. Please comment below if you got it working on another microcontroller (or if you're having trouble!)

Before we go about jumping around, let's discuss the general plan. Notably, the plan is NOT to jump to the bootloader directly from the application code, as that would wreak havoc on the peripherals. Bootloaders are written assuming they are seeing a fresh microcontroller with nothing setup yet, except what the microcontroller itself normally does when booting, such as mapping the right part of flash or RAM into memory (0x0000 0000 in the STM32F0 line).

The Plan

Therefore, here's the rough order of events once a jump is requested:

  • Set a flag in RAM, somewhere persistent, that we want to jump to the bootloader.
  • Restart the microcontroller (via register write)
  • Check the flag as early as possible

The Execution

We will need a few separate pieces of code to make this work:

  • Firstly, we need some way for the application code to be triggered to attempt the jump. This one is up to you.
  • A 'signal' setup in the linker script.
  • Code to signal that a jump to the bootloader is eminent.
  • Code to receive that signal and do the jump.

For the first one, in my application I added a callback in my firmware that gets called when the user asks to reboot into the bootloader via the normal UI. However, this part is entirely up to your application's protocols and needs.

The Signal

In the linker script, a quick-and-easy way to put something in RAM where it won't get overwritten is to put it in its own section entirely.

/* Define a section named 'bootloader_flags' in case we want to send more than
this 'jump' flag to the bootloader, down the line.
This can be useful to send the bootloader some quickstart information, such as
IP addresses or other protocol configuration.
*/
SECTIONS
{
  /* ... */
  .bootloader_flags :
  {   
    /* Reserve 4 bytes for the bootloader flag. */
    _bootloader_flag = .;
    . += 4;
  } > RAM
  /* ... */
}

With that setup, now let's expose the signal in our C/C++ code:

extern uint32_t _bootloader_flag;

This tells the compiler there will be a symbol named _bootloader_flag made available that points/names a 4-byte integer. If you choose a different size for the flag, use the appropriate type here (such as uint16_t if you chose 2 bytes).

The Signal Sender

Now that the signal is ready and we have some function called in the application runtime, here's what we want inside that function:

void reboot_into_bootloader() {
  _bootloader_flag = 1;
  NVIC_SystemReset();
}

NOTE: NVIC_SystemReset() is specific to my microcontroller line and vendor-specific library (in my case, a subset of mbed). If you're on another microcontroller line or using another library, you should replace that line.

The Signal Receiver

We need to intercept the boot process as early as possible, before system clocks and other peripherals are setup. The way to do this for me was to write a small amount of assembly code that runs almost right away (2 instructions after the reset interrupt vector function is called). Here's that assembly code (in GCC ARM thumb format):

/* Check if we should jump into the bootloader */
ldr r0, =_bootloader_flag // Load the address of _bootloader_flag into r0
                          // (in assembly, _bootloader_flag is a symbol so
                          // it's an address)

ldr r1, [r0, #0]          // Load the value stored at the address.
                          // This is equivalent to 'r1 = *_bootloader_flag;'

ldr r2, =0                // Load 0 into r2. This is because the next
                          // instruction requires a register to hold the value,
                          // otherwise we'd use an immediate (#0)

str r2, [r0, #0]          // Clear the flag so we don't get stuck in the
                          // bootloader if it decides to jump back to us.

cmp r1, #1                // Compare r1 (the value stored at _bootloader_flag)
                          // against 1 (my signal value).
                          // I chose 1 as my signal, but many others online use
                          // cool looking values like 0xDEADBEEF. I wanted to
                          // use the other bits for other signals, so I used a
                          // simpler constant, but you can use any 4 byte
                          // constant you like!

beq RebootBootloader_asm  // In ARM thumb, all conditional branches have a
                          // limited distance they can jump (+/- ~250 in 16-bit
                          // thumb). I wanted to write the rest of this in C, but
                          // I didn't want to deal with placing that C function
                          // somewhere special and the compiler rightfully
                          // complained about a truncation because my C
                          // function could land almost anywhere.


// Here goes the normal system initialization assembly you likely already have.
// In my system, this sets the stack pointer, copies the .data section from
// flash to SRAM (these are the constants/initializations of data in your
// code). The compiler wrote it into flash for you, but your code will expect
// it in RAM-based addresses, so it's gotta be copied before any C is called.
// Then SystemInit() is called for my system to setup the clock configs, right
// before _start() is called.
bl SystemInit

// _start() is special in that it's a C/C++-specific function whose job it is
// to set everything up for you, just as global initialization code that you
// expect to be run before anything in main(), then it calls main().
bl _start

// Rounding out the normal sequence is a loop that ensures that if main()
// returns (it shouldn't), you don't go executing whatever's in flash right
// after this section.
LoopForever:
  b LoopForever


// Finally, the important bit! Just unconditionally branch to the reboot code!
RebootBootloader_asm:
  bl RebootBootloader

The Main Event

Now for the C code that actually jumps into the bootloader!

The first thing it has to do is pretend like the microcontroller wanted to boot into the bootloader. For the STM32F0 line, that means mapping system ROM to 0x0, rather than flash. To do that, we have to interact with the SYSCFG (system configuration) peripheral, so we enable its clock then alter the memory map.

void RebootBootloader() {
  // Enable the SYSCFG peripheral.
  RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
  // Remap to system ROM (also called system flash since it IS writable).
  // This is memory mode 0, and equivalent to this if you're not using mbed:
  // SYSCFG->CFGR1 &= ~(SYSCFG_CFGR1_MEM_MODE);
  // SYSCFG->CFGR1 |= SYSCFG_CFGR1_MEM_MODE_0;
  __HAL_SYSCFG_REMAPMEMORY_SYSTEMFLASH();
  // Create a typedef for a basic no-arg-no-return function.
  typedef void(*SystemRom)();
  // This address on the STM32F070xB points to the start of the bootloader.
  // This is NOT the start itself, but simply a pointer to it.
  SystemRom *rom = (SystemRom*)0x1FFF'C804;
  // Then let's set the stack pointer.
  // In assembly this would be 'ldr sp, =0x1FFFC800'
  // In C++17: __set_MSP(0x1FFF'C800);
  __set_MSP(0x1FFFC800);
  // Jump into the bootloader!
  (*rom)();
}

There you have it! You now have the ability to jump to your bootloader from within a running application!

In my case, I needed this to enable reprogramming via USB DFU in the field. However, it looks like STM32's builtin DFU-enabled bootloader writes very slowly (~500B/s). My next task will be evaluating third-party bootloaders and choosing a good one (or sticking with the built-in one?).

© Fahrzin Hemmati. Built using Pelican. Theme by Giulio Fidente on github.