Part 2: Embedded Rust Ecosystem and Essential Crates
Introduction
In the first part of this series, we discussed the advantages of using Rust for embedded systems development. Now, let's delve into the current state of the embedded Rust ecosystem. The Rust ecosystem for embedded systems is already very rich and still growing, offering a variety of tools and libraries (called "crates") that facilitate embedded development. This is one of the main reasons that embedded Rust development is so much fun!
This second part of the series provided an overview of how the embedded Rust ecosystem is organized, some essential crates and useful tools for embedded Rust programming.
Terminology
There are some important terms you will come across as you use Rust for embedded development. If you are already familiar with these, you may skip this list.
- PAC: Peripheral Access Crate. These provide (usually safe) access to a specific microcontroller's memory-mapped peripherals.
- HAL: Hardware Abstraction Layer. The HAL provides ergonomic access to common functionality like peripheral setup. It often uses a PAC under the hood to do so.
- BSC/BSP: Board Support Crate/Package. Even more ergonomic than a HAL, these crates are tailored to a specific (development) board you may buy for your microcontroller.
- MCU: Microcontroller Unit. Often referred to as MC or µC. That is, a single microchip containing CPU, memory and peripherals.
- MMIO: Memory-mapped Input/Output. The same address space is used to access both the memory and I/O devices like I2C, SPI etc.
- std: Standard Library. Usually referring to Rust's standard library. When programming for bare-metal, i.e., a computer without an operating system, you will have to live without its convenient functionality. Rust marks such programs as
no_std
. - SPI: Serial Peripheral Interface. A common communication standard with 4 signals.
- I2C: Inter-Integrated Circuit. Pronounced "eye-two-see" or "eye-squared-see". Another communication standard with a bus architecture and only two signals.
- CAN: Controller Area Network. Yet another communication standard, often used for automotive applications. It is a bus with two differential signals.
Ecosystem Overview
The Rust embedded ecosystem consists of two parts: official and unofficial crates.
The official ones are maintained by the Rust Embedded Working Group. It is a community-driven initiative aimed at making Rust a great choice for embedded development. This group maintains a list of key crates, provides guidelines and ensures that the ecosystem evolves in a cohesive and integrated manner. It also standardizes commonly used interfaces to allow for platform-agnostic driver development. The most commonly used crate providing such interfaces is the embedded-hal and its associated crates.
The unofficial ones are developed by kind people around the world that share their libraries, drivers and tools with other users. Their maintenance depends on the individual developer's time and capacity and there are no guarantees about long-term support provided for any of them. There are some guarantees for most of them, e.g., "guaranteed to compile with a compiler version X or newer". Still, they make up for the largest part of the ecosystem and most of them are in a very good shape.
Core Crates
Several crates form the foundation of embedded Rust programming. Some of them are inherent to Rust and some are developed and maintained by the Rust Embedded Working Group. These crates include but are not limited to:
core
: The minimal subset of Rust's standard library, which is essential forno_std
(i.e., not the regular Rust standard library) environments commonly used in embedded systems.alloc
: Provides collection types likeVec
,String
andBox
for heap-allocated data structures inno_std
contexts. Using alloc requires you to define an allocator and reserve memory for the heap.embedded-hal
: The Hardware Abstraction Layer (HAL) for embedded systems, defining traits for common hardware interfaces such as GPIO, SPI, I2C, and serial communication.
When compiling bare-metal programs, there is no operating system available that provides many features of the standard library. Therefore, the Rust standard library is not available in bare-metal programs. If you are running embedded Linux, however, you might be able to use it.
cortex-m and cortex-m-rt
For the widespread ARM Cortex-M microcontrollers, there are two essential crates for low-level interaction.
cortex-m
: This crate provides low-level access to ARM Cortex-M microcontrollers, including register manipulation and interrupt/exception handling.cortex-m-rt
: A runtime crate that sets up the vector table, initializes static variables and provides a minimal startup code for Cortex-M microcontrollers.
Usually, the HAL will make use of them, and you do not often have to use them directly.
For example, use the cortex-m
to set a Systick timer:
use SystClkSource;
use Peripherals;
let p = take.unwrap;
let mut syst = p.SYST;
// Configure the system timer to trigger a SysTick exception every second
syst.set_clock_source;
syst.set_reload; // Assume 16 MHz clock
syst.enable_counter;
syst.enable_interrupt;
With the cortex-m-rt
crate, you can define the program entry point:
use entry;
!
Note: cortex-m-rt
also allows you to define a #[pre_init]
function that will be called at the beginning of the reset handler. For details, please refer to the crate documentation.
embedded-hal
The previously mentioned embedded-hal
(HAL = Hardware Abstraction Layer) crate defines a set of traits that serve as a common interface for various hardware components like digital I/O pins, I2C, SPI, and more. This allows for writing device drivers that are platform-agnostic.
For example, implementing a driver for an I2C sensor might look like this:
// Original Source: https://docs.rs/embedded-hal/1.0.0/embedded_hal/i2c/index.html
use ;
const ADDR: u8 = 0x15;
// Generic type: I2C
// Implementation constrains the generic I2C type to implement embedded-hal's I2c trait
Now you can use this driver on any platform that has an implementation of embedded-hal
's I2c
trait. Usually, the HAL will provide such an implementation.
The embedded-hal
project contains many more abstraction layer crates like embedded-can
for CAN, embedded-hal-async
for async interfaces, embedded-hal-bus
for sharing peripherals like SPI and I2C among multiple drivers and embedded-io
for a no_std
alternative to std::io
.
Embassy
Embassy (name: Embedded + Async) is a modern Rust framework for embedded systems. It provides multiple crates ranging from HALs for STM32, nRF or Raspberry Pi controllers to timer abstractions, async executors and synchronization primitives. It leverages Rust's async/await syntax to provide efficient, non-blocking I/O operations.
The following is a non-exhaustive list of its most important crates:
embassy-executor
: A no-alloc (i.e., no heap requires) async/await executor for embedded development. It can be used with other HALs that are not part of the embassy project and even lets you assign different priorities to your tasks.embassy-net
: Ano_std
and no-alloc network stack featuring Ethernet, IPv4/6, TCP/UDP, DNS and DHCP. It makes creating network applications on platforms like ESP32, Raspberry Pi Pico W or STM32 very convenient.embassy-sync
: Synchronization primitives and data structures that can be used with async/await. It provides implementations of Channels, Signals, Mutexes and others.embassy-time
: A crate for timekeeping, delays and timeouts. It provides the time since boot, Instants and Durations as well as convenient ways of delaying the program. Integrates very well with embassy HALs for different platforms.embassy-stm32
: The probably most daring endeavor anyone has ever tried on embedded Rust: a single HAL for all STM32 microcontroller variants. It uses metadata from different sources to automatically generate a HAL for almost all STM32 MCUs. This crate has saved us tons of work in past projects, and we are happy to have contributed some of our additions back to this open-source project. We highly recommend using the version from GitHub instead of crates.io because this crate is under very active development, and you do not want to miss out on any of the new features. Also make sure to look up documentation at https://docs.embassy.dev/ as you can choose documentation for your specific microcontroller there.
You have already used some of these crates in the example project in the first part of this series.
defmt
defmt
(name: "deferred formatting") is an efficient logging framework designed for resource-constrained environments. It provides a way to log messages with minimal overhead, both in terms of memory and CPU usage.
This is done by deferring the string formatting to the host PC instead of doing it on the microcontroller.
Moreover, string literals like in info!("Init complete")
are not transmitted to the host. Instead, a table of string literals is built at compile-time and the MCU only sends a table index to the host, not the entire string.
Logging a message over the Real-time Transfer (RTT) interface using defmt
looks like this:
use defmt_rtt as _; // global logger
!;
info
RTT, invented by SEGGER Microcontroller, employs a ring buffer and dedicated memory locations to facilitate efficient data communication between a host PC and an embedded target system via JTAG or SWD interfaces. This setup ensures minimal latency and continuous data flow, enhancing real-time data logging and debugging capabilities without interrupting the target system's execution.
probe-rs
probe-rs
, which we have already used in the first part of this series, is a modern, open-source tool for flashing and debugging embedded devices. It supports a wide range of microcontrollers and provides a unified interface for development and debugging.
You can either use cargo to build the program, and probe-rs for flashing and running:
where the argument for --chip
needs to be adapted to your specific chip (probe-rs chip list
gives you all available options) and the target subfolder depends on your compilation target.
For details on available targets and the nomenclature see https://doc.rust-lang.org/rustc/platform-support/arm-none-eabi.html.
Or you can define a custom "runner" in your cargo config in <project root>/.cargo/config.toml
:
[]
# replace STM32F469NIHx with your chip as listed in `probe-rs chip list`
= "probe-rs run --chip STM32F469NIHx"
and then just do cargo run
to compile, flash and run the firmware in a single step. That's as convenient as it gets in embedded software development.
Other crates
Describing every crate that may be helpful for your embedded project would be impossible. However, there are a few more crates that you might want to check out (no particular order):
- rtic (concurrency framework for real-time systems)
- serde (the Rust serialization framework), especially together with postcard
- heapless
- panic-probe
- static-cell
- chrono
- fixed
- nusb
- smoltcp
- bitflags
- zerocopy
- embedded-graphics
Current Challenges and Areas for Improvement
While the embedded Rust ecosystem is already very robust and growing, there are still some challenges and areas that need improvement:
Documentation
Although Rust has excellent documentation, the resources specific to embedded development could be more comprehensive and detailed. Tutorials, guides and example projects are sometimes outdated because the used crates have seen a lot of new development in the meantime. To keep up with the rapid evolution of embedded Rust, more up-to-date and thorough documentation is essential. This would help both new and experienced developers to quickly adapt to the latest changes and best practices in the ecosystem.
Tooling
While tools like probe-rs
and defmt
are incredibly useful, there is always room for improvement in terms of stability, features and user experience.
Enhancements in debugging tools, better integration with existing IDEs and more robust error reporting would significantly streamline the development process.
Additionally, expanding the capabilities of these tools to support a wider range of microcontrollers and development boards would make embedded Rust accessible to a broader audience.
Display Abstraction
There is a notable absence of a comprehensive display abstraction crate. Efforts are underway to develop such a crate, which will significantly enhance the development of applications involving graphical displays. A unified display abstraction layer would simplify the process of interfacing with different types of displays and make it easier to develop portable graphical applications. For more information, see the not-yet-awesome-embedded-rust list on GitHub. Spoiler: Systemscape is working on a first draft of such an abstraction layer. Stay tuned for more information on this.
Ecosystem Maturity
The ecosystem around embedded Rust is still maturing. While there are many high-quality crates available, some areas lack the polish and stability found in more established languages. Continued efforts to stabilize and mature critical libraries, improve documentation and provide long-term support for key crates will help solidify Rust's position as a leading language for embedded systems development.
Integration with Existing Systems
Integrating Rust into existing embedded systems that are traditionally developed in C or C++ can be challenging. Tools and guides that help developers gradually introduce Rust into their workflows, along with better interoperability with existing codebases, would facilitate a smoother transition. This includes improving bindings to existing libraries and providing better tools for mixed-language projects.
Conclusion
The embedded Rust ecosystem has made significant strides but still faces several challenges that need addressing to reach its full potential. By focusing on improving documentation, tooling, display abstraction, ecosystem maturity and integration with existing systems, the Rust community can continue to advance the capabilities and adoption of Rust in embedded systems development.
We are happy to be a part of that.
Summary: The Embedded Rust Ecosystem
The embedded Rust ecosystem offers a rich set of crates and tools that make it a powerful option for embedded systems development. From low-level hardware access to efficient, non-blocking I/O operations, Rust supports a wide range of embedded use cases. While there are areas for improvement, the community-driven nature of Rust ensures that these gaps will be addressed over time. And the great thing about Rust being open-source: You can help with it, too!
In the final part of this series, we will perform a time comparison between setting up and deploying an embedded project on an STM32 MCU using Rust and a traditional C/C++ approach. If you haven't been convinced yet, the time saved during project setup will change that.