5 Rust Runtimes Every Embedded Developer Needs to Know

The fast and efficient Rust can be used for embedded software applications, and these runtimes could help.

Jacob Beningo

April 23, 2024

7 Min Read
Rust embedded software
AnnaStills/Getty Images/iStockphoto

At a Glance

  • Baremetal using no_std is suited to embedded applications.
  • Real-Time Interrupt-Driven Concurrency can be used for automotive, industrial automation, & other embedded control systems.
  • Other solutions include Embassy, Drone OS, and async-embedded-hal.

The need for more secure software at the edge has been driving an effort by governments and large businesses to push Rust adoption. Rust offers many benefits to developers, such as:

  • Memory safety without garbage collection

  • Concurrency

  • Modern tooling

When you combine all these benefits, you’ll find that you can write more-secure and higher-quality software. While embedded developers might hesitate to learn Rust, it’s often called a “zero-cost abstraction” language because it is fast and efficient. 

Developers getting started might struggle to figure out how to use Rust in an embedded environment. If you’ve played with Rust on a desktop, you may have encountered runtimes like Tokio, but these aren’t well-suited for embedded work. 

Let’s explore five runtimes you can use to develop embedded software using Rust. 

Rust Runtime #1: Baremetal Using no_std

Just like in C and C++, you can write bare-metal Rust code. By default, Rust will include many runtime features like a dynamically allocated heap, collections, stack overflow protection, init code, and libstd. While this is great for a desktop, mobile, or server application, it’s a lot of overhead for an embedded application. 

Instead, you can use the directive #![no_std] as a crate-level attribute that will disable all these features. Using no_std tells the Rust compiler not to use the std-crate but a bare-metal implementation that includes libcore but no heap, collections, stack overflow protection, etc. 

Related:3 Reasons to Use Rust in Embedded Systems

The no_std attribute is perfect for developers looking to write embedded software in a bare-metal equivalent to what you do in C and C++.

Rust Runtime #2: Real-Time Interrupt-Driven Concurrency (RTIC)

RTIC, which stands for Real-Time Interrupt-Driven Concurrency, is a framework specifically designed for building real-time embedded applications using the Rust programming language. RTIC primarily targets bare-metal systems and leverages Rust’s zero-cost abstractions and type safety to offer a concurrent execution environment where tasks are managed and prioritized according to their hardware interrupts. 

The RTIC framework ensures that applications meet real-time guarantees by handling tasks with minimal overhead and predictable behavior. RTIC is particularly well-suited for applications that require strict timing constraints and high reliability, such as automotive systems, industrial automation, and other embedded control systems. The framework simplifies handling shared resources and prevents data races by design, thanks to Rust's ownership and type system.

Related:5 Embedded Software Trends to Watch in 2024

An example of how RTIC code might look involves defining tasks bound to specific interrupts and specifying their priorities. For instance, an RTIC application could be set up to read sensor data when a timer interrupt occurs and process this data at a different priority level. Here’s a simplified example:

#![no_std]

#![no_main]

use rtic::app;

use stm32f4xx_hal::{prelude::*, stm32};

#[app(device = stm32f4xx_hal::stm32)]

const APP: () = {

    #[init]

    fn init(cx: init::Context) {

        // Initialization code here

    }

    #[task(binds = TIM2, priority = 2)]

    fn timer_tick(cx: timer_tick::Context) {

        // Code to handle timer tick here

        // This could involve reading sensors or updating control outputs

    }

    #[task(priority = 1)]

    fn process_data(cx: process_data::Context) {

        // Lower priority task to process data collected at the timer tick

    }

};

In this example, the #[app] attribute marks the beginning of the RTIC application, specifying the hardware platform it targets. In our example, we are targeting the stm32f4. 

The init function serves to set up necessary initial configurations. You can imagine we might initialize clocks, peripherals, and other application components. 

The timer_tick task is bound to the TIM2 timer interrupt, having a higher priority to ensure timely data reading. 

Another task, process_data, is designated to process this data at a lower priority, which helps manage task executions according to their criticality and urgency. This structure ensures that critical tasks preempt less critical ones, maintaining the system's responsiveness and stability. As you can see, it’s also not directly tied to a hardware peripheral, showing flexibility for tasks that interact with hardware and other purely application-related tasks. 

Rust Runtime #3: async-embedded-hal

The async-embedded-hal is an experimental extension of the Rust embedded-hal (Hardware Abstraction Layer) project, tailored to support asynchronous programming in embedded systems. It aims to bridge the gap between the synchronous operations typically provided by the standard embedded hal and the needs of modern embedded applications that can benefit from non-blocking, asynchronous I/O operations. 

The async-embedded-hal allows developers to write more efficient and responsive applications on microcontroller-based systems where blocking operations can be costly regarding power and performance. By integrating async/await semantics into the HAL, async-embedded-hal makes it possible for tasks such as reading sensors, communicating over networks, or interacting with peripherals to be performed without stalling the microcontroller. The result is improvements to the system's ability to handle multiple tasks concurrently.

