Rust Embedded: Microcontroller, no_std, dan Embassy
Rust embedded berarti menulis Rust untuk komputer-komputer kecil yang tinggal di dalam benda: termostat, keyboard, drone, sensor penyiram tanaman. Komputer seperti ini disebut microcontroller. Satu chip berisi prosesor, memori beberapa ratus kilobyte, dan pin-pin yang terhubung ke LED, tombol, dan sensor. Biasanya tidak ada sistem operasi sama sekali. Programmu adalah satu-satunya yang berjalan, sejak listrik dinyalakan sampai dimatikan.
Kedengarannya menakutkan, tapi ini juga sangat seru: kamu menulis beberapa baris kode, dan sebuah lampu sungguhan berkedip di mejamu. Halaman ini menunjukkan apa saja yang mungkin, board dan crate mana yang sebaiknya dipilih, dan cara memasukkan program pertamamu ke chip. Sambil jalan, kamu akan melatih “cara berpikir embedded” lewat contoh yang bisa langsung dijalankan di browser.
Kenapa Rust sangat cocok untuk embedded
Section titled “Kenapa Rust sangat cocok untuk embedded”Selama puluhan tahun, kode embedded kebanyakan ditulis dengan C. Rust membawa beberapa hal yang sangat berarti di chip kecil:
- Tanpa garbage collector. Tidak ada yang menghentikan programmu sejenak untuk membersihkan memori, jadi waktu eksekusi tetap bisa diprediksi. Ini penting ketika kamu harus bereaksi terhadap sinyal dalam hitungan mikrodetik.
- Keamanan memori tanpa runtime. Aturan ownership (kepemilikan) dan borrowing (peminjaman) dicek saat compile, jadi tidak ada biayanya di chip. Banyak jenis crash (dangling pointer, data race antara interrupt dan loop utama) sudah tertangkap sebelum kamu mem-flash apa pun.
- Zero-cost abstraction. HAL memungkinkan kamu menulis
led.set_high()alih-alih mengutak-atik alamat memori, dan hasil compile-nya tetap satu kali tulis ke register, sama seperti kalau kamu menulisnya sendiri. - Ownership memodelkan hardware. Sebuah pin adalah sebuah nilai. Begitu pin itu dipindahkan (move) ke dalam
Output, tidak ada bagian kode lain yang bisa mengambilnya dan tidak sengaja memakainya sebagai input. Compiler menegakkan aturan yang dulu cuma berupa komentar di file C. - Tool yang bagus.
cargo, targetrustup, danprobe-rsmenggantikan banyak IDE dan makefile dari vendor.
Jujur soal bagian yang masih kasar: vendor chip masih merilis SDK dan contoh mereka dalam C terlebih dulu, jadi untuk beberapa chip dukungan Rust lebih tipis atau hanya dari komunitas. Kamu tetap perlu membaca datasheet. Dan beberapa crate masih sebelum 1.0 dan mengubah API-nya antar-rilis, jadi selalu ikuti contoh untuk versi yang kamu pakai.
Apa yang bisa kamu buat
Section titled “Apa yang bisa kamu buat”- Sensor dan gadget: stasiun cuaca, alarm kelembapan tanah, penghitung langkah.
- Perangkat USB: keyboard custom, controller game, controller MIDI.
- Perangkat terhubung (IoT): sensor Wi-Fi atau Bluetooth dengan ESP32 yang mengirim hasil bacaan ke server.
- Robot dan motor: robot line follower, kontrol motor stepper dan servo, LED strip dan layar.
- Sistem serius: Oxide Computer menulis Hubris (bahasa Inggris), sistem operasi kecil untuk microcontroller, sepenuhnya dengan Rust. Espressif, perusahaan di balik ESP32, merawat dukungan Rust-nya sendiri dan menerbitkan The Rust on ESP Book (bahasa Inggris).
Board yang mudah untuk memulai
Section titled “Board yang mudah untuk memulai”Board mana pun di bawah ini bagus sebagai board pertama. Kalau bingung, ambil Raspberry Pi Pico: murah, dokumentasinya sangat lengkap, dan contoh Embassy di bawah memang ditulis untuk board ini.
| Board | Chip | Target Rust | Kenapa enak dipakai | Mulai dengan |
|---|---|---|---|---|
| Raspberry Pi Pico / Pico W | RP2040 (Arm Cortex-M0+) | thumbv6m-none-eabi |
Murah, populer, dukungan Embassy sangat bagus | embassy-rp, atau rp2040-hal + rp-pico |
| Raspberry Pi Pico 2 | RP2350 (Arm Cortex-M33, atau RISC-V) | thumbv8m.main-none-eabihf |
Lebih baru dan lebih cepat, punya floating-point unit | embassy-rp (feature rp235xa), rp235x-hal |
| Board ESP32-C3 / ESP32-C6 | Chip RISC-V dari Espressif | riscv32imc-unknown-none-elf / riscv32imac-unknown-none-elf |
Wi-Fi dan Bluetooth bawaan, dukungan Rust resmi | esp-hal dan tool esp-generate |
| STM32 Nucleo, “Black Pill” | STM32 (misalnya STM32F4) | thumbv7em-none-eabihf |
Keluarga chip besar yang dipakai di industri; board Nucleo punya debug probe bawaan | embassy-stm32, stm32f4xx-hal |
| BBC micro:bit v2 | nRF52833 (Arm Cortex-M4F) | thumbv7em-none-eabihf |
Debug probe, LED, tombol, dan sensor sudah ada di board | microbit-v2, plus buku Discovery |
micro:bit adalah board yang dipakai oleh Embedded Rust Discovery book resmi (bahasa Inggris), kursus gratis langkah demi langkah untuk pemula. ESP32 versi original dan ESP32-S3 memakai core Xtensa, bukan RISC-V; untuk chip itu, tool espup dari Espressif menginstal toolchain Rust khusus. LED di Pico W tersambung ke chip Wi-Fi-nya, bukan ke pin biasa, jadi butuh blinky yang berbeda (Embassy punya contoh wifi_blinky untuk itu).
Penjelasan no_std dan no_main
Section titled “Penjelasan no_std dan no_main”Program Rust biasa memakai standard library (std). std mengharapkan ada sistem operasi: file, thread, heap, terminal untuk println!. Microcontroller tidak punya semua itu. Karena itu program embedded dimulai dengan dua baris:
#![no_std] // jangan link standard library, hanya `core`#![no_main] // jangan pakai titik masuk `fn main()` yang biasa#![no_std]artinya kamu hanya dapatcore(bahasa Inggris): integer,Option,Result, iterator, slice, trait, formatting. Tidak adaVec,String,Box, file, atau thread. (Dengan sebuah allocator, kamu bisa memakai lagiVecdan kawan-kawannya lewat cratealloc, tapi banyak proyek tidak pernah membutuhkannya.)#![no_main]diperlukan karena di chip tidak ada sistem operasi yang memanggilmain. Crate runtime seperticortex-m-rtmenyiapkan memori setelah reset, lalu melompat ke fungsi milikmu, yang kamu tandai dengan atribut seperti#[cortex_m_rt::entry]atau, kalau memakai Embassy,#[embassy_executor::main].- Kamu juga harus menentukan apa yang terjadi saat panic, karena tidak ada terminal untuk mencetak pesan. Crate seperti
panic-probeataupanic-haltmenyediakan ini; kamu menambahkannya denganuse panic_probe as _;.
Kalau tidak ada Vec, bagaimana cara menyimpan daftar? Crate heapless memberimu Vec, String, dan antrean dengan kapasitas tetap yang ditentukan saat compile. Semuanya tinggal di stack atau di sebuah static, jadi tidak ada heap dan tidak ada kejutan kehabisan memori. Buffer yang penuh cukup mengembalikan nilaimu:
use core::fmt::Write;use heapless::{String, Vec};
fn main() { // Muat tepat 4 hasil bacaan. Memorinya bagian dari variabel itu sendiri: // tanpa heap, tanpa allocator, dan ukurannya diketahui saat compile. let mut readings: Vec<i32, 4> = Vec::new();
for value in [215, 218, 221, 224, 227] { match readings.push(value) { Ok(()) => println!("stored {value}"), Err(rejected) => println!("buffer full, dropped {rejected}"), } }
// String dengan kapasitas tetap 16 byte let mut line: String<16> = String::new(); write!(line, "avg={}", readings.iter().sum::<i32>() / readings.len() as i32).unwrap(); println!("{line} (capacity {})", line.capacity());}heapless juga bisa dipakai di PC, jadi kamu bisa mencobanya dengan cargo add [email protected] dan cargo run (crate ini tidak ada di Playground):
stored 215stored 218stored 221stored 224buffer full, dropped 227avg=219 (capacity 16)Ekosistem dalam lapisan-lapisan
Section titled “Ekosistem dalam lapisan-lapisan”Crate embedded Rust bertumpuk satu di atas yang lain. Kebanyakan kamu akan bekerja di level HAL atau framework, tapi ada baiknya tahu apa yang ada di bawahnya:
| Lapisan | Apa itu | Contoh |
|---|---|---|
| PAC (Peripheral Access Crate) | Dibuat otomatis dari file deskripsi register chip. Satu fungsi yang lumayan aman per register dan field. Sangat low-level. | rp2040-pac, stm32f4, esp32c3 |
| HAL (Hardware Abstraction Layer) | Tipe-tipe yang ramah untuk satu keluarga chip: Output, Uart, I2c, timer. Dibangun di atas PAC. |
rp2040-hal, esp-hal, stm32f4xx-hal, embassy-rp, embassy-stm32 |
Trait embedded-hal |
Trait bersama seperti OutputPin, I2c, SpiDevice, dan DelayNs. HAL mengimplementasikannya; driver memakainya. Versi 1.0 sudah stabil. |
embedded-hal, embedded-hal-async |
| BSP (Board Support Package) | Sebuah HAL plus nama untuk pin dan komponen satu board tertentu (“LED-nya”, “tombol A”). | rp-pico, microbit-v2 |
| RTIC | Framework untuk aplikasi berbasis interrupt. Task terikat ke interrupt dan prioritas, dan compiler mengecek data bersama saat build. | rtic |
| Embassy | Framework async: executor, timer, networking, dan USB, plus HAL miliknya sendiri. Kamu menulis task async fn dan melakukan .await pada event hardware. |
embassy-executor, embassy-time, embassy-rp |
Keajaibannya ada di baris tengah. Driver sensor suhu yang ditulis untuk trait I2c dari embedded-hal bisa jalan di Pico, ESP32, dan STM32 tanpa diubah, karena setiap HAL mengimplementasikan trait yang sama. Kamu akan menulis driver seperti itu sendiri di bagian bawah.
Instal target dan probe-rs
Section titled “Instal target dan probe-rs”PC-mu punya prosesor desktop x86 atau Arm; Pico punya Cortex-M0+. Jadi kamu melakukan cross-compile: build di satu mesin, untuk mesin lain. rustup mengunduh library core yang sudah di-compile untuk chip tersebut:
rustup target add thumbv6m-none-eabi # RP2040 (Pico)rustup target add thumbv8m.main-none-eabihf # RP2350 (Pico 2)rustup target add thumbv7em-none-eabihf # micro:bit v2, STM32F4Untuk menyalin program ke chip dan melihat log-nya, kamu butuh probe-rs dan sebuah debug probe: perangkat USB kecil yang tersambung ke pin debug chip. Board micro:bit dan STM32 Nucleo sudah punya probe bawaan. Untuk Pico, pakai Raspberry Pi Debug Probe, atau Pico kedua yang di-flash dengan firmware gratis debugprobe. Instal probe-rs dengan script installer-nya (lihat halaman instalasi (bahasa Inggris) untuk Windows) atau dengan cargo:
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.sh | sh# atau, lebih lambat, dari source:cargo install probe-rs-tools --locked
probe-rs list # menampilkan probe yang terdeteksiBelum punya probe? Tahan tombol BOOTSEL di Pico sambil mencolokkannya, dan Pico akan muncul sebagai USB drive. Tool seperti picotool atau elf2uf2-rs bisa menyalin programmu ke sana dengan cara itu. Tapi tanpa probe, kamu tidak akan bisa melihat log defmt.
Di ESP32-C3 atau C6 biasanya kamu sama sekali tidak butuh probe: chip-nya punya USB bawaan, dan tool espflash mem-flash-nya lewat kabel USB biasa. Jalankan cargo install esp-generate espflash --locked, lalu esp-generate, pilih chip dan opsimu, dan cargo run --release akan mem-build, mem-flash, dan menampilkan log-nya.
Blinky dengan Embassy di Raspberry Pi Pico
Section titled “Blinky dengan Embassy di Raspberry Pi Pico”“Blinky” adalah “Hello, world!”-nya dunia embedded: nyalakan LED, tunggu, matikan, tunggu, selamanya. Ini adalah contoh blinky RP2040 resmi dari Embassy (dengan tambahan komentar). Kode ini butuh board sungguhan dan tidak bisa dijalankan di Playground.
#![no_std]#![no_main]
use defmt::*;use defmt_rtt as _; // kirim log defmt ke probeuse embassy_executor::Spawner;use embassy_rp::gpio;use embassy_time::Timer;use gpio::{Level, Output};use panic_probe as _; // saat panic: cetak pesannya lewat probe, lalu berhenti
#[embassy_executor::main(executor = "embassy_rp::executor::Executor", entry = "cortex_m_rt::entry")]async fn main(_spawner: Spawner) { // Ambil ownership semua peripheral di chip, tepat satu kali let p = embassy_rp::init(Default::default()); // LED di Pico ada di pin 25. PIN_25 dipindahkan (move) ke `led`. let mut led = Output::new(p.PIN_25, Level::Low);
loop { info!("led on!"); led.set_high(); Timer::after_millis(500).await;
info!("led off!"); led.set_low(); Timer::after_millis(500).await; }}Beberapa hal yang perlu diperhatikan:
mainadalahasync fn.Timer::after_millis(500).awaittidak berputar-putar di loop sibuk; executor menidurkan chip sampai timer berbunyi. Kalau ada lebih banyak task (di-spawn lewatSpawner), task lain berjalan selagi task ini menunggu. Kalau async/await masih baru buatmu, idenya sama seperti Tokio di PC, hanya saja tanpa sistem operasi.p.PIN_25dipindahkan (move) keOutput::new. Coba pakaip.PIN_25untuk kedua kalinya, dan compiler akan menghentikanmu: ownership, diterapkan ke hardware.use defmt_rtt as _;danuse panic_probe as _;meng-import crate hanya untuk hal yang mereka siapkan (saluran log dan panic handler), bukan untuk nama apa pun.
Cara tercepat untuk melihatnya berkedip adalah folder contoh resminya sendiri: clone repository Embassy, lalu di examples/rp jalankan cargo run --bin blinky --release. Untuk proyekmu sendiri, dependency-nya diambil dari Cargo.toml milik contoh itu:
[package]name = "blinky"version = "0.1.0"edition = "2024"
[dependencies]embassy-executor = { version = "0.10.0", features = ["defmt"] }embassy-time = { version = "0.5.1", features = ["defmt", "defmt-timestamp-uptime"] }embassy-rp = { version = "0.10.0", features = ["defmt", "unstable-pac", "time-driver", "critical-section-impl", "rp2040", "executor-thread"] }defmt = "1.0.1"defmt-rtt = "1.0.0"panic-probe = { version = "1.0.0", features = ["print-defmt"] }cortex-m = { version = "0.7.6", features = ["inline-asm"] }cortex-m-rt = "0.7.0"
[profile.release]debug = true # simpan info debug untuk probe-rs; info ini tidak ikut di-flash ke chipDua file lagi memberi tahu cargo apa yang harus di-build dan bagaimana menjalankannya. .cargo/config.toml menentukan target dan membuat cargo run mem-flash lewat probe-rs:
[target.'cfg(all(target_arch = "arm", target_os = "none"))']runner = "probe-rs run --chip RP2040"
[build]target = "thumbv6m-none-eabi"
[env]DEFMT_LOG = "debug"Terakhir, salin memory.x (di mana letak flash dan RAM di chip) dan build.rs (yang meneruskan linker script) dari folder examples/rp yang sama. Lalu colokkan probe-mu dan jalankan:
cargo run --releasecargo mem-build untuk thumbv6m-none-eabi, probe-rs mem-flash chip, LED mulai berkedip, dan pesan led on! / led off! mengalir ke terminalmu lengkap dengan timestamp.
Logging dengan defmt
Section titled “Logging dengan defmt”Microcontroller tidak punya layar, dan memformat teks dengan core::fmt memakan banyak ruang flash. defmt (“deferred formatting”) menyelesaikan keduanya. String format disimpan di PC-mu, bukan di chip; chip hanya mengirim ID kecil plus nilai mentahnya lewat probe, lalu probe-rs mengubahnya kembali menjadi teks yang bisa dibaca.
use defmt::{info, warn, Format};
#[derive(Format)] // seperti #[derive(Debug)], tapi untuk defmtpub struct Reading { pub sensor_id: u8, pub milli_celsius: i32,}
pub fn report(r: &Reading) { info!("new reading: {}", r); if r.milli_celsius > 60_000 { // `=u8` dan `=i32` memberi tahu defmt tipe persisnya, sehingga lebih hemat ruang lagi warn!("sensor {=u8} is too hot: {=i32} mC", r.sensor_id, r.milli_celsius); }}Level-nya adalah trace!, debug!, info!, warn!, dan error!, sama seperti crate log. Pengaturan DEFMT_LOG di .cargo/config.toml menentukan level mana yang ikut di-compile; level di bawahnya sama sekali tidak memakan byte.
Berpikir dalam register: bit dan mask
Section titled “Berpikir dalam register: bit dan mask”Di bawah setiap HAL, mengendalikan hardware berarti menulis angka ke register: alamat memori khusus di mana setiap bit menyalakan atau mematikan sesuatu di chip. Pada chip pada umumnya, register output GPIO 32-bit punya satu bit per pin. Di sini sebuah u32 biasa berperan sebagai register itu, supaya kamu bisa melatih empat gerakan yang dipakai setiap developer embedded:
const LED: u32 = 25; // LED bawaan Pico ada di pin 25
fn main() { // Anggap u32 ini adalah register output GPIO milik chip. // Bit 0 mengendalikan pin 0, bit 1 mengendalikan pin 1, dan seterusnya. let mut gpio_out: u32 = 0b1000; // pin 3 sudah menyala
// Set satu bit: OR dengan mask gpio_out |= 1 << LED; println!("LED on: {gpio_out:#034b}");
// Baca satu bit: AND dengan mask, lalu bandingkan dengan nol let led_is_on = (gpio_out & (1 << LED)) != 0; let pin2_is_on = (gpio_out & (1 << 2)) != 0; println!("LED on? {led_is_on}, pin 2 on? {pin2_is_on}");
// Balik (toggle) satu bit: XOR dengan mask gpio_out ^= 1 << LED; println!("toggled: {gpio_out:#034b}");
// Hapus (clear) satu bit: AND dengan mask yang dibalik gpio_out |= 1 << LED; gpio_out &= !(1 << 3); println!("pin 3 off: {gpio_out:#034b}"); println!("as hex: {gpio_out:#010x}");}Output:
LED on: 0b00000010000000000000000000001000LED on? true, pin 2 on? falsetoggled: 0b00000000000000000000000000001000pin 3 off: 0b00000010000000000000000000000000as hex: 0x02000000Label di output sengaja dibiarkan dalam bahasa Inggris, karena spasinya disusun supaya angka-angka binernya rata satu sama lain. Di Rust, ! pada integer membalik setiap bit (C memakai ~ untuk ini). {:#034b} mencetak nilai dalam biner dengan awalan 0b dan padding sampai 32 digit, jadi kamu bisa melihat bit 25 muncul dan hilang. Lihat Operator di Rust untuk daftar lengkap operator bitwise.
Banyak register mengemas beberapa field dalam satu angka: beberapa bit untuk pengaturan kecepatan, satu bit untuk “aktif”. Untuk mengubah satu field, kamu melakukan read-modify-write: hapus bit-bit milik field itu, lalu OR-kan nilai yang baru, dan biarkan semua bit lain apa adanya:
// Register kontrol UART karangan:// bit 0 = enable// bit 4..=6 = pengaturan kecepatan (angka 3-bit, 0 sampai 7)const ENABLE: u32 = 1 << 0;const SPEED_SHIFT: u32 = 4;const SPEED_MASK: u32 = 0b111 << SPEED_SHIFT;
fn read_speed(reg: u32) -> u32 { (reg & SPEED_MASK) >> SPEED_SHIFT}
fn write_speed(reg: u32, speed: u32) -> u32 { // Hapus field yang lama, lalu masukkan nilai yang baru. // Semua bit lain tetap persis seperti sebelumnya. (reg & !SPEED_MASK) | ((speed << SPEED_SHIFT) & SPEED_MASK)}
fn main() { let mut ctrl: u32 = ENABLE | (2 << SPEED_SHIFT); println!("start: {ctrl:#010b}, speed = {}", read_speed(ctrl));
ctrl = write_speed(ctrl, 5); println!("speed 5: {ctrl:#010b}, speed = {}", read_speed(ctrl));
// 9 tidak muat dalam 3 bit. Mask memotongnya alih-alih // membiarkannya meluber ke bit-bit tetangga. ctrl = write_speed(ctrl, 9); println!("speed 9?: {ctrl:#010b}, speed = {}", read_speed(ctrl));
let still_enabled = (ctrl & ENABLE) != 0; println!("still enabled: {still_enabled}");}Output:
start: 0b00100001, speed = 2speed 5: 0b01010001, speed = 5speed 9?: 0b00010001, speed = 1still enabled: truePerhatikan bahwa 9 diam-diam berubah menjadi 1. Bug seperti inilah yang dicegah oleh PAC dan HAL: mereka memberi setiap field method-nya sendiri yang bernama (sering kali dengan nilai-nilai bernama juga), jadi kamu jarang menulis mask sendiri. Di hardware sungguhan, register adalah alamat memori yang tetap, yang ditulis dengan core::ptr::write_volatile di dalam kode unsafe; PAC mengerjakan bagian itu untukmu.
Tulis driver yang bisa diuji di PC
Section titled “Tulis driver yang bisa diuji di PC”Kebiasaan embedded terbaik yang bisa kamu pelajari: tulis logikamu berdasarkan trait, bukan berdasarkan satu chip. Dengan begitu kode yang sama berjalan di board dengan HAL sungguhan, dan di PC-mu dengan komponen palsu (“mock”) di dalam test biasa. Di sini, StatusLed hanya butuh “sesuatu yang bisa kunyalakan dan kumatikan” dan “sesuatu yang bisa menunggu”:
use std::cell::RefCell;use std::rc::Rc;
// Dua kemampuan yang dibutuhkan driver kedip. Keduanya meniru trait OutputPin// dan DelayNs dari embedded-hal 1.0, disederhanakan supaya bisa jalan di Playground.trait OutputPin { fn set_high(&mut self); fn set_low(&mut self);}
trait Delay { fn delay_ms(&mut self, ms: u32);}
// Driver ini tidak tahu apa-apa soal chip mana pun. Ia hanya memakai trait.struct StatusLed<P: OutputPin, D: Delay> { pin: P, delay: D,}
impl<P: OutputPin, D: Delay> StatusLed<P, D> { // Berkedip `times` kali: nyala selama `on_ms`, lalu mati selama `off_ms`. fn blink(&mut self, times: u32, on_ms: u32, off_ms: u32) { for _ in 0..times { self.pin.set_high(); self.delay.delay_ms(on_ms); self.pin.set_low(); self.delay.delay_ms(off_ms); } }}
// --- Komponen palsu untuk testing di PC ---// Keduanya menulis ke log bersama yang sama, jadi kita bisa melihat urutannya.type Log = Rc<RefCell<Vec<String>>>;
struct MockPin { log: Log,}
impl OutputPin for MockPin { fn set_high(&mut self) { self.log.borrow_mut().push("on".to_string()); } fn set_low(&mut self) { self.log.borrow_mut().push("off".to_string()); }}
struct MockDelay { log: Log,}
impl Delay for MockDelay { fn delay_ms(&mut self, ms: u32) { self.log.borrow_mut().push(format!("wait {ms}")); }}
fn main() { let log: Log = Rc::new(RefCell::new(Vec::new())); let pin = MockPin { log: Rc::clone(&log) }; let delay = MockDelay { log: Rc::clone(&log) };
let mut led = StatusLed { pin, delay }; led.blink(2, 100, 400);
let events = log.borrow(); println!("{}", events.join(", "));
assert_eq!(events.len(), 8); assert_eq!(events[0], "on"); assert_eq!(events[1], "wait 100"); println!("all checks passed");}Output:
on, wait 100, off, wait 400, on, wait 100, off, wait 400all checks passedKedua mock berbagi satu log lewat Rc<RefCell<...>> (lihat Smart pointer), jadi kamu bisa mengecek urutan event-nya dengan tepat. Di proyek sungguhan, baris-baris assert_eq! itu masuk ke fungsi #[test] dan dijalankan dengan cargo test di PC-mu, tanpa butuh board (lihat Testing di Rust dan Pengujian lanjutan).
Di board, kamu akan menulis driver yang sama berdasarkan trait embedded-hal 1.0 yang asli. Perubahannya hanya ini: method pin mengembalikan Result (pin di chip I/O expander bisa gagal), jadi kamu meneruskan error dengan ?, dan kodenya sama sekali tidak butuh std:
#![no_std]
use embedded_hal::delay::DelayNs;use embedded_hal::digital::OutputPin;
pub struct StatusLed<P, D> { pin: P, delay: D,}
impl<P: OutputPin, D: DelayNs> StatusLed<P, D> { pub fn new(pin: P, delay: D) -> Self { StatusLed { pin, delay } }
pub fn blink(&mut self, times: u32, on_ms: u32, off_ms: u32) -> Result<(), P::Error> { for _ in 0..times { self.pin.set_high()?; self.delay.delay_ms(on_ms); self.pin.set_low()?; self.delay.delay_ms(off_ms); } Ok(()) }}Output dari Embassy mengimplementasikan OutputPin, dan embassy_time::Delay mengimplementasikan DelayNs, jadi StatusLed::new(led, embassy_time::Delay) bisa dipakai di Pico. Untuk test di PC dengan trait asli ini, crate embedded-hal-mock menyediakan pin, bus I2C, dan bus SPI palsu yang siap pakai.
Matematika sensor tanpa float
Section titled “Matematika sensor tanpa float”Banyak chip kecil, seperti Cortex-M0+ di RP2040, tidak punya floating-point unit. Float tetap bisa dipakai, tapi dikerjakan oleh software: lebih lambat, dan membuat programmu lebih besar. Karena itu kode embedded sering memakai angka fixed-point: integer dalam satuan yang lebih kecil. Alih-alih 25.25 derajat, kamu menyimpan 25250 miliderajat.
Sensor suhu bergaya TMP102 mengirim dua byte. 12 bit teratas adalah angka bertanda (signed) dalam langkah 1/16 °C:
// 12 bit teratas adalah suhu dalam langkah 1/16 °C (0,0625 °C),// sebagai angka bertanda (two's complement).fn to_millicelsius(bytes: [u8; 2]) -> i32 { // i16 menjaga tanda. `>> 4` pada angka bertanda juga menjaganya. let raw = i16::from_be_bytes(bytes) >> 4; // 1/16 °C = 62,5 m°C, jadi kalikan dengan 625 lalu bagi dengan 10. raw as i32 * 625 / 10}
// Cetak 25250 sebagai "25.250" tanpa pernah menyentuh float.fn format_milli(m: i32) -> String { let sign = if m < 0 { "-" } else { "" }; let m = m.abs(); format!("{sign}{}.{:03}", m / 1000, m % 1000)}
fn main() { let samples = [ [0x19, 0x40], // ruangan yang hangat [0x00, 0x10], // langkah terkecil di atas nol [0xFF, 0x80], // sedikit di bawah titik beku [0xE7, 0x00], // freezer ];
for bytes in samples { let m = to_millicelsius(bytes); println!("{bytes:02X?} -> {m:>6} m°C = {} °C", format_milli(m)); }}Output:
[19, 40] -> 25250 m°C = 25.250 °C[00, 10] -> 62 m°C = 0.062 °C[FF, 80] -> -500 m°C = -0.500 °C[E7, 00] -> -25000 m°C = -25.000 °CAda tiga detail yang membuat ini berhasil. i16::from_be_bytes membaca dua byte itu sebagai satu angka bertanda, byte paling signifikan lebih dulu (“big-endian”), yang memang cara kebanyakan sensor mengirim data. >> 4 pada integer bertanda menjaga tandanya, jadi suhu negatif keluar dengan benar. Dan format_milli menangani tanda secara terpisah: -500 / 1000 hasilnya 0 dalam aritmetika integer, jadi tanpa langkah itu -0.5 °C akan tercetak sebagai 0.500. (Program ini memakai titik sebagai pemisah desimal, seperti kebiasaan di dunia pemrograman.)
Pembagian integer juga memotong (truncate): 62,5 m°C menjadi 62. Setengah miliderajat tidak masalah untuk sensor ruangan, tapi selalu tanyakan pada dirimu sendiri seberapa presisi sebuah satuan. Dan perhatikan jangkauannya: mengalikan bacaan besar dengan faktor besar bisa membuat i32 overflow (lihat Integer overflow di Rust). Di PC, String tidak masalah; di chip, kamu akan mencetak dengan defmt atau menulis ke heapless::String.
Kesalahan umum
Section titled “Kesalahan umum”Memakai fitur std di program no_std. println!, Vec, dan String berasal dari std, jadi memang tidak tersedia:
error: cannot find macro `println` in this scope --> src/main.rs:4:5 |4 | println!("hello"); | ^^^^^^^Pakai defmt::info! untuk logging, dan heapless (atau crate alloc dengan sebuah allocator) untuk koleksi.
Lupa panic handler. Binary no_std harus menentukan apa yang dilakukan saat panic. Tanpa crate seperti panic-probe, kamu akan mendapat:
error: `#[panic_handler]` function required, but not foundTambahkan panic-probe (atau panic-halt) ke Cargo.toml dan use panic_probe as _; ke main.rs.
Menimpa seluruh register. reg = 1 << 3 menyalakan pin 3, dan mematikan semua pin lain. Untuk mengubah satu bit, pakai reg |= 1 << 3. Untuk field, lakukan read-modify-write seperti di atas.
Mengikuti tutorial lama. Crate embedded berkembang cepat, dan tulisan blog dari beberapa tahun lalu sering memakai embedded-hal 0.2 atau API Embassy yang lebih lama. Mulailah dari folder contoh milik versi crate yang benar-benar kamu pakai.
Latihan
Section titled “Latihan”1. Pin mana yang menyala?
Section titled “1. Pin mana yang menyala?”Tulis toggle(reg: u32, pin: u32) -> u32, yang membalik satu bit, dan pins_on(reg: u32) -> Vec<u32>, yang mendaftar setiap pin yang bit-nya 1. Mulai dengan pin 0, 5, dan 25 menyala, cetak daftarnya dan berapa banyak pin yang menyala, lalu toggle pin 5 dan 7 dan cetak lagi. Petunjuk: uji setiap pin dari 0 sampai 31 dengan sebuah mask, dan u32 punya method count_ones().
Lihat solusi
fn toggle(reg: u32, pin: u32) -> u32 { reg ^ (1 << pin)}
fn pins_on(reg: u32) -> Vec<u32> { (0..32).filter(|pin| reg & (1 << pin) != 0).collect()}
fn main() { let mut reg: u32 = (1 << 0) | (1 << 5) | (1 << 25); println!("menyala: {:?} ({} pin)", pins_on(reg), reg.count_ones());
reg = toggle(reg, 5); reg = toggle(reg, 7); println!("menyala: {:?} ({} pin)", pins_on(reg), reg.count_ones());}Output:
menyala: [0, 5, 25] (3 pin)menyala: [0, 7, 25] (3 pin)XOR membalik sebuah bit apa pun nilai lamanya, jadi toggle pin 5 mematikannya dan toggle pin 7 menyalakannya. count_ones() menghitung bit yang bernilai 1 untukmu, jadi kamu tidak butuh loop untuk bagian itu.
2. Level baterai tanpa float
Section titled “2. Level baterai tanpa float”ADC 12-bit memberi bacaan dari 0 sampai 4095 untuk 0 sampai 3300 mV. Baterainya tersambung lewat pembagi tegangan (voltage divider) yang membagi dua tegangannya, jadi tegangan sebenarnya adalah dua kali tegangan yang terukur. Tulis battery_mv(raw: u16) -> i32 dan battery_percent(mv: i32) -> i32, di mana 3000 mV adalah 0% dan 4200 mV adalah 100%, tidak pernah di bawah 0 atau di atas 100. Coba bacaan mentah 2606, 2482, 2234, dan 1861. Petunjuk: kalikan dulu sebelum membagi, supaya presisinya tidak hilang, dan lihat clamp.
Lihat solusi
const EMPTY_MV: i32 = 3000;const FULL_MV: i32 = 4200;
fn battery_mv(raw: u16) -> i32 { // ADC 12-bit (0..=4095) mengukur 0..=3300 mV, di belakang pembagi yang membagi dua tegangan raw as i32 * 3300 * 2 / 4095}
fn battery_percent(mv: i32) -> i32 { let percent = (mv - EMPTY_MV) * 100 / (FULL_MV - EMPTY_MV); percent.clamp(0, 100)}
fn main() { for raw in [2606, 2482, 2234, 1861] { let mv = battery_mv(raw); println!("mentah {raw} -> {mv} mV -> {}%", battery_percent(mv)); }}Output:
mentah 2606 -> 4200 mV -> 100%mentah 2482 -> 4000 mV -> 83%mentah 2234 -> 3600 mV -> 50%mentah 1861 -> 2999 mV -> 0%Membagi dengan 4095 di akhir menjaga presisi: raw / 4095 * 6600 akan bernilai 0 untuk setiap bacaan di bawah 4095. Hasil perkalian terbesar, 4095 * 6600, sekitar 27 juta, yang masih muat dengan mudah di i32.
Ringkasan
Section titled “Ringkasan”- Rust embedded berjalan di microcontroller tanpa sistem operasi. Tanpa garbage collector, keamanan saat compile, dan zero-cost abstraction membuat Rust sangat cocok untuk ini.
#![no_std]hanya menyisakancore;#![no_main]membiarkan crate runtime memanggil fungsi masukmu; kamu wajib memilih panic handler.heaplessmemberimu koleksi berkapasitas tetap.- Lapisan-lapisannya: PAC (register), HAL (satu keluarga chip), trait
embedded-hal(bersama), BSP (satu board), dan framework seperti RTIC dan Embassy di atasnya. - Tambahkan target chip dengan
rustup target add, lalu flash dan lihat log dengan probe-rs dan debug probe (atauespflashdi ESP32).defmtmembuat logging tetap mungil. - Berlatihlah di PC: bit mask untuk register, driver yang ditulis berdasarkan trait dengan komponen mock, dan integer fixed-point alih-alih float.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Apa fungsi #![no_std]?
2. Lapisan mana yang membuat satu driver sensor bisa bekerja di Pico, ESP32, dan STM32 tanpa diubah?
3. Bagaimana cara menyalakan bit 3 dari reg tanpa mengubah bit lain?
4. Kenapa kode embedded sering menyimpan 25,25 °C sebagai 25250 miliderajat?
Skor: 0 / 4
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- The Embedded Rust Book (bahasa Inggris): pengantar resmi tentang
no_std, peripheral, register, dan ekosistem embedded. - Discovery (micro:bit v2) (bahasa Inggris): kursus pemula gratis yang membawamu dari LED berkedip sampai sensor, langkah demi langkah, di BBC micro:bit.
- The Embassy Book (bahasa Inggris): cara memulai dengan Embassy, cara kerja executor dan HAL-nya, dan cara menjalankan contoh resminya.
- The Rust on ESP Book (bahasa Inggris): panduan resmi Espressif tentang
esp-hal,esp-generate,espflash, dan toolchain ESP32. - Instalasi probe-rs (bahasa Inggris): cara menginstal probe-rs di setiap sistem operasi dan menyiapkan debug probe-mu.