Chapter 17: GPIO with embassy-rp
September 17, 2026 · View on GitHub
Introduction
Two of our three drivers are GPIO programs. The blink driver owns an output
pin and flips it. The button driver owns an input pin, reads it, and flips
the same kind of output. This chapter explores embassy_rp::gpio — the four
pieces (Level, Output, Input, Pull) and the calls every driver makes.
Level
Level is the electrical truth we converted to a boolean in led.rs:
pub enum Level {
High,
Low,
}
Two lines of code, two voltage states on the pin:
+-----+ +-- HIGH (~3.3 V)
| | |
+-----+ | +-- LOW (~0 V)
| | |
+-------+-----+
Time -->
Construction takes the initial state: Output::new(p.PIN_16, Level::Low) starts
the pin low. Our led_state_to_level maps a LedState to 0/1 (Chapter 5);
GPIO maps that bool back to a Level-like decision when calling set_high/
set_low.
Output Pins
An output pin is created once, configured fully, and then used for its whole life:
let mut led = Output::new(p.PIN_16, Level::Low);
Output::new(pin, level)— Claims the pin, configures the pad (Chapter 11), sets the initial level.led.set_high()— Drive the pin high.led.set_low()— Drive the pin low.led.toggle()— Invert the current state (available on theToggleableOutputPintrait).
The mut is required because these methods take &mut self — the pin's state
changes. The blink loop leans on exactly two calls:
if led_state_to_level(state) {
led.set_high();
} else {
led.set_low();
}
The Output<'d, T> generic remembers which pin it owns ('d is the pin's
lifetime), so the compiler checks you never mix up GPIO16 and GPIO17.
Input Pins and Pull
Reading a pin needs the electrical opposite setup: the pad must float or be
held, and the chip samples the level. That floating question is where Pull
enters:
pub enum Pull {
None,
Up, // hold the line HIGH when nothing drives it
Down, // hold the line LOW when nothing drives it
}
The button driver configures its input:
let button = Input::new(p.PIN_15, Pull::Up);
With Pull::Up the pin reads high when the button is open. Pressing the button
grounds it, so the pin reads low — the active-low wiring of Chapter 22.
Without the pull-up, a floating input reads random noise and the whole debounce
design falls apart.
Reading and Writing
The two reading calls available on Input:
is_high()—trueif the input level is high.is_low()— the complement.
The button loop samples with is_high:
controller.update(button.is_high());
After waking from its Timer, the loop asks the pin "high or low?" and feeds
the answer into the debounce filter as "released or pressed?" The bool is the
interface — no bit, no register, no pad knowledge ever appears in application
code.
GPIO in Our Drivers
Put the pieces together for both loops:
Blink: Button:
+-------------+ +---------------+
| Output::new | | Input::new |
| PIN_16, Low | | PIN_15, Up |
+-------------+ +---------------+
| |
v v
+-------------+ +---------------+
| toggle state| | update(sample)|
+-------------+ +---------------+
| |
v v
+-------------+ +---------------+
| set_high/ | | set_high/ |
| set_low | | set_low |
+-------------+ +---------------+
Every driver touches GPIO at the same three moments: construct the pin once, read or write it in a loop, and let Embassy own the registers forever after.
Summary
Level::High/Lowis the electrical state of a pin.Output::new(pin, level)configures and claims an output pin; methods areset_high,set_low, andtoggle.Input::new(pin, pull)configures an input pin;is_high/is_lowsample it.Pull::Upholds the line high when nothing drives it — the foundation of the button driver.- Overly precise types (
Output<'d, T>) let the compiler catch pin misuse at build time.
Part III is complete: you can now read any Embassy GPIO program. Part IV builds the first complete driver on top of it — the blink driver, file by file.