Maker.io main logo

Building an Arduino-Based Spectrum Analyzer - The Theoretical Aspects

2021-06-30 | By Maker.io Staff

The first part of this series discussed the MAX7219 display controllers and how to interface them with an ATMega328PU. This part takes a closer look at the MCU's built-in analog-to-digital converter and how to employ it to collect audio samples. Besides that, the article also introduces the Fourier transform and how to use it to detect prominent frequencies in an audio sample.

Configuring the ADC of the ATMega328PU

As mentioned in part one of this series, the MCU receives the audio input on the two analog pins A0 and A1. Alternatively, it’s also possible to physically combine the two channels before feeding the result into a single ADC input. Either way, reading the state of the analog input using the built-in analogRead() function of the Arduino API is not good enough in this case, as this method doesn’t offer the options required to sample the input signal properly. Besides that, analogRead() is a blocking operation, and it’s too slow to achieve good results in this case.

Before sampling a signal, we’ll have to determine what its maximum frequency is. The sampling frequency needs to be at least twice as much as the highest frequency observed in the input to sample an input signal. That means if we take 40 thousand samples each second (which corresponds to a sampling frequency of 40kHz), we can only reliably sample input signals that have a frequency of less than 20kHz. Otherwise, the sampling will become inaccurate, and the process will introduce aliasing artifacts:

Building an Arduino-Based Spectrum Analyzer-The Theoretical Aspects

In the upper example, the sampling frequency is adequate. And even though the frequency is barely high enough, the result closely resembles the original input waveform. The second example samples the input signal too slowly. The observed sampling result doesn’t match the input frequency accurately enough. Typically, the audible spectrum for humans ranges up to around 20kHz. Therefore, we should aim at a sampling frequency of at least around 40kHz.

As mentioned earlier, I didn’t employ the built-in analogRead() function of the Arduino API. Instead, I opted to use the AVR registers, which gives me more control over the ADC operation. The following code initializes ADC0 in the setup() method:

Copy Code
// Enable the AD conversion and set the ADC prescaler to 32
// (sampling rate = approx. 39kHz)
ADCSRA = 0b11001101;

// Use the internal voltage reference (5V) and ADC0
ADMUX = 0b01000000;

// turn off the digital input for adc0
DIDR0 = 0x01;

As you can see, I set the individual bits of the ADCSRA and ADMUX register manually. For DIDR0, I used the hexadecimal value 0x01, which corresponds to a decimal 1. ADCSRA stands for "ADC Control and Status Register A.” This register configures how the ADC operates.

The last bit (the leftmost bit) enables the ADC. The following one starts a conversion. We have to set this bit every time we request a reading from the ADC. The next bit configures auto-triggering of the ADC. In this case, I turned the auto-trigger mode off. The fourth bit is set to HIGH whenever the ADC completes a conversion. The next bit lets the ADC trigger an interrupt whenever it finishes an operation. The three least significant bits encode the division factor between the system clock (16MHz) and the ADC. Thus, they determine the sampling frequency of the ADC.

When all three bits are HIGH, the system clock is divided by 128, resulting in a clock frequency of 125kHz that the ADC uses. This is the recommended clock frequency for optimum ADC operation. That, however, is not the sampling frequency, as the ADC typically requires 13 cycles to finish a conversion. Therefore, we can achieve an effective sampling frequency of just under 40kHz with a division factor of 32 and a system clock that runs at 16Mhz. Setting the prescaler to 32 requires us to sacrifice accuracy for a higher sampling frequency. As a side note, the built-in analogRead() function uses the recommended prescaler settings and achieves an effective sampling rate of about 9600 samples per second.

Besides the settings in the ADCSRA register, I also changed the ADMUX register to use channel zero (this corresponds to A0 in the Arduino API) and the internal reference voltage (+5V). As discussed in the last part of this series, you could use a more precise external voltage. Refer to the ATMega328PU datasheet for more information on that matter.

How to Collect Audio Samples with the ATMega328PU

Now that the ADC’s set up and ready to go, it’s time to collect samples. Remember that we set the ADCSRA register to start the conversion right away. When the ADC finishes the conversion, it invokes an ISR:

Copy Code
// ADC complete ISR
ISR (ADC_vect)
{
  if (collectedSamples < NUM_FFT_SAMPLES)
  {
	reals[collectedSamples] = ADC;
	imags[collectedSamples] = 0.0;
	collectedSamples += 1;
  }
 
  // Ignore the interrupt if the buffer is already full
  // Reset the ADC status register to capture the next sample
  ADCSRA = 0b11001101;
}
EMPTY_INTERRUPT (TIMER1_COMPB_vect);

The ISR stores the most recent conversion result of the ADC in an array called reals. It does this until the number of collected samples surpasses the number of FFT samples we want to collect and process. After each call of the ISR, the ADCSRA register is reset to signal to the ADC that it should start a new conversion.

An Introduction to FFT on the Arduino

Without going into too much detail, the fast Fourier transform (FFT) allows us to analyze time-domain signals such as an audio sample. The discrete Fourier transform gauges how much an input signal correlates to a known sine wave with a fixed amplitude and frequency. By overlaying many sine waves of different wavelengths, we can generate a result that indicates how much the input correlates to each of the sine waves. In this project, we can use this technique to determine the dominant frequencies in an audio sample. The FFT is a limited version of the discrete Fourier transform that enables simpler processing devices, such as the ATMega328PU, to perform the otherwise computationally very complex DFT operation.

You can choose from various Arduino libraries that implement an FFT algorithm. I used the arduinoFFT library, as it's easy to install using the Arduino IDE library manager.

Summary

In this project, I configured and used the built-in ADC of the ATMega328PU by directly modifying the MCU’s internal registers. From running various tests, I found this method to yield fast and accurate results. I set the prescaler of the ADC’s clock signal to a value that allows the MCU to collect audio samples with a frequency of roughly 39kHz. Remember that the sampling frequency needs to be at least twice as high as the highest expected input frequency to prevent aliasing artifacts. Once the ADC finishes a conversion, it calls an ISR (interrupt service routine) that stores the result in a buffer. The next part of this series discusses how to use FFT to extract the dominant wavelengths from the audio input and send that information to the LED matrix displays.

制造商零件编号 A000066
ARDUINO UNO R3 ATMEGA328P BOARD
Arduino
¥190.97
Details
制造商零件编号 ATMEGA328-PU
IC MCU 8BIT 32KB FLASH 28DIP
Microchip Technology
¥21.41
Details
Add all DigiKey Parts to Cart
TechForum

Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.

Visit TechForum