This article explores the embedded Rust ecosystem, as of the Updated date at the top. It's intended for people new to embedded Rust, who don't yet have a feel for what tools and libraries are available. This may be especially useful to people experienced with Rust, but not embedded. Hopefully after reading (or referencing) this article, you come away with a feel for when you might need a certain tool, and will be able to recognize it when a use case arises. You should be able to identify which tools are suitable, and not suitable, for a given use.
This article does not go into detail on individual tools; to find more info, check out the tools' documentations, linked.
Each section below describes a category of tool, with the most relevant ones listed. If you'd like something added, please send us an email. If at any point you become overwhelmed by the quantity of options, take comfort that you generally only need one from each category.
Choose one of these tools (or collections of tools) to flash your firmware over SWD, and log relevant debugging data, eg to a terminal on your PC.
These tools are designed to be used together, and offer, subjectively, the smoothest experience. An example of how to use them together is provided in the Knurling app template. This is a great place to get started for new projects. It wraps Probe Run, described below for flashing. It includes all the basic project files, including several pre-populated config files, needed for embedded Rust projects.
Probe Run is used to flash firmware. Once set up, you can compile, flash, and debug from a terminal, using
cargo run, if you have a hardware probe connected.
Defmt is a logging framework, capable of a range of logging outputs and levels. It includes a
println! macro that's similar to the one used for non-embedded Rust. It is much faster than ITM and other logging libraries.
Flip-Link provides stack overflow protection, using a neat trick, described on its Github page.
If using the Knurling App template, you don't need to do anything further to get Probe Run, Defmt, and Flip-Link.
Probe-rs is a debugging toolkit, used to flash your program to MCU connected using a debugging probe. It supports ST-Link, J-Link, and CMSIS-DAP hardware. It's easiest to use when wrapped by Probe Run, so in most cases, there's no need to directly install this.
This tool is a more flexible alternative to
probe-run; it supports features like standalone flashing, and GDB support.
Open OCD is an open-source On-Chip Debugger. It's a popular way to flash MCUs, including from C and C++ code bases. When paired with a debugger, it's an alternative to
Probe Run. Note that it requires both a config file, and a long console command to run; the console command is best wrapped in a shell script due to its verbosity.
RTT-Target provides debug output, and is an alternative to defmt. From its official description:
RTT implements input and output to/from a debug probe using in-memory ring buffers and memory polling. This enables debug logging from the microcontroller with minimal delays and no blocking, making it usable even in real-time applications where e.g. semihosting delays cannot be tolerated.
ITM-dump is another debugging tool. It is very slow; don't use it.
Cargo Flash is a flashing tool with a different use case from the above: It compiless and flashes binaries using a hardware debugger without any debugging. It operates using a CLI command + arguments, eg to specify debug vs release version, target, and a few other options.
ravedude is used to flash and debug AVR chips like Arduino. It's similar to
probe-run in that once installed (via Cargo), you can flash and debug using
These are low-level tools that provide APIs that read and write to MCU registers, usually with register and field names. They're sometimes called Peripheral Access Crates (PAC). They may be used by application firmware directly, or wrapped by higher-level libraries like Hardware Abstraction Layers (HAL). Which one you choose will depend on the MCU you're using.
This collection of libraries provides broad coverage of register access for most STM32 variants. It's based on SVD files provided by ST, with their many errors patched using a series of YAML files. The linked repository automatically builds crates with names like stm32h7, which are how you use the library. There's a separate crate for each STM32 Family. Eg, a crate for G4, one for H7 etc. To differentiate MCU within a family (eg H735 vs H743), you use feature gates provided by each crate.
STM32 PACs are generated using an automated tool called svd2rust.
HALs that wrap these may expose the PAC directly as a secondary API, as a module named
The more popular STM32 variants have most SVD errors patched, but you may run into errors (missing features, mislabeled registers etc) on less popular ones. If you find one, post an issue or PR on the stm32-rs github. Also note that PACs for some variants include cleaner (indexed) APIs for repetitive registers, like DMA channels.
STM32 RAL accomplishes the same thing as stm32-rs PACs, but does so using a concise macro-based API. Consider this if you're using register-access code directly in your firmware.
Embassy, which we'll discuss further later, includes both its own HAL, and its own PAC for STM32. It has a unique goal of a unified API, and single library, for all supported STM32 devices. This makes adapting code between different STM32 variants easier, and provides a smaller surface area for maintenance. It can be used as a standalone library, without the Embassy Async framework or HAL, by using the standalone metapac crate.
These are similar to STM32 PACs, but are for Nordic nRF microcontrollers, eg nRF52 and nRF53. They generally have fewer errors than STM-32 PACs, even without patching.
Same idea, for Espressif ESP. Uses SVD files to generate PAC crates for ESP32, ESP32-S2, ESP32-S3, and ESP8266.
This register access library fills the same role as above, but for Amtel AVR MCUs.
HALs provide high-level APIs to conduct operations using MCU peripherals; they're are a good choice for use in application firmware. HALs are usually specific to a group of related MCU, and the one you choose will largely depend on the MCU you're using. Compared to register access libs (PACs), HALs provide higher-level APIs, that perform what can be considered recipes of register writes. For example, you might have HAL functions that set a GPIO pin high, read a GPIO pin, send data over a bus using a function call, or initiate a DMA transfer. They often include structs that each represent a peripheral, and perform operations using methods. They might also contain config data, and apply it.
These libraries for STM-32 are split based on Family. Eg, there is a separate library for F4, H7 etc. They share similar APIs in some areas, but have separate code bases, and development teams. Here are some of the more popular ones:
stm32h7xx-hal has the most robust feature set and documentation. These make use of typestates to manage GPIO pins, and peripherals that use them, checking pin compatibility at compile time. Their APIs vary, but are often based on
This library supports many STM32 families. It doesn't include support for older ones like F1. Where possible, the API is similar or identical between variants, making it feasible to use the same firmware on multiple MCUs. It's geared towards practical firmware, with DMA and interrupts as first-class APIs, and typestates eschewed for ergonomics and clearer docs. Disclaimer: Written and maintained by this article's author, hence the glowing review.
This Embassy module, like STM32-HAL, supports many STM32 families using a single library and API. It's designed for use with the Embassy async framework, but can be used separately. (Use the
embassy-rs crate, but don't use the async executor). It uses the Embassy metapac internally, so the differentiation between MCUs is done at a lower level, and the HAL code is comparatively free of feature-gates.
This HAL supports all nRF51, nRF52 and nRF91 MCUs, and has a relatively clean API. Like
stm32yyxx-hals, it leverages typestates to check peripheral configuration at compile time. Note that while these MCUs are geared towards RF uses, this HAL does not include radio functionality.
This library supports ESP32, ESP32-C3, ESP32-S2, and ESP32-S3 chips. Note that Rust support for these is a WIP, but support is rapidly improving. It's the second-best supported architecture after ARM.
This library supports Microchip ATSAMD MCUs.
Embedded HAL is a popular library. Its name is a frequent source of confusion among those new to embedded Rust: It's not a HAL in the traditional sense. From the top of its Github:
embedded-hal serves as a foundation for building an ecosystem of platform agnostic drivers. (driver meaning library crates that let a target platform interface an external device like a digital sensor or a wireless transceiver).
This library provides a number of rust traits, each used to describe capabilities of a peripheral. Its intent is for HAL libraries to implement its traits for specific hardware. Driver libraries can be written that are hardware-agnostic: A library that implements the appropriate embedded-hal trait should work on any hardware that has a HAL implementing that trait. Hardware examples are busses like I2C and SPI, certain timer functionality, and GPIO pins. The Awesome Embedded Rust page (linked at the top of this article) lists a number of these drivers.
This is ideal conceptually, and for experimenting with hardware. Beyond that, embedded-hal traits are impractical due to lack of support for non-blocking functionality, like DMA, interrupts, and Async. They're necessarily generic, supporting only the most universal features of a peripheral: The resulting drivers (or firmware code) that accepts one of these trait implementations won't be able to implement functionality beyond that included in the trait, which for most project requirements, is required.
Additionally, embedded-HAL traits can result in verbose APIs; the canonical example being requiring error handling on GPIO operations, even when infallible on a given platform.
Note that there are a few spin-offs of
embedded-hal that provide additional traits. For example, embedded-dma.
Real-Time Interrupt-Driven Concurrency RTIC) is a runtime execution framework that provides 3 related features:
RTIC has excellent documentation. It's lightweight, and is intended to be used in cases where a user might use an RTOS like FreeRTOS in C. For applications that use multiple interrupts (A very common case, and why it's common to reach for an RTOS by default in C), RTIC is a great choice. It doesn't add much performance overhead or code complexity, and allows smart resource locking between interrupts and software tasks.
Embassy is an Async executor framework. It can be viewed as an embedded equivalent to Tokio. It uses Rust's builtin async syntax to handle concurrency: This pattern will be familiar if you've used async Rust in non-embedded settings. Internally, Embassy's async functionality wraps interrupts and DMA. As described above, it also includes integrated register access libraries and HALs for STM-32 and nRF-5x MCUs. Embassy uses built-in Rust
These libraries work for one or more types of MCU, and provide access to MCU peripherals in a standalone way. They can be used independently, or wrapped by a HAL. For example, most of the STM32-HALs linked above provide USB, CAN, Ethernet etc modules that thinly wrap these.
This graphics library provides tools used to create and manipulate 2d graphics, eg for use with a display buffer. For example, it has tools to load images, write text, and draw shapes. It may be used standalone, or as a dependency by drivers for displays.
Overall, the Rust embedded ecosystem is changing quickly. Notably, Espressif architecture support is rapidly improving, providing a (WiFi-enabled!) alternative to ARM. All of the tools and libraries listed above are open source; if something doesn't work for you, post an issue on Github, or try to make the patch yourself. This is a great time to do it!