Skip to content

Zephyr RTOS Bootloader

Introduction

Zephyr RTOS Bootloader

In this Codelab, you will first learn the basic principles of integrating a bootloader application in Zephyr RTOS. You will then learn how to download and install new applications securely.

What you’ll build

  • You will integrate the MCUboot bootloader application into your Zephyr RTOS application.
  • You will then implement a download mechanism for storing a new application and installing it.

What you’ll learn

  • You will learn the basic principles of bootloaders and their implications for Zephyr RTOS program images.
  • You will also understand how the boot sequence is modified to start an application with a bootloader.

What you’ll need

The Bootloader Principle

A bootloader is a type of application that allows system or application software to be updated without the use of specialised hardware, such as a JTAG programmer (or other programmers for different MCUs). The bootloader manages and installs system or application images. In specific cases, it may also download them. Throughout the rest of this document, we will often refer to the system or application image as ‘firmware’. We will also refer to the process of downloading and installing new firmware as ‘DFU’.

DFUs/bootloaders can communicate using a variety of protocols to download firmware and save it to the non-volatile memory of the embedded system. These protocols can be UART, Ethernet, USB or a wireless protocol for systems with wireless connectivity. As well as downloading new firmware, DFUs/Bootloaders are responsible for verifying its integrity and authenticity.

Usually, systems with bootloaders have at least two firmware images coexisting on the non volatile memory of the MCU. They must also include code for branching installation of a new firmware image.

It is very common that the need for a bootloader mechanism is overlooked when conceiving embedded system software, because the bootloader is not the end product. However, the bootloader is an essential component of an embedded system. It allows to launch a system with software that only fulfills a subset of the final requirements and to add features to the product once it has already been launched to the market. And, even more importantly, it also allows the developers to fix bugs that are discovered over the lifetime of the product and to deploy corrections.

Writing a bootloader application requires an understanding of how a program image is built and how the MCU uses this memory. In this codelab, we will develop a simple bootloader and explain all components required for its development.

The Zephyr RTOS Memory Model

From the lecture notes, recall that the Zephyr RTOS/MCUboot memory model is as depicted in the figure below:

_Zephyr RTOS_/MCUboot Memory Model

Zephyr RTOS/MCUboot Memory Model

It is important to note that this is not the only applicable model, and that other memory models are also possible (for instance, where the bootloader is not located before the application in ROM). However, on Cortex-M MCUs, the MCU expects the application to start by reading the SP and PC at the start of the ROM. Therefore, locating the immutable bootloader at the start of the ROM is not really an option.

Boot sequences

In general, a boot sequence can comprise several stages before reaching the application. These stages include several bootloader stages and the application itself. Except for the immutable bootloader, all stages can evolve over time and be upgraded to add new features and correct bugs. Upgrades are possible for boot sequences with two or more stages: any active stage can replace the next stage in the sequence, meaning that when the system restarts, the updated components (including the updated application) will be executed. The very first stage is usually immutable because it takes control on startup, so a faulty upgrade could be very difficult to recover from.

Usually, for protecting against faults in a newly updated component, multiple versions of each stage are stored in the non volatile memory. Each stage is responsible for detecting faults in the next stage and for rolling back if a fault is discovered.

Our simple implementation

In this codelab, we will implement different options for a simple two-stage boot sequence: a bootloader that won’t be upgraded and an application that will be upgraded. Most boot sequences usually comprise at least three stages: a root bootloader or boot selector that is not upgraded, and two upgradable stages (bootloader and application) with multiple versions stored on the device. In our implementation, the bootloader acts as both a boot selector and a bootloader, capable of installing new images.

Compile and Install the MCUboot Bootloader Application

Integrating multiple applications using sysbuild

The integration of the MCUboot bootloader with a Zephyr RTOS application is often achieved using the Zephyr RTOS sysbuild higher-level build system. This system enables multiple builds to be combined and multiple applications to be flashed or debugged simultaneously. For the sake of simplicity, we will build and flash the MCUboot bootloader and the BikeComputer program as two separate applications.

Import MCUboot into Your Workspace

