# 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}]"}