Introduction

Embedded software development differs from conventional software development in several respects. The differences arise from low-level hardware access, architectural diversity, and the constraints imposed by microcontrollers. Rust development, as we know it, runs on hosted environments using the std library with mostly hardware-agnostic abstractions. This means there is an underlying operating system providing a standard system interface. The std library relies on these interfaces to implement its functionality. In addition, std provides a runtime with several features, including a starting point for the program’s main.

Some embedded systems do support hosted environments. These are systems that run on an underlying operating system, such as the Raspberry Pi. However, another, probably more common, development approach in embedded systems is to avoid hosted environments. This means that there is no underlying operating system, and the compiled code runs directly on the hardware. This is called bare-metal development. In this case, we cannot use std anymore because there is no underlying operating system to provide a standard system interface.

So what’s the alternative?

Rust supports bare-metal development through the core or no-std library. The no-std library is a stripped-down version of the std library that does not require operating-system abstractions. As such, when using no-std, there are gaps left by the removal of the std library that need to be filled. This also includes the runtime provided by std. Essentially, we would need to build a minimal stack on top of the hardware to implement some useful abstractions and access low-level features. The good news is that this stack is built by importing different crates already created by the community. You can imagine that part of it includes a runtime implementation to replace the one we lost.

It’s worth noting that in std development, crates are typically hardware-agnostic and, for the most part, utility crates that help us perform certain tasks better/faster. On the other hand, crates in no-std development are a mixed bag of utility (hardware-agnostic) and hardware-specific crates. The higher we go in abstractions, the more hardware-agnostic the crates are.

The Embedded Rust Ecosystem

The image above captures a high-level stacking overview of the embedded Rust ecosystem. In this overview, certain layers are essential for building a minimal stack, while others offer better abstractions, application utilities, and development tools. With this stack, we can express the level of abstraction in our system.

In the following sections, we’ll go over what each of these layers offers. I have also created an ecosystem overview with clickable links to serve as a reference for commonly used community crates. This should serve as a valuable resource for learners. You can download the ecosystem overview for free below.

Embedded Rust Ecosystem Overview Sheet v2.1

Embedded Rust Ecosystem Overview Sheet v2.1

Your perfect companion for your embedded Rust journey!

$0.00 usd

Microarchitecture Crates

Microarchitecture crates are our interface to the processor core's registers, interrupts, and assembly instructions.

In embedded applications, we use #[no_std] and #[no_main] attributes in code templates. #[no_main] tells the compiler not to use the standard entry point; instead, the runtime crate provides startup code, and the user marks their entry function with the ⁠ #[entry] ⁠ attribute. Similarly, #[no_std] is used to tell the compiler not to load the standard library.

So now, we need a main definition and a minimal runtime to replace what we lost. Where do we get that? From microarchitecture runtime crates. Microarchitecture runtime crates are technically a layer above microarchitecture crates. Among other things, runtime crates provide a minimal runtime and a path to #[entry]. Microarchitecture crates are also specific to a particular architecture. For example, if the underlying controller uses a RISC-V processor, then you need a RISC-V runtime crate like riscv-rt.

Peripheral Access Crates

PACs are typically auto-generated from vendor-provided SVD (System View Description) files using a tool called svd2rust. SVD files describe the memory-mapped registers of a microcontroller’s peripherals in XML format. The resulting PAC exposes a structured Rust API for every register in the chip. Since PACs are generated from SVD files, the quality of the generated API depends on the quality of the SVD file provided by the vendor.

Working directly with PACs is low-level and sometimes requires unsafe Rust, since you are directly manipulating hardware registers. This also requires more in-depth knowledge of the registers and the bits in a controller. As such, a developer would find themselves constantly referring back to controller datasheets. For this reason, most developers do not use PACs directly. They rely on the next layer up: HAL crates.

Hardware Abstraction Layer Crates

HAL crates sit atop PACs and provide a safe, ergonomic API for working with microcontroller peripherals. Instead of manipulating raw registers, you interact with abstractions like a Pin, a Uart, or an I2cBus.

What makes the HAL layer particularly powerful is integrating the embedded-hal crate. This crate defines a set of hardware-agnostic traits. These traits serve as interfaces that describe common peripheral behaviors. HAL implementations for specific chips implement these traits, and device driver crates are written against them. This means a driver written for a sensor over SPI can work across any chip that has a HAL implementing the embedded-hal SPI traits, without any changes to the driver code.

