IoT

Rust on ESP32: Embedded Development dengan Rust

TOKEN

Pelajari cara membangun aplikasi embedded menggunakan Rust di ESP32 dengan esp-hal, esp-idf-hal, dan async Embassy

1. Mengapa Rust untuk Embedded?

Rust telah menjadi bahasa pemrograman yang sangat populer untuk embedded development berkat fitur keamanan memori (memory safety) yang dimilikinya tanpa garbage collector. Di dunia embedded, di mana setiap byte dan microsecond sangat berharga, Rust menawarkan performa yang setara dengan C/C++ namun dengan jaminan keamanan yang jauh lebih baik.

Untuk ESP32, komunitas Rust menyediakan dua pendekatan utama: esp-hal (bare-metal, tanpa OS) dan esp-idf-hal (menggunakan ESP-IDF sebagai RTOS backend). Keduanya menggunakan Rust standar library dan bisa menjalankan async code.

Fitur C/C++ (Arduino) Rust (esp-hal) Rust (esp-idf-hal)
Memory SafetyManual (raw pointers)Compile-time (borrow checker)Compile-time (borrow checker)
RuntimeArduino FrameworkCustom (no_std)ESP-IDF (FreeRTOS)
Async SupportTidak (tanpa library)Ya (Embassy)Ya (tokio-esp32)
WiFi/BLEBuilt-inExperimentalLengkap (via esp-idf-svc)
Binary SizeKecilSangat kecilLebih besar (~500KB+)
Kurva BelajarMenengahSulit (Rust + embedded)Menengah-Sulit
đŸĻ€
esp-hal (Bare Metal)
Kontrol Penuh Hardware
  • ✅ Binary sangat kecil
  • ✅ Tanpa RTOS
  • ✅ Full async dengan Embassy
  • ✅ Startup cepat
  • ❌ WiFi/BLE belum stabil
  • ❌ Komunitas lebih kecil
đŸĻ€
esp-idf-hal (ESP-IDF)
Fitur Lengkap + Rust
  • ✅ WiFi, BLE, NVS lengkap
  • ✅ Kompatibel ESP-IDF SDK
  • ✅ Driver lengkap
  • ✅ Lebih stabil untuk produksi
  • ❌ Binary lebih besar
  • ❌ Boot lebih lambat

2. Setup Toolchain

Untuk memulai development Rust di ESP32, kita perlu menginstall toolchain yang tepat. Ada dua pendekatan: espup (untuk esp-hal) atau esp-idf-rs (untuk esp-idf-hal).

Install Rust dan ESP Toolchain

Bash
# 1. Install Rust (jika belum ada)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env

# 2. Install espup — ESP32 toolchain manager
cargo install espup
espup install

# 3. Install target untuk ESP32
rustup target add xtensa-esp32-none-elf    # ESP32 original
rustup target add xtensa-esp32s2-none-elf   # ESP32-S2
rustup target add xtensa-esp32s3-none-elf   # ESP32-S3
rustup target add riscv32imc-unknown-none-elf  # ESP32-C3 (RISC-V)

# 4. Install tool flasher
cargo install espflash
cargo install espmonitor

# 5. Install cargo-generate untuk template
cargo install cargo-generate

# 6. Clone template project
cargo generate esp-rs/esp-idf-template cargo --name esp32-rust-demo

# Atau untuk bare-metal (esp-hal):
cargo generate esp-rs/esp-template cargo --name esp32-rust-bare

# 7. Set environment variables (tambahkan ke .bashrc/.zshrc)
export LIBCLANG_PATH="$HOME/.rustup/toolchains/esp/xtensa-esp32-elf-clang/esp-18.1.2_20240912/esp-clang/lib"
export PATH="$HOME/.rustup/toolchains/esp/xtensa-esp32-elf-clang/esp-18.1.2_20240912/esp-clang/bin:$PATH"
💡 Tips Platform

Untuk ESP32-C3 (RISC-V), toolchain Rust standar sudah mendukung target riscv32imc tanpa perlu espup khusus. Ini membuat ESP32-C3 paling mudah untuk mulai belajar Rust embedded.

