# Embedded Rust BSPs with µFerris & Xiao: Intro to BSPs

> This is the third 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](https://blog.theembeddedrustacean.com/embedded-rust-bsp-with-ferris-xiao-getting-started-with-ferris-bsp) we met the board, wired up the toolchain, and flashed our first program. This post lays the foundation for everything else.

## 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)
    

## Introduction

Writing PAC code requires knowledge about microcontroller registers. Writing HAL code requires knowledge about controller connections and schematics. As you go up, the pattern repeats: each layer's knowledge base grows, abstracting what's underneath. You can imagine the natural evolution: a **Board Support Package (BSP)** that requires knowledge of board components.

In this post, we'll dig deeper into what a BSP actually is. The purpose is to start a series of building a BSP for the µFerris platform. As such, we will set up a project with base code that will carry through the rest of the series.

## 📚 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. If this is your first post in the series, the [**device setup and getting started post**](https://blog.theembeddedrustacean.com/embedded-rust-bsps-with-ferris-xiao-device-setup-getting-started) covers the hardware and the toolchain setup.
    

## 🧩 So, what is a BSP?

A BSP is a crate that lives on top of a HAL (Hardware Abstraction Layer) crate, integrating board-specific information. Simply put, the BSP lets us **i**nteract with the board, not just the controller. The controller itself is just another component on the board. As such, a BSP abstracts away the HAL-level interfaces and configurations. You can also think of the BSP as a board driver.

When you write against a HAL like`esp-hal`, you work with the controller's peripherals like GPIO, Timers, I2C, etc. A HAL has no idea what you soldered or wired onto controller pins. Instead, you end up baking that information into your application.

On the other end, a BSP combines board knowledge with the HAL and hands you a board-level abstraction. Along the way, it configures the peripherals so you can jump straight from initialization to usage. So, yay! No more configuration hell.

![](https://cdn.hashnode.com/uploads/covers/6227094756920671339a1788/e02b26b1-7aec-4d0d-92c7-fd4889aaa3c6.png align="center")

Under the hood, a BSP, more or less, takes care of two things:

*   **Initialization.** A single call brings the board up into a known-good state, configuring the peripherals and pins along the way so your application starts from something like `let board = SomeBoard::init(board_pac)`.
    
*   **Exposing Board Behavior, not Wiring.** The application leverages a collection of calls that allow it to control a component directly `led.on()`.
    

The payoff is more portable application code. Move the LED to a different pin, change one line in the BSP, and every program that uses it keeps working.

## 💾 Software Setup

All the code presented in this post is available in the [**µFerris & XIAO series repo**](https://github.com/theembeddedrustacean/learn-bsp-rs). Every post in the series has its own **self-contained** Cargo project in a numbered folder, so there is no shared workspace to set up. The code for this post lives in `03-intro-to-bsps/`:

```bash
git clone https://github.com/theembeddedrustacean/learn-bsp-rs
cd learn-bsp-rs/03-intro-to-bsps
cargo run --release
```

Every project in the series starts life as a [`xiao-generate`](https://blog.theembeddedrustacean.com/xiao-generate) scaffold, then gets extended by the post. The examples revolve around the ESP32-C3, so if you want to follow along with a different XIAO, generate your own project instead of cloning:

```bash
cargo install xiao-generate --locked
xiao-generate --chip <chip name> --name <project name>
```

You can find more details about `xiao-generate` [here](https://blog.theembeddedrustacean.com/xiao-generate).

### 🗂 Project Structure

A BSP lives in a **library crate**. A library, however, can't be flashed on its own. As such, every project in this series carries both pieces: the BSP as a library, and a binary that exercises it on real hardware.

`xiao-generate` already lays out the shape we need. An empty `src/lib.rs` next to a binary at `src/bin/main.rs`. The posts will fill the `lib,rs` library in, and use `main.rs` to test the additions:

```plaintext
03-intro-to-bsps/
├── .cargo/
│   └── config.toml       # target, runner, build-std
├── examples/
│   └── board_init.rs     # optional extra demos
├── src/
│   ├── bin/
│   │   └── main.rs       # demo/test app — this is what gets flashed
│   └── lib.rs            # the BSP
├── build.rs              # passes -Tlinkall.x to the linker
├── Cargo.toml
└── rust-toolchain.toml
```

`Cargo.toml` declares both targets explicitly, so it's obvious which file is the library and which one gets flashed:

```toml
[package]
name = "uferris"
version = "0.1.0"
edition = "2024"

[lib]
name = "uferris"
path = "./src/lib.rs"

[[bin]]
name = "uferris-demo"
path = "./src/bin/main.rs"
```

The binary depends on the library by name, `use uferris::uferris_init;`, even though both live in the same package. That split is not cosmetic: `#![no_main]`, the `#[main]` entry point, and the panic handler (`esp-backtrace`) belong to the **binary only**. The library stays clear of all three, which is exactly what lets an application crate depend on the BSP later without inheriting an entry point or a second panic handler.

`cargo run --release` builds both and flashes the binary. Any additional demos go in `examples/` and run with `cargo run --release --example <name>`.

## 🛠 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.

## 👨‍🎨 Software Design

This post provides a BSP starting template for the µFerris platform. This lets us build on it and add functions until we have a full BSP implementation.

### 🔭 Design Scope

The µFerris baseboard is designed to accept different [Seeed Studio XIAO](https://www.seeedstudio.com/xiao-series-page) modules: the ESP32-C3, ESP32-C6, ESP32-S3, and more. As a result, a good BSP design would support all of them from day one, hiding the differences behind feature flags and traits so the same crate builds for any of them. However, we won't take on that scope from the start. Instead, for now, we will build for exactly **one** controller: the **XIAO ESP32-C3,** then later show how to expand. There are a couple of reasons for that:

1.  Many development boards in the field come with one controller soldered on. This means the controller isn't changeable. So it would be helpful to first understand how to create a BSP for a board that supports one controller. As such, in the first phase of the series, we are going to build the BSP for the µFerris baseboard assuming it supports only one controller.
    
2.  Supporting multiple controllers from the get-go means added complexity. Doing it from the start means introducing an entire second layer before we have written a single working driver, making it harder to learn either. Abstractions are only worth their weight once you have felt the concrete pain they remove.
    

This is the classic challenge of scale, and we are deliberately deferring it. Later in the series, once the board actually does something, we will come back and generalize: we will refactor the C3-specific BSP into a design that supports every XIAO variant, and you will see exactly which lines had to move and why. Building the naive version first is what makes that refactor make sense.

### 🖼️ The Template

At a minimum, a BSP in code is two pieces: a **type** that stands for the board, and an initialization **function** that brings it to life. The BSP code would roughly be composed of three parts:

1.  **The Board struct**: a **type** that stands for the board. Its members represent board components that are tied to controller peripheral HAL types.
    
2.  **The Board Initialization Function**: a function that would populate the board type and configure the HAL peripherals.
    
3.  **The Board Control Functions**: a collection of functions to interact with the board components.
    

For now, we will introduce a struct that represents the board and an initialization method for that struct. Also, we will not populate either with anything for now. Control functions will be added as we add peripherals.

`Uferris`: **The Board Driver Struct**

This `UFerris` type struct is what the BSP will revolve around. The struct members will hold peripheral HAL types representing the board components. This struct will drive the board and its components.

```rust
/// A handle to the µFerris board and everything on it.
pub struct UFerris {
    // peripherals members land here
}
```

At first, the struct will hold nothing. However, each post in this series will add fields like an LED, a buzzer, an I2C bus, and an RTC until the struct represents the whole board. Think of it as the board's component list and the peripheral function it's paired with.

`uferris_init`: **The Board Driver Initialization Method**

From the get-go, the `UFerris` struct is nothing but a definition of a type with empty members. As such, we would need to initialize it with some `init` function in order to use it. This would be the one entry point every application calls. It consumes the controller peripherals and returns a ready-to-use board.

```rust
/// Consume the raw chip peripherals and hand back a ready-to-use board.
pub fn uferris_init(_peripherals: Peripherals) -> UFerris {
    UFerris {}
}
```

An `init` method typically does what a `new` method does plus some initializations. As such, later in this function, we will place the configuration/initialization code for the board controller peripherals.

Note that the `init` method takes ownership of the peripherals. By consuming`Peripherals`, the BSP becomes the single owner of the board's hardware. That is Rust's ownership model doing our bookkeeping for us: nothing else can grab `GPIO3` behind our back, because we handed the whole peripheral set to the BSP.

## 👨‍💻 Code Implementation

In this part, the code implementation is simple. It's putting together the type and its initialization function in `src/lib.rs`.

```rust
#![no_std]

use esp_hal::peripherals::Peripherals;

/// A handle to the µFerris board and everything on it.
pub struct UFerris {
    // peripherals members
}

/// Board initialization method
pub fn uferris_init(_peripherals: Peripherals) -> UFerris {
    // Returns an empty struct for now.
    UFerris {}
}
```

That's the whole BSP, for now. Right now it does nothing, and that's the point. The board is a value, and `uferris_init` owns the hardware configuration. Everything else will be baked into these.

## 🏃‍♂️ Running the Code

Although the board doesn't *do* anything yet, it's good to prove the whole chain works: the crate compiles, the toolchain targets the C3, and the firmware flashes and runs. That's what the demo binary in `src/bin/main.rs` is for:

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

use esp_backtrace as _;
use esp_hal::{
    clock::CpuClock,
    main,
    time::{Duration, Instant},
};
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 _uferris = uferris_init(peripherals);

    // Nothing to drive yet — the board struct is still empty.
    loop {
        let delay_start = Instant::now();
        while delay_start.elapsed() < Duration::from_millis(500) {}
    }
}
```

Flash it with the runner we configured:

```bash
cargo run --release
```

If `espflash` finds the board, builds, flashes, and drops into the monitor without complaint, the foundation works.

%%[oxidize-xiao] 

## ✅ Conclusion

A BSP is not a framework or a heavy abstraction. It's a thin, deliberate layer that lets your application speak in terms of *the board* instead of *the chip*. In this post, we laid the foundation for a BSP for µFerris. We used a `no_std` library crate targeting the XIAO ESP32-C3, pinned our dependencies, and wrote the two pieces of scaffolding; `UFerris` and `uferris_init`. The rest of the series will build on these. We also chose to build for a single controller first and defer multi-board generalization until we've earned it.

Next post, we make the board do its first real thing: drive the on-board LED through the BSP and read a button.

## 📱 Full Code

`src/lib.rs`

```rust
#![no_std]

use esp_hal::peripherals::Peripherals;

/// A handle to the µFerris board and everything on it.
pub struct UFerris {
    // peripherals land here as the series progresses
}

/// Consume the raw chip peripherals and hand back a ready-to-use board.
pub fn uferris_init(_peripherals: Peripherals) -> UFerris {
    UFerris {}
}
```

`src/bin/main.rs`

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

use esp_backtrace as _;
use esp_hal::{
    clock::CpuClock,
    main,
    time::{Duration, Instant},
};
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 _uferris = uferris_init(peripherals);

    // Nothing to drive yet — the board struct is still empty.
    loop {
        let delay_start = Instant::now();
        while delay_start.elapsed() < Duration::from_millis(500) {}
    }
}
```
