Skip to content
rusthacks.dev
Systems

RTIC Versus Embassy for Real Time Embedded Rust Compared

RTIC schedules interrupt driven tasks with compile time priorities; Embassy runs async tasks cooperatively. Compare the models, resource sharing, latency and when each fits.

3 min read
Summarize in AI
Flat illustration for the post “RTIC Versus Embassy for Real Time Embedded Rust Compared”, composed around embedded.

Both frameworks solve the same problem, doing several things on one small chip, with opposite philosophies. One lets the hardware interrupt controller be the scheduler. The other turns your code into state machines on a cooperative executor. The comparison below is from the projects' documented designs, not benchmarks; measure latency on your own board before relying on a number.

The RTIC model

rust
#[rtic::app(device = stm32f4xx_hal::pac, dispatchers = [EXTI0, EXTI1])]
mod app {
    #[shared]
    struct Shared {
        counter: u32,
    }

    #[local]
    struct Local {}

    #[init]
    fn init(_cx: init::Context) -> (Shared, Local) {
        (Shared { counter: 0 }, Local {})
    }

    #[task(binds = TIM2, shared = [counter], priority = 2)]
    fn tick(mut cx: tick::Context) {
        cx.shared.counter.lock(|c| *c += 1);
    }

    #[task(shared = [counter], priority = 1)]
    async fn report(mut cx: report::Context) {
        let n = cx.shared.counter.lock(|c| *c);
        let _ = n;
    }
}

binds = TIM2 ties tick to a hardware interrupt at priority 2. report is lower priority and the lock call raises the priority just enough to exclude tick while it reads. The framework computes the ceiling for each resource at compile time. There is no heap, no run-time scheduler and no way to write a data race on counter. This is what makes RTIC attractive for timing analysis: worst case blocking is a function of the critical section lengths.

The Embassy model

rust
use embassy_executor::Spawner;
use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, channel::Channel};
use embassy_time::{Duration, Ticker};

static EVENTS: Channel<ThreadModeRawMutex, u32, 8> = Channel::new();

#[embassy_executor::task]
async fn producer() {
    let mut ticker = Ticker::every(Duration::from_millis(100));
    let mut n = 0u32;
    loop {
        ticker.next().await;
        n += 1;
        EVENTS.send(n).await;
    }
}

#[embassy_executor::task]
async fn consumer() {
    loop {
        let n = EVENTS.receive().await;
        let _ = n;
    }
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    spawner.spawn(producer()).unwrap();
    spawner.spawn(consumer()).unwrap();
}

Tasks suspend at .await, and the executor sleeps the core when everything waits. Sharing is through channels, signals and mutexes in embassy-sync. A task that computes for 5 ms without awaiting delays every other task on its executor by 5 ms.

Side by side

RTICEmbassy
Schedulinghardware priorities, preemptivecooperative per executor
Resource safetycompile time ceilingschannels and mutex types
Latency analysisdirect from prioritiesdepends on longest await-free section
Waiting on I/Ocallbacks or async tasksnative .await
Driversembedded-hal cratesasync HALs for many chips plus USB, networking
Heapnonenone (optional)
Learning curvemacros and priority thinkingasync Rust and pinned futures

Getting preemption in Embassy

Run a second executor on a software interrupt at a higher priority and spawn urgent tasks there. They preempt the main executor's tasks, like an RTIC priority level. This works but you reason about it by hand; RTIC does it with proofs.

How to choose

Pick RTIC when deadlines are hard (motor control, protocol timing), you want to prove sharing is safe, and tasks are mostly event driven. Pick Embassy when firmware is dominated by waiting on peripherals, timers and networking, and you value its drivers. Mixed designs are fine. On the interrupts-and-shared-state fundamentals both build on, read interrupts and shared state, and for an Embassy project from scratch, the Pico setup.

Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.