The following steps explain how to import MCUboot into your Zephyr RTOS workspace:

  • Add the following to your west.yml file:
    manifest-repo/west.yml
        ...
          import:
            path-prefix: deps
            file: west.yml # not strictly needed since it is the value by default
            name-allowlist:
              ...
              - mcuboot
              - mbedtls
              ...
    
  • Run west update.
  • Check that the MCUboot bootloader is available in the “deps/bootloader” folder.

Build the MCUboot Application

MCUboot not fully compatible with Zephyr RTOS v4.2.1

Unfortunately, at this time, MCUboot is not fully compatible with Zephyr RTOS v4.2.1. Changes in the “mbedtls” module require to patch the MCUboot distribution as documented below.

The following steps are required to build and flash the Zephyr RTOS MCUboot bootloader application:

  • Add the file “Kconfig.mbedtls_fragment” with the following content in the “deps/bootloader/mcuboot/boot/zephyr/” folder:
Kconfig.mbedtls_fragment
deps/bootloader/mcuboot/boot/zephyr/Kconfig.mbedtls_fragment
# Kconfig fragment: Kconfig.mbedtls_fragment
#
# This fragment tries to enable Zephyr's mbedTLS module and the minimal
# symbols MCUboot needs. If your environment overrides some symbols,
# adjust as needed.

config MBEDTLS_FORCE_ENABLE
    bool "Force-enable mbedTLS minimal config for MCUboot"
    default y

if MBEDTLS_FORCE_ENABLE

config MBEDTLS
    bool "Mbed TLS library"
    default y
help
  Enable mbed TLS module support.

config MBEDTLS_CFG_FILE
    string "mbed TLS config header"
    default "config-mbedtls.h"

# Minimal components needed by MCUboot for ECDSA/RSA
config MBEDTLS_SHA256_C
    bool "mbed TLS SHA-256"
    default y

config MBEDTLS_ECP_C
    bool "mbed TLS ECC primitives"
    default y

config MBEDTLS_ECDSA_C
    bool "mbed TLS ECDSA"
    default y

config MBEDTLS_BIGNUM_C
    bool "mbed TLS bignum"
    default y

config MBEDTLS_PK_C
    bool "mbed TLS pk abstraction"
    default y

config MBEDTLS_PK_PARSE_C
    bool "mbed TLS pk parse"
    default y

config MBEDTLS_PK_WRITE_C
    bool "mbed TLS pk write"
    default y

endif # MBEDTLS_FORCE_ENABLE
  • Source this file in the “deps/bootloader/mcuboot/boot/zephyr/Kconfig” file by adding rsource "Kconfig.mbedtls_fragment" right before mainmenu "MCUboot configuration"

  • Create a “prj_bootloader.conf” file at the root of your workspace. This configuration file will be used to build the MCUboot bootloader application. Some changes are required for a proper MbedTLS configuration and the following content must be added:

    {workspace}/prj_bootloader.conf
    ...
    # Base mbedtls
    CONFIG_MBEDTLS=y
    CONFIG_MBEDTLS_CFG_FILE="config-mbedtls.h"
    
    # Required curve for MCUboot ECDSA-P256
    CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y
    

  • Modify the “deps/bootloader/mcuboot/boot/zephyr/CMakeLists.txt” file by replacing
    if(DEFINED CONFIG_MBEDTLS)
    zephyr_library_include_directories(
      ${ZEPHYR_MBEDTLS_MODULE_DIR}/include
    )
    endif()
    
    with
    if(DEFINED CONFIG_MBEDTLS)
    zephyr_library_include_directories(
      ${ZEPHYR_BASE}/../modules/crypto/mbedtls/include
    )
    endif()
    
    For this change, make sure that you defined the environment variable ZEPHYR_BASE correctly.
  • Build the MCUboot bootloader application as a standard Zephyr RTOS application, running west build deps/bootloader/mcuboot/boot/zephyr --pristine --extra-conf "full path to prj_bootloader.conf". You will see a warning saying “Using default MCUboot signing key file, this file is for debug use only and is not secure!”. This is not an issue at this point.

  • Flash the application. The following output should appear on a serial monitor:

    *** Booting MCUboot v2.2.0-54-g4eba8087fa60 ***
    *** Using Zephyr OS build v4.2.1 ***
    I: Starting bootloader
    I: Primary image: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
    I: Secondary image: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
    I: Boot source: none
    W: Failed reading image headers; Image=0
    E: Image in the primary slot is not valid!
    E: Unable to find bootable image
    

