Blue Pill Bootloader connected to Windows

STM32 Blue Pill USB Bootloader — How I fixed the USB Storage, Serial, DFU and WebUSB interfaces

The STM32 Blue Pill is a remarkable microcontroller for US$ 2. I proved it by running the USB Storage, USB Serial, USB DFU (Direct Firmware Upgrade) and WebUSB interfaces all on the same Blue Pill concurrently, without any additional hardware!

Why did I do this? I was building a USB Bootloader for the MakeCode visual programming tool that’s web-browser based and requires all these interfaces. I ran into lots of difficulty making all these interfaces coexist, almost giving up, thinking it’s a Blue Pill hardware limitation… But it turned out to be a software problem.

This article documents all the fixes I have made to create a working Blue Pill bootloader that has been tested on Windows, Mac and Linux. If you’re creating a USB device based on STM32 microcontrollers, this article might help you learn about the complex world of USB protocols. The complete source code is here…

Capture USB Data with Wireshark

I highly recommend installing Wireshark to capture and view USB data. After installing Wireshark (be sure to select USBPcap or usbmon when prompted during installation), check this article for further instructions (Windows and Linux)…

For Mac check this article…

Once you have Wireshark installed, you may download and view the USB logs that I have captured while connecting our Blue Pill bootloader to Windows, Mac and Ubuntu.

The Original Bootloader

I started with this bootloader code by Michał Moskal and Devan Lai…

It’s based on the open-source libopencm3 library for STM32. It didn’t build with the latest version of libopencm3 on Visual Studio Code with the PlatformIO extension, so I applied some patches from here…

The patches fixed the build to support WebUSB, USB 2.1 and Windows USB (more about this later). After flashing the Blue Pill with the ST Link V2 and PlatformIO, I soon ran into interesting problems…

USB Storage

The fixed code for supporting USB storage is in msc.c.

To implement USB storage in any USB device, we need to implement the USB Mass Storage Class (MSC) specs. So that we can connect the Blue Pill to a computer and it will appear as a USB drive. Within the specs, we’ll see that it actually uses the (very old!) SCSI protocol for reading and writing to the USB drive, and also for fetching the storage details. The Wireshark screen above shows a simple USB MSC command.

Thankfully we don’t need to implement the SCSI protocol ourselves. libopencm3 handles it for us, according to this sample code… just call usb_msc_init() and libopencm3 does everything automagically. However the USB MSC implementation usb_msc_init() in libopencm3 is missing some features, so I copied the libopencm3 source file and patched myself…

1️⃣ libopencm3 doesn’t implement the SCSI_READ_FORMAT_CAPACITIES and SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL SCSI commands

SCSI_READ_FORMAT_CAPACITIES is important because Windows will send this command to query the storage size. Without it, our USB bootloader won’t work with Windows. I found the implementation here..

And patched it in my code here. Note that the bytes_to_write setting in the article is incorrect. I have also applied a patch for MSC request handling that’s mentioned in the article.

According my logs, Windows is also sending the command SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL. I cooked up a dummy implementation here.

2️⃣ libopencm3 doesn’t implement SCSI_INQUIRY completely

This bug took me a while to track down. When I logged the USB requests from Windows, I noticed that Windows kept sending the SET_ADDRESS USB request to Blue Pill every minute. Windows was telling Blue Pill, “use USB address 5 now… switch to address 6 now… switch back to address 5 now…”

I soon discovered that’s a sign that Windows is resetting our Blue Pill because it didn’t like the response to one of the previous commands. I traced it to the SCSI_INQUIRY command, which missed out some important bits, and I patched my version here. (Yes I had to dig through the tedious SCSI specs.)

3️⃣ libopencm3 doesn’t play nice with with multiple USB interfaces

The official documentation for usb_msc_init() says cryptically, “Currently you can only have this profile active.” I found out the hard way that it doesn’t coexist well with other USB interfaces (e.g. USB Serial). Instead of rejecting unrecognised USB requests with an error (USBD_REQ_NOTSUPP), I fixed the code to hand off the unknown request to the next USB interface (USBD_REQ_NEXT_CALLBACK).

For our bootloader, we expect MakeCode users to copy UF2 files in order to flash the Blue Pill ROM. So in the USB storage code we register the callbacks read_block(), write_block() (defined in ghostfat.c) that will flash each 512-byte block of the UF2 file into ROM.

By handling the read_block(), write_block() callbacks, the code in ghostfat.c emulates a 4 MB flash drive formatted with the FAT filesystem (sector size 512). The Blue Pill hardware doesn’t actually have 4 MB of storage. When we copy a UF2 file into the drive for flashing, ghostfat.c will read in 1 sector (512 bytes) from the computer, write the 512 bytes into flash memory, and repeat until the entire UF2 file is processed.

Check this file for a RAM Disk implementation of the FAT filesystem.

USB Serial

The code for handling USB Serial interface is in cdc.c. This is based on the sample code here.

When you connect the Blue Pill to your computer, the bootloader code exposes a USB serial port to the computer, which is useful for debugging Blue Pill programs. For completeness I added the implementation of USB_CDC_REQ_GET_LINE_CODING just to satisfy Windows when it asks for the serial port parameters (e.g. 9600 bps, 1 stop bit, 8 data bits).

