Šta treba znati, šta treba razumjeti, i šta može čekati — iskren vodič za junior embedded programera.
80% posla u praksi. Bez ovoga nema junior levela. Ovo je core.
Razlika između juniora i jačeg juniora. Koristi se, ali ne svaki dan.
Nice to know, ali ne must know. Dolazi sa iskustvom, ne studijem.
GPIO (General Purpose Input/Output) je osnova svega u embedded programiranju. Kontrolišeš fizičke pinove MCU-a — svjetiš LED, čitaš stanje dugmeta, aktiviraš relej. U Embassy-ju ovo radi async i bezbjedno.
// Osnovna LED + dugme aplikacija na RP2040 use embassy_rp::gpio::{Input, Output, Level, Pull}; #[embassy_executor::main] async fn main(spawner: Spawner) { let p = embassy_rp::init(Default::default()); // Pin 25 = onboard LED na Pico let mut led = Output::new(p.PIN_25, Level::Low); // Dugme na pinu 15, s pull-up (aktivno-nisko) let button = Input::new(p.PIN_15, Pull::Up); loop { if button.is_low() { // dugme pritisnuto led.set_high(); } else { led.set_low(); } } }
Pull::Up ili Pull::Down.
Timers su fundamentalni za skoro sve u embedded-u: debounce dugmeta, periodični taskovi, timeout za komunikaciju, PWM generisanje, tajming senzora. Embassy koristi async Timer::after() koji ne blokira executor — CPU može raditi nešto drugo dok čeka.
Timer::after(Duration) — async čekanjeloop + Timeruse embassy_time::{Timer, Duration, Ticker}; // Primjer 1: Jednostavni blink loop { led.set_high(); Timer::after(Duration::from_millis(500)).await; led.set_low(); Timer::after(Duration::from_millis(500)).await; } // Primjer 2: Ticker - precizno svakih 100ms let mut ticker = Ticker::every(Duration::from_millis(100)); loop { ticker.next().await; // ovo se zove tačno svakih 100ms, bez drift-a read_sensor().await; } // Primjer 3: Debounce dugmeta if button.is_low() { Timer::after(Duration::from_millis(20)).await; // čekaj bounce if button.is_low() { // i dalje pritisnuto = pravo handle_button_press().await; } }
cortex_m::delay::Delay ili klasično for _ in 0..1000 {} blokira CPU u potpunosti. Embassy-jev Timer::after().await oslobađa executor da radi druge taskove. Uvijek preferuj async.
Timer::after(1000ms) i čitanje senzora traje 50ms — da li ćeš stvarno čitati svakih 1000ms? Šta bi riješilo ovaj problem preciznije?
UART je serijska komunikacija koja ti omogućava da šalješ debug poruke s MCU-ja na računar. RTT (Real-Time Transfer) je moderniji pristup koji koristi SWD debug interfejs — brži je i ne treba UART pinove. Oba su nezamjenljiva za debug.
use defmt::*; // RTT logging makroi use defmt_rtt as _; // RTT transport // Logging nivo: info, warn, error, debug, trace info!("Program startao!"); info!("Temperatura: {} stepeni", temp); warn!("Napon prenizak: {}mV", voltage); error!("I2C greška: {:?}", err); // UART primjer (ako nemaš probe-rs) use embassy_rp::uart::{Uart, Config}; let mut uart = Uart::new( p.UART0, p.PIN_0, p.PIN_1, // TX, RX Irqs, p.DMA_CH0, p.DMA_CH1, Config::default() // 115200 baud default ); uart.write(b"Hello from RP2040!\r\n").await.unwrap();
cargo embed. Instalacija: cargo install probe-rs --features cli
defmt feature flags i kako Rust "uklanja" mrtav kod u release modu.I2C (Inter-Integrated Circuit) je protokol s dvije žice: SDA (data) i SCL (clock). Jedan master (RP2040) komunicira s više slave uređaja, svaki ima jedinstven 7-bit adresu. Koriste ga OLED displavi, RTC moduli, senzori temperature, EEPROM — gotovo sve u hobby i industrijskim projektima.
use embassy_rp::i2c::{I2c, Config}; let i2c = I2c::new_async( p.I2C1, p.PIN_3, // SCL p.PIN_2, // SDA Irqs, Config::default() // 100kHz standard mode ); const BME280_ADDR: u8 = 0x76; // iz datasheet-a! // Čitanje ID registra (0xD0) — trebao bi vratiti 0x60 let mut buf = [0u8; 1]; i2c.write_read(BME280_ADDR, &[0xD0], &mut buf).await?; info!("BME280 chip ID: {:#04x}", buf[0]); // treba biti 0x60 // Write: postavi register 0xF2 (humidity control) i2c.write(BME280_ADDR, &[0xF2, 0x01]).await?;
SPI (Serial Peripheral Interface) je brži od I2C i koristi 4 žice: MOSI, MISO, SCK i CS (Chip Select). Nema adresiranje — umjesto toga, svaki uređaj ima vlastiti CS pin koji se povuče LOW da "aktivira" taj uređaj. Koristi ga za TFT displaje, SD kartice, ADC čipove, flash memoriju.
use embassy_rp::spi::{Spi, Config}; use embassy_rp::gpio::{Output, Level}; let mut spi = Spi::new( p.SPI0, p.PIN_18, p.PIN_19, p.PIN_16, // SCK, MOSI, MISO p.DMA_CH0, p.DMA_CH1, Config::default() ); // CS pin ručno kontrolišemo let mut cs = Output::new(p.PIN_17, Level::High); // Tipična SPI transakcija cs.set_low(); // aktiviraj uređaj let tx_data = [0x9F]; // JEDEC ID komanda let mut rx_data = [0u8; 3]; spi.transfer(&mut rx_data, &tx_data).await?; cs.set_high(); // deaktiviraj info!("Flash ID: {:?}", rx_data);
Za embedded Rust, ne trebaš biti Rust ekspert — ali moraš razumjeti osnove ownership-a i zašto Rust sprječava određene greške koje su u C-u fatalne za embedded sisteme. Embassy koristi async/await koji čini concurrent taskove čitljivim.
&T i &mut T — pozajmljivanje referencistruct za grupiranje podatakaasync fn i .await osnoveResult i ? operator za greškeimpl Trait — generički interfejsi// 1. Result i error handling — embedded stil async fn read_temperature(i2c: &mut I2c) -> Result<f32, I2cError> { let mut buf = [0u8; 2]; i2c.read(SENSOR_ADDR, &mut buf).await?; // ? = vrati grešku Ok(raw_to_celsius(buf)) } // 2. Struct za stanje sistema struct SensorData { temperature: f32, humidity: f32, timestamp: u64, } // 3. Embassy: dva concurrent taska #[embassy_executor::task] async fn blink_task(mut led: Output<'static>) { loop { led.toggle(); Timer::after_millis(500).await; } } // Main spawnuje taskove — rade "paralelno" spawner.spawn(blink_task(led)).unwrap();
.await?
Na junior intervjuu za embedded, ne testiraju te koliko znaš arhitekturu MCU-a. Testiraju koliko brzo možeš pročitati datasheet, spojiti senzor, pronaći bug, i koristiti debug log. Ovo je zapravo najvažnija vještina — i rijetko se eksplicitno uči.
Svaka funkcija, svaki state change. info!("Ulazim u loop, i={}", i) — precizno.
Testiraj jednu komponentu posebno. Radi li I2C sam? Radi li display sam? Kombiniraj kad svako radi.
Provjeri adresu, registre, timing zahtjeve. 80% bug-ova je u pogrešnom razumijevanju datasheet-a.
Napon? Mase spojene? Pull-up otpornici? Logic analyzerom provjeri SDA/SCL signal. Multimetar je tvoj prijatelj.
PWM generira pravokutni signal promjenjivog duty cycle-a. MCU ne može izlaziti analogni napon, ali brzo uključivanje/isključivanje pina efektivno "simulira" međuvrijednosti. Servo motori čekaju PWM na 50Hz s duty 1–2ms za poziciju.
use embassy_rp::pwm::{Pwm, Config}; let mut pwm = Pwm::new_output_b(p.PWM_CH4, p.PIN_25, Config::default()); // Postavi top na 10000 (period) — svaki wrap = 1 period let mut cfg = Config::default(); cfg.top = 10_000; cfg.compare_b = 5_000; // 50% duty = polovina brightness pwm.set_config(&cfg); // LED fade loop: 0% do 100% for duty in (0..=10_000).step_by(100) { cfg.compare_b = duty; pwm.set_config(&cfg); Timer::after_millis(5).await; }
Interrupt je mehanizam kojim hardver "prekida" CPU i kaže "nešto se desilo — reaguj odmah". Bez interrupata, CPU mora stalno provjeravati (polling) — gubi energiju i propusti brze događaje. Embassy koristi interrupte internost — input.wait_for_low().await ne blokira CPU, čeka interrupt.
wait_for_rising_edge() i sl.// Umjesto ručnog interrupt handlera, Embassy nudi: let mut button = Input::new(p.PIN_15, Pull::Up); loop { // Čeka interrupt (falling edge) — CPU slobodan za drugi posao button.wait_for_falling_edge().await; info!("Dugme pritisnuto!"); // Čeka da se otpusti button.wait_for_rising_edge().await; info!("Dugme otpušteno!"); }
DMA je hardverski modul koji prenosi podatke između memorije i periferala direktno, bez intervencije CPU-a. Dok DMA prenosi 4096 bajtova SPI podataka za display, CPU može raditi kalkulacije ili spavati. Važno je razumjeti KADA i ZAŠTO koristiti DMA — ne i svaki detalj implementacije.
Embassy je async executor za embedded. Umjesto OS-a koji scheduler dodjeljuje CPU taskovima, Embassy koristi Rust async mehanizam — taskovi sami predaju kontrolu na .await pointu. Ovo je cooperative multitasking s Rust-ovom type safety garancijom bez heap allocacije.
spawner.spawn()use embassy_sync::channel::{Channel, Sender, Receiver}; use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex; static CHANNEL: Channel<ThreadModeRawMutex, SensorData, 8> = Channel::new(); #[embassy_executor::task] async fn sensor_task(sender: Sender<...>) { loop { let data = read_sensor().await; sender.send(data).await; // šalje u channel Timer::after_secs(1).await; } } #[embassy_executor::task] async fn display_task(receiver: Receiver<...>) { loop { let data = receiver.receive().await; // čeka podatke update_display(data).await; } }
.await, šta se dešava s ostalim taskovima? Zašto ovo može biti opasno za real-time sisteme? Kako bi to riješio?
Ove teme su stvarne i korisne — ali ne testiraju se na junior intervjuima i nećeš ih koristiti u prvim 6–12 mjeseci posla. Dovoljno je znati da postoje i šta rade na visokom nivou.
Postavi Embassy toolchain. Blink LED. Čitaj dugme. Logiraj sve.
Spoji I2C senzor. Spoji SPI display. Čitaj datasheet-ove.
Dimuj LED s PWM. Reaguj na dugme s interruptom. Spawnuj multiple taskove.
Napravi nešto korisno: weather station, servo kontroler, data logger.