The bootloader is running correctly at this point, but, as expected, it cannot find a bootable image.

Modify Your BikeComputer Application To Integrate a Bootloader

To enable the BikeComputer to run from the bootloader application, you must modify the prj.conf configuration of the BikeComputer program as follows:

  • Modify the build so that the image is chain-loaded by MCUboot:

    bike_computer/prj.conf
    CONFIG_BOOTLOADER_MCUBOOT=y
    

  • Instruct west build to sign the image with the appropriate default key file:

    bike_computer/prj.conf
    CONFIG_MCUBOOT_SIGNATURE_KEY_FILE="{workspace_folder}/deps/bootloader/mcuboot/root-rsa-2048.pem"
    
    At this stage, the default MCUboot signing key file is used.

  • Add versioning for signing the image:

    bike_computer/prj.conf
    CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION="1.0.0+0"
    

Once you have modified the prj.conf file, you can build and flash the BikeComputer program. When you start up the board, the bootloader should now jump to the first image slot and the BikeComputer program should start up as normal.

Signing the BikeComputer Program and Provide the Key to the Bootloader application

An important aspect of the image update process is that images uploaded to the device must be signed. If this is not the case, and if the MCUboot is configured to verify the image, the MCUboot application will reject the candidate images, as it needs to verify their authenticity. To this end, the bootloader must embed the key used to sign the application.

In the previous steps, the MCUboot application was instructed to use the default key and the BikeComputer program was signed with this key. However, each program should use its own generated key.

The MCUboot project comes with the “bootloader/mcuboot/scripts/imgutil.py” script, which allows you to, among other things, generate a pair of keys, extract the public key, and generate the “.c” file containing the public key that will be integrated into the bootloader program. It is therefore necessary to:

  • First install the dependencies listed in “deps/bootloader/mcuboot/scripts/ requirements.txt” using the command pip install -r requirements.txt.

  • Then generate a key pair using python deps/bootloader/mcuboot/scripts/imgtool.py keygen -k mykey.pem -t rsa-2048. The file “mykey.pem” contains the key pair that will be used for signing the application.

Once the key has been generated, you must instruct the MCUboot application to embed it and sign the BikeComputer program with it. This can be done as follows:

  • Modify the “prj_bootloader.conf” configuration file by adding:
    {workspace}/prj_bootloader.conf
    CONFIG_BOOT_SIGNATURE_KEY_FILE="full path to private_key_rsa-2048.pem"
    
  • Build the MCUboot application and flash it. The BikeComputer program installed in the primary slot should now be detected as invalid!

  • Modify the BikeComputer prj.conf configuration file by changing the path to the key file. Build and flash the application. It should then be recognized as valid again and it should start up normally.

The MCUboot and BikeComputer program are now ready to receive updates. The next sections will cover two different methods of updating a Zephyr RTOS program.

Update the BikeComputer Program using Serial Recovery

One way to update a Zephyr RTOS program is to download the software update from the bootloader itself, as shown in the diagram below:

DFU from the Bootloader

DFU from the Bootloader

