# Embedded Rust BSPs with uFerris & Xiao: Buzzer Support with PWM

> This is the fifth post in the µFerris & Xiao BSP series, where we build a Board Support Package from scratch, one peripheral at a time.

## 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)
    
4.  [LED & Button Support with GPIO](https://blog.theembeddedrustacean.com/embedded-rust-bsps-with-uferris-xiao-led-button-support-with-gpio)
    

## Introduction

In the last post, we added the first two components to the BSP; LED 1 and SW5. This post adds the buzzer. The buzzer is an audible transducer on GPIO4, and making it sound means feeding it a PWM square wave at around 2.7 kHz. For that, we will use the ESP LEDC peripheral, which generates PWM in hardware.

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 PWM concepts, mainly frequency and duty cycle.
    
*   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 `05-buzzer-pwm/`:

```shell
git clone https://github.com/theembeddedrustacean/learn-bsp-rs
cd learn-bsp-rs/05-buzzer-pwm
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 component added in this post is connected on the µFerris as follows:

| Device | XIAO Pin | Notes |
| --- | --- | --- |
| Buzzer | GPIO4 | Magnetic transducer. Resonant around 2.7 kHz. |

The buzzer is loudest when driven at its resonance frequency. 2700 Hz is the figure the µFerris buzzer is specified at; this value will be a constant in the BSP and not a parameter the application passes in.

## 👨‍🎨 Software Design

This post builds on the BSP from the previous post, where we added LED and button support. This post assumes you understand that code. Recall the BSP code is composed of three parts; **The Board struct**, **The Board Initialization Function**, and **The Board Control Functions**. Each post adds to all three.

### The Board Struct

Recall that the board struct represents the board, with one member per component. We add the buzzer as a third member. This is the form of the updated struct:

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

`Channel` is an LEDC type from `esp_hal::ledc::channel`. `LowSpeed` marks which of the two LEDC speed modes it belongs to. The ESP32-C3 only implements low-speed channels, so `LowSpeed` is the only choice here.

Note what the struct does **not** hold. Setting up the buzzer produces three items; an `Ledc` driver, a `Timer`, and a `Channel`. We keep only the channel. The next section explains why.

### The Board Initialization Function

The LEDC peripheral is organized in two layers:

*   A **timer** produces a frequency and a duty resolution.
    
*   A **channel** attaches to a timer, drives one output pin, and holds the duty value for that pin.
    

We configure the buzzer in four steps:

1.  Create the `Ledc` driver from the LEDC peripheral.
    
2.  Configure a Timer.
    
3.  Promote the configured timer to `'static`.
    
4.  Configure the `Ledc` channel & attach the GPIO pin.
    

One thing to note is that a channel's configuration holds a reference to its timer, and that reference has to be `'static` because the channel lives in the board struct for the rest of the program. A timer created as a local in `uferris_init` is dropped when the function returns, so the borrow checker rejects it.

The issue, however, is that we cannot declare the timer `` `static `` at compile time since we would not have initialized it yet. This means we need a mechanism to turn the timer into a `` `static `` after the program runs. `static_cell::StaticCell` solves this. It is a `static` slot that starts empty and is filled once at runtime. We create a new StaticCell as follows:

```rust
static BUZZER_TIMER: StaticCell<Timer<'static, LowSpeed>> = StaticCell::new();
```

then later in `main` after initializing `buzzer_timer` we would initialize the `StaticCell` as follows:

```rust
let buzzer_timer = BUZZER_TIMER.init(buzzer_timer);
```

What happens here is that `init` moves the value into the static and hands back a `&'static mut` to it.

### The Board Control Functions

We add three functions:

| Function | Behaviour |
| --- | --- |
| `buzz_on(percent)` | Sounds the buzzer when called, at the given duty percentage. |
| `buzz_off()` | Silences the buzzer when called. |

`buzz_on` takes a **duty** as a percentage, not a frequency. The buzzer's resonance frequency is fixed. The BSP resolves the percentage against the timer's duty resolution, so the application never sees the raw duty range.

> Note that 100% duty is not the loudest setting; it is silence. Holding the pin permanently high is DC, and the transducer only moves on transitions. The output is strongest at 50%.

### Test Application Design

The test application steps are as follows:

1.  Poll `sw5_pressed()` in a loop.
    
2.  Mirror the result on LED 1 and the buzzer; both on while the button is held, both 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 LEDC types.** The GPIO imports from the last post stay. To them, we add the LEDC types, the `SetDutyCycle` trait, `Rate` to express the frequency, and `StaticCell`. Below is the code:

```rust
use embedded_hal::pwm::SetDutyCycle;
use esp_hal::{
    gpio::{DriveMode, Input, InputConfig, Level, Output, OutputConfig},
    ledc::{
        LSGlobalClkSource, Ledc, LowSpeed,
        channel::{self, Channel, ChannelIFace},
        timer::{self, Timer, TimerIFace},
    },
    peripherals::Peripherals,
    time::Rate,
};
use static_cell::StaticCell;
```

Note that `ChannelIFace` and `TimerIFace` are traits. They carry the `configure` methods, so the code will not compile without them in scope, even though neither name appears anywhere else.

**2️⃣ Declare the static timer slot and the buzzer frequency.** Both live at module level. Below is the code:

```rust
static BUZZER_TIMER: StaticCell<Timer<'static, LowSpeed>> = StaticCell::new();

/// Resonant frequency of the µFerris buzzer, in hertz.
const BUZZER_FREQ_HZ: u32 = 2700;
```

**3️⃣ Populate the board struct.** Add the buzzer `Channel` as a member alongside the two existing GPIO components:

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

**4️⃣ Populate the initialization method.** The GPIO setup from the previous post is unchanged, and the buzzer setup follows it. Below is the code added to the `init` method:

```rust
// Instantiate the LEDC peripheral
let mut ledc = Ledc::new(peripherals.LEDC);
ledc.set_global_slow_clock(LSGlobalClkSource::APBClk);

// Configure Timer 0
let mut buzzer_timer = ledc.timer::<LowSpeed>(timer::Number::Timer0);
buzzer_timer
    .configure(timer::config::Config {
        duty: timer::config::Duty::Duty14Bit,
        clock_source: timer::LSClockSource::APBClk,
        frequency: Rate::from_hz(BUZZER_FREQ_HZ),
    })
    .expect("buzzer timer configuration failed");

// Promote the timer to 'static
let buzzer_timer = BUZZER_TIMER.init(buzzer_timer);

// Configure the LEDC Channel and attach GPIO
let mut buzzer = ledc.channel(channel::Number::Channel0, peripherals.GPIO4);
buzzer
    .configure(channel::config::Config {
        timer: buzzer_timer,
        duty_pct: 0,
        drive_mode: DriveMode::PushPull,
    })
    .expect("buzzer channel configuration failed");

UFerris {
    led1,
    sw_btn5,
    buzzer,
}
```

> Note the shadowing on the timer. `BUZZER_TIMER.init(buzzer_timer)` consumes the local `Timer` and returns a reference to where it now lives, so we reuse the name to keep the following code readable.

**5️⃣ Add the control functions.** These go in the same `impl` block as the LED and button functions. Below is the code:

```rust
/// Sound the buzzer at the given duty, as a percentage from 0 to 100.
pub fn buzz_on(&mut self, duty_percent: u8) {
    let _ = self.buzzer.set_duty_cycle_percent(duty_percent);
}

/// Silence the buzzer.
pub fn buzz_off(&mut self) {
    let _ = self.buzzer.set_duty_cycle_fully_off();
}
```

`set_duty_cycle_percent` comes from the `SetDutyCycle` trait and scales the percentage against the channel's maximum duty, which the timer's resolution sets. It returns a `Result` whose error type is `Infallible` for this channel, so we discard the result with `let _ =`.

Optionally, you can add a `buzz_beep(ms)` function that sounds the buzzer for a fixed time and returns. That is left for the reader as an exercise.

## 🏃‍♂️ Test Code & Run

The application asks for a duty as a percentage, so nothing in it changes if the BSP's duty resolution does:

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

use esp_backtrace as _;
use esp_hal::{clock::CpuClock, main};
use esp_println::println;
use uferris::{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 = uferris_init(peripherals);

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

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

    loop {
        let pressed = uferris.sw5_pressed();

        // LED 1 and the buzzer both follow SW5
        if pressed {
            uferris.led1_on();
            uferris.buzz_on(50);
        } else {
            uferris.led1_off();
            uferris.buzz_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 no LEDC, no timer, and no duty resolution anywhere in that code. The application knows the board has a buzzer and that the buzzer takes a duty. Everything else stays in the BSP.

Flash it and open the monitor:

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

The board beeps once on reset. After that, holding SW5 lights LED 1 and sounds the buzzer, and the terminal prints one line per transition:

```plaintext
SW5 pressed
SW5 released
```

If the boot beep sounds but pressing does nothing, look at the button. If nothing sounds at all, check that `set_global_slow_clock` is called before the timer is configured. A timer with no clock configures without complaint and produces no output.

## ✅ Conclusion

In this post, we added the buzzer to the µFerris BSP, and the board struct now holds a driver for it. The API the application sees did not get more complicated. It gained `buzz_on` and `buzz_off`, and it still has no idea the LEDC peripheral exists.

The next post adds the LDR, it will be the first component that hands data back rather than taking a command.

## 📱 Full Code

`src/lib.rs`

```rust
#![no_std]

use embedded_hal::pwm::SetDutyCycle;
use esp_hal::{
    gpio::{DriveMode, Input, InputConfig, Level, Output, OutputConfig},
    ledc::{
        LSGlobalClkSource, Ledc, LowSpeed,
        channel::{self, Channel, ChannelIFace},
        timer::{self, Timer, TimerIFace},
    },
    peripherals::Peripherals,
    time::Rate,
};
use static_cell::StaticCell;

/// The LEDC timer the buzzer channel reads its frequency from. The channel
static BUZZER_TIMER: StaticCell<Timer<'static, LowSpeed>> = StaticCell::new();

/// Resonant frequency of the µFerris buzzer, in hertz.
const BUZZER_FREQ_HZ: u32 = 2700;

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

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

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

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

    // Instantiate the LEDC peripheral
    let mut ledc = Ledc::new(peripherals.LEDC);
    ledc.set_global_slow_clock(LSGlobalClkSource::APBClk);

    // Configure the timer attached to the LEDC
    let mut buzzer_timer = ledc.timer::<LowSpeed>(timer::Number::Timer0);
    buzzer_timer
        .configure(timer::config::Config {
            duty: timer::config::Duty::Duty14Bit,
            clock_source: timer::LSClockSource::APBClk,
            frequency: Rate::from_hz(BUZZER_FREQ_HZ),
        })
        .expect("buzzer timer configuration failed");

    // Promote the timer to 'static
    let buzzer_timer = BUZZER_TIMER.init(buzzer_timer);

    // Configure the LEDC Channel and attach pin.
    let mut buzzer = ledc.channel(channel::Number::Channel0, peripherals.GPIO4);
    buzzer
        .configure(channel::config::Config {
            timer: buzzer_timer,
            duty_pct: 0,
            drive_mode: DriveMode::PushPull,
        })
        .expect("buzzer channel configuration failed");

    UFerris {
        led1,
        sw_btn5,
        buzzer,
    }
}

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()
    }

    /// Sound the buzzer at the given duty, as a percentage from 0 to 100.
    pub fn buzz_on(&mut self, duty_percent: u8) {
        let _ = self.buzzer.set_duty_cycle_percent(duty_percent);
    }

    /// Silence the buzzer.
    pub fn buzz_off(&mut self) {
        let _ = self.buzzer.set_duty_cycle_fully_off();
    }

}
```

`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, 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 = uferris_init(peripherals);

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

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

    loop {
        let pressed = uferris.sw5_pressed();

        // LED 1 and the buzzer both follow SW5 state
        if pressed {
            uferris.led1_on();
            uferris.buzz_on(50);
        } else {
            uferris.led1_off();
            uferris.buzz_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);
    }
}
```