Developing async-embedded-hal leverages Rust's powerful asynchronous programming features, primarily used in web and server applications, and adapts them to the constrained environments of embedded systems. This involves providing asynchronous traits for standard embedded interfaces like SPI, I2C, and USART, among others. 

Asynchronous programming in this context allows tasks to yield control rather than block, which is particularly beneficial in systems where tasks vary significantly in priority and response time requirements. For instance, a high-priority task like handling a critical sensor input can preempt a low-priority task such as logging data to a storage device. The challenge and innovation lie in implementing these features that adhere to the strict size and performance constraints typical of embedded devices without sacrificing the safety and concurrency benefits Rust naturally provides. This approach not only streamlines the development process but also improves the scalability and maintainability of embedded applications.

Rust Runtime #4: Embassy

Embassy is an asynchronous runtime for embedded systems built entirely in Rust. It is explicitly designed to cater to the needs of resource-constrained environments typical of embedded devices, utilizing Rust’s async/await capabilities to enable efficient and non-blocking I/O operations. 

Embassy is an ideal choice for developers looking to implement complex applications on microcontrollers, where traditional synchronous blocking can lead to inefficient use of the limited computational resources. Embassy provides a framework that supports various embedded platforms, offering a scalable and safe approach to concurrent execution in embedded systems. This runtime leverages the predictable performance characteristics of Rust, ensuring that tasks are executed without the overhead of traditional multitasking operating systems.

One of the critical strengths of Embassy is its extensibility and the ease with which it can interface with a wide range of device peripherals. The runtime facilitates the creation of responsive and reliable applications by managing asynchronous tasks and events to optimize power consumption and processing time. For instance, developers can handle multiple communication protocols simultaneously without needing complex and resource-intensive threading mechanisms typically found in more generic programming environments. 

In embedded systems, we often need to blink a timer. Below is an example application in Rust that uses Embassy to do just that:

#![no_std]

#![no_main]

use embassy::executor::Executor;

use embassy::time::{Duration, Timer};

use embassy_stm32::gpio::{Level, Output, Speed};

use embassy_stm32::Peripherals;

#[embassy::main]

async fn main(sp: Peripherals) {

    let mut led = Output::new(sp.PB15, Level::High, Speed::VeryHigh);

    loop {

        led.set_high();

        Timer::after(Duration::from_secs(1)).await;

        led.set_low();

        Timer::after(Duration::from_secs(1)).await;

    }

}

In this code, the embassy::main macro sets up the main function to run with the Embassy executor, which handles the scheduling and executing of asynchronous tasks. The Peripherals structure provides access to the device's peripherals, configured through the device-specific HAL. The LED connected to pin PB15 is toggled every second using the Timer::after function, which asynchronously delays execution without blocking, allowing other tasks to run concurrently if necessary. This example illustrates the simplicity and power of using Embassy for asynchronous operations in embedded Rust applications.

Rust Runtime #5: Drone OS

Drone OS is a cutting-edge, embedded operating system written entirely in Rust, explicitly designed for real-time applications on ARM Cortex-M microcontrollers. By leveraging Rust’s safety features and zero-cost abstractions, Drone OS provides a robust platform for developing high-performance embedded software that requires precise timing and resource efficiency. 

The OS facilitates low-level hardware access while maintaining high safety, minimizing the risks of bugs and memory errors commonly associated with embedded development. Drone OS stands out in the realm of embedded systems for its modular design and support for concurrent programming, making it an ideal choice for developers seeking to create scalable, reliable, and maintainable real-time applications in the demanding environments of modern embedded systems.

The Bottom Line

Rust is an exciting language that offers memory safety, security, concurrency, and a modern toolchain that can be used to develop embedded applications. There’s currently not just a single way to use Rust to build your embedded applications. We’ve explored several different runtimes that range from a more traditional bare-metal approach to an operating system. 

If you explore these runtimes, you’ll discover they can help you get up and running quicker than you might think. Don’t be fooled, though, by thinking they are complete runtimes. Depending on your choice, you may find that not all features work at a level that meets your expectations. 

Rust has been around for about a decade, but it’s still evolving. You can do a lot with it from an embedded perspective, but there are still many unknowns. Don’t let that stop you from learning this rich and exciting language.

About the Author(s)

Jacob Beningo

Jacob Beningo is an embedded software consultant who currently works with clients in more than a dozen countries to dramatically transform their businesses by improving product quality, cost and time to market. He has published more than 300 articles on embedded software development techniques, has published several books, is a sought-after speaker and technical trainer and holds three degrees which include a Masters of Engineering from the University of Michigan.

Sign up for the Design News Daily newsletter.

You May Also Like