(source: https://academy.nordicsemi.com/courses/nrf-connect-sdk-intermediate/lessons/lesson-9-bootloaders-and-dfu-fota/topic/device-firmware-update-dfu-essentials/)

To implement this solution, the download will be executed from the bootloader application via UART. The setup is shown below:

DFU from the Bootloader Setup

DFU from the Bootloader Setup

(source: https://academy.nordicsemi.com/courses/nrf-connect-sdk-intermediate/lessons/lesson-9-bootloaders-and-dfu-fota/topic/exercise-1-dfu-over-uart/)

The application itself does not require any changes, but the bootloader application does. These changes are implemented by using another prj.conf file for the MCUboot application:

  • First copy the existing “prj_bootloader.conf” file created in the previous step and rename it “prj_serial_recovery.conf”. Then, apply the following changes.

  • Enable Serial Recovery over UART:

    {workspace}/prj_serial_recovery.conf
    CONFIG_MCUBOOT_SERIAL=y
    CONFIG_BOOT_SERIAL_UART=y 
    

  • Disable console UART, since Serial Recovery uses UART:

    {workspace}/prj_serial_recovery.conf
    CONFIG_UART_CONSOLE=n
    

  • Since with this configuration, the application is updated in place, only one slot is required to store the application. Configure the bootloader to use a single slot:

    {workspace}/prj_serial_recovery.conf
    CONFIG_SINGLE_APPLICATION_SLOT=y
    

  • Configure LED indication when Serial Recovery mode is active:

    {workspace}/prj_serial_recovery.conf
    CONFIG_MCUBOOT_INDICATION_LED=y
    

Before building the MCUboot application, you need to add the zcbor dependency to the west.yml file (right below the mbedtls dependency) and run west update. You can then build the MCUboot application using west build deps/bootloader/mcuboot/boot/zephyr --pristine --extra-conf "full path to prj_serial_recovery.conf" It should start and execute as before. As the board is configured for the use of the MCUboot application in Serial Recovery mode, it is ready for testing.

Upload a Firmware Update

We use a desktop tool called AuTerm to send firmware updates via UART. Other tools can also be used, and alternative tools are presented here.

Read the instructions for downloading and installing the AuTerm tool. Once ready, launch AuTerm and follow the instructions below to update your BikeComputer program using Serial Recovery:

  • Reset the board while pressing Button1. It should restart but instead of jumping to the BikeComputer program, it should wait for an update to be receive. LED1 should turn on.
  • Close any open UART connections to the board.
  • In AuTerm, configure the COM port to use under the “Config” tab and press “Open”. The connection should be established.
  • In AuTerm, select the “MCUmgr” tab. Under “Images”, select “Get” and press “Go”. You should see that the board is configured with one slot (“Slot 0”).
  • Modify your “BikeComputer” program so that a difference in logging appears at application start. Build the program without flashing it.
  • In AuTerm, select the “MCUmgr” tab. Under “Upload”, select the “build/zephyr/zephyr.signed.bin” file, check the “Reset after upload” box and press “Go” to start the upload. Upload should start and after it is finished, the board should reset and start again.
  • In AuTerm, select the “Terminal” tab. You should see that your updated program has been successfully installed and started on the board.

Intermediate Wrap-up

Before moving to the next section, make sure that you have accomplished the following:

  • You have modified the MCUboot application configuration, built and flashed it. Upon reset, the board launches the MCUboot application and successfully jumps to the BikeComputer program
  • You have understood how the memory model of your board has been modified to use a MCUboot bootloader application.
  • You have performed a firmware update download using Serial Recovery successfully and observed that the updated BikeComputer program launches successfully.

Update the BikeComputer Program over UART from the Application

In the previous section, we implemented a software update using Serial Recovery by utilising DFU over UART with the onboard UART-to-USB converter available on the development kit. However, incorporating a UART-to-USB converter into an embedded system is not usually practical, as this increases the bill of materials, power consumption, and complexity.

In this section, we implement a different mechanism that does not require an onboard UART-to-USB converter but rather makes uses of the USB MCU interface. This setup is illustrated in the diagram below:

DFU from the Application Setup

DFU from the Application Setup

(source: https://academy.nordicsemi.com/courses/nrf-connect-sdk-intermediate/lessons/lesson-9-bootloaders-and-dfu-fota/topic/exercise-4-dfu-over-usb/)

Using this mechanism with a SoC that comes with a built-in USB peripheral eliminates the need for additional hardware, thus reducing the cost and complexity of a system that uses wired DFU.

In order to implement this mechanism, you will first need to build and flash the MCUboot bootloader application without the additional configuration file. This enables logging in the bootloader again and allows updates with two slots. Once you have flashed the updated bootloader and reset the board, you should see the following log in the console:

console
I: Starting bootloader
I: Primary image: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
I: Secondary image: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
I: Boot source: none
I: Image index: 0, Swap type: none
I: Bootloader chainload address offset: 0x10000
I: Image version: v1.0.1
I: Jumping to the first image slot
The next section explains how to interpret this log.

Understand MCUboot Logging

It is important to understand the logging information provided by the MCUboot application in order to understand the update mechanism. The log contains the following information:

  • Information about images:

       I: Primary image: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
       I: Secondary image: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
    

    • The possible values for magic are BOOT_MAGIC_GOOD (1), BOOT_MAGIC_BAD (2) or BOOT_MAGIC_UNSET (3).

    • The possible values for swap_type are

Value Symbol Description
1 BOOT_SWAP_TYPE_NONE Attempt to boot the contents of the primary slot.
2 BOOT_SWAP_TYPE_TEST Swap to the secondary slot.
Absent a confirm command, revert back on next boot
3 BOOT_SWAP_TYPE_PERM Swap to the secondary slot,
and permanently switch to booting its contents.
4 BOOT_SWAP_TYPE_REVERT Swap back to alternate slot.
A confirm changes this state to NONE.
5 BOOT_SWAP_TYPE_FAIL Swap failed because image to be run
is not valid.
0xFF BOOT_SWAP_TYPE_PANIC Swapping encountered an unrecoverable error
* The possible values for `copy_done` and `image_ok` are `BOOT_FLAG_SET` (`1`), `BOOT_FLAG_BAD` (`2`) or `BOOT_FLAG_UNSET` (`3`).
  • Information about the Boot source. It is set to none, unless the magic of the primary slot is BOOT_MAGIC_GOOD, the copy_done of the primary slot is BOOT_FLAG_UNSET and the magic of the secondary slot is not BOOT_MAGIC_GOOD, in which case the Boot source is the primary slot.

  • The other information can be deduced from the preceding ones.

After flashing the BikeComputer program, the bootloader will simply attempt to jump to the application in the primary slot.

** WORK IN PROGRESS**

Understand logging at start (no swap -> jump to the first image slot)

  • Modify the BikeComputer program for enabling upload from the Application
  • Understand logging at start (two )

  • Logging at start: Boot source: none means that no candidate is present for swaping Swap type /** Attempt to boot the contents of the primary slot. / BOOT_SWAP_TYPE_NONE = 1 /*

  • Swap to the secondary slot.
  • Absent a confirm command, revert back on next boot. */

define BOOT_SWAP_TYPE_TEST 2

/** * Swap to the secondary slot, * and permanently switch to booting its contents. */

define BOOT_SWAP_TYPE_PERM 3

/** Swap back to alternate slot. A confirm changes this state to NONE. */

define BOOT_SWAP_TYPE_REVERT 4

/** Swap failed because image to be run is not valid */

define BOOT_SWAP_TYPE_FAIL 5

/** Swapping encountered an unrecoverable error */

define BOOT_SWAP_TYPE_PANIC 0xff

Copy done / Image ok

define BOOT_FLAG_SET 1

define BOOT_FLAG_BAD 2

define BOOT_FLAG_UNSET 3

Final Wrap-up

For obtaining a fully functional bootloading process, make sure that you have accomplished the following:

  • You have added the code for checking the availability of new firmware candidates in the bootloader application.
  • You have added the code for installing a new valid firmware candidate.
  • You have validated the entire process and that you can thus update your BikeComputer application over a serial connection using an update client unning in the BikeComputer program itself.

Note

The implementation of the bootloader process should be more resilient to errors like the failure to install a new firmware. As already mentioned, it should also protect itself for not exceeding storage spaces and against attacks by authenticating the applications binaries.

Also, like any embedded system program, the BikeComputer program should integrate a watchdog mechanism for protecting itself from being locked (in this case in the download process).

These improvements are beyond the scope of this codelab and lesson.