Az ESP32 az egyik legnépszerűbb hobbi és ipari mikrokontroller, és az elmúlt egy-két évben a Rust embedded közösség komoly erőfeszítéseket tett, hogy ezen a chipen is első osztályú legyen a fejlesztői élmény. Ma megnézzük, hogyan állítsd össze a környezetet, és hogyan írj egy minimális no_std programot, amit tényleg flashelhetsz egy fejlesztőpanelre.

Az ESP32 Rust ökoszisztémája: esp-hal, espup és a toolchain

Az Espressif csapata (esp-rs szervezet a GitHubon) aktívan fejleszti a Rust támogatást a saját chipjeikhez. A legfontosabb darabok, amikkel dolgozni fogsz:

  • esp-hal – hardware abstraction layer, ez adja a GPIO, UART, timer, SPI stb. absztrakciókat.
  • espup – egy telepítő eszköz, ami leszedi és beállítja a megfelelő Rust toolchaint és a szükséges linkereket.
  • esp-println és esp-backtrace – debug printeléshez és panic-kezeléshez hasznos kiegészítő crate-ek no_std környezetben.
Megjegyzés

Az eredeti ESP32 chip (nem az ESP32-C3 vagy C6) Xtensa architektúrájú, ami nem része az upstream LLVM/Rust célplatformoknak. Ezért ehhez az Espressif egy saját, forkolt Rust toolchaint tart karban, amit az espup telepít neked. Ha ESP32-C3 vagy C6 panelt használsz, az már RISC-V, és a sima stabil Rust toolchain is elég – de ez a cikk az Xtensa alapú, klasszikus ESP32-re fókuszál.

A telepítés első lépése az espup binárisának letöltése (cargo install espup, vagy előre buildelt bináris), majd:

espup install
. $HOME/export-esp.sh

Ez feltölti egy nightly-alapú, Xtensa-kompatibilis Rust toolchaint (esp toolchain néven), és beállítja a szükséges környezeti változókat, hogy a linker megtalálja az Xtensa-specifikus komponenseket.

Tipp

Az export-esp.sh szkriptet minden új terminál sessionben újra futtatnod kell, vagy tedd bele a .bashrc/.zshrc fájlodba, különben a build furcsa linker hibákkal fog elhalni.

Minimális no_std projekt létrehozása

A legegyszerűbb, ha a cargo-generate eszközzel az esp-rs csapat sablonjából indulsz, de most nézzük meg gyalog is, mit tartalmaz egy ilyen projekt, hogy értsd a részleteket.

A Cargo.toml fájlban a legfontosabb rész a függőségek és a profil beállítások:

[package]
name = "esp32-blink"
version = "0.1.0"
edition = "2021"

[dependencies]
esp-hal = { version = "0.9", features = ["esp32"] }
esp-backtrace = { version = "0.7", features = ["esp32", "panic-handler", "exception-handler"] }
esp-println = { version = "0.5", features = ["esp32"] }

[profile.release]
opt-level = "s"

A no_std projektnél szükséged van egy .cargo/config.toml fájlra is, ami beállítja a target-et és a runner parancsot:

[build]
target = "xtensa-esp32-none-elf"

[target.xtensa-esp32-none-elf]
runner = "espflash flash --monitor"

[env]
ESP_LOG = "INFO"

Az espflash egy külön telepítendő eszköz (cargo install espflash), ami a flashelést és a soros monitorozást végzi.

A src/main.rs a legminimálisabb no_std szkeleton így néz ki:

#![no_std]
#![no_main]

use esp_backtrace as _;
use esp_hal::prelude::*;

#[entry]
fn main() -> ! {
    loop {}
}

Figyeld meg a két attribútumot a fájl elején: no_std kikapcsolja a standard könyvtárat (nincs allokátorunk, nincs fájlrendszerünk, nincs OS), a no_main pedig azt jelzi, hogy nem a szokásos fn main() belépési pontot használjuk, hanem az esp_hal::entry makró által generáltat, ami a chip indítási szekvenciájához illik.

Figyelem

Ha kifelejted az esp_backtrace (vagy más panic handler crate) importot, a linker #[panic_handler] function required, but not found hibával fog leállni. no_std környezetben neked kell megadnod, mi történjen panic esetén – nincs alapértelmezett viselkedés.

GPIO kezelése: LED villogtatás lépésről lépésre

Most bővítsük ki a programot, hogy tényleg csináljon is valamit: villogtasson egy LED-et a GPIO2-n (ez a legtöbb ESP32 DevKit panelen az onboard LED lába, de ellenőrizd a saját panelod dokumentációját).

#![no_std]
#![no_main]

use esp_backtrace as _;
use esp_hal::{
    clock::ClockControl,
    delay::Delay,
    gpio::IO,
    peripherals::Peripherals,
    prelude::*,
};

#[entry]
fn main() -> ! {
    let peripherals = Peripherals::take();
    let system = peripherals.SYSTEM.split();
    let clocks = ClockControl::boot_defaults(system.clock_control).freeze();

    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
    let mut led = io.pins.gpio2.into_push_pull_output();

    let mut delay = Delay::new(&clocks);

    loop {
        led.set_high().unwrap();
        delay.delay_ms(500u32);
        led.set_low().unwrap();
        delay.delay_ms(500u32);
    }
}

