Introduction: 1024 Samples FFT Spectrum Analyzer Using an Atmega1284

This relatively easy tutorial (considering the complexity of this subject matter) will show you how you can make a very simple 1024 samples spectrum analyser using an Arduino type board (1284 Narrow) and the serial plotter. Any kind of Arduino compatible board will do, but the more RAM it has, the best frequency resolution you will get. It will need more than 8 KB of RAM to compute the FFT with 1024 samples.

Spectrum analysis is used to determine the main frequency components of a signal. Many sounds (like those produced by a musical instrument) are composed of a fundamental frequency and some harmonics that have a frequency that is an integer multiple of the fundamental frequency. The spectrum analyzer will show you all these spectral components.

You might want to use this setup as a frequency counter or to check any kind of signals that you suspect is bringing some noise in your electronic circuit.

We will focus here on the software part. If you would like to make a permanent circuit for a specific application you will need to amplify and filter the signal. This pre-conditioning is totally dependant of the signal you want to study, depending on its amplitude, impedance, maximum frequency etc... You can check https://www.instructables.com/id/Analog-Sensor-Sig...

Step 1: Installing the Library

We will be using the ArduinoFFT library written by Enrique Condes. Since we want to spare RAM as much as possible we will use the develop branch of this repository that is allowing to use the float data type (instead of double) to store the sampled and computed data. So we have to install it manually. Don't worry, just download the archive and uncompress it in your Arduino library folder (for example on Windows 10 default configuration: C:\Users\_your_user_name_\Documents\Arduino\libraries)

You can check that the library is correctly installed by compiling one of the examples provided, like "FFT_01.ino."

Step 2: Fourier Transform and FFT Concepts

Warning : if you can't stand seeing any mathematical notation you might want to skip to Step 3. Anyway, if you don't get it all, just consider the conclusion at the end of the section.

The frequency spectrum is obtained through a Fast Fourier Transform algorithm. FFT is a digital implementation that approximates the mathematical concept of the Fourier Transform. Under this concept once you get the evolution of a signal following a time axis, you can know its representation in a frequency domain, composed of complex (real + imaginary) values. The concept is reciprocal, so when you know the frequency domain representation you can transform it back to the time domain and get the signal back exactly like before the transform.

But what are we going to do with this set of computed complex values in the time domain? Well, most of it will be left to engineers. For us we will call another algorithm that will transform these complex values into spectral density data: that is a magnitude (= intensity) value associated with each frequency band. The number of frequency band will be the same as the number of samples.

You are certainly acquainted with the equalizer concept, like this one Back to the 1980s With the Graphic EQ. Well, we will obtain the same kind of results but with 1024 bands instead of 16 and much more intensity resolution. When the equalizer gives a global view of the music, the fine spectral analysis allows to precisely computate the intensity of each of the 1024 bands.

A perfect concept, but:

  1. Since the FFT is a digitalized version of the Fourier transform, it approximates the digital signal, and looses some information. So, strictly speaking, the result of the FFT if transformed back with an inverted FFT algorithm would not give exactly the original signal.
  2. Also the theory considers a signal that is not finite, but that is an ever-lasting constant signal. Since we will digitalize it only for a certain period of time (i.e. samples), some more errors will be introduced.
  3. Finally the resolution of the analog to digital conversion will impact the quality of the computed values.

In practice

1) The sampling frequency (noted fs)

We will sample a signal, i.e. measure its amplitude, every 1/fs seconds. fs is the sampling frequency. For example if we sample at 8 KHz, the ADC (analog to digital converter) that is onboard the chip will provide a measurement every 1/8000 of seconds.

2) The number of samples (noted N or samples in the code)

Since we need to get all the values before running the FFT we will have to store them and so we will limit the number of samples. The FFT algorithm needs a number of samples that is a power of 2. The more samples we have the better but it takes a lot of memory, all the more that we will also need to store the transformed data, that are complex values. The Arduino FFT library saves some space by using

  • One array named "vReal" to store the sampled data and then the real part of the transformed data
  • One array named "vImag" to store the imaginary part of the transformed data

The needed amount of RAM equals to 2 (arrays) * 32 (bits) * N (samples).

So in our Atmega1284 that has a nice 16 KB of RAM we will store a maximum of N = 16000*8 / 64 = 2000 values. Since the number of values must be a power of 2, we will store a maximum of 1024 values.

3) The frequency resolution

The FFT will compute values for as many frequency bands as the number of samples. These bands will span from 0 HZ to the sampling frequency (fs). Hence, the frequency resolution is:

Fresolution = fs / N

The resolution is better when lower. So for better resolution (lower) we want:

  • more samples, and/or
  • a lower fs

But...

4) Minimal fs

Since we want to see a lot of frequencies, some of them being much higher than the "fundamental frequency" we can't set fs too low. In fact there is the Nyquist–Shannon sampling theorem that forces us to have a sampling frequency well above twice the maximum frequency we would like to test.

