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 releasedWhat 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.