Mari kita mulai dengan program klasik — blink LED. Ini adalah "Hello World" di dunia embedded.

Project Structure

Project Layout
esp32-rust-blink/
├── .cargo/
│   └── config.toml       # Konfigurasi target dan runner
├── src/
│   └── main.rs           # Kode utama aplikasi
├── Cargo.toml            # Dependencies dan metadata
└── build.rs              # Build script (opsional)

Cargo.toml

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

[dependencies]
esp-hal = { version = "0.22", features = ["esp32"] }
esp-backtrace = { version = "0.14", features = ["esp32", "panic-handler", "exception-handler", "println"] }
esp-println = { version = "0.12", features = ["esp32"] }

[profile.release]
opt-level = "s"      # Optimasi untuk ukuran
lto = true           # Link-Time Optimization
debug = false        # Tanpa debug info

.cargo/config.toml

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

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

[unstable]
build-std = ["core", "alloc"]

src/main.rs — Blink LED

Rust
#![no_std]
#![no_main]

use esp_hal::clock::CpuClock;
use esp_hal::delay::Delay;
use esp_hal::gpio::Level;
use esp_hal::gpio::Output;
use esp_hal::main;
use esp_println::println;

#[main]
fn main() -> ! {
    // Inisialisasi hardware dengan CPU clock 240 MHz
    let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
    let peripherals = esp_hal::init(config);

    // Setup GPIO — LED built-in pada pin 2
    let mut led = Output::new(peripherals.GPIO2, Level::Low);

    // Delay handler
    let delay = Delay::new();

    println!("ESP32 Rust Blink dimulai! đŸĻ€");

    // Loop utama — blink LED
    loop {
        led.toggle();
        println!("LED toggle!");
        delay.delay_millis(500);
    }
}
Terminal — Build & Flash
# Build dan flash ke ESP32
cargo run --release

# Atau manual:
cargo build --release
espflash flash --monitor target/xtensa-esp32-none-elf/release/esp32-rust-blink

4. GPIO: Input & Output

GPIO adalah interface paling dasar antara mikrokontroler dan dunia luar. Di Rust, GPIO diakses melalui HAL (Hardware Abstraction Layer) yang memastikan keamanan tipe data (type safety).

Rust
#![no_std]
#![no_main]

use esp_hal::clock::CpuClock;
use esp_hal::delay::Delay;
use esp_hal::gpio::{Input, Level, Output, Pull};
use esp_hal::main;
use esp_println::println;

#[main]
fn main() -> ! {
    let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
    let p = esp_hal::init(config);

    // Output: LED pada GPIO 2
    let mut led = Output::new(p.GPIO2, Level::Low);

    // Input: Tombol pada GPIO 0 (BOOT button)
    let button = Input::new(p.GPIO0, Pull::Up);

    // Input: Sensor digital pada GPIO 4
    let sensor = Input::new(p.GPIO4, Pull::Down);

    let delay = Delay::new();
    let mut debounce_counter = 0u32;

    println!("GPIO Demo mulai!");

    loop {
        // Baca tombol dengan debounce sederhana
        if button.is_low() {
            debounce_counter += 1;
            if debounce_counter > 5 {
                led.toggle();
                println!("Tombol ditekan! LED: {:?}", if led.is_set_high() { "ON" } else { "OFF" });
                debounce_counter = 0;
            }
        } else {
            debounce_counter = 0;
        }

        // Baca sensor digital
        if sensor.is_high() {
            println!("Sensor HIGH — objek terdeteksi!");
        }

        // PWM sederhana dengan software
        for duty in (0..100).step_by(5) {
            led.set_high();
            delay.delay_micros(duty);
            led.set_low();
            delay.delay_micros(100 - duty);
        }
    }
}

Konfigurasi Pin Modes

Mode Tipe Deskripsi Contoh Penggunaan
OutputPush-PullOutput HIGH/LOW standarLED, Relay, Buzzer
OutputOpen-DrainHIGH = floating, LOW = groundI2C, sensor kapasitif
InputPull-UpInput dengan internal pull-upTombol aktif-low
InputPull-DownInput dengan internal pull-downTombol aktif-high
InputFloatingTanpa pull resistorSensor digital eksternal
AlternateUART/SPI/I2CPin di-assign ke peripheralPerangkat komunikasi