For example, if we would like to analyze all the spectrum from 0 Hz to let say 15 KHz, which is approximately the maximum frequency most humans can hear distinctly, we have to set the sampling frequency at 30 KHz. In fact electronicians often set it at 2.5 (or even 2.52) * the maximum frequency. In this example that would be 2.5 * 15 KHz = 37.5 KHz. Usual sampling frequencies in professional audio are 44.1 KHz (audio CD recording), 48 KHz and more.

Conclusion:

Points 1 to 4 lead to: we want to use as many samples as possible. In our case with a 16 KB of RAM device we will consider 1024 samples. We want to sample at the lowest sampling frequency as possible, as long as it is high enough to analyze the highest frequency we expect in our signal (2.5 * this frequency, at least).

Step 3: Simulating a Signal

For our first try, we will slightly modify the TFT_01.ino example given in the library, to analyse a signal composed of

  • The fundamental frequency, set to 440 Hz (musical A)
  • 3rd harmonic at half the power of the fundamental ("-3 dB")
  • 5th harmonic at 1/4th of the power of the fundamental ("-6 dB)

You can see in the picture above the resulting signal. It looks indeed very much like a real signal one can sometimes see on an oscilloscope (I would call it "Batman") in situation when there is a clipping of a sinusoidal signal.

Step 4: Analysis of a Simulated Signal - Coding


0) Include the library

#include "arduinoFFT.h"<br>


1) Definitions

In the declarations sections, we have

const byte adcPin = 0;                      // A0
const uint16_t samples = 1024;              // This value MUST ALWAYS be a power of 2
const uint16_t samplingFrequency = 8000;    // Will affect timer max value in timer_setup() SYSCLOCK/8/samplingFrequency should be a integer

Since the signal has a 5th harmonics (frequency of this harmonic = 5 * 440 = 2200 Hz) we need to set the sampling frequency above 2.5*2200 = 5500 Hz. Here I chose 8000 Hz.

We also declare the arrays where we will store the raw and computed data

float vReal[samples];
float vImag[samples];


2) Instantiation

We create an ArduinoFFT object. The dev version of ArduinoFFT uses a template so we can use either the float or the double data type. Float (32 bits) is enough in regard to the overall precision of our program.

ArduinoFFT<float> FFT = ArduinoFFT<float>(vReal, vImag, samples, samplingFrequency);<br>


3) Simulating the signal by populating the vReal array, instead of having it populated with ADC values.

At the beginning of the Loop we populate the vReal array with:

  float cycles = (((samples) * signalFrequency) / samplingFrequency); //Number of signal cycles that the sampling will read
  for (uint16_t i = 0; i < samples; i++)
  {
    vReal[i] = float((amplitude * (sin((i * (TWO_PI * cycles)) / samples))));/* Build data with positive and negative values*/
    vReal[i] += float((amplitude * (sin((3 * i * (TWO_PI * cycles)) / samples))) / 2.0);/* Build data with positive and negative values*/
    vReal[i] += float((amplitude * (sin((5 * i * (TWO_PI * cycles)) / samples))) / 4.0);/* Build data with positive and negative values*/

    vImag[i] = 0.0; //Imaginary part must be zeroed in case of looping to avoid wrong calculations and overflows
  }<br>

We add a digitalisation of the fundamental wave and the two harmonics with less amplitude. Than we initialise the imaginary array with zeroes. Since this array is populated by the FFT algorithm we need to clear it again before each new computation.


4) FFT computing

Then we compute the FFT and the spectral density

 FFT.windowing(FFTWindow::Hamming, FFTDirection::Forward);
 FFT.compute(FFTDirection::Forward); /* Compute FFT */
 FFT.complexToMagnitude(); /* Compute magnitudes */<br>

FFT.windowing(...) operation modifies the raw data because we run the FFT on a limited number of samples. The first and last samples present a discontinuity (there is "nothing" on one of their side). This is a source of error. The "windowing" operation tends to reduce this error.

FFT.compute(...) with the direction "Forward" computes the transformation from the time domain to the frequency domain.

Then we compute the magnitude (i.e. intensity) values for each of the frequency bands. The vReal array is now filled with magnitudes values.


5) Serial plotter drawing

Let's print the values on the serial plotter by calling the function printVector(...)

  PrintVector(vReal, (samples >> 1), SCL_FREQUENCY);<br>

This is a generic function that allows to print data with a time axis or a frequency axis.

We also print the frequency of the band that has the highest magnitude value

  float x = FFT.majorPeak();
  Serial.print("f0=");
  Serial.print(x, 6);
  Serial.println("Hz");<br>

Step 5: Analysis of a Simulated Signal - Results

We see 3 spikes corresponding to the fundamental frequency (f0), the 3rd and 5th harmonics, with half and 1/4th of the f0 magnitude, as expected. We can read at the top of the window f0= 440.430114 Hz. This value is not exactly 440 Hz, because of all the reasons explained above, but it is very close to the real value. It was not really necessary to show so many insignificant decimals.

Step 6: Analysis of a Real Signal - Wiring the ADC

Since we know how to proceed in theory, we would like to analyze a real signal.

