STM32F4 Embedded Rust at the HAL: PWM Buzzer

Search for a command to run...

No comments yet. Be the first to comment.
Learn Rust at the HAL level with the STM32F4 microcontroller.
This blog post is the fifth of a multi-part series of posts where I explore various peripherals in the STM32F401RE microcontroller using embedded Rust at the HAL level. Please be aware that certain co
A community giveaway program by The Embedded Rustacean × Seeed Studio to grow the embedded Rust ecosystem

Introduction Another year comes to an end; as always, it is an opportunity to reflect on the achievements of the past year and look forward to the next. It's been over 3.5 years since I embarked on my embedded Rust journey, and it only gets more exci...

Introduction Another year comes to an end, as such, this is an opportunity to reflect on the achievements of 2024 and look forward to 2025. It's been over 2.5 years since I embarked on my embedded Rust journey, and it remains as exciting as ever. If ...

Introduction 🚀 On Friday, May 17th, 2024, the release of Simplified Embedded Rust was announced marking a new kind of book in the embedded Rust learning space. I'm happy to say that this book, design

This post is the sixth of a multi-part series where I'm exploring the use of Bluetooth Low Energy along embedded Rust on the ESP32. Information in this post might rely on knowledge presented in past p

This blog post is the fourth of a multi-part series of posts where I explore various peripherals in the STM32F401RE microcontroller using embedded Rust at the HAL level. Please be aware that certain concepts in newer posts could depend on concepts in prior posts.
Prior posts include (in order of publishing):
In this post, I will be exploring the usage of the PWM peripheral in the stm32f4xx-hal. I will be configuring and setting up the PWM peripheral to play different tones on a buzzer. The different tones will be used to generate a tune.
To understand the content of this post, you need the following:
All the code presented in this post in addition to instructions for the environment and toolchain setup are available on the The Embedded Rustacean STM32F4 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.

