# Embedded Rust BSPs with uFerris & Xiao: LED & Button Support with GPIO

> This is the fourth post in the µFerris & Xiao BSP series, where we build a Board Support Package from scratch, one peripheral at a time. In the previous post, we set up the template. This post is the first one that starts populating functions.

## Series Past Posts

1.  [Device Setup & Getting Started](https://blog.theembeddedrustacean.com/embedded-rust-bsps-with-ferris-xiao-device-setup-getting-started)
    
2.  [Getting started with µferris-bsp](https://blog.theembeddedrustacean.com/embedded-rust-bsp-with-ferris-xiao-getting-started-with-ferris-bsp)
    
3.  [Intro to BSPs](https://blog.theembeddedrustacean.com/embedded-rust-bsps-with-ferris-xiao-intro-to-bsps)
    

## Introduction

In the last post, we created a template for the BSP that compiles but does nothing. We created an empty struct for the board; `UFerris`and a single initialization method `uferris_init` that takes the peripherals and throws them away. That was on purpose to give shape to what we are going to work with.

This post adds in the first two GPIO devices, the two simplest on the board. **LED 1** hangs off GPIO3 and **SW5** off GPIO5. Neither needs a bus, a clock, or a driver. This makes them the right place to establish the changes that every later post would repeat: *add the peripheral to the board struct, configure it in the init function, then expose it through a control function.*

Let's get started.

## 📚 Knowledge Prerequisites

To understand the content of this post, you need the following:

*   Basic knowledge of coding in Rust.
    
*   Basic understanding of embedded systems development concepts.
    
*   Familiarity with `no_std` development in Rust, preferably using `esp-hal`.
    
*   Familiarity with the µFerris platform and flashing a XIAO module.
    

## 💾 Software Setup

All the code presented in this post is available in the µFerris & XIAO series repo. Every post in the series has its own self-contained Cargo project in a numbered folder. The code for this post lives in `04-led-button-gpio/`:

```shell
git clone https://github.com/theembeddedrustacean/learn-bsp-rs
cd learn-bsp-rs/04-led-button-gpio
cargo run --release
```

The examples revolve around the ESP32-C3. If you want to follow along with a different device, please refer to the [xiao-generate](https://blog.theembeddedrustacean.com/xiao-generate) post for more detail.

## 🛠 Hardware Setup

The required hardware includes:

*   **µFerris Megalops Baseboard** — the board this BSP is written for. Available from [**The Embedded Rustacean Store**](https://shop.theembeddedrustacean.com/).
    

![](https://cdn.hashnode.com/uploads/covers/6227094756920671339a1788/92f8f236-b2d5-4d7c-aa52-bc253037e3b6.png align="center")

*   **Seeed Studio XIAO ESP32-C3** — the controller used throughout this phase of the series. Available from the [**SeeedStudio Store**](https://www.seeedstudio.com/Seeed-XIAO-ESP32C3-p-5431.html?utm_source=blog&utm_medium=TER&utm_campaign=uFerris).
    

![](https://cdn.hashnode.com/uploads/covers/6227094756920671339a1788/a3473b09-3e39-42e5-bfb1-8c89b868afde.png align="center")

*   **USB-C cable** for power, flashing, and serial output.
    

%%[shop-uferris] 

%%[shop-xiao] 

### **🔌 Connections**

No connections are required. Every component the BSP will eventually drive is prewired; you only need to assemble the board. If you haven't assembled the board yet, [this](https://blog.theembeddedrustacean.com/embedded-rust-bsps-with-ferris-xiao-device-setup-getting-started) post walks through it.

The two devices this post covers are connected on the µFerris as follows:

| Device | XIAO Pin | Polarity |
| --- | --- | --- |
| LED 1 | GPIO3 | Active high. Driving the pin high lights it up. |
| SW5 | GPIO5 | Active low. Pressing pulls the pin to ground (0V). |

## 👨‍🎨 Software Design

This post builds on the BSP template from the [previous post](https://blog.theembeddedrustacean.com/embedded-rust-bsps-with-ferris-xiao-intro-to-bsps). This post assumes you understand the base template and its abstractions. Recall the BSP code is composed of three parts; **The Board struct**, **The Board Initialization Function**, and **The Board Control Functions**.

### The Board Struct

Remember `UFerris` was empty. Also recall that the board struct should represent the board. This struct becomes the access point for methods that control the board. As such, the board struct members represent components on the board, capture the type for each component, and allow us to perform actions on them. This is the form of the updated struct:

```rust
pub struct UFerris {
    led1: Output<'static>,
    sw_btn5: Input<'static>,
}
```

`Output` and `Input` are GPIO types derived from `esp-hal::gpio`. Notice the following:

**The board struct owns the pins.** Once a GPIO pin has been turned into an `Output` or `Input` and moved into the struct, no other part of the program can reconfigure it or drive it.

**The lifetime is** `'static`. `Output<'d>` & `Input<'d>` borrow the pins they drive. Since the peripherals singleton lives for the whole program, the pins taken out of it are `'static`, and the board struct can be stored and passed around without dragging a lifetime parameter through every function that touches it.

### The Board Initialization Function

We will update `uferris_init` to take the two pins out of `Peripherals` and configure each one as follows:

*   **LED 1** as an `Output` starting at `Level::Low`. Because the LED is active high, low means off.
    
*   **SW5** as an `Input`. We won't configure any pulls since the board has an external pull-up.
    

Everything else about the two pins is left at the defaults. A BSP should configure what the *board* demands and no more.

### The Board Control Functions

We'll introduce three functions that the BSP will support:

| Function | Behaviour |
| --- | --- |
| `led1_on()` | Drives GPIO3 high when called, lighting LED 1. |
| `led1_off()` | Drives GPIO3 low when called. |
| `sw5_pressed()` | Reads SW5 line when called. `true` while SW5 is pressed. |

The names match the shipping [`uferris-bsp`](https://crates.io/crates/uferris-bsp) crate, which is where this series ends up. Also note how `sw5_pressed` returns `true` for pressed, which is the inverse of what the pin reads. Optionally, you can also add a `led1_toggle` function. That is left for the reader as an exercise.

### Test Application Design

The test application steps are as follows:

1.  Poll `sw5_pressed()` in a loop.
    
2.  Mirror the result on LED 1; On while the button is held, off when it is released.
    
3.  Print the state over the serial port only when the state changes.
    
4.  Wait 20 ms (allows button to settle from bouncing).
    
5.  Go back to step 1.
    

## 👨‍💻 Code Implementation

**1️⃣ Bring in the GPIO types.** The template only needed `Peripherals`. Driving a pin needs the input and output types and their configuration structs:

```rust
use esp_hal::{
    gpio::{Input, InputConfig, Level, Output, OutputConfig},
    peripherals::Peripherals,
};
```

**2️⃣ Populate the board struct.** Add the components and their types as members to the board struct.

```rust
/// A handle to the µFerris board and everything on it.
pub struct UFerris {
    led1: Output<'static>,
    sw_btn5: Input<'static>,
}
```

**3️⃣ Populate the initialization function.** In the init function, we need to now configure both LED1 and SW5 pins, as `Output` and `Input`, respectively. Below is the code:

```rust
pub fn uferris_init(peripherals: Peripherals) -> UFerris {

    let led1 = Output::new(peripherals.GPIO3, Level::Low, OutputConfig::default());

    let sw_btn5 = Input::new(peripherals.GPIO5, InputConfig::default());

    UFerris { led1, sw_btn5 }
}
```

Note the configuration passed for each pin, as defined earlier.

**4️⃣ Add the control functions.** Now that the pins are added to the struct and configured, we can access the struct members and call the type methods. In an `impl` block on `UFerris`, we create board methods that call those type methods as follows:

```rust
impl UFerris {
    /// Turn LED 1 on.
    pub fn led1_on(&mut self) {
        self.led1.set_high();
    }

    /// Turn LED 1 off.
    pub fn led1_off(&mut self) {
        self.led1.set_low();
    }

    /// Read switch 5.
    pub fn sw5_pressed(&mut self) -> bool {
        self.sw_btn5.is_low()
    }
}
```

Note the difference between the board-level and the controller-level methods. Here is where the abstraction wrapping happens. The BSP method defined in the `UFerris` `impl` block has a method name that represents board behavior. Under the hood, it calls a HAL-level method that corresponds to that board behavior.

## 🏃‍♂️ Test Code & Run

The application never touches a pin. It initializes the board and then talks to it in the board's own vocabulary:

```rust
#![no_std]
#![no_main]

use esp_backtrace as _;
use esp_hal::{clock::CpuClock, main};
use esp_println::println;
use uferris::uferris_init;

esp_bootloader_esp_idf::esp_app_desc!();

#[main]
fn main() -> ! {
    // Configure the device and hand the peripherals to the BSP
    let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
    let peripherals = esp_hal::init(config);
    let mut uferris = uferris_init(peripherals);

    // Variable to keep track of the last state of the button.
    let mut was_pressed = false;

    // Instantiate a delay provider
    let delay = esp_hal::delay::Delay::new();

    loop {
        let pressed = uferris.sw5_pressed();

        // LED 1 mirrors SW5
        if pressed {
            uferris.led1_on();
        } else {
            uferris.led1_off();
        }

        // Only announce a change
        if pressed != was_pressed {
            println!("SW5 {}", if pressed { "pressed" } else { "released" });
            was_pressed = pressed;
        }

        // Read every 20 ms so a bouncing settles
        delay.delay_millis(20);
    }
}
```

There is not a single `GPIO3`, `Level`, or `is_low` in that code. The application knows the board has an LED and a button; it does not know which pins they are on or which way round they read.

Flash it and open the monitor:

```shell
cargo run --release
```

Press and release SW5 a few times, and the terminal shows one line per transition while the LED tracks your finger:

```plaintext
SW5 pressed
SW5 released
SW5 pressed
SW5 released
```

If a single press produces several `pressed` lines in a row, the switch is bouncing through the 20 ms window. Raise the delay time, and it will settle.

## ✅ Conclusion

We created the first part of the BSP that does something. We started with GPIO. Now two devices live in the board struct, `uferris_init` we configure them, and three control functions are the only way to reach them, absorbing any board-specific behavior.

The pattern is set and won't change for the rest of the series: a field in the struct, configuration in the init function, and function(s) per component\*\*.\*\* From here, only the peripheral's difficulty changes.

## 📱 Full Code

`src/lib.rs`

```rust
#![no_std]

use esp_hal::{
    gpio::{Input, InputConfig, Level, Output, OutputConfig},
    peripherals::Peripherals,
};

/// A handle to the µFerris board and everything on it.
pub struct UFerris {
    led1: Output<'static>,
    sw_btn5: Input<'static>,
}

/// Consume the raw chip peripherals and hand back a ready-to-use board.
pub fn uferris_init(peripherals: Peripherals) -> UFerris {
    // LED 1 is driven by GPIO3 and is active high, so it starts out low.
    let led1 = Output::new(peripherals.GPIO3, Level::Low, OutputConfig::default());

    // SW5 sits on GPIO5. The baseboard pulls it up, so no internal pull.
    let sw_btn5 = Input::new(peripherals.GPIO5, InputConfig::default());

    UFerris { led1, sw_btn5 }
}

impl UFerris {
    /// Turn LED 1 on.
    pub fn led1_on(&mut self) {
        self.led1.set_high();
    }

    /// Turn LED 1 off.
    pub fn led1_off(&mut self) {
        self.led1.set_low();
    }

    /// Read switch 5. Returns `true` while the button is held down.
    pub fn sw5_pressed(&mut self) -> bool {
        self.sw_btn5.is_low()
    }
}
```

`src/bin/main.rs`

```rust
#![no_std]
#![no_main]

use esp_backtrace as _;
use esp_hal::{clock::CpuClock, main};
use esp_println::println;
use uferris::uferris_init;

esp_bootloader_esp_idf::esp_app_desc!();

#[main]
fn main() -> ! {
    // Configure the device and hand the peripherals to the BSP
    let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
    let peripherals = esp_hal::init(config);
    let mut uferris = uferris_init(peripherals);

    // Variable to keep track of the last state of the button so we can announce changes.
    let mut was_pressed = false;

    // Instantiate a delay provider to use for debouncing the button.
    let delay = esp_hal::delay::Delay::new();

    loop {
        let pressed = uferris.sw5_pressed();

        // LED 1 mirrors SW5 for as long as it is held down.
        if pressed {
            uferris.led1_on();
        } else {
            uferris.led1_off();
        }

        // Only announce a change, not every sample.
        if pressed != was_pressed {
            println!("SW5 {}", if pressed { "pressed" } else { "released" });
            was_pressed = pressed;
        }

        // Sample every 20 ms so a bouncing settles between reads.
        delay.delay_millis(20);
    }
}
```
