STM32 Blue Pill — Bootloading the WebUSB Bootloader

The year is 2029. At Moon Base One, two Moon Base Operators nearly wiped out their entire crop of beautiful red tomatoes… All because of a firmware bug in their IoT monitoring device that slipped past Embedded Unit Testing.

Operator 1: Are we all set to flash our STM32-BLUEST-PILL monitoring devices with the new firmware? The one that passed Embedded Unit Testing?

Operator 2: Yes! Lemme launch MakeCode version 2029… Hitting the DOWNLOAD button… Now regenerating the new UF2 firmware image… Transmitting firmware image to all STM32-BLUEST-PILL Bootloaders… And Firmware version 2029 has been flashed to all 1,000 devices! Wait we got 1,000 errors… “Critical system library libopencm3 version 2029 not found”…

Operator 1: WHAT’S HAPPENING TO OUR SENSORS ??? OUR IOT DASHBOARDS ALL TURNED BLANK !!! We’re running Bootloader version 2028, which contains libopencm3 version 2028! We don’t use version 2029 here!

Operator 2: So we just upgrade the Bootloader to version 2029. Piece of cake right? Oh wait… There’s no button for MakeCode Bootloader Upgrade. We haven’t implemented the Bootloader Upgrade yet. How often do we upgrade the Bootloader right? Heh heh…

Operator 1: The sprinklers have stopped !!! Our juicy tomatoes are drying up and turning into… SUN-DRIED TOMATOES !!! NOOOOOOO…

With a Bootloader installed, programming the STM32 Blue Pill via USB gets a lot simpler and faster… A single click is all it takes to flash the Blue Pill!

In the previous article we learned how our MakeCode Bootloader flashed Applications to Blue Pill via WebUSB. The chronicles of Moon Base One have taught us that we need a way to upgrade the Bootloader as well.

But how do we upgrade the Bootloader when it’s always running in the background, waiting for flashing requests? This article explains a special technique I used to upgrade the MakeCode Bootloader over WebUSB… I call it “Baseloading”. We’ll learn…

  1. How to write to Blue Pill’s flash memory without calling any libraries
  2. How we used this method of flashing to update the Bootloader code
  3. How to use Backup Registers to remember the state of the Blue Pill after restarting

Flashing with Zero Dependencies

Take a look at this code that we use for flashing the Blue Pill WebUSB Bootloader

Looks like typical Blue Pill code… baseloader_start() calls some functions to lock, unlock, erase and update Blue Pill’s flash memory. Now look at the Arm Cortex-M3 assembly code that the C compiler generated for baseloader_start()

Do you see the magic here? There are no calls to other functions! It’s as though all the lock / unlock / erase / update functions are done within this baseloader_start() function… baseloader_start() has Zero Dependencies!

How is that possible? That’s because Blue Pill uses Memory-Mapped I/O. By reading/writing some special memory location, we can control the Blue Pill peripherals, including the flash memory. The lock / unlock / erase / update functions above are actually C macros that read/write the special memory addresses that control the Blue Pill flash memory.

Also note that baseloader_start() uses Relative Addresses (instead of Absolute Addresses) when branching conditionally to other parts of the function. Just look at the above branch instructions like bls.n, b.n, bcs.n.

Zero Dependencies + Relative Addressing… That means the code is perfect for Relocation anywhere in Blue Pill memory! We can copy the baseloader_start() function into any chunk of Blue Pill memory and it will still work! Assuming of course we don’t mess around with the global variables accessed by baseloader_start().

That’s the key reason why our Bootloader code is able to update itself… It’s able to clone itself into a new chunk of ROM memory AND execute the cloned copy of Bootloader to update the old ROM copy of Bootloader!

From now on we’ll call this “code that updates the Bootloader” as the Baseloader.

Now if the Baseloader can’t fetch the new Bootloader code from the USB port (remember that the Baseloader only uses flash memory), how can the Baseloader update the Bootloader?

By carefully juggling flash memory, as we shall soon see…

Upgrading the Bootloader, step by step

How does our Blue Pill WebUSB Bootloader upgrade its own code? Let’s watch how it’s done with the Baseloader

1️⃣ We have partitioned Blue Pill’s ROM with this layout: Bootloader in the lower ROM region, Application in the upper ROM region

2️⃣ When we click Download to update the Blue Pill, the MakeCode Website generates a Blue Pill firmware image with the same ROM layout

3️⃣ MakeCode Website transfers the firmware image in chunks to Blue Pill via the USB HF2 protocol, from low to high address (Bootloader followed by Application)

4️⃣ Blue Pill Bootloader receives the new Bootloader code and writes temporarily into the old Application region

5️⃣ Bootloader compares the old and new Bootloader code. Assuming that the code has changed, the Bootloader clones the Baseloader code (inside the Bootloader) into the next available ROM space. Bootloader reboots the Blue Pill and starts the Baseloader.

