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

Mark Zachmann
Home Wireless
Published in
7 min readJan 4, 2023

Updated June 2024

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 (statically named mcuboot_button0) is held down at system start then the bootstrapper will go into DFU mode. Otherwise, the image is checked and then executed.

Here’s the little piece of dts where the button is defined. Note the name mcuboot_button0.

 gpio_keys {
compatible = "gpio-keys";
mcuboot_button0: button_0 {
label = "SW1";
gpios = <&gpioa 3 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; // uart2rx
zephyr,code = <INPUT_KEY_0>;
};
};

And the config file

CONFIG_PM=n

CONFIG_MAIN_STACK_SIZE=10240
CONFIG_MBEDTLS_CFG_FILE="mcuboot-mbedtls-cfg.h"

CONFIG_BOOT_SWAP_SAVE_ENCTLV=n
CONFIG_BOOT_ENCRYPT_IMAGE=n

CONFIG_BOOT_UPGRADE_ONLY=n
CONFIG_BOOT_BOOTSTRAP=n

### mbedTLS has its own heap
# CONFIG_HEAP_MEM_POOL_SIZE is not set

CONFIG_FLASH=y

# set up logging
CONFIG_LOG=y
CONFIG_LOG_MODE_MINIMAL=y # former CONFIG_MODE_MINIMAL
### Ensure Zephyr logging changes don't use more resources
CONFIG_LOG_DEFAULT_LEVEL=4
### Decrease footprint by ~4 KB in comparison to CBPRINTF_COMPLETE=y
CONFIG_CBPRINTF_NANO=y
### Use the minimal C library to reduce flash usage
CONFIG_MINIMAL_LIBC=y

# Disable Zephyr console and speed up log mode
CONFIG_LOG_MODE_IMMEDIATE=y
CONFIG_CONSOLE=n
CONFIG_CONSOLE_HANDLER=n
CONFIG_UART_CONSOLE=n

# Multithreading
CONFIG_MULTITHREADING=y

# MCUBoot settings
CONFIG_BOOT_MAX_IMG_SECTORS=256
CONFIG_MCUBOOT_INDICATION_LED=n

# MCUboot serial recovery
CONFIG_MCUBOOT_SERIAL=y
CONFIG_BOOT_SERIAL_DETECT_DELAY=450

# Size of mcuboot partition
CONFIG_SIZE_OPTIMIZATIONS=y

# MZ!
CONFIG_SINGLE_APPLICATION_SLOT=y
# CONFIG_BOOT_VALIDATE_SLOT0=y
# we can't debug with watchdog going afaik
# CONFIG_BOOT_WATCHDOG_FEED=y

# Generated by Kconfiglib (https://github.com/ulfalizer/Kconfiglib)
CONFIG_BOOT_SIGNATURE_TYPE_NONE=y
#CONFIG_BOOT_SIGNATURE_TYPE_RSA=y
#CONFIG_BOOT_SIGNATURE_KEY_FILE="..\\..\\mz_zephyr_key.pem"

# if no application go into serial load
CONFIG_BOOT_SERIAL_NO_APPLICATION=y
# allow go into serial using switch mcuboot_button0
CONFIG_BOOT_SERIAL_ENTRANCE_GPIO=y
# increase these two from default for use of higher MTU
# here mtu of 1024 works for 8x speedup frmo default 127 MTU
CONFIG_BOOT_SERIAL_MAX_RECEIVE_SIZE=2048
CONFIG_BOOT_MAX_LINE_INPUT_LEN=2048

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
CONFIG_BOOTLOADER_MCUBOOT=y
# build an image with checksum only signature
CONFIG_MCUBOOT_GENERATE_UNSIGNED_IMAGE=y

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 github.com/apache/mynewt-mcumgr-cli/mcumgr@latest

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 two lines shown at the end of the CONF file above increase the buffer size to increase the MTU from default 128 to 1024 for a speedup of 8x.

Now change your mcumgr connection to have a 1024 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=1024"

Note that if you don’t allow an mtu of 1024 the line above won’t connect (no warning, it just sits there). The mtu in the line must not exceed the max compiled in.

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.

To use it, just boot the device with that pin held down then release it after power-up. The bootstrapper will wait for an upload command (forever).

bugfeature of wio-e5 mini

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.