Reading a button, the obvious way
Output was easy; input is where wiring details start to matter. This part connects a push button to the board and lights the onboard red LED while it’s held down. The code is crates/02-digital-input:
#![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 button = Input::new(p.P0_03, Pull::Up);
loop {
if button.is_low() {
info!("Pressed...");
onboard_red.set_low();
} else {
info!("Not Pressed...");
onboard_red.set_high();
}
}
}Why Pull::Up
A button is just two contacts. Wire one side to a GPIO pin and the other to ground, and you have half a circuit: when the button is pressed the pin is connected to ground, but when it’s released the pin is connected to nothing. A floating pin reads garbage — it picks up whatever electrical noise is nearby.
Pull::Up fixes that by enabling the chip’s internal pull-up resistor, which gently ties the pin to the supply voltage. Now the pin has a defined state at all times: high when released, low when pressed. That’s why the check is button.is_low() for “pressed” — the same inverted logic we met with the active-low LED, this time coming from the circuit rather than the board layout.
What’s wrong with this picture
It works, and for a first input program it’s exactly right. But look at what the loop does when nothing is happening: it checks the pin, checks it again, and again, millions of times per second. The log output makes the problem visible — Not Pressed... scrolls past faster than the terminal can render it.
On a desktop that’s merely wasteful. On a battery-powered board it’s disqualifying: the CPU never idles, so the chip never drops into a low-power state. And the busy loop doesn’t scale — this main can do nothing else while it’s spinning on the button.
We already know the shape of the fix. In part one, Timer::after_millis(1000).await let the chip sleep through a delay. What we want here is the same thing for a pin: sleep until the button changes. That’s exactly what Embassy’s async inputs do, and it’s next.