5. I2C: Komunikasi Sensor

I2C (Inter-Integrated Circuit) adalah protokol komunikasi serial dua-kabel (SDA, SCL) yang sangat umum untuk sensor, EEPROM, display OLED, dan peripheral lainnya. Di Rust, kita menggunakan esp-hal I2C driver.

Rust
#![no_std]
#![no_main]

use esp_hal::clock::CpuClock;
use esp_hal::delay::Delay;
use esp_hal::i2c::master::{I2c, Config as I2cConfig};
use esp_hal::main;
use esp_hal::time::Rate;
use esp_println::println;

// Alamat I2C untuk sensor BMP280
const BMP280_ADDR: u8 = 0x76;

// Register BMP280
const REG_ID: u8 = 0xD0;
const REG_CTRL_MEAS: u8 = 0xF4;
const REG_CONFIG: u8 = 0xF5;
const REG_PRESS_MSB: u8 = 0xF7;
const REG_TEMP_MSB: u8 = 0xFA;

#[main]
fn main() -> ! {
    let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
    let peripherals = esp_hal::init(config);
    let delay = Delay::new();

    // Setup I2C pada GPIO 21 (SDA) dan GPIO 22 (SCL)
    let mut i2c = I2c::new(peripherals.I2C0, I2cConfig::default())
        .expect("Gagal inisialisasi I2C")
        .with_sda(peripherals.GPIO21)
        .with_scl(peripherals.GPIO22)
        .with_bus_speed(Rate::from_khz(400))  // 400 kHz Fast Mode
        .into();

    println!("I2C scan bus...");

    // Scan I2C bus untuk menemukan device
    for addr in 0x08u8..=0x77 {
        let mut buf = [0u8; 1];
        if i2c.read(addr, &mut buf).is_ok() {
            println!("Device ditemukan di alamat: 0x{:02X}", addr);
        }
    }

    // Baca Chip ID BMP280
    let mut chip_id = [0u8; 1];
    i2c.write_read(BMP280_ADDR, &[REG_ID], &mut chip_id)
        .expect("Gagal membaca chip ID");
    println!("BMP280 Chip ID: 0x{:02X} (harus 0x58)", chip_id[0]);

    // Konfigurasi BMP280: oversampling x16, normal mode
    i2c.write(BMP280_ADDR, &[REG_CTRL_MEAS, 0b10110111])
        .expect("Gagal menulis CTRL_MEAS");
    i2c.write(BMP280_ADDR, &[REG_CONFIG, 0b10000000])
        .expect("Gagal menulis CONFIG");

    // Baca calibration data
    let mut cal_data = [0u8; 26];
    i2c.write_read(BMP280_ADDR, &[0x88], &mut cal_data)
        .expect("Gagal baca calibration");
    let dig_t1 = u16::from_le_bytes([cal_data[0], cal_data[1]]);
    let dig_t2 = i16::from_le_bytes([cal_data[2], cal_data[3]]);
    let dig_t3 = i16::from_le_bytes([cal_data[4], cal_data[5]]);
    println!("Calibration: T1={}, T2={}, T3={}", dig_t1, dig_t2, dig_t3);

    // Baca data secara periodik
    loop {
        // Baca 6 byte: Pressure (3 byte) + Temperature (3 byte)
        let mut data = [0u8; 6];
        i2c.write_read(BMP280_ADDR, &[REG_PRESS_MSB], &mut data)
            .expect("Gagal baca data");

        // Konversi raw data ke suhu
        let raw_temp: i32 = ((data[3] as i32) << 12)
                           | ((data[4] as i32) << 4)
                           | ((data[5] as i32) >> 4);

        // Kompensasi suhu menggunakan formula datasheet
        let var1 = (((raw_temp >> 3) - ((dig_t1 as i32) << 1))
                    * (dig_t2 as i32)) >> 11;
        let var2 = (((((raw_temp >> 4) - (dig_t1 as i32))
                    * ((raw_temp >> 4) - (dig_t1 as i32))) >> 12)
                    * (dig_t3 as i32)) >> 14;
        let t_fine = var1 + var2;
        let temp = (t_fine * 5 + 128) >> 8;

        println!("Suhu: {:.1}°C", temp as f32 / 100.0);

        delay.delay_millis(1000);
    }
}

