RTOS Integration

Using Rust with FreeRTOS and async embedded with Embassy.

FreeRTOS with Rust

FreeRTOS is a popular RTOS that can be used from Rust via FFI bindings.

FreeRTOS Bindings

// Using freertos-rust crate
use freertos_rust::*;

fn main() {
    let task1 = Task::new()
        .name("task1")
        .stack_size(256)
        .priority(TaskPriority(1))
        .start(|| {
            loop {
                // Task work
                CurrentTask::delay(Duration::ms(100));
            }
        })
        .unwrap();

    FreeRtosUtils::start_scheduler();
}

Add to Cargo.toml:

[dependencies]
freertos-rust = "0.1"

Tasks and Synchronization

use freertos_rust::*;

static QUEUE: Queue<u32> = Queue::new(10).unwrap();
static MUTEX: Mutex<u32> = Mutex::new(0).unwrap();
static SEMAPHORE: Semaphore = Semaphore::new_binary().unwrap();

fn producer_task() {
    loop {
        QUEUE.send(42, Duration::infinite()).unwrap();
        CurrentTask::delay(Duration::ms(100));
    }
}

fn consumer_task() {
    loop {
        if let Ok(value) = QUEUE.receive(Duration::ms(500)) {
            // Process value
        }
    }
}

fn mutex_example() {
    let mut guard = MUTEX.lock(Duration::infinite()).unwrap();
    *guard += 1;
}

Embassy - Async Embedded Rust

Embassy is a modern async runtime for embedded systems, providing:

  • Async/await for embedded
  • Zero-cost abstractions
  • No heap required
  • HAL implementations

Embassy Project Setup

# Cargo.toml
[dependencies]
embassy-executor = { version = "0.5", features = ["arch-cortex-m", "executor-thread"] }
embassy-time = { version = "0.3", features = ["tick-hz-32_768"] }
embassy-stm32 = { version = "0.1", features = ["stm32f411ce", "time-driver-any"] }
cortex-m = "0.7"
cortex-m-rt = "0.7"
defmt = "0.3"
defmt-rtt = "0.4"
panic-probe = { version = "0.3", features = ["print-defmt"] }

Basic Embassy Application

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_stm32::gpio::{Level, Output, Speed};
use embassy_time::Timer;
use defmt::info;
use {defmt_rtt as _, panic_probe as _};

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());

    let mut led = Output::new(p.PC13, Level::High, Speed::Low);

    loop {
        info!("LED on");
        led.set_low();
        Timer::after_millis(500).await;

        info!("LED off");
        led.set_high();
        Timer::after_millis(500).await;
    }
}

Multiple Tasks

use embassy_executor::Spawner;
use embassy_time::{Duration, Timer};
use embassy_sync::channel::Channel;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;

static CHANNEL: Channel<CriticalSectionRawMutex, u32, 4> = Channel::new();

#[embassy_executor::task]
async fn producer() {
    let mut counter = 0u32;
    loop {
        CHANNEL.send(counter).await;
        counter += 1;
        Timer::after(Duration::from_millis(100)).await;
    }
}

#[embassy_executor::task]
async fn consumer() {
    loop {
        let value = CHANNEL.receive().await;
        info!("Received: {}", value);
    }
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let _p = embassy_stm32::init(Default::default());

    spawner.spawn(producer()).unwrap();
    spawner.spawn(consumer()).unwrap();

    // Main task can do other work or just idle
    loop {
        Timer::after(Duration::from_secs(1)).await;
    }
}

Embassy Synchronization Primitives

use embassy_sync::mutex::Mutex;
use embassy_sync::signal::Signal;
use embassy_sync::channel::Channel;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;

// Async mutex
static SHARED_DATA: Mutex<CriticalSectionRawMutex, u32> = Mutex::new(0);

// Signal for notifications
static SIGNAL: Signal<CriticalSectionRawMutex, ()> = Signal::new();

async fn update_shared() {
    let mut data = SHARED_DATA.lock().await;
    *data += 1;
}

async fn wait_for_signal() {
    SIGNAL.wait().await;
}

fn trigger_signal() {
    SIGNAL.signal(());
}

Embassy UART Example

use embassy_stm32::usart::{Config, Uart};
use embassy_stm32::bind_interrupts;
use embassy_stm32::peripherals;

bind_interrupts!(struct Irqs {
    USART2 => embassy_stm32::usart::InterruptHandler<peripherals::USART2>;
});

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());

    let config = Config::default();
    let mut usart = Uart::new(
        p.USART2,
        p.PA3,  // RX
        p.PA2,  // TX
        Irqs,
        p.DMA1_CH6,
        p.DMA1_CH5,
        config,
    ).unwrap();

    // Echo received data
    let mut buf = [0u8; 64];
    loop {
        let n = usart.read_until_idle(&mut buf).await.unwrap();
        usart.write(&buf[..n]).await.unwrap();
    }
}

Embassy vs FreeRTOS

Aspect Embassy FreeRTOS
Language Pure Rust C with Rust bindings
Concurrency async/await Tasks with preemption
Memory No heap required Heap for tasks
Size Very small Larger footprint
Ecosystem Growing Mature
Debugging Rust tooling Traditional RTOS tools

RTIC - Real-Time Interrupt-driven Concurrency

RTIC is a concurrency framework for building real-time systems:

#![no_std]
#![no_main]

use panic_halt as _;
use rtic::app;
use stm32f4xx_hal::{pac, prelude::*};

#[app(device = stm32f4xx_hal::pac, peripherals = true)]
mod app {
    use super::*;

    #[shared]
    struct Shared {
        counter: u32,
    }

    #[local]
    struct Local {
        led: gpio::PC13<gpio::Output<gpio::PushPull>>,
    }

    #[init]
    fn init(ctx: init::Context) -> (Shared, Local) {
        let gpioc = ctx.device.GPIOC.split();
        let led = gpioc.pc13.into_push_pull_output();

        (
            Shared { counter: 0 },
            Local { led },
        )
    }

    #[task(shared = [counter])]
    async fn task1(mut ctx: task1::Context) {
        ctx.shared.counter.lock(|counter| {
            *counter += 1;
        });
    }

    #[task(binds = EXTI0, local = [led], shared = [counter])]
    fn button_pressed(mut ctx: button_pressed::Context) {
        ctx.local.led.toggle();
        ctx.shared.counter.lock(|counter| {
            *counter += 1;
        });
    }
}

Choosing an RTOS Approach

Use Case Recommendation
New project, Rust-first Embassy
Existing FreeRTOS codebase freertos-rust bindings
Hard real-time, simple RTIC
Complex async workflows Embassy
Legacy hardware support FreeRTOS

Summary

Framework Model Best For
Embassy Async/await Modern embedded Rust
RTIC Interrupt-driven Hard real-time
FreeRTOS Traditional RTOS Legacy/mixed codebases

See Also

Next Steps

Learn about Cross-Compilation for different targets.


Back to top

Rust Programming Guide is not affiliated with the Rust Foundation. Content is provided for educational purposes.

This site uses Just the Docs, a documentation theme for Jekyll.