Az embedded Rust világában sokáig két tábor élt egymás mellett: aki interrupt-vezérelt, kézzel írt állapotgépekkel dolgozott, és aki RTIC-et használt a determinisztikus, prioritásalapú feladatkezeléshez. Az Embassy egy harmadik utat hoz: async/await-et, ami natívan illik a mikrovezérlők eseményvezérelt természetéhez, de közben nem igényel se heap-allokátort, se hagyományos RTOS-t.

Miért jó ötlet az async/await beágyazott környezetben

Az async Rust lényege, hogy egy Future-t addig nem futtatunk, amíg nincs mit tennie – amikor blokkolna (pl. várna egy timer lejárására, vagy adatra egy UART-on), egyszerűen visszaadja az irányítást az executornak. Ez pontosan az a modell, amire egy mikrovezérlőnek szüksége van: sok apró, egymástól független feladat, amik többségében semmit sem csinálnak, csak várnak egy interruptra.

Hagyományos megoldásban ezt kézzel írt callback-ekkel és állapotgépekkel oldanád meg, ami gyorsan olvashatatlanná válik, ha három-négy periféria kommunikál egymással. Az Embassy ezt async/await szintaxisba csomagolja, és a fordító generálja neked az állapotgépet.

Az Embassy runtime ráadásul:

  • nem igényel heap-et – minden Future statikusan allokált méretű, a taskok memóriaigénye fordításidőben ismert;
  • egyetlen szálon fut kooperatív módon, tehát nincs szükség mutex-ekre a taskok között (bár interruptokkal szemben továbbra is kell védekezni);
  • közvetlenül a hardveres interruptokra épít – amikor egy periféria interruptot vált ki, az ébreszti fel a rá váró Future-t, nincs polling;
  • HAL-szintű támogatást ad számos chipcsaládhoz (STM32, nRF, RP2040), így a GPIO, UART, I2C, SPI mind async API-val érkezik.
Megjegyzés

Az Embassy 2023 őszén még aktív fejlesztés alatt áll, és a crates.io-n publikált verziók mögött gyakran a git repo legfrissebb commitja van praktikusabb választás – a projekt README-je mindig megadja az ajánlott git rev-et.

Fejlesztői környezet előkészítése

Mielőtt belevágnánk, néhány eszközre szükségünk lesz. Első lépésben a cross-compile célplatformot kell hozzáadni:

rustup target add thumbv7em-none-eabihf

Ez a target STM32F4-es Cortex-M4F magra vonatkozik (FPU-val), de ha más STM32 családot használsz, ellenőrizd a megfelelő thumbv* triplet-et.

A flashelést és debugolást a probe-rs eszközkészlettel érdemes csinálni, ami mostanra érett, ST-Link-kel és J-Link-kel is jól működő megoldás:

cargo install probe-rs --features cli
cargo install cargo-embed

A cargo-embed a probe-rs köré épít egy cargo-integrációt, amivel egy cargo embed paranccsal fordíthatod és flashelheted a firmware-t, élő RTT log kimenettel együtt.

Tipp

Az RTT (Real-Time Transfer) logolás sokkal gyorsabb és kevésbé invazív, mint a hagyományos UART-os println!-szerű debug kimenet – érdemes defmt + defmt-rtt kombóval használni, mert bájt-hatékony és formázott logokat ad.

Egy no_std Embassy projekt vázának felépítése

Hozzunk létre egy új projektet, és állítsuk be a Cargo.toml-t. A no_std és no_main attribútumok itt kötelezőek, mert nincs alattunk operációs rendszer.

[package]
name = "embassy-blinky"
version = "0.1.0"
edition = "2021"

[dependencies]
embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = ["arch-cortex-m", "executor-thread"] }
embassy-time = { git = "https://github.com/embassy-rs/embassy", features = ["tick-hz-32_768"] }
embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", features = ["stm32f429zi", "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"] }

[profile.release]
debug = true
lto = true
opt-level = "s"

A stm32f429zi feature helyett persze a saját chipedet írd be – az Embassy HAL feature-flag-ekkel dönti el, melyik chip regisztertáblázatát fordítsa be.

A .cargo/config.toml-ban állítsd be a target-et és a runner-t:

[build]
target = "thumbv7em-none-eabihf"

[target.thumbv7em-none-eabihf]
runner = "probe-rs run --chip STM32F429ZITx"

Az első async task: LED villogtatás

Nézzük meg a legegyszerűbb, de mégis meggyőző demót: egy LED villogtatása egy async taskban, embassy_time::Timer segítségével.

#![no_std]
#![no_main]

use defmt_rtt as _;
use panic_probe as _;

use embassy_executor::Spawner;
use embassy_stm32::gpio::{Level, Output, Speed};
use embassy_time::{Duration, Timer};

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());
    let mut led = Output::new(p.PB7, Level::High, Speed::Low);

    loop {
        led.set_high();
        Timer::after(Duration::from_millis(300)).await;
        led.set_low();
        Timer::after(Duration::from_millis(300)).await;
    }
}