Some widely used HAL crates include stm32f4xx-hal, esp-hal, rp2040-hal, and nrf52840-hal. Each implements embedded-hal traits for their respective hardware.

Runtime Framework Crates

Once you have HAL abstractions, the next challenge is structuring your application. How do you handle multiple concurrent tasks? How do you write asynchronous code without an OS? Runtime framework crates address this.

The two major frameworks in the embedded Rust ecosystem are:

  • Embassy: An asyncawait framework for embedded systems. Embassy provides an executor that runs async tasks, along with its own HAL implementations embassy-stm32, embassy-nrf, etc.) that expose async-friendly APIs. With Embassy, you can write concurrent embedded code using Rust’s async/await syntax. Embassy code is far more readable than traditional interrupt-driven state machines.

  • RTIC (Real-Time Interrupt-driven Concurrency): A framework that leverages Rust’s ownership model to provide safe, zero-cost interrupt-driven concurrency. RTIC tasks are triggered by hardware interrupts, and the framework enforces that shared resources are accessed safely through a priority-based system, with all checks performed at compile time.

Both are widely used and actively maintained. The choice typically comes down to whether you prefer async/await style (Embassy) or interrupt-driven concurrency (RTIC).

RTOS Crates

For applications requiring more complex scheduling, inter-task communication, or integration with existing RTOS-based ecosystems, there are RTOS crates. These provide traditional OS primitives like tasks, mutexes, queues, timers, in a Rust no-std environment.

Some options in this space include:

  • Tock OS: An embedded OS written entirely in Rust, designed for security and isolation.

  • Drone OS: An async RTOS for microcontrollers focused on safety and efficiency.

  • RIOT OS: A port of the RIOT OS bringing its multi-threading model to embedded Rust.

RTOS crates are generally used when application complexity outgrows what Embassy or RTIC can comfortably handle, or when integration with an existing RTOS infrastructure is required.

Application Crates

Application crates are where the ecosystem really shines. These are hardware-agnostic (or near hardware-agnostic) crates that provide drivers, algorithms, and utilities for building embedded applications.

Some key categories and examples:

  • Drivers: Sensor and peripheral driver crates written against embedded-hal traits. Examples include bme280 (temperature/humidity/pressure), ssd1306 (OLED display), and hundreds more on crates.io.

  • Graphics: embedded-graphics is the standard crate for drawing 2D graphics; text, shapes, images on displays in no-std environments.

  • Networking: smoltcp is a no-std TCP/IP stack that enables network connectivity on resource-constrained devices.

  • Data Structures: heapless provides fixed-capacity data structures (Vec, String, HashMap) that work without a heap allocator.

  • Serialization: serde paired with postcard or serde_json_core enables data serialization/deserialization in no-std.

  • Logging: defmt is a highly efficient logging framework designed for embedded targets, minimizing the overhead of log output.

The application layer is where embedded Rust starts to feel remarkably ergonomic — the ecosystem has matured to the point where most common tasks have well-maintained crates available.

Development Tool Crates

Last but not least are development tool crates. These are crates that aid in embedded development. Note that a lot of these tools run on the host, not necessarily on the controller. We categorize these tools into roughly three areas of interest:

  1. Flash & Debug: These are crates that run on the host and aid in flashing and debugging microcontrollers.

  2. Project Generation: These are crates that also run on the host and aid in generating projects or register mappings, as well as installing toolchains.

  3. Logging & Testing: These crates help create test harnesses or logging environments.

Some notable Flash & Debug crates include probe-rs . It is the de facto standard for flashing and debugging embedded targets, supporting many debug probes (J-Link, CMSIS-DAP, ST-Link) and a wide range of chips. It also includes a VS Code extension for integrated debugging.

For Project Generation, cargo-generate is a project scaffolding tool that works with community templates. Several embedded Rust templates exist for quickly bootstrapping new projects. svd2rust generates PAC crates from SVD files, while chiptool offers an alternative with improved ergonomics.

For Logging & Testing, defmt paired with defmt-rtt provides efficient logging via RTT (Real-Time Transfer), sending log output to the host with minimal overhead. panic-probe is a panic handler that formats and transmits panic messages via defmt. embedded-test provides a test harness for running tests directly on embedded targets, and flip-link protects against stack overflows by placing the stack at the bottom of RAM.

Reply

Avatar

or to participate

Keep Reading