# The Peripheral Pattern: Create → Configure → Control 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: ```rust // 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 ```rust // 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: ```rust // 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_hal` trait methods** — these are your standard control interface - **Inherent methods** — chip-specific extras beyond the standard traits --- ## 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 initial `Level`, and an `OutputConfig` ### Configure - `OutputConfig` has methods like: - `.with_drive_strength(DriveStrength)` — how much current can the pin source/sink? - `.with_open_drain(bool)` — push-pull or open-drain output? - `DriveStrength` enum: how many options are there? Look it up! ### Control - Inherent methods: `set_high()`, `set_low()`, `toggle()`, `set_level(Level)` - Trait implementations: implements `OutputPin` from `embedded_hal::digital` - But also: `is_set_high()`, `is_set_low()` — you can *read back* the output state --- ## 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.
The Embedded Rustacean · Rust Week 2026