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
- Zephyr Development Environment for developing and debugging your program in C++.
- The Memory Profiling and Optimization codelab is a prerequisite for this codelab.
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:
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.ymlfile: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
# 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 beforemainmenu "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
with
if(DEFINED CONFIG_MBEDTLS) zephyr_library_include_directories( ${ZEPHYR_MBEDTLS_MODULE_DIR}/include ) endif()For this change, make sure that you defined the environment variableif(DEFINED CONFIG_MBEDTLS) zephyr_library_include_directories( ${ZEPHYR_BASE}/../modules/crypto/mbedtls/include ) endif()ZEPHYR_BASEcorrectly. -
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.confCONFIG_BOOTLOADER_MCUBOOT=y -
Instruct
west buildto sign the image with the appropriate default key file:bike_computer/prj.confAt this stage, the default MCUboot signing key file is used.CONFIG_MCUBOOT_SIGNATURE_KEY_FILE="{workspace_folder}/deps/bootloader/mcuboot/root-rsa-2048.pem" -
Add versioning for signing the image:
bike_computer/prj.confCONFIG_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
BikeComputerprogram installed in the primary slot should now be detected as invalid! -
Modify the
BikeComputerprj.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:
To implement this solution, the download will be executed from the bootloader application via UART. The setup is shown below:
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.confCONFIG_MCUBOOT_SERIAL=y CONFIG_BOOT_SERIAL_UART=y -
Disable console UART, since Serial Recovery uses UART:
{workspace}/prj_serial_recovery.confCONFIG_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.confCONFIG_SINGLE_APPLICATION_SLOT=y -
Configure LED indication when Serial Recovery mode is active:
{workspace}/prj_serial_recovery.confCONFIG_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
BikeComputerprogram, 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
BikeComputerprogram - 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
BikeComputerprogram 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:
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:
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
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
magicareBOOT_MAGIC_GOOD(1),BOOT_MAGIC_BAD(2) orBOOT_MAGIC_UNSET(3). -
The possible values for
swap_typeare
-
| 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
magicof the primary slot isBOOT_MAGIC_GOOD, thecopy_doneof the primary slot isBOOT_FLAG_UNSETand the magic of the secondary slot is notBOOT_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.