Amikor először hallottam a no_std kifejezésről, azt hittem, hogy ez valami exotikus, ritkán használt niche feature. Aztán elkezdtem embedded projekteken dolgozni, és rájöttem: ez nem opcionális extra, hanem az egyetlen út, ha mikrovezérlőn akarsz Rust kódot futtatni. Nézzük végig együtt, mi ez pontosan, és hogyan állítunk össze egy futóképes projektet a nulláról.
Mi az a no_std és miért van rá szükség mikrovezérlőknél
A Rust standard könyvtára (std) hatalmas mennyiségű funkcionalitást ad neked: fájlkezelést, hálózatot, szálakat, heap-alapú kollekciókat, és még sok mást. Ehhez viszont egy operációs rendszerre támaszkodik – syscall-okat hív, feltételezi, hogy van egy allokátor, egy fájlrendszer, egy scheduler a szálakhoz.
Egy tipikus mikrovezérlőn (gondolj egy Cortex-M4-es chipre, pár tíz-száz KB RAM-mal és flash-sel) semmi ilyen nincs. Nincs operációs rendszer, nincs fájlrendszer, gyakran még dinamikus memóriafoglalás sincs (vagy csak korlátozottan van értelme használni). Ilyen környezetben a std egyszerűen nem tud lefordulni, mert olyan primitívekre épül, amik nem léteznek.
Itt jön képbe a #![no_std] attribútum, amit a crate gyökerében (a main.rs vagy lib.rs tetején) helyezünk el. Ez kikapcsolja a std automatikus linkelését, és helyette a core crate-re hagyatkozunk.
A no_std nem jelenti azt, hogy "nincs Rust" – csupán azt, hogy a standard könyvtár azon részeit nem használjuk, amik operációs rendszer-specifikusak. A nyelv maga, a trait rendszer, a generikusok, az ownership modell mind ugyanúgy működik.
A core és az alloc crate szerepe std nélkül
A core crate a Rust nyelv legalapvetőbb, platform-független részét tartalmazza: primitív típusokat, Option, Result, iterátorokat, a slice és str típusokat, és a trait-eket, mint Copy, Clone, Debug. Ez az a réteg, ami mindig elérhető, függetlenül attól, hogy van-e alattad operációs rendszer.
Amit viszont a core NEM ad, az minden, ami dinamikus memóriafoglalást igényel: Vec, String, Box, HashMap. Ezekhez a alloc crate-re van szükség, ami egy köztes réteg a core és a std között. Az alloc crate feltételez egy globális allokátort, amit neked kell megadnod – embedded világban ez tipikusan egy egyszerű bump allokátor vagy egy linked-list allokátor egy statikusan lefoglalt memóriaterületen.
#![no_std]
#![no_main]
extern crate alloc;
use alloc::vec::Vec;
// Egy globális allokátor beállítása szükséges ahhoz,
// hogy a Vec és társai működjenek.
#[global_allocator]
static ALLOCATOR: linked_list_allocator::LockedHeap =
linked_list_allocator::LockedHeap::empty();
fn hasznal_vectort() {
let mut v: Vec<u8> = Vec::new();
v.push(1);
v.push(2);
}
Sok embedded projektben egyáltalán nem használunk alloc-ot – helyette fixed-size tömböket és heapless crate-eket (pl. heapless::Vec) alkalmazunk, amik statikus kapacitással működnek, így elkerülhető a heap fragmentáció és a nem determinisztikus futásidő.
Panic handler és megszakításkezelés minimál környezetben
A std-ben a panic esetén a runtime gondoskodik arról, hogy kiírja a hibaüzenetet és leállítsa (vagy unwind-olja) a programot. no_std környezetben nincs ilyen runtime, ezért neked kell megadnod, mi történjen panic esetén. Ezt a #[panic_handler] attribútummal jelölt függvénnyel tesszük meg.
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
Ez a legegyszerűbb megoldás: végtelen ciklusba küldjük a programot. Gyakorlatban persze hasznosabb, ha előtte valamit csinálunk – például egy LED-et villogtatunk, vagy a hibaüzenetet egy UART-on keresztül kiküldjük debug célból. Erre kiváló a panic-halt vagy a panic-probe crate, amiket egyszerűen csak importálunk, és ők regisztrálják a handler-t helyettünk.
A megszakításkezelés (interrupt handling) egy másik kritikus pont. Mikrovezérlőknél az interrupt vector table egy fix memóriacímen található, és a hardware ide ugrik, amikor egy megszakítás bekövetkezik (pl. egy timer lejár, vagy egy GPIO pin állapota változik). A cortex-m-rt crate ezt nagyon elegánsan kezeli egy #[interrupt] makróval:
use cortex_m_rt::interrupt;
#[interrupt]
fn TIM2() {
// Ez a függvény akkor fut, amikor a TIM2 timer
// megszakítást generál.
// Itt törölnéd a flag-et és végeznéd a teendőt.
}
Megszakításkezelőben óvakodj a hosszú futásidejű kódtól! Ha egy interrupt handler túl sokáig fut, más, magasabb prioritású megszakítások várni fognak, ami a rendszer válaszidejét ronthatja el. Tartsd rövidnek, és a hosszabb feldolgozást tedd át a fő ciklusba egy flag vagy queue segítségével.
Egy egyszerű "Hello, LED" projekt felépítése thumbv7em-none-eabihf targetre
Nézzünk egy konkrét, futtatható példát. Tegyük fel, hogy egy STM32F4-es fejlesztőpanelt (pl. egy "Blue Pill" vagy "Discovery" board) használunk, aminek a magja egy Cortex-M4F. A célplatform a thumbv7em-none-eabihf – ez a -none- rész jelzi, hogy nincs alatta operációs rendszer, az eabihf pedig azt, hogy van hardware floating point unit.
Először telepítenünk kell a target-et:
rustup target add thumbv7em-none-eabihf
A Cargo.toml-ban a szükséges crate-ek:
[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
panic-halt = "0.2"
stm32f4xx-hal = { version = "0.14", features = ["stm32f411"] }
A main.rs egy egyszerű LED-villogtató program:
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _;
use stm32f4xx_hal::{pac, prelude::*};
#[entry]
fn main() -> ! {
let dp = pac::Peripherals::take().unwrap();
let gpioc = dp.GPIOC.split();
let mut led = gpioc.pc13.into_push_pull_output();
let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.freeze();
let mut delay = dp.TIM1.delay_ms(&clocks);
loop {
led.set_high();
delay.delay_ms(500u32);
led.set_low();
delay.delay_ms(500u32);
}
}
Ez a kód a #[entry] makróval jelöli a belépési pontot – ez a cortex-m-rt biztosítja, és lecseréli a szokásos main-t, mivel no_main attribútummal jelöltük, hogy nem a szokásos C-stílusú main szignatúrát várjuk.
A #[entry] függvény soha nem térhet vissza – ezért -> ! a visszatérési típusa. Ha egy mikrovezérlő fő programja "visszatérne", nem lenne hova visszatérnie, hiszen nincs alatta operációs rendszer, ami átvegye az irányítást.
Cargo build és linker script beállítások
Ahhoz, hogy a fordító és a linker tudja, hova helyezze a kódot és az adatokat a memóriában, egy memory.x linker script fájlra van szükség, amit a cortex-m-rt crate link.x scriptje importál:
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 512K
RAM : ORIGIN = 0x20000000, LENGTH = 128K
}
Ez megmondja a linkernek, hogy a flash 512 KB-tól kezdődik 0x08000000 címen, a RAM pedig 0x20000000-tól 128 KB méretben áll rendelkezésre – ezek természetesen a konkrét chip adatlapjából származnak.
A .cargo/config.toml fájlban beállítjuk a default target-et és a linker flag-eket:
[build]
target = "thumbv7em-none-eabihf"
[target.thumbv7em-none-eabihf]
rustflags = [
"-C", "link-arg=-Tlink.x",
]
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-run --chip STM32F411RETx"
A probe-run (vagy alternatívaként cargo-flash) segítségével flashelhetjük és futtathatjuk a programot valós hardware-en, cargo run paranccsal – pontosan úgy, mintha egy asztali alkalmazást indítanánk, csak itt a bináris a mikrovezérlőre kerül fel.
Ha nincs kéznél fizikai hardware, a qemu-system-arm emulátorral is tesztelheted a kódot bizonyos Cortex-M chipek esetén, bár a valós periféria-viselkedés (mint a GPIO vagy timer) emulációja korlátozott.
Összefoglalás
A no_std Rust világa elsőre ijesztőnek tűnhet – nincs println!, nincs heap alapból, nincs Vec a core-ban –, de ha megérted, miért van szükség ezekre a korlátozásokra, gyorsan rájössz, hogy ez egy nagyon tudatos, jól megtervezett rendszer. A core crate biztosítja a nyelv alapjait, az alloc opcionálisan hozzáadja a dinamikus memóriakezelést, a panic_handler és az interrupt makrók pedig kézbe adják a kontrollt a legalapvetőbb runtime viselkedések felett is.
A legjobb módja annak, hogy ezt igazán megértsd, hogy leülsz egy valós fejlesztőpanellal (vagy legalább egy emulátorral), és végigcsinálod a fent bemutatott LED-villogtató projektet. Onnantól már csak az érdekel majd, hogyan olvassunk ki szenzoradatokat I2C-n, hogyan kommunikáljunk UART-on, vagy hogyan írjunk saját HAL réteget egy egzotikusabb chiphez – de ez már egy másik cikk témája lesz.