Ami itt fontos: a Timer::after(...).await nem egy blokkoló delay, hanem visszaadja az irányítást az executornak, ami eközben más taskokat futtathatna. Egyetlen taskkal ez nem tűnik izgalmasnak, de rögtön másképp néz ki, ha van egy második, párhuzamos feladatunk is.

Több párhuzamos task és interruptok bekötése

Az Embassy execut­orában minden #[embassy_executor::task]-kel jelölt async függvény egy önálló, statikusan allokált taskként fut. A spawner.spawn(...) hívással indíthatod el őket a main-ből.

#![no_std]
#![no_main]

use defmt_rtt as _;
use panic_probe as _;

use embassy_executor::Spawner;
use embassy_stm32::exti::ExtiInput;
use embassy_stm32::gpio::{Level, Output, Pull, Speed};
use embassy_time::{Duration, Timer};

#[embassy_executor::task]
async fn blink(mut led: Output<'static>) {
    loop {
        led.toggle();
        Timer::after(Duration::from_millis(500)).await;
    }
}

#[embassy_executor::task]
async fn button_watch(mut button: ExtiInput<'static>) {
    loop {
        button.wait_for_falling_edge().await;
        defmt::info!("gomb megnyomva!");
    }
}

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

    let led = Output::new(p.PB7, Level::High, Speed::Low);
    let button = ExtiInput::new(p.PC13, p.EXTI13, Pull::Up);

    spawner.spawn(blink(led)).unwrap();
    spawner.spawn(button_watch(button)).unwrap();
}

A button_watch task nem pollingol, nem néz meg minden ciklusban egy GPIO-regisztert – az ExtiInput az EXTI perifériát async wrapperbe csomagolja, és a hardveres interrupt maga ébreszti fel a Future-t, amikor a gomb lenyomásra vált. Ez a lényeg: a HAL rétegben az interrupt handler nem az üzleti logikát futtatja, csak jelez a runtime-nak, hogy egy adott Waker-t hívjon meg.

Figyelem

Ügyelj arra, hogy a periféria-objektumokat (mint a led és button) csak egyszer birtokolhatja egy task – az ownership modell itt is szigorúan érvényesül, tehát ha két helyen szeretnél GPIO-t vezérelni, célszerű egy megosztott állapotot (pl. embassy_sync::Mutex) bevezetni.

RTIC és Embassy összehasonlítása

Mindkét keretrendszer megoldja a "kézzel írt interrupt spagetti" problémáját, de más filozófiával.

Az RTIC (Real-Time Interrupt-driven Concurrency) egy erősen priorizált, megszakítás-vezérelt modellt ad, ahol a taskok explicit prioritásokkal futnak, és a keretrendszer fordításidőben garantálja a race condition-mentességet a resource-lock rendszerén keresztül. Ez kiváló választás, ha kemény real-time determinizmusra van szükséged, és pontosan tudni akarod, melyik task melyik prioritáson, mennyi ideig futhat.

Az Embassy ezzel szemben async/await-re épít, egyetlen (vagy kevés) executor szálon kooperatív módon ütemez. Nincs explicit prioritásmodell – a taskok addig futnak, amíg .await-nél meg nem állnak. Ez cserébe sokkal ismerősebb, "normál Rust" kódolási stílust ad, és kiváló, ha I/O-vezérelt, hálózati vagy kommunikáció-intenzív rendszert építesz (Wi-Fi modulok, BLE stack-ek, több UART/SPI periféria egyszerre).

Szempont RTIC Embassy
Ütemezés Prioritásalapú, preemptív Kooperatív, async/await
Determinizmus Nagyon erős, fordításidőben garantált Jó, de nem hard real-time
Tanulási görbe Meredekebb (resource, priority) Ismerős async Rust
Ideális eset Motorvezérlés, kemény időzítés Kommunikációs stack-ek, IoT
Jó tudni

A választás nem életre-halálra megy: számos projekt RTIC-et használ a kritikus, időzítés-érzékeny magért, és Embassy-stílusú async taskokat a kevésbé kritikus, kommunikációs rétegekhez – bár ezek keverése egyazon binárisban jelenleg még nem trivális, és külön odafigyelést igényel.

Összefoglalás

Az Embassy megmutatja, hogy az async Rust nem csak szerverek és webalkalmazások kiváltsága – a mikrovezérlők eseményvezérelt, megszakításokra épülő világába is természetesen illik. A no_std + no_main alapokra épülő projektstruktúra, a HAL-szintű async API-k és az interruptokkal integrált Future-ök együtt egy meglepően kényelmes fejlesztői élményt adnak, minimális futásidejű overhead mellett. Ha most kezdesz beágyazott Rustba, érdemes mindkét keretrendszert kipróbálni egy-egy kisebb projekten – az RTIC megtanít a real-time gondolkodásra, az Embassy pedig arra, hogy az async/await tényleg univerzális eszköz, nem csak a tokio kiváltsága.