6. SPI: Komunikasi High Speed

SPI (Serial Peripheral Interface) adalah protokol komunikasi serial empat-kabel (MOSI, MISO, SCK, CS) yang mendukung kecepatan hingga 80 MHz di ESP32. SPI cocok untuk SD card, display TFT, RF module, dan komponen high-throughput lainnya.

Rust
#![no_std]
#![no_main]

use esp_hal::clock::CpuClock;
use esp_hal::delay::Delay;
use esp_hal::gpio::{Level, Output};
use esp_hal::main;
use esp_hal::spi::master::{Config as SpiConfig, Spi};
use esp_hal::spi::Mode as SpiMode;
use esp_hal::time::Rate;
use esp_println::println;

#[main]
fn main() -> ! {
    let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
    let peripherals = esp_hal::init(config);
    let delay = Delay::new();

    // Chip Select (CS) pin
    let mut cs = Output::new(peripherals.GPIO5, Level::High);

    // Setup SPI
    let mut spi = Spi::new(peripherals.SPI2, SpiConfig::default())
        .with_sck(peripherals.GPIO18)
        .with_mosi(peripherals.GPIO23)
        .with_miso(peripherals.GPIO19)
        .with_baudrate(Rate::from_mhz(10))  // 10 MHz
        .with_mode(SpiMode::Mode0)           // CPOL=0, CPHA=0
        .into();

    println!("SPI Master dimulai!");

    // Contoh: Komunikasi dengan SPI flash W25Q64
    // Read JEDEC ID
    cs.set_low();
    spi.transfer_in_place(&mut [0x9F, 0x00, 0x00, 0x00])
        .expect("SPI transfer gagal");
    cs.set_high();

    println!("JEDEC ID: {:02X} {:02X} {:02X}", result[1], result[2], result[3]);

    // Baca data dari SPI Flash
    fn read_flash(spi: &mut ..., cs: &mut Output, addr: u32, buf: &mut [u8]) {
        let cmd = [
            0x03,  // Read Data command
            ((addr >> 16) & 0xFF) as u8,
            ((addr >> 8) & 0xFF) as u8,
            (addr & 0xFF) as u8,
        ];

        cs.set_low();
        // Kirim command + address
        for byte in &cmd {
            let _ = spi.transfer_in_place(&mut [*byte, 0x00]);
        }
        // Baca data
        for byte in buf.iter_mut() {
            let result = spi.transfer_in_place(&mut [0x00]);
            *byte = result[0];  // Simplified
        }
        cs.set_high();
    }

    // Tulis data ke SPI TFT display (contoh ILI9341)
    fn send_command(spi: &mut ..., dc: &mut Output, cmd: u8) {
        dc.set_low();  // Command mode
        let _ = spi.transfer_in_place(&mut [cmd, 0x00]);
    }

    fn send_data(spi: &mut ..., dc: &mut Output, data: u8) {
        dc.set_high();  // Data mode
        let _ = spi.transfer_in_place(&mut [data, 0x00]);
    }

    loop {
        delay.delay_millis(1000);
    }
}

7. WiFi & HTTP Client

WiFi adalah fitur utama ESP32. Dengan esp-idf-svc, kita bisa mengakses WiFi stack yang sama dengan ESP-IDF C framework namun dengan API Rust yang ergonomis.

TOML (Cargo.toml untuk esp-idf)
[package]
name = "esp32-wifi-demo"
version = "0.1.0"
edition = "2021"

[dependencies]
esp-idf-svc = "0.49"
esp-idf-hal = "0.45"
esp-idf-sys = "0.35"
anyhow = "1"
log = "0.4"
esp-println = { version = "0.12", features = ["esp32"] }

