This project presents our preliminary work in Firmware Programming. We developed this project to help us better understand the concepts and applications of the course through hands on experience. Our project, called the Electronic Tone Generator, produces different sounds through a small speaker. By pressing different buttons or button combinations, the system generates various frequencies, resulting in different tones.
The main objective of this project is to help us understand the principles of frequency generation, sound production, and digital electronics. It demonstrates how electronic devices receive user inputs, process them through a microcontroller (MCU), and generate corresponding outputs. Through this project, we gain practical knowledge of how firmware and hardware work together in an embedded system.
How It WorksConcept
In this project, we are building a system that generates audio completely from scratch by rapidly turning a single pin HIGH and LOW to push and pull a speaker cone (creating a square wave).
Because we are programming in bare metal (without an operating system), our microcontroller runs in a permanent loop doing two things:
Polling: It constantly checks if you are pressing any buttons.
Bit Banging: When we press Play, it toggles the speaker pin on and off using precise microsecond delays. This means we are manually manipulating the bits to generate the audio without using any DAC hardware.
When we press the Up or Down buttons, we are simply changing the math for that microsecond delay. A shorter delay makes the pin toggle faster (creating a higher pitch), and a longer delay makes it toggle slower (creating a lower pitch).
Software &Hardware Architecture
For the software, the following functions are used throughout the project.
update_buttons() function is the main backbone of this project. This polls the buttons for inputs and perform actions when the specific buttons are pressed. This function is called every time in a while loop running forever.
The following is how update_buttons() work under the hood:
This process is being called in an infinite loop in our main() while(1) loop. It will only perform tasks when buttons are pressed. If no buttons are pressed, it skips the tasks described above and proceeds to check the buttons again. This method is polling the buttons to check for user input.
For the hardware aspect of the project, we are using the following circuit diagram
SW1, SW2 and SW3 are mapped to the internal buttons of the RT Spark 1 board. The speaker is connected to AUDIO_OUT and GND. For the buttons, it is configured as ACTIVE LOW buttons. The circuit diagram above does not show pull up resistors as STM32F407XX MCU takes care of this in the code configuration. However, pull up resistors are still important to prevent floating voltages.
Hardware SetupThe Hardware part of this project involves making an 8Ω 0.5W Speaker Module that contains a 330Ω resistor to limit current and a 1μF >16v Capacitor for Blocking DC Current from reaching the Speaker.
Below are the components required for the hardware of this Project
Also in addition, I will be using a Solid Core wire to solder all of the components later.
The diagram below shows the exact circuit diagram that we must follow.
The resistor and capacitor must be on the SIGNAL line of the 8Ω 0.5W speaker. Then the other input of the speaker must be connected to GND.
The image below shows how I soldered the components based on the schematic diagram shown above.
We actually did a mistake while soldering. We swapped the RED and BLACK Solid core wires. We used the BLACK wire for the SIGNAL side of the speaker, and RED Solid Core for the GND side of the speaker. We just realized this after we completely soldered all of the circuit. This will be okay, as the color of the wires won't affect how electricity flows through it, and should still work just fine, provided that the correct wire is connected to the correct pin in the RT Spark 1 Development Board later.
Software SetupMCU Configuration Setup
To Get Started with the software, open the STM32CubeMX Program and you should see an interface like this.
Go to File > New Project to Create a New Project
After that, a window should pop-up. This is the Project Creation Screen.
We will need to use RT Spark 1's Microcontroller Part Number which will be STM32F407ZGT6. Use that to search for the exact MCU in the STM32CubeMX Program and select it.
Once you have selected the exact MCU Part Number, click on Start Project button on the top right corner of the New Project Window
Then you should see the screen below. This indicates that the STM32CubeMX Project has been successfully created.
GPIO Configuration
We will first start by configuring the GPIOs we will need to utilize in this Project.
The following list are the GPIO Pins we will be utilizing for this Project
PA3- Output Mode, The pin that will drive the 8Ω 0.5W Speaker with our Generated tones.PC0- Input External Interrupt with Pull Up Resistor Mode, The pin that is physically connected on RT Spark 1's onboardBUTTON_LEFT. For triggering Increase Frequency to the Tone Generator function.PC1- Input External Interrupt with Pull Up Resistor Mode, The pin that is physically connected on RT Spark 1's onboardBUTTON_DOWN. For triggering Play/Pause function.PC4- Input External Interrupt with Pull Up Resistor Mode, The pin that is physically connected on RT Spark 1's onboardBUTTON_RIGHT. For triggering Decrease Frequency to the Tone Generator function.
Start by Configuring PC0as an Input Pin. Click pin PC0 or search it by typing on the bottom part of the screen.
From the context menu that will show, select GPIO_Input. This sets up PC0 to read a button press from the first onboard button of the RT Spark 1 board that triggers the Increase Frequency function of the Tone Generator
Next, Find PC1 pin (usually next to the PC0 pin in the Pinout Diagram) and select GPIO_Input from the context menu that will appear.
This configures the second onboard button of the RT Spark 1 board as an input that will trigger the Play/Pause of the Tone Generator.
Next, Find PC4 in the Pinout Diagram and on the context menu that will appear when you click, select and set it to GPIO_Input
This configures the third onboard button of the RT Spark 1 board as an input that triggers the Decrease Frequency function of the Tone Generator
Next, Find pin PA3 and select GPIO_Output from the context menu that will appear.
This configures PA3 as the Audio Output of the Tone Generator.
Now, Go to the GPIO configuration panel from System Core > GPIO. The following configurations are used for all of the GPIOs utilized in this project.
The Image blelow shows the detailed GPIO Configuration for every pin utilized in the MCU:
Now, Switch to the Project Manager tab. Enter the desired project name in the text box and also the project location.
After that, click on the Toolchain / IDE settings and choose STM32CubeIDE from the dropdown. This ensures compatibility when using the STM32CubeIDE Program to code our RT Spark 1 board later.
Next, Click on Generate Project button from the top right of the window. This will generate all of the boilerplate code that are required for STM32F407XX to run, which includes all HAL, CMSIS, and Low Level Drivers for the MCU.
Now, we will proceed to our program code.
Program Code Setup
To begin, we open our STM32CubeIDE and then we will import the Project which STM32CubeMX Generated Earlier. To do this, in STM32CubeIDE, go to File > Open Projects from File System...
After clicking, a new Window will then appear where we can select the directory of the Project in which STM32CubeMX generated earlier.
Make sure that the imported Project Folder is the root folder where STM32CubeMX generated all of the project files. Also make sure to tick the checkbox beside the project name in the table highlighted in RED Box above. After this, click on the Finish button to finish importing the project.
Creating the Main Program
We will start by first opening the boilerplate main.c code found in Core/Src/main.c.
Verify that the code had been properly generated and is not empty, by looking at the header. So far, this generated boilerplate looks good.
Now, for our project requirements, we will first create delay.c and delay.h source files for the project's delay functions.
Start by Right clicking on the Inc folder under Core, then go to New > Header File.
On the window that will appear, name the Header File "delay.h". This will be our delay function code's header file. Click on Finish after.
Now, use the code below for "delay.h" and put it in the delay.h header file we just created
#ifndef DELAY_H
#define DELAY_H
// Do not remove. This gives access to standard types and SystemCoreClock
#include "main.h"
// Function prototypes
void DWT_Init(void);
void delay_us(uint32_t us);
#endif /* DELAY_H */Now, Right click Src folder under Core folder again and go to New > Source File.
On the window that will popup, enter "delay.c" as the file name.
After that, click on Finish to continue creating the file.
Open the newly created delay.c file and use the code shown below:
#include "delay.h"
// Function to initialize the DWT Cycle Counter
void DWT_Init(void) {
// Enable the trace and debug block
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
// Enable the cycle counter
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
// Reset the cycle counter
DWT->CYCCNT = 0;
}
// Function to generate a microsecond delay
void delay_us(uint32_t us) {
// Get the current cycle count
uint32_t initial_ticks = DWT->CYCCNT;
// Calculate how many CPU ticks equal the requested microseconds.
uint32_t ticks = us * (SystemCoreClock / 1000000);
// Wait until the required number of ticks has passed
while ((DWT->CYCCNT - initial_ticks) < ticks);
}After that, Go back to main.c and import the delay.h header file we have just created.
#include "delay.h"Make sure to put the code insideBEGIN Includesand beforeEND Includesin the main.c file. Putting code outside these comments will result to code being lost when regenerating hardware configuration in STM32CubeMX.
After that, create the required variables for the project. Start by placing the code below inside USER CODE BEGIN 0.
// Initialize period to 1000 microseconds (1 ms)
unsigned int period = 1000;
// Variable flags to remember previous button states for edge detection
uint8_t prev_sw1_state = GPIO_PIN_SET;
uint8_t prev_sw3_state = GPIO_PIN_SET;For the play_tone() function, use the code below inside USER CODE BEGIN 4.
void play_tone(unsigned int duration_ms) {
// Calculate the needed value to pass to delay_us based on period
unsigned int half_period = period / 2;
// Calculate the number of times to toggle the output
// Total duration in microseconds divided by the period
unsigned int total_cycles = (duration_ms * 1000) / period;
for (unsigned int i = 0; i < total_cycles; i++) {
// AUDIO_OUT is set to HIGH
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_SET);
delay_us(half_period);
// AUDIO_OUT is set to LOW
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_RESET);
delay_us(half_period);
}
}Below play_tone() function inside USER CODE BEGIN 4, place the update_buttons() function coded.
void update_buttons(void) {
// Read current pin states
uint8_t current_sw1 = HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_0);
uint8_t current_sw2 = HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_1);
uint8_t current_sw3 = HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_4);
// Frequency UP = Period DOWN
// Check if SW1 state changed from unpressed to pressed
if (current_sw1 == GPIO_PIN_RESET && prev_sw1_state == GPIO_PIN_SET) {
if (period > 200) {
period -= 100; // Decrease period to raise frequency
} else {
period = 100; // Lower limit constraint
}
}
prev_sw1_state = current_sw1;
// Frequency DOWN = Period UP
if (current_sw3 == GPIO_PIN_RESET && prev_sw3_state == GPIO_PIN_SET) {
if (period < 9900) {
// Increase period to lower frequency
period += 100;
} else {
// Upper limit constraint
period = 10000;
}
}
prev_sw3_state = current_sw3;
// Play Tone if SW2 is held down
if (current_sw2 == GPIO_PIN_RESET) {
// Play the tone with a short duration
play_tone(50);
}
}Now, we define these function's Function Prototype inside USER CODE BEGIN PFP.
void play_tone(unsigned int duration_ms);
void update_buttons(void);After that, we will need to start polling the buttons and check for user inputs. To do this, we will need to call update_buttons() inside the main while(1) loop. Use the code below inside USER CODE BEGIN 3
update_buttons();Then after that, our delay_us() won't work yet. We will need to initialize the DWT Counter or Data Watchpoint Cycle Counter in ARM MCUs. To initialize this, our delay.c functions that we created earlier contains DWT_Init() function that enables this feature. Enable this by putting the code below inside USER CODE BEGIN 2.
DWT_Init();Now our code is complete. We can now build our project. Start by clicking on the Hammer Icon on the IDE's Toolbar. Click on the arrow down icon beside the Hammer Icon and select Debug.
After that, click on the Hammer Icon again and it should show a Console Window. Look for the Build Finished text in blue. This will indicate that the code compiled successfully.
After that, we will flash it to the RT Spark 1 Board.
To do this, we must first configure the project as a runnable program. To do this, right click the Project Name in the Project Explorer Window and go to Run As > 1 STM32 C/C++ Application
After that, a Window should appear for the Launch Configuration Properties.
Just click on the OK Button without changing anything.
Now, to flash this to the RT Spark 1 Development Board, Click on the Green Play Icon on the Toolbar. Click on the Down Arrow Icon beside it to show all available projects that can be flashed onto the board. Select the correct project name to ensure the correct code runs on the board.
After selecting the project, it should show the Console Window once again. It will show messages regarding the programming of the board. It Should say Download verified successfully.
Once done, our board is ready for testing!
TestingTo test our code & system, we can now connect our hardware with our RT Spark 1 Development Board.
As you can see on the image above, we connected the hardware directly through the GPIO Header of the RT Spark 1 board. The circuit seems simple enough for us to decide that soldering it in a PCB might be too overkill for this project.
After connecting it, we powered up the board with the code in it.
As you can see, the Audio plays the tones. However, the volume is so low and faint. So what we are going to do is to attach it to an amplifier. The only amplifier we have here at home is a 700W Stereo Power Amplifier. We will hook it via its modified AUX IN wires connected via an alligator clip in a jumper wire. We will connect the AUX IN GND to the GND of the RT Spark 1, and the AUX IN R (Right Channel) to the AUDIO_OUT of our project.
Th Yellow Aligtor clip is the GND, Black is the RIGHT Channel and Green is the LEFT Channel.
Once it is connected, we can adjust the volume of the amplifier to hear the tones generated.
Pressing the BTN_DOWN of the board plays the tone, and once the pressing is stopped, the tone should pause. Then while holding the BTN_DOWN of the board, pressing BTN_LEFT or BTN_RIGHT must increase or decrease the frequency of the generated tones.
As you can see, it worked as we expected. We can now proceed to our analysis and testing!
Analysis & InterpretationWe will analyze our project with an oscilloscope on how the tones are being generated.
First, we connect our CH1 and CH2 probes to the oscilloscope's input port. Then, CH1 probe clip should connect and analyze the Play / Pause button, while the CH2 probe clip should connect and analyze the AUDIO_OUT pin to analyze the generated tone.
However, since we are monitoring our Play / Pause button, we will need to drive an external pin in the RT Spark 1 Development Board, since the pin that is connected in our built in button (in this case, PC1), we can't easily attach a probe to it.
So to monitor the button presses, we will need a debug pin. For this project, let's configure one.
Let's start by going back to STM32CubeMX Again. For testing the button presses with the oscilloscope, we will use PB14.
On the STM32CubeMX pinout diagram, find PB14 and set it to GPIO_Output.
After setting it to an output pin, go to System Core > GPIO and configure PB14 with the following:
Once configured, double check all of the GPIO Configurations to ensure it will work on the first try.
Now, click on GENERATE CODE button on the top right corner of STM32CubeMX. Wait for the files to be reloaded in STM32CubeIDE.
After that, we will slightly modify the code in order to add support for the oscilloscope.
First, modify USER CODE BEGIN 0 by adding the code below
#define OSC_PORT GPIOB
#define OSC_PIN GPIO_PIN_14
// Oscilloscope Testing Macros
#define OSC_TEST_HIGH() (OSC_PORT->BSRR = OSC_PIN)
#define OSC_TEST_LOW() (OSC_PORT->BSRR = (uint32_t)OSC_PIN << 16U)Then modify the USER CODE BEGIN 4 to signal the test / debug pin for the oscilloscope to see:
if (current_sw2 == GPIO_PIN_RESET) {
// PC1 is pressed, signal test pin HIGH
OSC_TEST_HIGH();
} else {
// PC1 is released, signal test pin LOW
OSC_TEST_LOW();
}Now, we are ready to test the signals!
Once again, we will connect our CH1 and CH2 probes to the oscilloscope's input port. Then, CH1 probe clip should connect and analyze the Play / Pause button, while the CH2 probe clip should connect and analyze the AUDIO_OUT pin to analyze the generated tone.
The oscilloscope can be set into Single mode via MENU (Vertical) button to capture the signals once. Set CH1 as the trigger with RISING as the Slope. If you want to view continuous signal stream, set it to Auto mode.
You might need to adjust the Volts/Divs and Time knobs to view the signals with more clarity. You can also position the 2 channels on top of each other via the Vertical Position knob. Also, double check the probe's x1 and x10 physical switch located on each probe. If their settings are not the same, you might notice that the signal of the channel is much sensitive than the other channel. That's why the photo below shows CH1 having 2.00v and CH2 in 200mV because we have not checked this. But the reading won't affect anything. Just the sensitivity.
Now, here we have our first reading. Once the oscilloscope set to Single mode, press the RUN / STOP button to clear everything on the screen. Depending on the oscilloscope brand & model, the RUN / STOP button should illuminate RED when paused and GREEN when running. The button should be green to capture the signals.
Now, we pressed the Play button and it captured this signals
CH1 is the button press, and CH2 is the tone generated.
Now, we will measure some things. These are the things we will identify in our project:
First is Tisr where Tbtn is the exact time the button pulse go HIGH and Taudio is the exact time the first HIGH tone is generated and outputted by AUDIO_OUT.
We are aware that the button is using the polling mechanism where it checks if the buttons are pressed via an infinite loop. We just called it Tisr here since it's more standardized. This means the time it took to generate the audio after it recognizes the button press.
As you can see on the reading above, There are two yellow vertical lines. This is our cursor. Cursor 1 and Cursor 2. For cursor 1 it is placed in CH1's Rising edge while Cursor 2 is in CH2's rising edge. So the reading shows that Ta is = 800.0ns which will become our Tbtn. Tb is -5.600us which will become our Taudio. Plugging them into the formula, it will become
So our Tisr is equivalent to 6400.0ns or 6.400μs which is exactly what ΔV shows in the oscilloscope above. This means that the time it takes for the MCU to process and generate the tone from the moment the play button is pressed, is 6400.0ns or 6.400μs
Next is we find the following:
Vpp is our Voltage peak-to-peak where the total voltage is derived from the highest voltage in the signal minus the lowest voltage in the signal. Since the oscilloscope will show the signals in a specific position in the screen, the lower voltage will not be exactly at 0v unless the graph is positioned on the very bottom of the screen. Therefore we need to calculate Vpp in order to verify the voltage of the signal.
To do this, we will first enable Volts mode in the Cursor menu. Then adjust the cursor position so that Cursor 1 will be on top of the signal and Cursor 2 will be below the signal as shown in the image below
As you can see, we are measuring the Vpp of CH2 which will be the AUDIO_OUT. Cursor 1 will be the horizontal line above the signal which measures the highest voltage achieved by the signal HIGH and Cursor 2 will be the horizontal line below the signal which measures the lowest voltage achieved by the signal LOW.
Va will be our Highest Voltage in the signal and Vb will be the lowest. Notice how the voltages are negative. This is becaused we freely adjusted the vertical position of the signal in the graph which will also adjust this voltage reading.
To solve for Vpp, we plug in the values into the formula and we get:
Therefore we have successfully verified that the signal RT Spark 1 generates for our AUDIO_OUT is indeed approximately at Voltage peak of 3.6v. This means that our code is working perfectly and generates the correct voltages.
So the problem we encountered earlier is due to the speaker module requiring more current and voltage to sound louder and not our code. We even placed a resistor on the audio line which further decreased the current and voltage that passes through. So what we did is we plug it into an amplifier to hear it loudly.
Finally, we will check out how the width of CH2 changes when we adjust the frequency of the generated tone up or down.
As you can see in the video above, when I press the Play button and cycle between frequency up and down buttons, the width of CH2 changes.
Look at the 2 oscilloscope reading images I attached below.
The reading with the longer pulse width outputs the lower frequency tone as heard in the video. When the oscilloscope shows the shorter pulse width, it outputs the higher frequency as heard in the video.
This means that the pulse width is what the code manipulates in order to change pitch in the audio tones generated. The pulse width is being controlled by time in the code. The time is the delay between every toggling of the AUDIO_OUT pin.
Project ConclusionsThis Electronic Tone Generator project successfully demonstrated the core principles of firmware hardware integration within an embedded system. By configuring the MCU’s GPIOs and utilizing precise cycle counting (DWT) for timing, the system we made effectively processed physical button inputs to generate and modulate audio frequencies.
Key Findings
Frequency & Pulse Width: Oscilloscope analysis visually confirmed that sound pitch is inversely proportional to pulse width. Modifying the time delays in the code successfully altered the pin toggling speed, directly manipulating the output frequency.
System Responsiveness: The measured response time from button press to the first signal generation Tisr was highly efficient at 6.4μs, proving the reliability of the polling and delay functions.
Signal Verification & Hardware Constraints: The MCU accurately outputted the expected peak-to-peak voltage Vpp of approx 3.6V. The initial low volume was correctly identified as a hardware limitation due to the current-limiting resistor and the power requirements of the 8Ω speaker rather than a software flaw. Routing the accurate 3.6V signal through an external amplifier successfully bridged this problem.
In the end, this project provided practical, hands on validation of how digital time manipulation in firmware translates directly into physical, analog outputs.






Comments