DAC to generate a signal, ADC to read it and FFT to analyse

Hi all

I made a simple multi-tasking code to experiment with signals in a "realistic" (hopefully) way

The code
a) generate a signal by a DAC on one task
b) sample the signal on the ADC on the other task

It works reasonably well. Now on task b I wish to compute the FFT of the sampled signal. I use the arduinoFFT library, but unfortunately the FFT.majorPeak() always gives me twice the expected frequency.

In particular I have a const float signalFrequency = 100.0; and I always get 200.249745 and similar results can be obtained with other frequencies

Hereafter the code

#include "arduinoFFT.h"

// Define the DAC and ADC pins
const int dacPin = 25;   // DAC1 (GPIO 25) for sinusoid output
const int adcPin = 34;   // ADC1 (GPIO 34) for reading the sinusoid

// Parameters for the sine wave
const int amplitude = 100;   // Amplitude of the sine wave (max 255 for 8-bit DAC)
const int offset = 128;      // DC offset (middle of the DAC range)
const float signalFrequency = 100.0;  // Frequency of the sine wave in Hz
const int sampleRate = 1000;  // Sample
int samplingFrequencyADC = 1000; 
const uint16_t samples = 1024; 

// Task Handles
TaskHandle_t dacTaskHandle = NULL;
TaskHandle_t adcTaskHandle = NULL;

/*
These are the input and output vectors
Input vectors receive computed results from FFT
*/
double vReal[samples];
double vImag[samples];

/* Create FFT object */
ArduinoFFT<double> FFT = ArduinoFFT<double>(vReal, vImag, samples, samplingFrequencyADC);

// DAC task function
void dacTask(void *parameter) {
 
  
  // Generate and output the sine wave on DAC
  while (true)  {
    for (int i = 0; i < sampleRate; i++) {
      int sineValue = (int)(amplitude * sin(2.0 * PI * signalFrequency * i / sampleRate) + offset);
      dacWrite(dacPin, sineValue);  // Write to DAC (8-bit value)
      /*
      Serial.print(">");
      Serial.print("dac:");    
      Serial.println(sineValue);   
      */
      // portTICK_PERIOD_MS is the number of ticks per ms
      // vTaskDelay Delay a task for a given number of ticks.
      vTaskDelay(1.0/sampleRate*1000 * portTICK_PERIOD_MS); 
    }
  }
}

void adcTask(void *parameter) {
  
  int i = 0;

  while (true) {

  int adcValue = analogRead(adcPin);  // Read the ADC value
  /*
  Serial.print(">");
  Serial.print("adc:");    
  Serial.println(adcValue);   
  */

  if (i<samples){
       vReal[i] = adcValue-512;
       vImag[i] = 0.0;
       i=i+1;
  }

  if (i==samples){
    i=0;
    
    FFT.windowing(FFTWindow::Hamming, FFTDirection::Forward);	/* Weigh data */
    FFT.compute(FFTDirection::Forward); /* Compute FFT */
    FFT.complexToMagnitude(); /* Compute magnitudes */
    double x = FFT.majorPeak();
    Serial.println(x, 6);
  }

  // portTICK_PERIOD_MS is the number of ticks per ms
  // vTaskDelay Delay a task for a given number of ticks.
  vTaskDelay(1.0/samplingFrequencyADC*1000 * portTICK_PERIOD_MS); 

  }

}

void setup() {
  Serial.begin(115200);

  // Initialize DAC pin (GPIO 25)
  dacWrite(dacPin, 0);  // Initialize DAC with a low value

  // Initialize ADC pin (GPIO 34)
  analogReadResolution(10);  // Set ADC resolution to 12 bits
  analogSetAttenuation(ADC_11db);  // Set ADC attenuation (default 0dB)

  // Create DAC task
  //xTaskCreate(dacTask, "DAC Task", 2048, NULL, 1, &dacTaskHandle);
  xTaskCreatePinnedToCore(dacTask, "DAC Task", 2048, NULL, 1, &dacTaskHandle,0);


  // Create ADC task
  //xTaskCreate(adcTask, "ADC Task", 2048, NULL, 1, &adcTaskHandle);
  xTaskCreatePinnedToCore(adcTask, "ADC Task", 2048, NULL, 1, &adcTaskHandle,1);
}

void loop() {
  // Empty loop, as tasks handle everything
}

Any idea on the reasons of this behavior?

Thanks in advance, Andrea

If you haven't used a low pass filter on the output of the DAC, suspect harmonics in the audio spectrum due to steps in the DAC output.

For example with musical instruments, harmonics often have amplitudes higher than the fundamental.