[build-dependencies]
embuild = "0.32"
Rust — WiFi + HTTP
use esp_idf_svc::eventloop::EspSystemEventLoop;
use esp_idf_svc::hal::prelude::Peripherals;
use esp_idf_svc::http::client::{EspHttpClient, EspHttpMethod};
use esp_idf_svc::nvs::EspDefaultNvsPartition;
use esp_idf_svc::wifi::{AuthMethod, BlockingWifi, ClientConfiguration, EspWifi};
use std::time::Duration;

fn main() -> anyhow::Result<()> {
    esp_idf_sys::link_patches();

    let peripherals = Peripherals::take()?;
    let sys_loop = EspSystemEventLoop::take()?;
    let nvs = EspDefaultNvsPartition::take()?;

    // Inisialisasi WiFi
    let mut wifi = BlockingWifi::wrap(
        EspWifi::new(peripherals.modem, sys_loop.clone(), Some(nvs))?,
        sys_loop,
    )?;

    // Konfigurasi WiFi client
    let wifi_config = ClientConfiguration {
        ssid: "NamaWifi".try_into().unwrap(),
        password: "PasswordWifi".try_into().unwrap(),
        auth_method: AuthMethod::WPA2Personal,
        ..Default::default()
    };

    wifi.set_configuration(
        &esp_idf_svc::wifi::Configuration::Client(wifi_config)
    )?;

    // Connect ke WiFi
    wifi.start()?;
    wifi.connect()?;
    wifi.wait_netif_up()?;

    println!("WiFi terhubung!");
    println!("IP Address: {:?}", wifi.sta_netif().get_ip_info()?);

    // HTTP GET request
    let client = EspHttpClient::new_default()?;
    let mut response = client.get("https://httpbin.org/get")?;

    println!("Status: {}", response.status());
    let mut buf = vec![0u8; 1024];
    let read = response.read(&mut buf)?;
    let body = std::str::from_utf8(&buf[..read])?;
    println!("Response: {}", body);

    // HTTP POST request
    let json = r#"{"temperature": 25.5, "humidity": 60.2}"#;
    let mut request = EspHttpClient::new_default()?
        .post("https://httpbin.org/post")?;
    request.set_header("Content-Type", "application/json");
    let mut response = request.submit(&mut json.as_bytes())?;
    println!("POST Status: {}", response.status());

    Ok(())
}

8. Async dengan Embassy

Embassy adalah framework async/await untuk embedded Rust yang memungkinkan kita menulis kode async tanpa overhead. Embassy menggunakan executor yang sangat ringan dan mendukung timer, GPIO interrupt, SPI/I2C DMA, dan banyak lagi.

Rust — Embassy Async
#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_time::{Duration, Timer};
use esp_hal::clock::CpuClock;
use esp_hal::gpio::{Input, Level, Output, Pull};
use esp_hal::timer::timg::TimerGroup;
use esp_println::println;

#[esp_hal_embassy::main]
async fn main(spawner: Spawner) {
    // Inisialisasi hardware
    let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
    let peripherals = esp_hal::init(config);

    // Inisialisasi embassy timer
    let timg = TimerGroup::new(peripherals.TIMG0);
    esp_hal_embassy::TimerExecutor::init(timg.timer0);

    // Spawn async tasks
    spawner.spawn(blink_led(peripherals.GPIO2)).ok();
    spawner.spawn(read_button(peripherals.GPIO0)).ok();
    spawner.spawn(heartbeat()).ok();

    println!("Embassy async dimulai! đŸĻ€");
}

// Task 1: Blink LED async
#[embassy_executor::task]
async fn blink_led(pin: esp_hal::gpio::GpioPin<2>) {
    let mut led = Output::new(pin, Level::Low);
    loop {
        led.toggle();
        Timer::after(Duration::from_millis(500)).await;
    }
}

// Task 2: Button handler async (interrupt-driven)
#[embassy_executor::task]
async fn read_button(pin: esp_hal::gpio::GpioPin<0>) {
    let mut button = Input::new(pin, Pull::Up);
    loop {
        // Tunggu hingga tombol ditekan (edge detection)
        button.wait_for_falling_edge().await;
        println!("Tombol ditekan!");

        // Debounce 200ms
        Timer::after(Duration::from_millis(200)).await;

        // Tunggu hingga tombol dilepas
        button.wait_for_rising_edge().await;
        println!("Tombol dilepas!");
    }
}