USB Serial is more complex than USB Storage because we need to implement not one but two interfaces from the USB Communications Device Class (CDC) spec:

  1. Data Model for sending and receiving data over the serial port
  2. Abstract Control Model (ACM) for getting and setting the serial connection parameters, like 9600 bps, 1 stop bit, 8 data bits. USB_CDC_REQ_GET_LINE_CODING is part of the ACM interface. More details here.

We are stacking up multiple interfaces — USB MSC (for storage) and USB CDC (for serial comms). And within USB CDC interface we are stacking up the CDC Data and CDC ACM sub-interfaces.

How does USB support this stacking? With a USB Composite Device and multiple USB Descriptors.

USB Descriptors

The Blue Pill Bootloader is a USB Composite Device, meaning that it supports multiple USB interfaces (storage, serial, DFU). When we connect the Blue Pill to a computer, Windows (or Mac or Linux) will query the Blue Pill for the USB interfaces that it supports. In the USB world, everything is defined through “Descriptors”. Descriptors are very important for getting our Blue Pill to function as a proper USB device under Windows, Mac AND Linux. So pay attention…

Our Blue Pill USB Descriptors are defined here. As shown in the diagram above, the USB Descriptors are defined in a hierarchy…

1️⃣ Device Descriptor: At the top level we define the USB Device.

The Device Descriptor includes the USB Vendor ID and Product ID (the official designation of the device), plus the manufacturer and product names (defined as String Descriptors, explained below).

2️⃣ Configuration Descriptor: At the next level we define the device configuration, which includes the list of interfaces and the expected power to be supplied to the device.

3️⃣ Interface Descriptor: Defines the interfaces implemented by the device, like MSC for USB Storage and CDC for USB Serial.

Each interface includes a list of USB endpoints (usually 0, 1 or 2 endpoints).

4️⃣ Endpoint Descriptor: What’s a USB endpoint? It works like a TCP socket. When our Windows / Mac / Linux computer wishes to send a storage or serial request to the Blue Pill, the computer needs to send the request to a USB endpoint (or address) for the USB interface (storage or serial).

OUT endpoints are used by the computer to send data to our Blue Pill. OUT endpoints are numbered 0x01, 0x02, … 0x0F.

IN endpoints are used by the Blue Pill to send data to the computer. IN endpoints are numbered 0x81, 0x82, … 0x8F.

Endpoints 0x00 and 0x80 are reserved for USB device control messages. They are used by the computer to fetch the Blue Pill’s USB descriptors and to control the Blue Pill.

5️⃣ Interface Association Descriptor: Remember that the USB Serial CDC interface requires two sub-interfaces — CDC ACM and CDC Data?

To do this we need to define the parent interface for CDC and define ACM and Data as the child interfaces. The parent interface is defined using the Interface Association Descriptor.

Check out the sample code, which defines cdc_iface_assoc as the parent Interface Association Descriptor, and comm_iface, data_iface as the child interfaces.

How did I discover this obscure descriptor? By using Wireshark to capture the USB traffic from BBC micro:bit! The micro:bit runs a UF2 Bootloader that’s as complex as ours, so it’s a good reference for our Blue Pill bootloader.

6️⃣ String Descriptor: The last descriptor defines all the text strings referenced by the other descriptors, like the interface names. Each string is assigned a running sequence number 1, 2, 3, …

In the Wireshark capture logs we may see the computer requesting for String Descriptor with index 0.

This is actually a request for the Language ID that our device supports. By default the Blue Pill returns 0x0409, the Language ID for US English.

The above USB descriptors are standard and we don’t need to write any code to handle the descriptor processing — libopencm3 handles the requests for all these descriptors automagically.

The complete list of USB descriptors for our Blue Pill bootloader is rather long, and it’s amazing that they all work for Windows, Mac AND Linux! It’s unlikely that you’ll work on something this complex, but if you’re stuck, do what I did… use Wireshark to sniff out another device that works!

USB 2.1 Descriptors

Ready for more USB descriptors? Here’s a long and powerful one— the Binary Device Object Store (BOS) Descriptor. The BOS Descriptor allows us to define USB descriptors that for non-standard platforms, like Windows.

Remember that we defined in the USB Device Descriptor that we support USB 2.1 instead of the usual USB 2.0? USB 2.1 devices are required to support BOS Descriptors, which are not implemented in libopencm3, so we have to handle them in usb21_standard.c.

Why did we choose to implement USB 2.1 and BOS Descriptors? So that we could implement WebUSB and USB DFU (we’ll meet them later). The two BOS Descriptors we have implemented in webusb.c are…

1️⃣ WebUSB Descriptor: Here we declare that our Blue Pill bootloader supports the WebUSB standard, implemented by Google Chrome web browsers.

When the Chrome browser detects that our device supports WebUSB, it will send our device a USB request to fetch the landing page URL for our device.

This WebUSB request is implemented in webusb.c since it’s not a standard USB request handled by libopencm3. Chrome transmits the vendor code 0x22 in the request so that we know the request is from Chrome WebUSB.

