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 Safety | Manual (raw pointers) | Compile-time (borrow checker) | Compile-time (borrow checker) |
| Runtime | Arduino Framework | Custom (no_std) | ESP-IDF (FreeRTOS) |
| Async Support | Tidak (tanpa library) | Ya (Embassy) | Ya (tokio-esp32) |
| WiFi/BLE | Built-in | Experimental | Lengkap (via esp-idf-svc) |
| Binary Size | Kecil | Sangat kecil | Lebih besar (~500KB+) |
| Kurva Belajar | Menengah | Sulit (Rust + embedded) | Menengah-Sulit |
- â Binary sangat kecil
- â Tanpa RTOS
- â Full async dengan Embassy
- â Startup cepat
- â WiFi/BLE belum stabil
- â Komunitas lebih kecil
- â 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
# 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"
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.
3. Blink: Hello World ESP32 Rust
Mari kita mulai dengan program klasik â blink LED. Ini adalah "Hello World" di dunia embedded.
Project Structure
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
[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
[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
#![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);
}
}
# 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).
#![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 |
|---|---|---|---|
| Output | Push-Pull | Output HIGH/LOW standar | LED, Relay, Buzzer |
| Output | Open-Drain | HIGH = floating, LOW = ground | I2C, sensor kapasitif |
| Input | Pull-Up | Input dengan internal pull-up | Tombol aktif-low |
| Input | Pull-Down | Input dengan internal pull-down | Tombol aktif-high |
| Input | Floating | Tanpa pull resistor | Sensor digital eksternal |
| Alternate | UART/SPI/I2C | Pin di-assign ke peripheral | Perangkat 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.
#![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.
#![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.
[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"
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.
#![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;
}
}
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-sys | FFI bindings ke ESP-IDF | Akses langsung ke C API |
esp-idf-hal | HAL Rust untuk ESP-IDF | GPIO, I2C, SPI, UART, ADC, PWM |
esp-idf-svc | Service tingkat tinggi | WiFi, HTTP, MQTT, NVS, Sntp |
esp-idf-ota | OTA Update | Over-the-air firmware update |
esp-println | Console output | println! ke UART atau JTAG |
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(())
}