-
Notifications
You must be signed in to change notification settings - Fork 0
Home
In the embedded world, firmware update is an essential and extensive subject that has been on the rise. Efficiency and security have never been more discussed. As IoT solutions grow exponentially in numbers and complexity so does the concern to make the devices secure and updatable (in field!) in an efficient way.
Not so long ago, MCUboot surfaced as an open source bootloader project for small and low cost systems, that intended to simplify and standardize a solution for the aforementioned problems. It started as an Apache Mynewt subproject when the developers decided to separate the bootloader from OS development. Later it was ported for Zephyr RTOS and became its default bootloader.
Espressif Systems has been expanding its support for other RTOSes like Zephyr and NuttX as attractive options for use in its SoCs, and now this interesting bootloader alternative is being developed. Recently a port for Espressif SoCs has been added to MCUboot project and a basic support for the largely used ESP32 is available.
This guide instructs how to start with MCUboot in your ESP32-based project, presents the environment setup, the additional required configuration in the application side, and how to build and flash the device. The ESP32 DevKitC board was used to prepare this guide.
As previously mentioned, MCUboot is an open source secure bootloader for 32 bit microcontrollers. The project defines a common infrastructure for a secure boot and its architecture covers:
- System flash layout: the flash organization is well defined, using the concept of slots for placing the main bootable image and incoming image in different flash regions and a scratch area to help the swapping process when updating the firmware.
- Easy and secure image update: it is a simple process as whichever is the updating agent it only needs to correctly sign and place the application image in the right slot. MCUboot will handle the swap between the older and new image, and their security (integrity, authenticity and confidentiality).
- Security: MCUboot enables the secure image boot and update using:
- Image integrity through hash checking (SHA256).
- Source authenticity through signature validation using asymmetric key algorithms like RSA 2048 and 3072, ECDSA and ed25519.
- Image data confidentiality (encryption/decryption) while in transport and/or while being stored on external flash. MCUboot has support for encrypting/decrypting images on-the-fly while updating and uses AES or ECIES algorithms.
- Fault tolerance: the swapping mechanism enables recovering/reverting if a problem occurs, like a reset in the middle of a swap when updating. Also, if for some reason a new image has been updated and started but didn't signal itself as ok, MCUboot has a rollback mechanism to revert to the original image, once it is kept after swap.
The project aims to standardize those points in a comprehensive way. MCUboot is also independent from an operating system and hardware. It relies on hardware porting layers from the target host OS.
Let's take a look on a high-level overview of the boot process:
Important things to notice:
- Primary slot is where the main bootable image resides, code always runs from there.
- Secondary slot is used for updates. It stores the incoming image and, after the swap, it stores the original image ensuring the action can be reverted if there is any problem.
- Scratch region is used to help image swap when updating.
- A header and trailer are added to the image to track general information, swapping and update states.
See more at official MCUboot page
Firstly, we need to prepare the development environment. This guide assumes the use of Linux (Ubuntu 20.04.2 LTS).
In addition, make sure that you have Git, Python3, pip and CMake installed. If you haven't, you can run the following (this step is optional):
sudo apt update
sudo apt upgrade
sudo apt install git python3 python3-pip cmake ninja-build
- Clone the repository:
git clone -b feature/esp32s2_support https://github.com/almir-okato/mcuboot.git
- Install the additional dependencies needed by MCUboot:
cd mcuboot
pip3 install -r scripts/requirements.txt
- Update the submodules needed by the Espressif port. This may take a while since it will retrieve the ESP-IDF that contains the HAL for ESP32 and the toolchain used for compiling. Next, get the mbedtls submodule required by MCUboot.
git submodule update --init --recursive boot/espressif/hal/esp-idf
git submodule update --init --recursive ext/mbedtls
- Now we need to install IDF dependencies and set environment variables. This step may take some time:
-
Note: If you've already installed IDF at some point, check the toolchain in your system. You can run
ls ~/.espressif/tools/xtensa-esp32-elf/
to check if you already have theesp-2020r3-8.4.0
directory. If you don't have any, go ahead and run the commands below. If you have any other version (e.g. esp-2021r1-8.4.0), you'll need to move its directory to some other place in order to let the compatible one to be installed by the commands below.
cd boot/espressif/hal/esp-idf
./install.sh
. ./export.sh
cd ../..
As we have everything set, let's build our bootloader.
- Compile and generate the ELF:
cmake -DCMAKE_TOOLCHAIN_FILE=tools/toolchain-esp32.cmake -DMCUBOOT_TARGET=esp32 -B build -GNinja
cmake --build build/
- Convert the ELF to the final bootloader image, ready to be flashed:
esptool.py --chip esp32 elf2image --flash_mode dio --flash_freq 40m -o build/mcuboot_esp32.bin build/mcuboot_esp32.elf
- Finally, flash MCUboot in your board:
esptool.py -p /dev/ttyUSB0 -b 2000000 --before default_reset --after hard_reset --chip esp32 write_flash --flash_mode dio --flash_size detect --flash_freq 40m 0x1000 build/mcuboot_esp32.bin
You may adjust the USB port (-p /dev/ttyUSB0
) and baud rate (-b 2000000
) according to the connection with your board.
We can check the serial monitor on the UART port (same we use to flash):
idf_monitor.py -d --port /dev/ttyUSB0 build/mcuboot_esp32.elf
As we still have not flashed any image yet, we will see the following logging:
For any images that you may use with MCUboot, you'll need to sign them, and imgtool
is used for that. It is a tool that adds the MCUboot expected headers and trailers to the binary, signs the firmware and it can also generate the keys to be used. imgtool
can be found at <MCUBOOT_DIR>/scripts/imgtool.py
(<MCUBOOT_DIR> is the path for the cloned repository), or you can install as following (optionally):
pip3 install imgtool
MCUboot already has some default example keys. It is crucial that you never use these default keys in production since the private key is available in the MCUboot repository. You can find how to generate and manage keys at imgtool
I'm leaving two example applications to test with MCUboot, one from Zephyr RTOS and other for NuttX. You can also build from the scratch, just make sure to enable MCUboot compatibility in whichever RTOS you may use.
- Zephyr's Hello World app: zephyr.bin
- NuttX's nsh: nuttx.bin
Beforehand, we will need to sign it.
We'll use the imgtool
. As you may note below, we will use the one from the MCUboot directory. You can also use the one installed through pip instead. Replace "<app.bin>" with "nuttx.bin" or "zephyr.bin" depending on your choice, that is the source image and signed.bin
is the output signed image:
../../scripts/imgtool.py sign --align 4 -v 0 -H 32 --pad-header -S 0x00100000 <app.bin> signed.bin
Alternatively, if you installed imgtool through pip:
imgtool sign --align 4 -v 0 -H 32 --pad-header -S 0x00100000 <app.bin> signed.bin
Here is a quick look on what the imgtool sign action and its parameters are doing:
-
--align 4
: Specify the alignment of the flash device as 4 bytes (32 bit word). -
-v 0
: Specify the image version, in this case it is '0'. -
-H 32
: Specify the MCUboot header size to be added to the image binary. -
--pad-header
: Indicates that the MCUboot header needs to be explicitly added to the binary by the tool (the Zephyr build for some platforms already pad the binary beginning with 0s and may not need this parameter). -
-S 0x00100000
: Indicates the slot size, so the tool can add the trailer properly.
We are not dealing with updates yet. If that is your case, there are other required parameters for that. They will be covered in the next part of this series.
The next step is flashing into the device:
esptool.py -p /dev/ttyUSB0 -b 2000000 --before default_reset --after hard_reset --chip esp32 write_flash --flash_mode dio --flash_size detect --flash_freq 40m 0x10000 signed.bin
Checking the serial monitor we can finally see:
-
If you chose
zephyr.bin
: -
If you chose
nuttx.bin
:
MCUboot has successfully loaded the example application from the primary slot. Note that we manually flashed the image in the address 0x10000, which is the expected address for the primary slot by the bootloader. We'll see a little bit about the flash organization in the next section.
MCUboot defines a flash organization and a flash area can contain multiple executable images depending on its boot and update configuration. Each image area contains two image slots: a primary and a secondary, and by default the bootloader only runs an image from the primary slot. The secondary slot is where an incoming image is staged prior to being installed, then its content will be either swapped to the primary slot or will overwrite it when updating. Therefore, we can identify four types of flash areas in the layout:
AREA | ID | Description |
---|---|---|
FLASH_AREA_BOOTLOADER | 0 | This is the flash area for the Bootloader itself |
FLASH_AREA_IMAGE_PRIMARY(0) | 1 | Primary slot for the first executable image |
FLASH_AREA_IMAGE_SECONDARY(0) | 2 | Secondary slot for the first executable image |
FLASH_AREA_IMAGE_SCRATCH | 3 | This area is required to allow reliable image swapping and must have a size that is enough to store at least the largest sector to be swapped |
MCUboot also supports using multiple images and we may define another image areas, which will have a primary and a secondary slot as well. The current Espressif port, however, supports only one flash image area with its two slots, so the flash has been partitioned accordingly:
AREA | ADDRESS | SIZE |
---|---|---|
BOOTLOADER | 0x1000 | 0xF000 |
PRIMARY SLOT | 0x10000 | 0x100000 |
SECONDARY SLOT | 0x110000 | 0x100000 |
SCRATCH | 0x210000 | 0x40000 |
The information about the layout is placed at bootloader.conf
file from Espressif port. The addresses and sizes can be modified, though the following rules must be followed:
- Bootloader address must be kept since its where esp32 jumps to after reset by default.
- None of the slots must overlap.
- Primary and Secondary slots must be the same size.
- Scratch area must have a size that is enough to store at least the largest sector that is going to be swapped, so it should be the flash device's largest sector.
- The application and update agent must be aware of this layout so everything can be correctly placed.
In our example, 0x10000 is the address for the primary slot, from where the bootloader will boot the image. If we want to update the device, the update agent must be aware of the flash layout, sign the new image and place it at the secondary slot address. Note that the signing will be a bit different from this example. More about image update may be covered in the future.
MCUboot provides a solid structure and defines a standard flow for firmware update and secure boot. Therefore, since these features are already implemented as part of the bootloader, they can be easily enabled without many modifications when developing a firmware.
Furthermore, it is an open source project, which brings all the advantages of being develop by a common interested, but heterogeneous, community, like fast response and solution for issues, and engaged development.
We saw in this guide how to build MCUboot bootloader for ESP32, how to sign an image and also how the flash should be organized. The next step is to understand how updates work in the MCUboot and use this feature appropriately.