Chapter 28: Interrupts and DMA
September 17, 2026 · View on GitHub
Introduction
UART read(...).await looks like magic: bytes land, the future completes, no
inputs wasted — because the hardware does not touch the CPU. Two RP2350
mechanisms make that possible: interrupts, which route "a byte arrived" into
the executor, and DMA, which moves bytes itself. This chapter connects
bind_interrupts!, InterruptHandler, and the two DMA channels in
main.rs to the await calls the driver makes.
The CPU Would Waste Its Whole Loop
A polling receiver busy-loops sampling status bits waiting for a byte, burning a hundred percent of the CPU:
CPU: [read status] [no] [read status] [no] [read status] [no] ...
UART RX: ..... ......... came! ............................. (missed or
polled late)
The driver never does that. It wires the UART peripheral's own signaling so the CPU is interrupted by the hardware only when there is real work:
CPU: [await] ... [wakeup] echo [await] ... [wakeup] echo
UART RX: ...... byte! ................... byte! .........
Interrupts let the CPU sleep (or run other tasks) between bytes; this is the interrupt-driven half of the design.
bind_interrupts!
The driver only tells Embassy which peripheral owns which IRQ line once:
use embassy_rp::{bind_interrupts, peripherals::UART0, uart::InterruptHandler};
bind_interrupts!(struct Irqs {
UART0_IRQ => InterruptHandler<UART0>;
});
Reading the macro block: "RP2350's UART0_IRQ vector fires InterruptHandler
with the UART0 peripheral token." The macro-generates a struct Irqs that
statically registers the handler in the vector table and holds up to
embassy_rp::init. InterruptHandler wakes the executor's Uart future when
the FIFO signals a byte has been received or a transmission completes —
the exact moment the future's readiness flips from pending to ready.
Because the binding is called on the macro line (not at runtime) and Irqs is
consumed by Uart::new, the linkage is checked at compile time: a typo in the
IRQ name simply refuses to build.
The Two DMA Channels
Uart::new takes two more peripherals after the pins:
let mut uart = Uart::new(
p.UART0, p.PIN_0, p.PIN_1, Irqs, p.DMA_CH0, p.DMA_CH1, config,
);
p.DMA_CH0 and p.DMA_CH1 are the RP2350's two DMA controllers. Embassy
assigns one to reception and one to transmission, and once configured the DMA
engine drives the data bus on its own:
in / out CPU: off doing math
Host <-> UART FIFO <--DMA0--< RAM (buf) or RAM (echo_bytes) -->DMA1--> UART FIFO
The result is a DMA UART: the peripheral hands bytes to a memory buffer without the CPU ever copying them. That is the DMA half of the design.
Interrupts + DMA + Async = The Whole Story
Both halves land in one await:
if uart.read(&mut buf).await.is_ok() { ... }
Step by step:
- Embassy configures
DMA_CH0to move the next RX byte intobuf[0], and takes the "wait" branch — the future registers interest. - The DMA engine fills
buf[0]with no CPU help (when exactly one byte is requested). - The UART raises
UART0_IRQ; the registeredInterruptHandlermarks the waiter ready. - The executor resumes the future;
.awaitreturnsOk.
write is the mirror: Embassy configures DMA_CH1 to move echo_bytes to the
UART FIFO, the DMA drains it with the CPU watching no registers, and the write
future completes when the FIFO empties.
Nothing blocked, nothing polled, nothing hand-copied — and the whole arrangement is expressed as ordinary-looking async code.
Error Paths
read returns a Result, and the driver discards nothing silently:
if uart.read(&mut buf).await.is_ok() {
let echo_bytes = controller.process_char(buf[0]);
let _ = uart.write(echo_bytes).await;
}
On read errors the byte is skipped — safer than echoing garbage. The write's
let _ = ... acknowledges we do not recover if the wire is pulled; a future
driver could log the error. Embassy's error type (overrun, break, framing, or
parity conditions) is available whenever the code decides to care.
Summary
- Waiting on UART by polling wastes the CPU; interrupts wake it only when a byte matters.
bind_interrupts!statically routesUART0_IRQtoInterruptHandler<UART0>, compile time-checked.DMA_CH0/DMA_CH1move bytes between RAM and the FIFO without CPU copies.- Asynchronous
read/writeare the fusion: await, DMA fills, IRQ wakes, future completes. - Result-typed reads keep runaway errors from becoming silent garbage.
The finale of the UART driver is wiring all of it in main.rs.