6️⃣ Baseloader moves the new Bootloader code into the Bootloader ROM region, overwriting the old Bootloader. The Bootloader is not running now, so it’s safe to overwrite.

7️⃣ The Baseloader code is Relocatable, so it may be executed anywhere in ROM. That’s why the Baseloader can run outside the Bootloader ROM region. Unlike the Bootloader, which may only be executed at a fixed address in the Bootloader ROM region (0x0800 0000).

8️⃣ The Baseloader code is Relocatable because it doesn’t call any other functions (it uses only C macros that access I/O memory). And because it branches to other parts of its own code through Relative Addresses, not Absolute Addresses.

9️⃣ Baseloader reboots the Blue Pill and starts the new Bootloader

🔟 Bootloader continues to receive the firmware from MakeCode via USB. Bootloader writes the updated Application code into the Application ROM region, overwriting the old Application code.

After flashing, Bootloader reboots the Blue Pill and starts the new Application

As we can see, the Bootloader upgrade is made possible by the Baseloader, a special tiny program that has zero dependencies and performs only one job — overwrite the old Bootloader in ROM by the new Bootloader (also in ROM).

Once again, watch how the Bootloader works together with the Baseloader to update its own code…

Upgrading the WebUSB Bootloader

Remembering the Blue Pill state after restarting

In some of the above steps we had to restart Blue Pill in either the Bootloader, Baseloader or Application Modes. How does Blue Pill remember which mode it should switch to after restarting?

Before we answer that, here’s the libopencm3 function that we should call to restart the Blue Pill…


After restarting, all contents of the RAM are wiped out. ROM is non-volatile, so we could flash a value into ROM to indicate the restart mode. But on Blue Pill there’s an easier, safer way to remember the restart mode: Backup Registers.

The Blue Pill’s Backup Registers are ten 16-bit registers that we may use to store 20 bytes of data. The contents of the registers will be preserved across restarts, as long as there is power supplied to Blue Pill.

In the Bootloader code above we use macro RTC_BKP_DR() to read and write the contents of the Backup Registers. Note that the RTC_BKP_DR() macro is called twice, so that we can store a 32-bit value into two 16-bit registers.

Also note the calls to libopencm3 functions pwr_disable_backup_domain_write_protect() and pwr_enable_backup_domain_write_protect(). The Backup Registers are write-protected against any possible data corruption, in case the Blue Pill is restarted and the power is unstable. We need to call pwr_disable_backup_domain_write_protect() to disable the write-protection before writing to the Backup Registers, and call pwr_enable_backup_domain_write_protect() when we’re done.

So here’s what happens when our Blue Pill restarts…

1️⃣ Before restarting, the Bootloader sets the restart mode to Bootloader, Baseloader or Application Mode by calling boot_target_manifest_bootloader(), boot_target_manifest_baseloader() or boot_target_manifest_app(). These three functions store the restart mode into Backup Registers 0 and 1.

1️⃣ Upon restarting, bootloader_start() in our Bootloader is the function that runs first. It calls boot_target_get_startup_mode() to get the restart mode.

2️⃣ boot_target_get_startup_mode() in our Bootloader reads the Backup Registers 0 and 1 to get the restart mode.

End of the Journey?

The WebUSB Bootloader for MakeCode on Blue Pill is incredibly complex — it required five articles (plus this article) to explain the topics that we have covered today…

1️⃣ We implemented the WebUSB and WinUSB interfaces on Blue Pill…

2️⃣ We applied memory optimisation tools and techniques to squeeze the Bootloader into Blue Pill…

3️⃣ We created nano-float, an optimised version of the standard math library…

4️⃣ And we verified the accuracy of the nano-float library through Unit Testing…

5️⃣ Finally we combined all the topics from the above articles into a complex Bootloader…

We have done so many incredible things with Blue Pill… Is there a limit? Nope we haven’t hit the limit yet. So why don’t we create better development tools to let everyone enjoy the power of Blue Pill?

A good Bootloader is essential for a great programming experience on Blue Pill, and we have come a long way since the first Blue Pill Bootloader…

1️⃣ Blue Pill Factory-Installed Bootloader: Needs the UART pins to be connected before flashing. No USB support.

2️⃣ Blue Pill Arduino Bootloader: Works with USB. Flashing works only with Arduino IDE client.

3️⃣ Blue Pill MakeCode Bootloader: Works with Chrome and other WebUSB-compatible web browsers. Allows Bootloader to be reflashed via WebUSB. Probably the best Blue Pill Bootloader to date!

It’s been a long journey — join me if you’re keen to complete the Blue Pill port of MakeCode! So that we’ll finally have the perfect platform to build and test IoT prototypes.

There are too many topics to cover, so I kept a log of all things that I have tried (even the failed ones)…

Look forward to having you onboard the Blue Pill MakeCode journey! :-)