Also, what is the actual sample frequency of this loop? Serious artifacts arise with inappropriate sampling frequency (must be at least twice as high as the highest frequency signal present in the input stream).

  while (true) {

  int adcValue = analogRead(adcPin);  // Read the ADC value
  /*
  Serial.print(">");
  Serial.print("adc:");    
  Serial.println(adcValue);   
  */

  if (i<samples){
       vReal[i] = adcValue-512;
       vImag[i] = 0.0;
       i=i+1;
  }

Thanks

As far as concerns harmonics, being DAC artifacts, I guess they should appear always at the same frequency. In my experiments the output of FFT.majorPeak() is always almost double the signalFrequency. Anyway, I will try to experiment with low pass filters.

My hope is that the sample frequency of the ADC is as close as possible to
samplingFrequencyADC, that's why I have vTaskDelay(1.0/samplingFrequencyADC*1000 * portTICK_PERIOD_MS); ... clearly this depends on many aspects difficult to control (e.g. the time needed to perform other operations, FFT included), but this is the hope. Since samplingFrequencyADC = 1000 and signalFrequency=100 I should be in the safe zone!

No.

Few more details

I computed the FFT on the DAC and on the ADC. The FFT on the DAC is correct, then data are sent by the dacWrite and received by the ADC that produces results twice the signalFrequency

const float signalFrequency = 100.0;
DAC FFT: 100.087563
ADC FFT: 200.339728
const float signalFrequency = 50.0;
DAC FFT: 50.204871
ADC FFT: 100.690165
const float signalFrequency = 10.0;
DAC FFT: 9.949378
ADC FFT: 19.583938

You havent said WHICH arduino you are using, nor how the signal is connected from DAC to ADC.

The peak search function reports only the highest peak, which can be an artifact. You should be looking at the complete amplitude versus frequency spectrum, which is far more informative.

Please post a wiring diagram.

It is an ESP32-devkit-v1. I have two tasks on the same node, pinned to the two cores, one that generates the signal by the DAC (pin 25), the other that sample the signal on the ADC (pin 34). A wire connects pin25 with pin34

const int dacPin = 25;   // DAC1 (GPIO 25) for sinusoid output
const int adcPin = 34;   // ADC1 (GPIO 34) for reading the sinusoid

The reference code is the one in the first post

This is the log of the signal received by the ADC by

int adcValue = analogRead(adcPin);

when

const float signalFrequency = 20.0; // Frequency of the sine wave in Hz

devttyUSB0_2024_12_14.01.43.24.270.txt (51.4 KB)

the following is the results of the analysis in python

the following is the results of the analysis in python

Sorry, can't make much sense of that mess. The time domain signal, that you send to the DAC, should look like a sine wave, right?

But why bother with Python? Print out or plot the array resulting from the following step instead, which calculates the amplitude spectrum of interest, and is what the peak search attempts to interpret.

 FFT.complexToMagnitude(); /* Compute magnitudes */

I gather that you are just starting to learn about spectral analysis. So a hint: to check your usage of the library code, use ArduinoFFT to transform the original data array you are sending to the DAC, and verify that it gives the expected result.

The following is a major problem and probably explains the junk in your posted plots.

This code does not generate a continuous sine wave, instead it is generating and transforming a truncated version.

  // Generate and output the sine wave on DAC
  while (true)  {
    for (int i = 0; i < sampleRate; i++) {

A check of the DAC output with an oscilloscope will reveal this mistake.

It is a sine wave at a frequency of 20Hz sampled at 8 bits excepts for few peaks.

The hint has been already implemented see the post #5. DAC FFT is the FFT of the DAC value, namely int sineValue = (int)(amplitude * sin(2.0 * PI * signalFrequency * i / samplingFrequencyDAC) + offset);. Since DACWrite does not provide direct access to the output, this is the best approssimation

As you can see it approximate quite well the expected frequency, namely signalFrequency

This is the result of plotting what is the value of the variable sineValue
in input to the DAC (in red) - before the DAC actually samples it at 8bits and send it to the pin by the DACwrite - and what is sampled by the ADC in green. The fundamental frequency is the same, at least by eyes, but the artifacts generated by the DAC are evident. What is surprisingly to me is that a) these artifacts have more energy than the fundamental frequency, b) they appears always at twice the signal frequency ... note that we are pretty far from the Nyquest frequency

I made the following experiment:

  • In one ESP32_1 I run the DAC to generate the signal
  • In another ESP32_2 I run the ADC to sample the signal generated by ESP32_1

All works as expected. So I guess the problem is that the interrupts, delays etc in the tasks make the whole process unreliable.

It would be nice to explore what are the exact reasons and still It is quite strange that in the task configuration, the returned frequency is twice the signal one!

The basics of Fourier transforms. There there are the issues with the discrete version, with inaccurately defined sample intervals, etc. Have fun!

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.