// Task 3: Heartbeat / status update
#[embassy_executor::task]
async fn heartbeat() {
    let mut counter = 0u32;
    loop {
        counter += 1;
        println!("Heartbeat #{} | Free heap: {} bytes",
            counter,
            esp_alloc::HEAP.free());
        Timer::after(Duration::from_secs(10)).await;
    }
}
â„šī¸ Kelebihan Embassy vs FreeRTOS

Embassy menggunakan cooperative multitasking dengan zero-cost async/await. Task tidak pernah di-preempt — konteks switching terjadi hanya pada .await point. Ini sangat efisien karena tidak perlu menyimpan context register setiap saat. Memory footprint per task hanya ~200 bytes dibanding ~4KB untuk FreeRTOS task.

9. ESP-IDF HAL Overview

ESP-IDF HAL (esp-idf-hal, esp-idf-svc) memberikan akses ke seluruh ESP-IDF SDK dari Rust. Ini termasuk WiFi, BLE, NVS (Non-Volatile Storage), OTA update, HTTP server, mDNS, dan banyak lagi.

Crate Fungsi Fitur Utama
esp-idf-sysFFI bindings ke ESP-IDFAkses langsung ke C API
esp-idf-halHAL Rust untuk ESP-IDFGPIO, I2C, SPI, UART, ADC, PWM
esp-idf-svcService tingkat tinggiWiFi, HTTP, MQTT, NVS, Sntp
esp-idf-otaOTA UpdateOver-the-air firmware update
esp-printlnConsole outputprintln! ke UART atau JTAG
Rust — ESP-IDF MQTT Client
use esp_idf_svc::mqtt::client::{EspMqttClient, MqttClientConfiguration};
use std::time::Duration;

fn mqtt_demo() -> anyhow::Result<()> {
    let mqtt_config = MqttClientConfiguration::default();

    let mut client = EspMqttClient::new(
        "mqtt://broker.hivemq.com:1883",
        &mqtt_config,
        |event| {
            match event {
                Ok(msg) => println!("Diterima: {:?}", msg),
                Err(e) => println!("Error: {:?}", e),
            }
        },
    )?;

    // Subscribe ke topic
    client.subscribe("esp32/suhu", esp_idf_svc::mqtt::client::QoS::AtMostOnce)?;

    // Publish data
    let payload = "{\"temp\":25.5,\"humid\":60}";
    client.publish(
        "esp32/data",
        esp_idf_svc::mqtt::client::QoS::AtLeastOnce,
        false,
        payload.as_bytes(),
    )?;

    Ok(())
}

10. Quiz Pemahaman

Pertanyaan 1: Apa keunggulan utama Rust dibanding C/C++ untuk embedded?

a) Lebih cepat dari C++
b) Memory safety tanpa garbage collector
c) Lebih mudah dari Python
d) Tidak memerlukan compiler

Pertanyaan 2: esp-hal cocok digunakan ketika?

a) Butuh WiFi dan BLE lengkap
b) Ingin binary sangat kecil dan startup cepat
c) Ingin menggunakan library Arduino
d) Butuh OTA update

Pertanyaan 3: Embassy menggunakan model concurrency apa?

a) Preemptive multitasking
b) Parallel processing
c) Cooperative multitasking dengan async/await
d) Event-driven tanpa async

Pertanyaan 4: Target Rust untuk ESP32 (original, Xtensa) adalah?

a) riscv32imc-unknown-none-elf
b) arm-none-eabi
c) xtensa-esp32-none-elf
d) wasm32-unknown-unknown

Pertanyaan 5: I2C Fast Mode beroperasi pada kecepatan berapa?

a) 100 kHz
b) 400 kHz
c) 1 MHz
d) 3.4 MHz
← Sebelumnya ESP32 Web Bluetooth Selanjutnya → ESP32-CAM Streaming
🔍 Zoom
100%
🎨 Tema