MCUboot is a bootloader for microcontrollers. It has a fairly long history and has been under development for a number of years. Over the years, it has gained some significant functionality, and is used as the bootloader for a number of projects (TF-M and Zephyr coming to mind).
MCUboot is somewhat unique as an embedded application in that it tends to be fairly isolated. There are a lot of other projects that make use of MCUboot itself, but MCUboot itself does not have a large number of dependencies. It needs the ability to run as an early application on the target, operate on the flash device, and perform some cryptographic operations. It optionally can support some types of logging to help with debugging and development.
The code is mostly written in C, and makes use of several possibly platforms for board support, including Zephyr, MyNewt, as well as a couple of board-specific SDKs for bare-metal. Although there are some ifdefs throughout the code to support this, the code largely is indepent of the platform it runs on.
In addition to these targets, the project also contains a simulator, written in Rust, that links against the C code and stress tests the code, especially simulating bad powerdowns that would be difficult to recreate on real hardware.
Because of this somewhat isolated nature of MCUboot, it seems like it would be an almost ideal application to test the waters, as it were, of rust-embedded.
I have developed the beginnings of mcuboot-rs to do just this. The README there gives a better summary of the status, but suffice it to say that this is just a starting point, and only scratches the surface of what MCUboot does. But, it is still a good example of what can be done with rust-embedded.
I spend some of my time working on Zephyr. Zephyr is a fairly traditional type of real-time embedded OS (RTOS). It is a fairly large body of software that provides most of the functionality needed to develop an embedded application, including scheduling, board support and device drivers.
The rust-embedded project takes a bit of a different approach. Instead of a single entity that provides everything, the needed functionality is provided by various crates. Because the bootloader does not need threading or scheduling, I have not needed to pull in any kind of scheduler or executor. For my case, I pull in just a few crates into this project:
lpc55-hal. This is a hardware abstraction layer provider for the LPC55S69 board that I am starting with. It makes use of a “PAC” crate that contains the actual register definitions for the hardware, and provides a friendlier set of routines for accessing the devices. There is ongoing work to have the various HALs comply with an
embedded-halcrate. Because I am only starting with a single board, this generalization isn’t yet useful to me.
cortex-m-rt. These provide support for running bare-metal on various Cortex-M processors. I mainly need to directly reference these to make use of a routine to chain boot into another image.
embedded-storage. This is a set of traits that a hal can implement to provide device drivers for flash devices. I have built my main boot code around this abstraction to avoid tying it to any particular device.
There are two parts of the bootloader code, currently:
boot. This is a crate that doesn’t know anything about the hardware or environment it is running on. In fact, it builds just fine on a Linux machine, where
cargo testis able to run the unit tests from it. Being able to develop the core of the code as if it were just a regular Linux program is very convenient. Some care does have to be taken so that it can build and run in the
no_stdenvironment needed on the target.
boards/lpc55s69. This is the
mainof the program. Ideally, it will be rather small, and only contain the code that is specific to a given board. For this prototype, it is a bit larget. I am not using flash drivers, yet, since the code only reads from the flash, so a faked driver is part of this crate as well.
This configuration makes a fairly small bootloader that can verify the SHA256 of images, and chain boot into the application.
There is definitely a lot more work that would need to be done to make something that had the functionality of mcuboot. However, this is a really good start, and demonstrates some of the real benefits to developing this type of application in Rust.
Rust is very good at allowing abstractions. A signifant mechanism it uses is
that of a “trait”. A trait defines a set of methods, as well as associated types
and constants, that code be used as an interface. It can be thought of similarly
to an interface in Java. In the case of the bootloader, the interface to a flash
device is represented by the
trait, which merely requires a few things to be implemented:
READ_SIZEis an associated constant that gives the minimum size the read command supports.
capacityreturns the size of the area.
readdoes the actual read.
In addition, the trait inherits on
ErrorType which requires there to be an
error type. This error type has a query method
kind() which allows the code to
be generally used, in code that doesn’t have to know the specific error types
used in a given flash driver.
In addition to this, I have also added my own trait
that is supported by flash memories that are directly mapped into memory. For
many MCUboot targets, the primary flash is also where code is run from (execute
in place, or XIP). These drivers can support this trait to indicate how, given a
flash area, what is he base address in memory where that data will appear. It is
important, however, that boot not make use of this, but instead use the
abstractions provided by the
ReadNorFlash. Fortunately, Rust happily enforces
this for any code that does not directly state the dependency on
Later, I will make use of
which adds erase and write support.
It is easy to get a bit spoiled by Zephyr’s almost magical target and board
indepence. Much of the hardware is abstracted away, and MCUboot running on
Zephyr really has very little need to know abuot hardware specifics.
Rust-embedded, on the other hand, requires me to have a
main crate that knows
intimately about a particular platform. The general idea is to keep these small,
only performing the necessary initialization, and have the general boot code in
a crate that is completely hardware independent. It is definitely different, and
from the perspective of how most embedded work is done, where a focus is made on
a specific environment, this works well. The advantage is that not only is
boot not dependent on the hardware, but it is independent of even the OS or
platform the code might be running on.
Where to go from here
I believe this proof of concept demonstrates that Rust is a viable language and ecosystem to write something like mcuboot. To continue this work, there are multiple areas work can be done:
- Increase board support. By adding at least one more bare-metal board, it will become more clear what can be abstracted away, and made board independent, and what really needs to be board-specific.
- More features from mcuboot. Some of the features of mcuboot do depend, not
directly on the board, but on the capabilities of the board. This will
likely become features within the
bootcrate. Some ideas of improvements to make:
- Check signed images. This is a core feature of mcuboot, and fairly
straightfoward to do in Rust, as the needed cryptography libraries are
available in the
- Upgrade. This is also an important feature of MCUboot. There are several upgrade strategies, and this is also a good environment to experiment with additional strategies. One of the reasons I chose the LPC board is because it is not currently well supported by MCUboot, and this is a good place to experiment with upgrade algorithms that support this target better.
- Check signed images. This is a core feature of mcuboot, and fairly straightfoward to do in Rust, as the needed cryptography libraries are available in the
- Platforms other than bare-metal. There is some interest in allowing Rust development on top of a more classic RTOS, such as Zephyr. This would provide a useful application to develop this support on.