Embassy Rust on the XIAO BLE · Part 3 of 4

Waiting for a button instead of polling it

Part two ended with a diagnosis: polling burns the CPU to answer a question the hardware could answer for free. The nRF52840 has a peripheral — GPIOTE, “GPIO tasks and events” — whose whole job is to raise an interrupt when a pin changes. Embassy wraps it in an async method, and the fix is two lines. The code is crates/02-digital-input-async:

#![no_std]
#![no_main]

use defmt::info;
use embassy_nrf::gpio::{Input, Level, Output, OutputDrive, Pull};
use {defmt_rtt as _, panic_probe as _};

#[embassy_executor::main]
async fn main(_spawner: embassy_executor::Spawner) {
    let p = embassy_nrf::init(Default::default());

    info!("Initializing the onboard Red LED...");
    let mut onboard_red = Output::new(p.P0_26, Level::High, OutputDrive::Standard);

    info!("Initializing the button...");
    let mut button = Input::new(p.P0_03, Pull::Up);

    loop {
        button.wait_for_low().await;
        onboard_red.set_low();
        info!("Button pressed!");

        button.wait_for_high().await;
        onboard_red.set_high();
    }
}

Same wiring, same pull-up, same active-low logic. The if/else is gone, replaced by two awaits:

button.wait_for_low().await;   // sleep until pressed
// ...
button.wait_for_high().await;  // sleep until released

What actually happens at the await

wait_for_low() returns a future. When the executor polls it and the pin is still high, the future registers with GPIOTE — “raise an interrupt when this pin goes low” — and returns Pending. The executor has nothing else to run, so it executes a wait-for-interrupt instruction and the core stops. Not “loops quickly”: stops.

When you press the button, GPIOTE fires the interrupt, the interrupt handler wakes the task, and execution resumes on the line after the .await. The log tells the story: instead of Not Pressed... scrolling by endlessly, the terminal is silent until the moment you press.

Note one small signature change: the button is now let mut button. The async wait methods take &mut self, because registering for a pin event mutates the input’s GPIOTE channel state — the type system again tracking who’s allowed to touch the hardware.

The structural win

Reading top to bottom, the loop is a plain-language description of the behavior: wait for press, LED on, wait for release, LED off. There’s no state machine tracking “was it pressed last time I checked”, because the control flow is the state. This is the thing Embassy sells: interrupt-driven firmware that reads like blocking code.

One task still has a limit, though: this main can only wait for one thing at a time. If we also wanted a heartbeat LED blinking on its own schedule, this loop has nowhere to put it — waiting on the button means not counting time, and vice versa. Running both at once is what Embassy tasks are for, and that’s the finale.