The wiring is very simple. Connect the grounds together and the signal line to the A0 pin of your board through a series resistor with a value of 1 KOhm to 10 KOhm.

This series resistor will protect the analog input and avoid ringing. It must be as high as possible to avoid ringing, and as low as possible to provide enough current to charge the ADC rapidly. Refer to the MCU datasheet to know the expected impedance of the signal connected at the ADC input.

For this demo I used a function generator to feed a sinusoidal signal of frequency 440 Hz and amplitude around 5 volts (it is best if the amplitude is between 3 and 5 volts so the ADC is used near full scale), through a 1.2 KOhm resistor.

Step 7: Analysis of a Real Signal - Coding


0) Include the library

#include "arduinoFFT.h"

1) Declarations and instanciation

In the declaration section we define the ADC input (A0), the number of samples and the sampling frequency, like in the previous example.

const byte adcPin = 0;                      // A0
const uint16_t samples = 1024;              // This value MUST ALWAYS be a power of 2
const uint16_t samplingFrequency = 8000;    // Will affect timer max value in timer_setup() SYSCLOCK/8/samplingFrequency should be a integer

We create the ArduinoFFT object

ArduinoFFT<float> FFT = ArduinoFFT<float>(vReal, vImag, samples, samplingFrequency);


2) Timer and ADC setup

We set timer 1 so it cycles at the sampling frequency (8 KHz) and raises an interrupt on output compare.

void timer_setup(){
  // reset Timer 1
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1 = 0;
  TCCR1B = bit (CS11) | bit (WGM12);  // CTC, prescaler of 8
  TIMSK1 = bit (OCIE1B);
  OCR1A = ((16000000 / 8) / samplingFrequency) -1; 
}<br>

And set the ADC so it

  • Uses A0 as input
  • Triggers automatically on each timer 1 output compare match B
  • Generates an interrupt when the conversion is complete

The ADC clock is set at 1 MHz, by prescaling the system clock (16 MHz) by 16. Since every conversion takes approximately13 clocks at full scale, conversions can be achieved at a frequency of 1/13 = 0.076 MHz = 76 KHz. The sampling frequency should be significantly lower than 76 KHz to let the ADC have the time to sample the data. (we chose fs = 8 KHz) .

void adc_setup() {
  ADCSRA =  bit (ADEN) | bit (ADIE) | bit (ADIF);   // turn ADC on, want interrupt on completion
  ADCSRA |= bit (ADPS2);  // Prescaler of 16
  ADMUX = bit (REFS0) | (adcPin & 7); // setting the ADC input
  ADCSRB = bit (ADTS0) | bit (ADTS2);  // Timer/Counter1 Compare Match B trigger source
  ADCSRA |= bit (ADATE);   // turn on automatic triggering  
}<br>

We declare the interrupt handler that will be called after each ADC conversion to store the converted data in the vReal array, and clearing of the interrupt

// ADC complete ISR
ISR (ADC_vect)
{
    vReal[resultNumber++] = ADC;
            
    if(resultNumber == samples)
    {
      ADCSRA = 0;  // turn off ADC
    }
} 

EMPTY_INTERRUPT (TIMER1_COMPB_vect);<br>

You can have an exhaustive explanation on ADC conversion on the Arduino (analogRead).


3) Setup

In the setup function we clear the imaginary data table and call the timer and ADC setup functions

  zeroI(); // a function that set to 0 all the imaginary data - explained in the previous section
  timer_setup();
  adc_setup();


3) Loop

  FFT.dcRemoval();  // Remove the DC component of this signal since the ADC is referenced to ground
  FFT.windowing(FFTWindow::Hamming, FFTDirection::Forward);	// Weigh data
  FFT.compute(FFTDirection::Forward); // Compute FFT
  FFT.complexToMagnitude(); // Compute magnitudes 
  // printing the spectrum and the fundamental frequency f0
  PrintVector(vReal, (samples >> 1), SCL_FREQUENCY);
  float x = FFT.majorPeak();
  Serial.print("f0=");
  Serial.print(x, 6);
  Serial.println("Hz");

We remove the DC component because the ADC is referenced to ground and the signal is centered around 2.5 volts approximately.

Then we compute the data as explained in the previous example.

Step 8: Analysis of a Real Signal - Results

Indeed we see only one frequency in this simple signal. The computed fundamental frequency is 440.118194 Hz. Here again the value is a very close approximation of the real frequency.

Step 9: What About a Clipped Sinusoidal Signal?

Now lets overdrive a little the ADC by increasing the amplitude of the signal above 5 volts, so it is clipped. Don't push too mush not to destroy the ADC input!

We can see some harmonics appearing. Clipping the signal creates high frequency components.

You have seen the fundamentals of FFT analysis on an Arduino board. Now you can try to change the sampling frequency, the number of samples and the windowing parameter. The library also adds some parameter to compute the FFT faster with less precision. You will notice that if you set the sampling frequency too low, the computed magnitudes will appear totally erroneous because of spectral folding.