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.
Common sight during my USB development… “Drivers not installed” due to USB descriptor errors or USB request processing errors
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 Mass Storage Class (MSC) request to fetch the maximum logical unit number (i.e. drive number). From the Wireshark log usb-windows.pcapng.gz
USB Storage
The fixed code for supporting USB storage is in msc.c.
Blue Pill appears on Windows as a USB Mass Storage Device. At the lower left are the files emulated by Blue Pill (done by ghostfat.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…”
Windows sending SET_ADDRESS 5… 6… 5… 6…
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
).
ghostfat.c emulates a 4 MB flash drive and exposes the 3 files above
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.
Blue Pill appears as a USB Serial Device on COM3. We may use putty to connect to COM3 and Blue Pill will echo everything that we type
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:
- Data Model for sending and receiving data over the serial port
- 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…
Hierarchy of USB Descriptors. From "Standard USB descriptors"
Our Blue Pill USB Descriptors are defined here. As shown in the diagram above, the USB Descriptors are defined in a hierarchy…
USB Device Descriptor
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).
USB Configuration Descriptor
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.
USB Interface Descriptor for USB Storage (MSC) interface
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).
USB Endpoint Descriptors for USB Storage (MSC) interface
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.
USB Interface Association Descriptor for USB Serial (CDC) interface
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.
USB String Descriptor
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, …
String Descriptor 0 returns the supported Language ID (0x0409). From the Wireshark log usb-windows.pcapng.gz
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.
Long list of USB Descriptors used by our Blue Pill bootloader. From bluepill-bootloader/usb_conf.c
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.
On macOS, the landing page URL is displayed as a Chrome notification. On Windows the notification seems to be disabled.
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.
Windows requesting the BOS Descriptor from Blue Pill. From the Wireshark log usb-windows.pcapng.gz
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…
Compatible ID Descriptor
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\vvvvpppprrrrrwhere
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…
WebUSB
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…
await navigator.usb.requestDevice({ filters: [] })
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:
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…
Sample USB code from libopencm3/cdcacm.c
There are 2 functions used to register callbacks in USB programs…
usbd_register_set_config_callback()
: This registers a callback function that is called when the USB device has set its configuration.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…
- Change
usbd_register_set_config_callback
to aggregate_register_config_callback - Change
usbd_register_control_callback
to aggregate_register_callback
We should always check the status of the callback registration like this…
Aggregating USB callbacks, from bluepill-bootloader/cdc.c
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…