# Async Rust for Embedded Systems Dario Nieuwenhuis [@dirbaio](https://twitter.com/dirbaio) --- ## The hardware - "really" embedded microcontrollers (not embedded Linux) - No OS - 4kB - 256kB of RAM - 16kB - 1MB of FLASH - Often used with alloc (no heap) - No out-of-memory errors at runtime - No fragmentation --- ## Peripherals The CPU core has "peripherals" allowing it to communicate with the external world. For example: a serial port (UART) - Write registers to tell it to do stuff. - Read registers to know the status of the stuff. --- ## Blocking ```rust // Configure buffer for reads, using DMA let mut buf = [0u8; 8]; p.UARTE0.rxd.ptr.write(|w| unsafe { w.bits(buf.as_mut_ptr() as _) }); p.UARTE0.rxd.maxcnt.write(|w| unsafe { w.bits(buf.len() as _) }); // Start read p.UARTE0.tasks_startrx.write(|w| w.tasks_startrx().set_bit()); // Wait for read to finish. while !p.UARTE0.events_endrx.read().events_endrx().bit() {} info!("Read done, got {:02x}", buf); ``` --- ## Blocking: challenges - Can't do anything else while waiting! - Could do it in the loop, but it composes horribly. - Wastes power, core is spinning at 100% usage. --- ## Interrupts Peripherals can signal an "interrupt" to the core, which: - Saves registers to the stack - Jumps to the interrupt handler - When it returns, restores registers Similar to a thread, but more like Unix signals. Requires Send/Sync. --- ## Interrupts ```rust // Enable interrupt p.UARTE0.intenset.write(|w| w.endrx().set_bit()); NVIC::unmask(nrf52840_pac::Interrupt::UARTE0_UART0); // Start read p.UARTE0.tasks_startrx.write(|w| w.tasks_startrx().set_bit()); // Sleep in low-power mode, with Wait For Interrupt loop { cortex_m::asm::wfi(); } ``` --- ## Interrupt handler ```rust static mut BUF: [u8; 8] = [0; 8]; #[interrupt] fn UARTE0_UART0() { let p = unsafe { nrf52840_pac::Peripherals::steal() }; if p.UARTE0.events_endrx.read().events_endrx().bit() { p.UARTE0.events_endrx.reset(); info!("Read done, got {:02x}", unsafe { BUF }); } } ``` --- ## Interrupts: challenges - The handler is "another thread": sharing data needs `static` and `Send/Sync`. - Less readable: control flow no longer reads top-to-bottom. Callback hell. - You have to write logic as state machines. - Race conditions when waiting for multiple things --- ## RTOS - Real Time Operating System - Multiple threads, each with its own stack - Kernel schedules and switches threads - C: FreeRTOS, Zephyr, ThreadX - Rust: Tock, Hubris, bindings to C ones, `std` ports of varying quality and maintainedness --- ## Async! ```rust static UART_WAKER: AtomicWaker = AtomicWaker::new(); struct UartFuture; impl Future for UartFuture { type Output = (); fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { UART_WAKER.register(cx.waker()); let p = unsafe { nrf52840_pac::Peripherals::steal() }; if p.UARTE0.events_endrx.read().events_endrx().bit() { Poll::Ready(()) } else { Poll::Pending } } } #[interrupt] fn UARTE0_UART0() { let p = unsafe { nrf52840_pac::Peripherals::steal() }; if p.UARTE0.events_endrx.read().events_endrx().bit() { p.UARTE0.intenclr.write(|w| w.endrx().set_bit()); UART_WAKER.wake(); } } ``` --- ## Async! ```rust // Start read p.UARTE0.tasks_startrx.write(|w| w.tasks_startrx().set_bit()); // Wait for read done. UartFuture.await; info!("Read done, got {:02x}", buf); ``` --- ## Embassy ```rust #[embassy::main] async fn main(_spawner: Spawner, p: embassy_nrf::Peripherals) -> ! { let irq = interrupt::take!(UARTE0_UART0); let mut uart = uarte::Uarte::new(p.UARTE0, irq, p.P0_08, p.P0_06, Default::default()); let mut buf = [0u8; 8]; match with_timeout(Duration::from_secs(1), uart.read(&mut buf)).await { Ok(_) => info!("Read done, got {:02x}", buf), Err(_) => info!("Timeout!"), } } ``` --- - Faster, smaller code than RTOSs - Readable, no callback hell - Data stays in a single task, no `static`, no `Send`/`Sync`. - Interrupts are abstracted away - Composes nicely. `join`, `select`, `with_timeout`. --- # Challenge: no-alloc - Embassy executor is no-alloc: needs to store task futures in `static`s. - Currently using `type_alias_impl_trait`, nightly-only - See [source for #[embassy::task] macro](https://github.com/embassy-rs/embassy/blob/3d1501c02038e5fe6f6d3b72bd18bd7a52595a77/embassy-macros/src/macros/task.rs#L77) --- # Challenge: async traits - `async-trait` not usable, it needs alloc (`Box<dyn Future>`) - Currently using GAT+TAIT - Ideal would be "async fn in traits" :) - [embedded-hal-async](https://github.com/rust-embedded/embedded-hal/tree/master/embedded-hal-async) --- # Challenge: leak - Hardware does DMA writes to borrowed buffers. - If future is leaked, DMA is not stopped -> UB - Same problem as `io_uring` - Embedded can't use owned buffers though! - Idea: `Leak` auto trait --- # Thank you! Dario Nieuwenhuis [@dirbaio](https://twitter.com/dirbaio) Also, check out: - Full [code](https://github.com/Dirbaio/rust-embedded-async) for the snippets in these slides. - [Embassy](https://github.com/embassy-rs/embassy)
{"metaMigratedAt":"2023-06-17T00:48:42.508Z","metaMigratedFrom":"YAML","title":"Async Rust for Embedded Systems","breaks":true,"contributors":"[{\"id\":\"6e4882e2-aae6-4b26-8c3d-e1d0d86d5b32\",\"add\":5742,\"del\":476},{\"id\":\"db349910-53c1-45a5-aa34-1ac5434980b0\",\"add\":83,\"del\":0}]"}
    1929 views
   owned this note