How to Perform FFT Onboard ESP32, and Get Both Frequency and Amplitude
I wanted a library for performing FFT on ESP32 using the Arduino IDE, and extract both fundamental frequency and the amplitude at that frequency. The most popular library seems to be arduinoFFT and it gives excellent results for frequency. However, it is not clear how the magnitude in the output gets translated to amplitude, and therefore I was unable to use it for my application. The creator of the arduinoFFT library has also stated that the relation between the amplitude of the physical signal and the magnitude expressed on the FFT peaks by the arduinoFFT library is not established yet.
I then came across Robin Scheibler’s library, which was created for ESP-IDF and not for Arduino IDE. It took just a couple of minor modifications to adapt it to Arduino IDE. Converting the magnitude to amplitude is pretty straightforward with this library. Both the frequency and amplitude have excellent accuracy. I have converted the library to a .h file that just needs to be kept in the folder of your Arduino project that needs FFT.
This tutorial is organized into the following sections:
GitHub Repo:
The FFT.h file, along with a sample Arduino code can be found here: https://github.com/yash-sanghvi/ESP32/tree/master/FFT_on_ESP32_Arduino.
Code Walk-through
Edit: While this walkthrough will use the .h file I referred to above, GitHub user @MichielfromNL has taken efforts to convert this code into a library. A big shout out to him. You can find the details here.
Let’s do a short walk-through of the code:
First, let us look at the FFT_signal.h file:
#define FFT_N 2048 // Must be a power of 2
#define TOTAL_TIME 9.391904 //The time in which data was captured. This is equal to FFT_N/sampling_freqfloat fft_input[FFT_N];
float fft_output[FFT_N];float max_magnitude = 0;
float fundamental_freq = 0;/* Dummy data (Output of an accelerometer)
* Frequency: 5 Hz
* Amplitude: 0.25g
*/double fft_signal[FFT_N] = {
11100,10600,11200,11700,12200,12900,12900,.....,11000
};
As you can see, it defines the number of samples (FFT_N) and the time in which the samples were captured (which is equal to the number of samples divided by the sampling frequency). It also declares buffers for storing the input data and the output FFT. Finally, it defines variables for the two quantities of interest, the fundamental frequency, and the max magnitude. At the end of the file, the actual signal containing 2048 samples has been defined. The signal has been multiplied with 10,000 in this file. So 1.11g becomes 11100.
Now, let’s look at the main .ino file.
First, we import the FFT.h and the signal files:
#include "FFT.h"
#include "FFT_signal.h"
Within the setup, we will initialize the FFT with the following code:
fft_config_t *real_fft_plan = fft_init(FFT_N, FFT_REAL, FFT_FORWARD, fft_input, fft_output);
Here, FFT_N, fft_input, and fft_output, which were already defined in the FFT_signal.h file, are used as arguments. FFT_REAL and FFT_FORWARD indicate that the type and the direction of the FFT. Since we want to compute the frequencies from the time-domain signal and since we are dealing with real data, we have chosen FFT_FORWARD and FFT_REAL for the direction and type respectively. The other option for the direction is FFT_INVERSE and for the type is FFT_COMPLEX.
Next, we populate the input buffer with the signal. We have already defined fft_input as the input buffer for the FFT when initializing it.
for (int k = 0 ; k < FFT_N ; k++)
real_fft_plan->input[k] = (float) fft_signal[k];
Finally, we execute the FFT.
fft_execute(real_fft_plan);
The results get populated automatically in the fft_output buffer. Now, let’s print out the output and also determine the fundamental frequency (the frequency with the maximum magnitude) and the corresponding magnitude.
for (int k = 1 ; k < real_fft_plan->size / 2 ; k++)
{
/*The real part of a magnitude at a frequency is
followed by the corresponding imaginary part in the output*/float mag = sqrt(pow(real_fft_plan->output[2*k],2) + pow(real_fft_plan->output[2*k+1],2));
float freq = k*1.0/TOTAL_TIME;if(mag > max_magnitude)
{
max_magnitude = mag;
fundamental_freq = freq;
}
}
The real and imaginary parts of the FFT for each frequency follow one another. The output buffer, thus, looks like this: [DC, real1, imag1, real2, imag2, ….]
The frequencies, in turn, can be obtained by dividing the index with TOTAL_TIME. So, in our case, the total time is 9.39 seconds. So the maximum frequency is going to be (FFT_N/2/TOTAL_TIME) = 109 Hz. This is to be expected. According to the Nyquist criteria, the maximum frequency captured by FFT will be half of the sampling frequency. Our sampling frequency is (2048/9.39) = 218 Hz. So half of that is 109 Hz.
Now that we have the maximum magnitude, let us find the corresponding amplitude. To get the amplitude from the magnitude, just multiply the amplitude with (2/FFT_N).
/*Multiply the magnitude at all other frequencies with (2/FFT_N) to obtain the amplitude at that frequency*/sprintf(print_buf,"Fundamental Freq: %f Hz\t Mag: %f g\n",
fundamental_freq, (max_magnitude/10000)*2/FFT_N);
Serial.println(print_buf);
As was told earlier, the original signal was multiplied with 10,000. Therefore, we divide the magnitude by 10000 to get the actual amplitude. To extract the DC component from the signal, multiply the first component of the output with 1/FFT_N.
/*Multiply the magnitude of the DC component with (1/FFT_N) to obtain the DC component*/sprintf(print_buf,"DC component: %f g\n",(real_fft_plan->output[0])/10000/FFT_N); // DC is at [0]
Serial.println(print_buf);
In the end, once the results have been obtained, destroy the FFT instance.
// Clean up at the end to free the memory allocated
fft_destroy(real_fft_plan);
Speed Evaluation
A couple of tests were done to check the time taken by this algorithm to compute the FFT. Here are the results:
As can be seen, the algorithm is pretty quick, with a signal of 4096 points taking just 21 milliseconds for the entire computation.
When to use this library
This library by Robin Scheibler is excellent for performing FFT computations on ESP32. The execution times are negligible and you can get frequency, amplitude, and the DC component of the signal very easily. You should use this library especially when the amplitudes are important to you. Go ahead and use this library to perform on-board FFT computations on ESP32, especially when you are interfacing the ESP32 with an IMU like MPU6050, or with an ADC signal like an AC current/ voltage sensor.
Since you are here, you may find this course on Udemy, which explains how to link ESP32 with AWS IoT, to be quite interesting. Please note that the above is an affiliate link, meaning that if you make any purchases, I’ll be paid a small share of that amount.