Ami itt történik: a Peripherals::take() egyszeri hozzáférést ad az összes hardware perifériához (ez egy singleton pattern, ownership-alapú védelemmel – nem tudod kétszer levenni). A ClockControl beállítja az órajeleket, amire minden más periféria (timer, UART) épít. Az IO struktúrán keresztül érjük el a GPIO lábakat, és a into_push_pull_output() metódus konvertálja a lábat kimeneti móddá – ez tipikus embedded-rust típusállapot-gépezet (type-state pattern), ami fordítási időben garantálja, hogy csak olyan műveletet hívj meg a lábon, amire az aktuális módban képes.

Tipp

A type-state pattern miatt ha elfelejtenéd into_push_pull_output()-ra konvertálni a lábat, és próbálnád set_high()-t hívni rajta, a kód egyszerűen nem fordulna le – nincs futásidejű ellenőrzésre szükség, a borrow checker és a típusrendszer már fordítási időben kizárja a hibás használatot.

Soros port (UART) beállítása debug célból

Embedded fejlesztésnél a println!-hez hasonló debug kimenet az egyik legfontosabb eszköz. Az esp-println crate pontosan ezt adja, UART-on (vagy JTAG soros interfészen) keresztül küldi ki a szöveget:

#![no_std]
#![no_main]

use esp_backtrace as _;
use esp_hal::{clock::ClockControl, peripherals::Peripherals, prelude::*};
use esp_println::println;

#[entry]
fn main() -> ! {
    let peripherals = Peripherals::take();
    let system = peripherals.SYSTEM.split();
    let _clocks = ClockControl::boot_defaults(system.clock_control).freeze();

    println!("ESP32 elindult, Rust no_std alol koszontunk!");

    let mut counter: u32 = 0;
    loop {
        println!("Ciklus: {}", counter);
        counter = counter.wrapping_add(1);
        for _ in 0..1_000_000 {
            core::hint::spin_loop();
        }
    }
}

Ha direktebb kontrollt akarsz az UART felett (pl. saját baudrate, pin-kiosztás), az esp_hal::uart::Uart struktúrát is közvetlenül tudod inicializálni, de a legtöbb kezdő projekthez az esp-println bőven elég, és sokkal kevesebb boilerplate-tel jár.

A program flashelése és futtatása valódi ESP32 panelen

Ha mindent összeraktál, a flashelés egyetlen paranccsal megy:

cargo run --release

A .cargo/config.toml-ban beállított runner miatt ez valójában az espflash flash --monitor-t hívja meg a build után, ami:

  1. felismeri a soros portot (vagy megkérdezi, ha több is van csatlakoztatva),
  2. bootloader módba kapcsolja a chipet (a DevKit panelek erre általában automatikus áramkört tartalmaznak, USB-CDC-n keresztül),
  3. feltölti a binárist flash memóriába,
  4. resetel, és megnyitja a soros monitort, hogy lásd a println! kimeneteket.
Jó tudni

Ha a panelod nem támogatja az automatikus bootloader módot, kézzel kell tartanod a BOOT gombot, és a RESET gomb megnyomásával kell bootloader módba kapcsolnod flashelés előtt – ez sok olcsóbb kínai klón panelnél előfordul.

Gyakori kezdő hibák és hibaelhárítási tippek

Néhány dolog, amivel majdnem mindenki megküzd az első no_std ESP32 projektnél:

  • „error: language item required, but not found: eh_personality – ez azt jelzi, hogy a panic stratégiát nem állítottad be abort-ra, vagy hiányzik a panic handler crate. Nézd át a Cargo.toml-t és a esp-backtrace importot.
  • Linker hibák Xtensa target esetén – majdnem mindig az export-esp.sh szkript ki nem futtatásából jönnek. Ellenőrizd, hogy a xtensa-esp32-elf-gcc elérhető-e a PATH-on.
  • A panel nem jelenik meg soros portként – gyakran driver probléma (CP2102 vagy CH340 USB-UART chip), Linuxon néha dialout csoporttagság hiányzik a felhasználódnál.
  • Peripherals::take() panic „already taken” – ezt akkor kapod, ha véletlenül kétszer próbálod meghívni. Ez pontosan a Rust ownership modell embedded-specifikus alkalmazása: csak egyvalaki „birtokolhatja” a hardvert egyszerre.
  • Túl gyors delay hurok, ami nem villog láthatóan – ellenőrizd, hogy a Delay::new(&clocks) valóban a helyesen inicializált clocks objektumot kapja, különben az időzítés torzulhat.
Tipp

Ha elakadsz, az esp-rs GitHub organizáció esp-hal repójában rengeteg működő examples/ mappa van – ezekből a legtöbb hibát gyorsan kizárhatod, mert látod a hivatalosan tesztelt, aktuális API-t használó kódot.

Összefoglalás

Az ESP32 Rust-tal ma már egészen kellemes fejlesztői élményt ad, ha végigcsinálod a kezdeti toolchain-telepítést. A no_std világ elsőre ijesztő lehet – nincs heap allokátor alapból, nincs standard könyvtár –, de az esp-hal type-state alapú GPIO API-ja és az esp-println debug eszköze meglepően gyorsan produktívvá tesz. A legfontosabb tanulság: az ownership és a típusrendszer nem csak a memóriabiztonságot garantálja, hanem a hardver helyes használatát is fordítási időben kényszeríti ki – ez az, amiért embedded fejlesztésre a Rust egyre inkább versenyképes alternatíva a C-vel szemben.