Embedded Rust & Embassy: PWM Generation
Tags: rust, iot, tutorial, beginners
Published: 2022-12-05 14:56:19

> This blog post is the third of a multi-part series of posts Rust embassy. This post is going to explore generating PWM using the embassy HAL. Please be aware that certain concepts in newer posts could depend on concepts in prior posts.

#### If you find this post useful, and if Embedded Rust interests you, stay in the know and skyrocket your learning curve by subscribing to The Embedded Rustacean newsletter:

{% cta  http://www.theembeddedrustacean.com %} Subscribe Now to The Embedded Rustacean{% endcta%} 

## Introduction
Through the past posts thus far, embassy has delivered everything one would expect of a HAL. This includes less verbosity, ease of use, and a lot of flexibility. However, transitioning to an `async` mindset might not be the easiest for a beginner. As a result, what I reckon that is not highlighted strongly enough, is that using `async` is optional, especially if coding a single thread polled application. As such, one can leverage the ease of the HAL without dealing with the complexity of async.

I personally look at it from an educational view. For example, when teaching embedded systems, interrupts and DMAs for example are not leading topics. Neither are multi threaded applications for that matter. Additionally, HAL abstractions are sometimes necessary to ease beginners into embedded systems. This makes me feel its necessary not blur the lines and separate how Embassy HAL can be used in separation from the complexity of multithreaded and `async`. 

On a side note, this is maybe a recurring theme that I feel makes the learning embedded Rust curve steeper. I recall a similar experience when first when reading the Rust embedded books. Although the books would state that development was at a certain abstraction level (ex. HAL), the code examples were mixing in a lot of different abstraction level code (Ex. PAC) that made things ever more confusing. I feel that blurring the lines between abstractions and/or features not only makes learning a topic more challenging, but also makes it hard to appreciate the value introduced.

In this post, the purpose is to demonstrate how one can leverage the embassy HAL strictly without any `async/await` features. I will be rewriting the stm32f4xx-hal PWM buzzer application I created in an [older post](https://apollolabsblog.hashnode.dev/stm32f4-embedded-rust-at-the-hal-pwm-buzzer), albeit this time using the embassy STM32 HAL. The application sets up a PWM peripheral to play different tones on a buzzer. The different tones are then used to generate a tune. 

### 📚 Knowledge Pre-requisites
To understand the content of this post, you need the following:
- Basic knowledge of coding in Rust.
- Familiarity with the basic template for creating embedded applications in Rust.

### 💾 Software Setup
All the code presented in this post in addition to instructions for the environment and toolchain setup are available on the [apollolabsdev Nucleo-F401RE](https://github.com/apollolabsdev/stm32-nucleo-f401re) git repo. Note that if the code on the git repo is slightly different then it means that it was modified to enhance the code quality or accommodate any HAL/Rust updates.

### 🛠 Hardware Setup
#### Materials
- [Nucleo-F401RE board](https://s.click.aliexpress.com/e/_DdEhurV)

![Nucleo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/72o3n48bqdjvob9dlzqo.jpg)

- Seeed Studio [Grove Base Shield V2.0](https://www.seeedstudio.com/base-shield-v2.html)

![Base Shield](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0kd57cf7tjchyno0x4iw.png)
- Seeed Studio [Grove - Piezo Buzzer/Active Buzzer](https://www.seeedstudio.com/grove-buzzer.html).


![Buzzer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nhro00k89l9cvukzz31p.jpg)

#### 🔌 Connections
- Buzzer positive terminal connected to pin PA9 (through Grove Base Shield Connector D8).
- Buzzer negative terminal connected to GND (through Grove Base Shield Connector D8).

**🚨 Important Note:**
> I used the Grove modular system for connection ease. It is a more elegant approach and less prone to mistakes. One can directly wire the buzzer to the board pins if need be.

## 👨‍🎨 Software Design

Through the buzzer-connected signal pin, various tones can be generated by the controller PWM peripheral. This occurs by changing the PWM frequency to match the needed tone. As a result, to generate a certain tune a collection of tones at a certain rate (tempo) need to be provided to the PWM peripheral. This also means that the code would need to include some data structures storing the needed information to provide to the PWM peripheral. Two data structures are needed, the first would include a mapping between notes and their associated frequencies. The second would represent a tune that includes a collection of notes each played for a certain amount of beats.

Following that information, after configuring the device, the algorithmic steps are as follows:

1. From the tune array data structure obtain a note and its associated beat
2. From the tones array retrieve the frequency associated with the note obtained in step 1
3. Play the note for the desired duration (number of beats * tempo)
4. Include half a beat of silence (0 frequency) between notes
5. Go back to 1.

## 👨‍💻 Code Implementation

### 📥 Crate Imports
In this implementation, the following crates are required:
- The `cortex_m_rt` crate for startup code and minimal runtime for Cortex-M microcontrollers.
- The `panic_halt` crate to define the panicking behavior to halt on panic.
- The `embassy_stm32` crate to import the embassy STM32 series microcontroller device hardware abstractions. The needed abstractions are imported accordingly.
- The `embassy_time` crate to import timekeeping capabilities.
- `DelayMs` from `embedded_hal` which is required by `embassy_time::Delay`.

```rust
use cortex_m_rt::entry;
use embassy_stm32::pwm::simple_pwm::{PwmPin, SimplePwm};
use embassy_stm32::pwm::Channel;
use embassy_stm32::time::hz;
use embassy_time::Delay;
use embedded_hal::blocking::delay::DelayMs;
use panic_halt as _;
```

### 🎛 Peripheral Configuration Code

Ahead of code configuration, I'd like to note that now that all we want to do is use the HAL aspect of embassy, we can fall back to the original template for embedded. This means that we do not require this macro anymore:

```rust
#[embassy_executor::main]
```
which was also associated with `main` as an `async` block. 

Instead, we can go back to this:

```rust
#[entry]
fn main() -> ! {
// Application Code
}
```

#### PWM Peripheral Configuration:

1️⃣ **Initialize MCU and obtain a handle for the device peripherals**: A device peripheral handler p is created:
```rust
let p = embassy_stm32::init(Default::default());
```
2️⃣  **Obtain a handle and configure the PWM pin**: Here `PA9` needs to be configured such that it's connected to the internal circuitry of channel 2 of the TIM1 peripheral. It can be determined from the [device datasheet](https://www.st.com/resource/en/datasheet/stm32f401re.pdf) that pin PA9 is connected to channel 2 of the timer.This is done using the `new_ch2` instance method of the `PwmPin` type. I will name the handle `buzz_pin` and configure it as follows: 

```rust
    let buzz_pin = PwmPin::new_ch2(p.PA9);
```
Something to note here is that the flow is a bit odd and different than before. One would expect typically to attach a channel after configuring/choosing a timer rather than the other way around. Here we have configured the channel but haven't associated a particular timer yet.

3️⃣ **Obtain a PWM handle and configure the timer**:  As mentioned in the previous step pin PA9 needs to connect to the TIM1 peripheral in the microcontroller device. As such, this means we need to configure TIM1 to generate a PWM from channel 2 and somehow pass it to the handle of the pin we want to use. Digging into [documentation](https://docs.embassy.dev/embassy-stm32/git/stm32f030c6/pwm/simple_pwm/struct.SimplePwm.html), this is done using the `SimplePwm` struct from `embassy_stm32::pwm::simple_pwm` where there exists a `new` method to configure PWM with the following signature:

```rust
pub fn new(
    tim: impl Peripheral<P = T> + 'd,
    _ch1: Option<PwmPin<'d, T, Ch1>>,
    _ch2: Option<PwmPin<'d, T, Ch2>>,
    _ch3: Option<PwmPin<'d, T, Ch3>>,
    _ch4: Option<PwmPin<'d, T, Ch4>>,
    freq: Hertz
) -> Self
```
The first parameter `tim` expects an argument passing in a timer peripheral instance, the next four parameters expect a `PwmPin` argument for the associated channel, and finally `freq` expects a PWM frequency.

This results in the following line of code:

```rust
let mut pwm = SimplePwm::new(p.TIM1, None, Some(buzz_pin), None, None, hz(2000));
```
Note how a `None` is passed for unused channels. Additionally, `buzz_pin` is wrapped in a `Some()` since an `Option` enum is required to be passed. Finally, a frequency of 2kHz is chosen.
    
4️⃣ **Enable PWM & Configure the duty cycle**: Here all I need is to generate a regular square wave, so I need a duty cycle of 50%. This is being done over two steps as follows:

```rust
let max_duty = pwm.get_max_duty();
pwm.set_duty(Channel::Ch2, max_duty / 2);
``` 
The first line applies the `get_max_duty` method on the `pwm` handle which returns a `u16` value representing the maximum duty cycle. The second line applies the `set_duty` method that accepts two parameters, an enum of the channel that that pin is connected to and the duty cycle value.

One more thing remaining is to enable the PWM peripheral which can be done simply by using the `enable` method:

```rust
pwm.enable(Channel::Ch2);
```
This is it for configuration! Let's now jump into the application code.

### Application Code

According to the software design description, two arrays are needed to store the tone and tune information. The first array `tones`, contains a collection of tuples that provide a mapping of the note letter and its corresponding frequency. The second array `tune` contains a collection of tuples that present the note that needs to be played and the number of beats per note. Note that the `tune` array contains an empty note `' '` that presents silence and does not have a corresponding mapping in the `tones` array.

```rust
    let tones = [
        ('c', 261.Hz()),
        ('d', 294.Hz()),
        ('e', 329.Hz()),
        ('f', 349.Hz()),
        ('g', 392.Hz()),
        ('a', 440.Hz()),
        ('b', 493.Hz()),
    ];

    let tune = [
        ('c', 1),
        ('c', 1),
        ('g', 1),
        ('g', 1),
        ('a', 1),
        ('a', 1),
        ('g', 2),
        ('f', 1),
        ('f', 1),
        ('e', 1),
        ('e', 1),
        ('d', 1),
        ('d', 1),
        ('c', 2),
        (' ', 4),
    ];
```
Next, before jumping into the algorithmic loop the tempo needs to be defined which will be used in the `delay` handle. A `tempo` variable is created as follows:

```rust
let tempo = 300_u32;
```
Next, the application loop looks as follows:

```rust
    loop {
        // 1. Obtain a note in the tune
        for note in tune {
            // 2. Retrieve the freqeuncy and beat associated with the note
            for tone in tones {
                // 2.1 Find a note match in the tones array and update frequency and beat variables accordingly
                if tone.0 == note.0 {
                    // 3. Play the note for the desired duration (beats*tempo)
                    // 3.1 Adjust period of the PWM output to match the new frequency
                    pwm.set_freq(tone.1);
                    // 3.2 Enable the channel to generate desired PWM
                    pwm.enable(Channel::Ch2);
                    // 3.3 Keep the output on for as long as required
                    Delay.delay_ms(note.1 * tempo);
                } else if note.0 == ' ' {
                    // 2.2 if ' ' tone is found disable output for one beat
                    pwm.disable(Channel::Ch2);
                    Delay.delay_ms(tempo);
                }
            }
            // 4. Silence for half a beat between notes
            // 4.1 Disable the PWM output (silence)
            pwm.disable(Channel::Ch2);
            // 4.2 Keep the output off for half a beat between notes
            Delay.delay_ms(tempo / 2);
            // 5. Go back to 1.
        }
    }
```
Let's break down the loop line by line. The line 
```rust 
for note in tune
``` 
iterates over the `tune` array obtaining a note with each iteration. Within the first loop another `for` loop `for tone in tones` is nested which iterates over the `tones` array. The second loop retrieves the frequency and beat associated for each note obtained from the `tune` array. The statement 

```rust
if tone.0 == note.0
```
checks if there is a match for the mapping between the `note` and the `tone`. The `.0` index is in reference to the first index in the tuple which is the note letter. Once a match is found, the note is played for the desired duration which equals the beats multiplied by the tempo. This is done over three steps:

First, using the [`set_freq` method](https://docs.embassy.dev/embassy-stm32/git/stm32f030c6/pwm/simple_pwm/struct.SimplePwm.html#method.set_freq) in the `SimplePwm` abstraction, the tone frequency is adjusted to match the frequency of the found `tone`. The frequency of the `tone` corresponds to index `1` o the tuple and is configured as follows:

```rust 
pwm.set_freq(tone.1);
```
Second, using the [`enable` method](https://docs.embassy.dev/embassy-stm32/git/stm32f030c6/pwm/simple_pwm/struct.SimplePwm.html#method.enable) the `pwm` channel is enabled to activate the desired PWM.

```rust
pwm.enable(Channel::Ch2);
```
In the third and final step the output is kept on for a period of beat*tempo milliseconds. Here I leverage the `delay` handle created earlier as follows:

```rust
Delay.delay_ms(note.1 * tempo);
```
In the case a `' '` note is found the following lines are executed:

```rust
else if note.0 == ' ' {
         pwm.disable(Channel::Ch2);
         Delay.delay_ms(tempo);
}
```
Which disables the PWM channel output for one beat.

Finally, after exiting the inner loop, half a beat of silence is introduced between notes in the outer loop `tune` as follows:

```rust
pwm.disable(Channel::Ch2);
Delay.delay_ms(tempo / 2);
```

## Full Application Code
Here is the full code for the implementation described in this post. You can additionally find the full project and others available on the [apollolabsdev Nucleo-F401RE](https://github.com/apollolabsdev/stm32-nucleo-f401re) git repo.


```rust
#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]

use cortex_m_rt::entry;
use embassy_stm32::pwm::simple_pwm::{PwmPin, SimplePwm};
use embassy_stm32::pwm::Channel;
use embassy_stm32::time::hz;
use embassy_time::Delay;
use embedded_hal::blocking::delay::DelayMs;

use panic_halt as _;

#[entry]
fn main() -> ! {
    // Initialize and create handle for devicer peripherals
    let p = embassy_stm32::init(Default::default());

    // Configure the Buzzer pin as an alternate and obtain handler.
    // I will use PA9 that connects to Grove shield connector D8
    // On the Nucleo FR401 PA9 connects to timer TIM1

    // Instantiate PWM pin and connect to channel
    let buzz_pin = PwmPin::new_ch2(p.PA9);

    // Instantiate and Configure Timer 1 for PWM
    let mut pwm = SimplePwm::new(p.TIM1, None, Some(buzz_pin), None, None, hz(2000));

    // Get Maximum Duty
    let max_duty = pwm.get_max_duty();

    // Configure the Duty Cycle to 50%
    pwm.set_duty(Channel::Ch2, max_duty / 2);

    // Enable PWM
    pwm.enable(Channel::Ch2);

    // // Configure and create a handle for a second timer using TIM2 for delay puposes
    // let mut delay = Delay.delay_ms(100);

    // Define the notes and their frequencies
    let tones = [
        ('c', hz(261)),
        ('d', hz(294)),
        ('e', hz(329)),
        ('f', hz(349)),
        ('g', hz(392)),
        ('a', hz(440)),
        ('b', hz(493)),
    ];

    // Define the notes to be played and the beats per note
    let tune = [
        ('c', 1),
        ('c', 1),
        ('g', 1),
        ('g', 1),
        ('a', 1),
        ('a', 1),
        ('g', 2),
        ('f', 1),
        ('f', 1),
        ('e', 1),
        ('e', 1),
        ('d', 1),
        ('d', 1),
        ('c', 2),
        (' ', 4),
    ];

    // Define the tempo
    let tempo = 300_u32;

    // Application Loop
    loop {
        // 1. Obtain a note in the tune
        for note in tune {
            // 2. Retrieve the freqeuncy and beat associated with the note
            for tone in tones {
                // 2.1 Find a note match in the tones array and update frequency and beat variables accordingly
                if tone.0 == note.0 {
                    // 3. Play the note for the desired duration (beats*tempo)
                    // 3.1 Adjust period of the PWM output to match the new frequency
                    pwm.set_freq(tone.1);
                    // 3.2 Enable the channel to generate desired PWM
                    pwm.enable(Channel::Ch2);
                    // 3.3 Keep the output on for as long as required
                    Delay.delay_ms(note.1 * tempo);
                } else if note.0 == ' ' {
                    // 2.2 if ' ' tone is found disable output for one beat
                    pwm.disable(Channel::Ch2);
                    Delay.delay_ms(tempo);
                }
            }
            // 4. Silence for half a beat between notes
            // 4.1 Disable the PWM output (silence)
            pwm.disable(Channel::Ch2);
            // 4.2 Keep the output off for half a beat between notes
            Delay.delay_ms(tempo / 2);
            // 5. Go back to 1.
        }
    }
}
``` 

## Conclusion
In this post, a buzzer application that plays a tune that leverages the timer peripheral PWM output is rewritten using the STM32 embassy framework. The code is written for the STM32F401RE microcontroller on the Nucleo-F401RE development board. It is highlighted how the HAL aspect of embassy framework can be used without having to use any `async` features. Compared to prior code, embassy proves to be simpler to deal with configuring and using peripherals. Have any questions? Share your thoughts in the comments below 👇.  

#### If you found this post useful, and if Embedded Rust interests you, stay in the know and skyrocket your learning curve by subscribing to The Embedded Rustacean newsletter:

{% cta  http://www.theembeddedrustacean.com %} Subscribe Now to The Embedded Rustacean{% endcta%}