Make a serial bootloader for the STM32WL (Lora-E5) using Zephyr and MCUBoot

Mark Zachmann
Home Wireless
Published in
6 min readJan 4


Once my latest Zephyr project was in a box it became critical to have an update process that didn’t require connection to the debug port. Zephyr comes with a Zephyr-ish implementation of MCUBoot that works amazingly well but as usual the last few steps are agonizing.

A wio-e5-mini and enclosure

Unlike every single board with a configuration in the provided zephyr mcuboot/boards, the STM32wl doesn’t have a built-in USB interface and can’t provide the drive interface upgrade — where it mounts a drive when attached to a PC. It does, however, support MCUBoot in the serial mode just fine — so you can update from the USB port.

In serial mode MCUBoot can, for a time, poll the serial port for upload and other commands — allowing the firmware to be updated. It supports single and dual image modes. My current zephyr app is really large and so I run as a single image — a bad upload of the app causes it to not run until a valid app is uploaded.

Set up the VSCode Environment

Start by following all of these instruction: VSCode Building and Debugging Zephyr . The bootstrapper is a zephyr application that loads at the base of memory so a lot of this is familiar. When you’re done you’ll have an empty src folder.

Customize the environment for MCUBoot

The next step is to add the mcuboot code from zephyr. In Windows, do this with a symbolic link to the Zephyr project which works easily with existing stuff. Run a command prompt / admin and in your work folder type ->

mklink /D mcuboot my_zephyr_root\bootloader\mcuboot

The mcuboot ‘folder’ should look something like this. The main.c source code is in the boot/zephyr subfolder.

Now, we need some configuration settings for the bootstrapper along with changes to all of our embedded apps.

DeviceTree Upgrade

We must define the flash locations/limits for the bootstrapper and image. That’s done by defining specifically named flash partitions in the my_board.dts file. This is my setting for the STM32WL with 256K of flash available and my application takes up about 180KB. My apps use the LittleFs filesystem for persistent configurations, so I reserve 16K at the back of flash for a small volume.

&flash0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;

// the bootstrapper goes here
boot_partition: partition@0 {
label = "mcuboot";
reg = <0x00000000 DT_SIZE_K(32)>;
// the app image goes here
slot0_partition: partition@8000 {
label = "image-0";
reg = <0x00008000 DT_SIZE_K(196)>;
// we don't have room for a slot1 with this app
// Reserve 16kB of storage at the end of the 256kB
storage_partition: partition@3c000 {
label = "storage";
reg = <0x0003c000 DT_SIZE_K(16)>;

The bootstrapper uses the zephyr console definition for i/o which on the wio-e5 mini is the physical USB port going to a CP2102 and then the STM32WL usart1 pins, so this

 chosen {
zephyr,console = &usart1;

Configuration of Bootstrapper

The bootstrapper needs a few CONF settings for the serial support. I did this by adding a my_board.conf file to the boot/zephyr/boards folder. Here are the contents — with many things turned off.

If the DETECT_PIN (see below) is held down at system start then the bootstrapper will go into DFU mode. Otherwise, the image is checked and then executed.

# Disable Zephyr console


# Multithreading

# MCUBoot settings

# MCUboot serial support
# we only have one slot

# I can't debug with watchdog going afaik

#Optional encryption, but takes-space

I had trouble with watchdog feed when debugging so turned it off.

Note that for the bootloader the build is slightly different than applications in that the ‘src’ folder is inside the workspace root— note the workspaceFolder.AppUnderDev setting in .vscode/settings.json.

    "workspaceFolder": {
"path": ".",
"zephyrproject": "%HOMEPATH%/zephyrproject",
"AppUnderDev": "${workspaceRoot}/mcuboot/boot/zephyr"

Configuration of Applications

The applications need to know how to be uploaded, so we need to make a ‘signed’ binary — even though it’s just signed with a checksum — it also has the header block filled in.

First, you’ll want to add a new task option to your app vscode tasks.json file that runs the update.

"label": "Upload",
"type": "shell",
"group": "build",
"command": "mcumgr",
"args": [
"-c", "acm0", "image", "upload", "${config:app.build_dir}\\zephyr\\zephyr.signed.bin"
"dependsOn": [],
"problemMatcher": []

This relies on an mcumgr connection called acm0, which we’ll get to in a sec.

In the application, just add two lines to the prj.conf file

# -- enable this to have the module load at slot0
# build an image with checksum only signature

The first tells make to base the app at slot0, the second line creates a zephyr.signed.bin file with the right header and footer.

How to Update a Device with a new Application

Uploading the binary was the toughest piece of this in some ways.

This uses an application named mcumgr (see here.). It turns out this lives in the go language, so install go and then have it install mcumgr.

go install

Now mcumgr can run fully command line or you can have it cache some port settings. I do the latter because those settings can be then changed at will. To set up a connection definition named acm0 ->

mcumgr conn add acm0 type="serial" connstring="dev=COM22,baud=115200,mtu=512"

Notes: this sample uses COM22. Each device has its own mapped COM port. See the speedup section below about the hardcoded 512byte mtu limit.

Finally, to upload the binary you can use the task Upload option or manually type it as

mcumgr -c acm0 image upload my_build_dir\zephyr\zephyr.signed.bin

This will try to attach to the defined COM port and send the application file.

How to Speed up the Upload

Once you finish these steps the upload will go, in my experience, between 650 bytes/second and 1k bps. It’s glacial. My 180K application takes 3 minutes to upload. But… there’s a fix.

The bootloader is designed to run on a tiny machine (as is mcumgr). As such, the mtu is hardcoded to 512. To increase performance…

A) Edit the file boot_serial.c to increase the input maximum to 4096. There’s plenty of ram.

B) Change your mcumgr connection to have a 4096 mtu (maximum transfer unit/block size). The mcumgr code throttles this kind of but it still runs faster.

mcumgr conn add acm0 type="serial" connstring="dev=COM22,baud=115200,mtu=4096"

Using the Upload Button

Part of the definition requires that you define a button to guide the upload. You don’t have to use this feature (there is a check-for-short-time option) but it will make that pin input.


It turns out that the wio-e5 mini has a bugfeature. I tried for over a day to try to figure out why whenever an app opened the usb/serial port the unit restarted. Well, the CP2102 DTR pin is connected to the reset pin via a capacitor and so opening (and sometimes closing) the usb/serial port sometimes causes a reset.

So, when updating this unit make sure you keep the button down while running mcumgr in case it resets the cpu as part of opening the port.

  1. press and hold the PIN_BUTTON
  2. reset or power-up the device
  3. run mcumgr to start the upload
  4. release the PIN_BUTTON

The button being down during reset will put it into serial recovery waiting for an upload command.



Mark Zachmann
Home Wireless

Entrepreneur, software architect, electrical engineer. Ex-academic.