The Peripheral Pattern: Create → Configure → Control
~15 min
This is the mental model you'll carry out of this workshop. Every peripheral — GPIO, I2C, SPI, UART, timers — follows the same three-step pattern.
The Pattern
┌──────────┐ ┌─────────────┐ ┌───────────┐
│ CREATE │ ──→ │ CONFIGURE │ ──→ │ CONTROL │
│ │ │ │ │ │
│ new() │ │ Config │ │ Methods │
│ builder │ │ structs │ │ trait │
│ │ │ enums │ │ impls │
└──────────┘ └─────────────┘ └───────────┘
1. Create — Instantiate the Driver
Every peripheral starts with creating an instance. You'll see patterns like:
#![allow(unused)] fn main() { // Direct construction let led = Output::new(pin, initial_level, config); // Builder pattern let i2c = I2c::new(peripherals.I2C0, config) .with_sda(sda_pin) .with_scl(scl_pin); }
What to look for on docs.rs:
- The struct page — find
new(),builder(), or associated functions - What parameters does the constructor need? Pins? Config? Peripheral instance?
2. Configure — Set Up How It Behaves
Before using a peripheral, you configure it. Configurations are typically expressed as:
- Config structs — group related settings together
- Enums — define the options for each setting
- Builder methods — chain configuration calls
#![allow(unused)] fn main() { // GPIO: configure output behavior let config = OutputConfig::default() .with_drive_strength(DriveStrength::_20mA) .with_open_drain(false); // I2C: configure bus speed let config = Config::default() .with_frequency(Rate::from_khz(400)); }
What to look for on docs.rs:
- Config structs — what fields/methods do they have?
- Enums — what are the possible values? (
DriveStrength,Pull,Rate, etc.) - Default values — what happens if you just use
Config::default()?
3. Control — Read, Write, Toggle
Once created and configured, you use control methods to interact with the peripheral:
#![allow(unused)] fn main() { // GPIO Output: control the pin state led.set_high(); led.set_low(); led.toggle(); // GPIO Input: read the pin state let pressed: bool = button.is_low(); let level: Level = button.get_level(); // Returns an enum, not just a bool // I2C: read/write to devices i2c.write(address, &data)?; i2c.read(address, &mut buffer)?; }
What to look for on docs.rs:
- Trait implementations — scroll to "Trait Implementations" on the struct page
embedded_haltrait methods — these are your standard control interface- Inherent methods — chip-specific extras beyond the standard traits
The example you start with shows ONE configuration and ONE control method. The documentation shows ALL of them. Your job is to explore what else is possible.
Live Demo: GPIO Output on docs.rs
Let's walk through this pattern with a real example. Open the esp_hal::gpio::Output documentation:
Create
Output::new(pin, initial_level, config)— takes a GPIO pin, an initialLevel, and anOutputConfig
Configure
OutputConfighas methods like:.with_drive_strength(DriveStrength)— how much current can the pin source/sink?.with_open_drain(bool)— push-pull or open-drain output?
DriveStrengthenum: how many options are there? Look it up!
Control
- Inherent methods:
set_high(),set_low(),toggle(),set_level(Level) - Trait implementations: implements
OutputPinfromembedded_hal::digital - But also:
is_set_high(),is_set_low()— you can read back the output state
Open [esp_hal::gpio::Output](https://docs.espressif.com/projects/rust/esp-hal/latest/esp32c3/esp_hal/gpio/struct.Output.html) in your browser. Can you find:
1. All the variants of `DriveStrength`?
2. What `OutputConfig::default()` gives you?
3. A control method we haven't mentioned yet?
This Pattern Repeats Everywhere
| Peripheral | Create | Configure | Control |
|---|---|---|---|
| GPIO Output | Output::new() | OutputConfig | set_high(), toggle() |
| GPIO Input | Input::new() | InputConfig, Pull | is_high(), get_level() |
| I2C | I2c::new().with_sda().with_scl() | Config, Rate | write(), read() |
| SPI | Spi::new().with_mosi().with_sck() | Config, Mode | transfer(), write() |
| UART | Uart::new().with_tx().with_rx() | Config, BaudRate | write(), read() |
Once you internalize Create → Configure → Control, you can pick up any peripheral by reading its documentation. You already know what to look for.