- Seeed Studio [Grove - Piezo Buzzer/Active Buzzer](https://www.seeedstudio.com/grove-buzzer.html).
🚨 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.
📝 Note:
The code in this post is an adaptation in Rust of the buzzer example provided by Seeed Studio here.
The buzzer used is quite simple to operate. 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:
There are fine details in between relative to the PWM details that will be discussed in detail in the implementation.
In this implementation, the following crates are required:
cortex_m_rt crate for startup code and minimal runtime for Cortex-M microcontrollers.panic_halt crate to define the panicking behavior to halt on panic.stm32f4xx_hal crate to import the STMicro STM32F4 series microcontrollers device hardware abstractions on top of the peripheral access API.use cortex_m_rt::entry;
use panic_halt as _;
use stm32f4xx_hal::{
pac::{self},
prelude::*,
timer::Channel,
};
1️⃣ Obtain a handle for the device peripherals: In embedded Rust, as part of the singleton design pattern, we first have to take the PAC level device peripherals. This is done using the take() method. Here I create a device peripheral handler named dp as follows:
let dp = pac::Peripherals::take().unwrap();
2️⃣ Promote the PAC-level GPIO structs: In this application, I'm going to need one pin that will be generating the PWM output. The pin being used is PA9 as stated earlier and it's one of the pins that support PWM output. In fact, it can be connected internally to timer 1 or TIM1. Before I can obtain a handle for PA9 I need to promote the pac-level GPIOA struct to be able to create handles for individual pins. I do this by using the split() method as follows:
let gpioa = dp.GPIOA.split();
3️⃣ Obtain a handle and configure the PWM pin: Here PA9 needs to be configured such that it's connected to the internal circuitry of TIM1 that enables generating a PWM output. This is done using the into_alternate generic Pin type method. I will name the handle buzz and configure it as follows:
let buzz = gpioa.pa9.into_alternate();
1️⃣ Configure the system clocks: The system clocks need to be configured as they are needed in setting up the timer peripheral. To set up the system clocks we need to first promote the RCC struct from the PAC and constrain it using the constrain() method (more detail on the constrain method here) to give use access to the cfgr struct. After that, we create a clocks handle that provides access to the configured (and frozen) system clocks. The clocks are configured to use an HSE frequency of 8MHz by applying the use_hse() method to the cfgr struct. The HSE frequency is defined by the reference manual of the Nucleo-F401RE development board. Finally, the freeze() method is applied to the cfgr struct to freeze the clock configuration. Note that freezing the clocks is a protection mechanism by the HAL to avoid the clock configuration changing during runtime. It follows that the peripherals that require clock information would only accept a frozen Clocks configuration struct.
let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.use_hse(8.MHz()).freeze();
2️⃣ Obtain a PWM handle and configure the timer: The way PWM is structured in the stm32f4xx-hal is somewhat confusing. In the STM32F401 device the TIM1 timer, has 4 channels that can each be connected to PWM output pins. In past posts when configuring peripherals like the UART, it was demonstrated how the peripheral can be configured using extension traits or the peripheral abstraction to instantiate an instance. The confusing part here is that between the extension traits and the abstractions, it seems that the different pwm abstractions do not have a method to instantiate an instance. Instead, in order to instantiate a PWM peripheral (create a handle) there are the following two options:
Pin handles into one of the pwm extension traits and apply the split method to obtain a tuple of handles of type PWMChannel. Pin handle into one of the pwm extension traits to obtain a Pwm type.Going with option 2 will consequently require that I pass the channel number into the methods I use which will be seen later. Simply put, as the stm32f4xx-hal stands right now, PWM instances can be created only by applying traits on timer peripherals. So, since I'm using only a single pin, I need only a single channel so I elected to go with the second option for my application. I created a buzz_pwm handle as follows:
let mut buzz_pwm = dp.TIM1.pwm_hz(buzz, 2000.Hz(), &clocks);
Breaking this line down, you can see I am using the pwm_hz method which has the following signature:
fn pwm_hz<P, PINS>(
self,
pins: PINS,
freq: Hertz,
clocks: &Clocks
) -> PwmHz<Self, P, PINS>
where
PINS: Pins<Self, P>;
As can be seen, the pwm_hz method returns a PwmHz type in which I am also passing as arguments the buzz pin handle, the PWM frequency, and the frozen clocks handle. The reason that I chose is because pwm_hz among the other two available methods is that its methods are going to allow me to easier change the PWM output based on frequency values.
🚨 Important Note:
At the time of writing this post, I noticed that if going with option 1 stated earlier that returns a
PWMChannelcan prove to be quite problematic. In navigating the documentation, thePWMChannelimplementations do not include methods that allow to get and set the period of the peripheral. There is an issue that I submitted here for that.
3️⃣ Configure the PWM 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:
let max_duty = buzz_pwm.get_max_duty();
buzz_pwm.set_duty(Channel::C2, max_duty / 2);
Analyzing the above lines, the first line applies the get_max_duty method on the buzz_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. It can be determined from the device datasheet that pin PA9 is connected to channel 2 of the timer. In any case, if by mistake the programmer happens to insert the incorrect channel number, the compiler will generate an error.
🚨 Important Note:
I've found that by skipping this step (not configuring the duty cycle) the PWM output will not operate properly. I had assumed that the code would fall back to a default value but that was not the case.
In the algorithm, a delay will need to be introduced to control the tempo. Since I am already using TIM1 I need to leverage a different timer of which I will be using TIM2. I create a millisecond delay handle delay as follows:
let mut delay = dp.TIM2.delay_ms(&clocks);
This is it for configuration! Let's now jump into the 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.
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:
let tempo = 300_u32;
Next, the application loop looks as follows:
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
buzz_pwm.set_period(tone.1);
// 3.2 Enable the channel to generate desired PWM
buzz_pwm.enable(Channel::C2);
// 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
buzz_pwm.disable(Channel::C2);
delay.delay_ms(tempo);
}
}
// 4. Silence for half a beat between notes
// 4.1 Disable the PWM output (silence)
buzz_pwm.disable(Channel::C2);
// 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
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
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_period method in the PwmHz 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:
buzz_pwm.set_period(tone.1);
Second, using the enable method the buzz_pwm channel is enabled to activate the desired PWM.
buzz_pwm.enable(Channel::C2);
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:
delay.delay_ms(note.1 * tempo);
In the case a ' ' note is found the following lines are executed:
else if note.0 == ' ' {
buzz_pwm.disable(Channel::C2);
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:
buzz_pwm.disable(Channel::C2);
delay.delay_ms(tempo / 2);
🚨 Important Note:
Some of the previous methods do not have a description in the documentation. However, the name of the method itself is quite intuitive to figure out what it does.
Here is the full code for the implementation described in this post. You can additionally find the full project and others available on the The Embedded Rustacean STM32F4 git repo.
#![no_std]
#![no_main]
// Imports
use cortex_m_rt::entry;
use panic_halt as _;
use stm32f4xx_hal::{
pac::{self},
prelude::*,
timer::Channel,
};
#[entry]
fn main() -> ! {
// Setup handler for device peripherals
let dp = pac::Peripherals::take().unwrap();
// Set up the clocks
let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.use_hse(8.MHz()).freeze();
// 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
let gpioa = dp.GPIOA.split();
let buzz = gpioa.pa9.into_alternate();
let mut buzz_pwm = dp.TIM1.pwm_hz(buzz, 2000.Hz(), &clocks);
// Configure the duty cycle to 50%
// If duty not configured, PWM will not operate properly (suggest comments)
let max_duty = buzz_pwm.get_max_duty();
buzz_pwm.set_duty(Channel::C2, max_duty / 2);
// Configure and create a handle for a second timer using TIM2 for delay puposes
let mut delay = dp.TIM2.delay_ms(&clocks);
// Define the notes and their frequencies
let tones = [
('c', 261.Hz()),
('d', 294.Hz()),
('e', 329.Hz()),
('f', 349.Hz()),
('g', 392.Hz()),
('a', 440.Hz()),
('b', 493.Hz()),
];
// 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
buzz_pwm.set_period(tone.1);
// 3.2 Enable the channel to generate desired PWM
buzz_pwm.enable(Channel::C2);
// 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
buzz_pwm.disable(Channel::C2);
delay.delay_ms(tempo);
}
}
// 4. Silence for half a beat between notes
// 4.1 Disable the PWM output (silence)
buzz_pwm.disable(Channel::C2);
// 4.2 Keep the output off for half a beat between notes
delay.delay_ms(tempo / 2);
// 5. Go back to 1.
}
}
}
In this post, a buzzer application that plays a tune was created leveraging the timer peripheral PWM output for the STM32F401RE microcontroller on the Nucleo-F401RE development board. All the code was created at the HAL level using the stm32f4xx Rust HAL. Have any questions? Share your thoughts in the comments below 👇.