2️⃣ Microsoft OS 2.0 Descriptor: This defines a set of descriptors specific to Windows that we’ll cover in the next section.

Windows will send our device a USB request to fetch the set of Windows-specific descriptors.

This Windows descriptor request is implemented in winusb.c since it’s not a standard USB request handled by libopencm3. Windows transmits the vendor code 0x21 in the request so that we know the request is from Windows.

The Microsoft OS 2.0 Descriptor implementation is not part of the original bootloader source code. I added this code as a replacement for the older Microsoft OS 1.0 Descriptor implementation. The older implementation is still in winusb.c.

For details of the BOS Descriptor format, see the official USB 3.2 Specification, Section 9.6.2 “Binary Device Object Store (BOS)”. If you’re looking for the official USB 2.1 Specs… Don’t! Technically USB 2.1 doesn’t exist as a standard.

BOS is actually part of the USB 3.0 specs, but our Blue Pill bootloader doesn’t implement the entire USB 3.0 specs. The industry has adopted the name “USB 2.1” to refer to a USB 2.0 device that supports BOS. Which fits our purpose perfectly.

Windows USB Descriptors

Because of the BOS Descriptor, Windows will query our Blue Pill bootloader for the Microsoft OS 2.0 Descriptors documented above. The long list of Windows descriptors for Blue Pill is defined in winusb.c (obtained from here). We’ll look at two interesting descriptors…

Registry Property Descriptor

1️⃣ Compatible ID Descriptor: This declares to Windows that the first USB interface (DFU) of our Blue Pill bootloader is compatible with WinUSB.

Windows will then use the standard WinUSB.sys driver for the DFU interface.

2️⃣ Registry Property Descriptor: This declares to Windows that it should create a Windows registry setting named DeviceInterfaceGUIDs with value {9D32F82C-1FB2–4486–8501-B6145B5BA336}

Creating this registry setting is mandatory when using the WinUSB driver for a USB interface.

Why use WinUSB for the DFU interface?

The STM32 DFU USB Interface implemented in dfu.c is meant to allow us to flash the Blue Pill through the Chrome browser, without the installation of any drivers. The WinUSB driver is preloaded with Windows, so it doesn’t need any installation.

WinUSB exposes a low-level data transfer interface that WebUSB applications may call to transfer data to the DFU interface. So WinUSB is the right Windows USB driver for supporting the DFU interface. For more details, check this article…

When you’re changing the Blue Pill bootloader code and testing it, make sure you run regedit and delete the Windows registry key HLKM\SYSTEM\CurrentControlSet\Control\UsbFlags\vvvvpppprrrrr where vvvv is the vendor ID, pppp is the product ID and rrrrr is the device revision number. If the key exists, Windows will skip the querying of descriptors from the Blue Pill bootloader, and use the old descriptors values instead. This is mentioned here…


Now that WebUSB and WinUSB have been configured for our Blue Pill’s DFU interface, click the link above to test them. We should be able to communicate with the Blue Pill bootloader through the Chrome browser and prepare to flash the ROM (set the Transfer Size to 256 bytes) with the firmware.bin file. (But don’t run the flash command yet because the bootloader has exceeded the 16K limit and will clash with the flashed firmware.)

To experiment with WebUSB, open a Chrome console and enter this…

This lets us browse the Blue Pill bootloader descriptors through the JavaScript console in Chrome. If we sniff the USB data with Wireshark while this is running, we’ll see that the Chrome browser actually sends its own queries to refetch the USB descriptors.

I have tested the Blue Pill bootloader’s WebUSB support on Chrome for Windows, Mac and Ubuntu (requires root privilege). To troubleshoot the WebUSB interface, use the Chrome Device Log:

For details of the WebUSB JavaScript API, refer to these docs…

USB Callbacks

The sample USB programs for libopencm3 look like this…

There are 2 functions used to register callbacks in USB programs…

  1. usbd_register_set_config_callback(): This registers a callback function that is called when the USB device has set its configuration.
  2. usbd_register_control_callback(): This registers a callback function that is called when the USB device receives a request on the control channel.

Each of these functions support up to maximum of 4 callbacks. Since our Blue Pill bootloader has 4 USB interfaces + BOS Interface + WinUSB + WebUSB, we would have exceeded the limit. So I wrote my own code to aggregate the callbacks, supporting up to 10 callbacks.

To use the aggregated callbacks, we change the code as follows…

  1. Change usbd_register_set_config_callback
    to aggregate_register_config_callback
  2. Change usbd_register_control_callback
    to aggregate_register_callback

We should always check the status of the callback registration like this…

Finally It Works!

This version of the Blue Pill Bootloader worked successfully on Windows, Mac and Linux after lots of experimenting. It’s my first time working on USB firmware and I wished there were more articles explaining what’s really happening in the USB realm. I hope this article, source code and captured USB logs will get you started on USB firmware programming a lot faster.

And the work goes on…

I have started working on a new branch that supports WebUSB flashing using MakeCode’s HF2 protocol. Check out my updates here…

Is it possible to upgrade the Bootloader through another Bootloader? Yes we can! Check this article…