Analog Read Too Slow

Hi everyone,

I’m working on a project using FFT to analyze sounds and while doing so, i realized that the amount of time it takes for my Particle E-Series to fill a 256 sample buffer is around 2.8 seconds which is a long time. This is equivalent to ~91 samples per second.

I have tested the same code on a Metro M0 Express and the analog read speed was around 16,100 samples per second.

Here’s some references to what my project is using:

To sample sounds I have the Adafruit MAX9814 (microphone):

I am using a particle E-Series:
https://docs.particle.io/datasheets/cellular/e-series-datasheet/

I am using the following libraries:

Here’s the code being used:

#include <math.h>
const uint16_t samples = 256; //This value MUST ALWAYS be a power of 2
const double samplingFrequency = 48000;
const int AUDIO_INPUT_PIN = A1;
const int BOARD_LED_PIN = D1;

int sampleCounter = 0;
bool fillBuffer = false;

double vReal[samples];
double vImag[samples];

#define FFT_FORWARD 0x01
#define FFT_REVERSE 0x00

/* Windowing type */
#define FFT_WIN_TYP_HAMMING 0x01 /* hamming */
/*Mathematial constants*/
#define twoPi 6.28318531
#define fourPi 12.56637061
#define sixPi 18.84955593

#define SCL_INDEX 0x00
#define SCL_TIME 0x01
#define SCL_FREQUENCY 0x02
#define SCL_PLOT 0x03

void setup()
{
    pinMode(AUDIO_INPUT_PIN, INPUT);
    
    // Turn on the power indicator LED.
	pinMode(BOARD_LED_PIN, OUTPUT);

    Serial.begin(115200);
    Serial.println("Ready");
    startSampling();
}


void loop()
{
    
    if (fillBuffer == true) {
        
		// read from the input pin and start filling the buffer
		vReal[sampleCounter] = analogRead(AUDIO_INPUT_PIN);
		//Serial.println(vReal[sampleCounter]);
		// the buffer full of imaginary numbers for the FFT doesn't matter for our use, so fill it with 0s
		vImag[sampleCounter] = 0.0;
		// increment the counter so we can tell when the buffer is full
		sampleCounter++;
	}
	

	// if the buffer is full
	if (samplingIsDone()) {
	    // stop sampling
	    stopSampling();
	    
        Windowing(vReal, samples, FFT_WIN_TYP_HAMMING, FFT_FORWARD);	/* Weigh data */
        Compute(vReal, vImag, samples, FFT_FORWARD); /* Compute FFT */
        ComplexToMagnitude(vReal, vImag, samples); /* Compute magnitudes */
        
        Serial.println("Computed magnitudes:");
        PrintVector(vReal, (samples >> 1), SCL_FREQUENCY);
        double x;
        double v;
        MajorPeak(vReal, samples, samplingFrequency, &x, &v);
        Serial.print(x, 6);
        Serial.print(", ");
        Serial.println(v, 6);

        //while(1); /* Run Once */
        delay(1000); /* Repeat after delay */
        startSampling();
	}
}
void startSampling() {
	sampleCounter = 0;
	fillBuffer = true;
}

void stopSampling() {
	fillBuffer = false;
}

bool samplingIsDone() {
	return sampleCounter >= samples;
}

void PrintVector(double *vData, uint16_t bufferSize, uint8_t scaleType)
{
  for (uint16_t i = 0; i < bufferSize; i++)
  {
    double abscissa;
    /* Print abscissa value */
    switch (scaleType)
    {
      case SCL_INDEX:
        abscissa = (i * 1.0);
	break;
      case SCL_TIME:
        abscissa = ((i * 1.0) / samplingFrequency);
	break;
      case SCL_FREQUENCY:
        abscissa = ((i * 1.0 * samplingFrequency) / samples);
	break;
    }
    Serial.print(abscissa, 6);
    if(scaleType==SCL_FREQUENCY){
      Serial.print("Hz");
    }
    Serial.print(" ");
    Serial.println(vData[i], 4);
  }
  Serial.println();
}


void Compute(double *vReal, double *vImag, uint16_t samples, uint8_t dir)
{
	Compute(vReal, vImag, samples, Exponent(samples), dir);
}

void Compute(double *vReal, double *vImag, uint16_t samples, uint8_t power, uint8_t dir)
{	// Computes in-place complex-to-complex FFT
	// Reverse bits
	uint16_t j = 0;
	for (uint16_t i = 0; i < (samples - 1); i++) {
		if (i < j) {
			Swap(&vReal[i], &vReal[j]);
			if(dir==FFT_REVERSE)
				Swap(&vImag[i], &vImag[j]);
		}
		uint16_t k = (samples >> 1);
		while (k <= j) {
			j -= k;
			k >>= 1;
		}
		j += k;
	}
	// Compute the FFT
	double c1 = -1.0;
	double c2 = 0.0;
	uint16_t l2 = 1;
	for (uint8_t l = 0; (l < power); l++) {
		uint16_t l1 = l2;
		l2 <<= 1;
		double u1 = 1.0;
		double u2 = 0.0;
		for (j = 0; j < l1; j++) {
			 for (uint16_t i = j; i < samples; i += l2) {
					uint16_t i1 = i + l1;
					double t1 = u1 * vReal[i1] - u2 * vImag[i1];
					double t2 = u1 * vImag[i1] + u2 * vReal[i1];
					vReal[i1] = vReal[i] - t1;
					vImag[i1] = vImag[i] - t2;
					vReal[i] += t1;
					vImag[i] += t2;
			 }
			 double z = ((u1 * c1) - (u2 * c2));
			 u2 = ((u1 * c2) + (u2 * c1));
			 u1 = z;
		}
		c2 = sqrt((1.0 - c1) / 2.0);
		if (dir == FFT_FORWARD) {
			c2 = -c2;
		}
		c1 = sqrt((1.0 + c1) / 2.0);
	}
	// Scaling for reverse transform
	if (dir != FFT_FORWARD) {
		for (uint16_t i = 0; i < samples; i++) {
			 vReal[i] /= samples;
			 vImag[i] /= samples;
		}
	}
}

void ComplexToMagnitude(double *vReal, double *vImag, uint16_t samples)
{	// vM is half the size of vReal and vImag
	for (uint16_t i = 0; i < samples; i++) {
		vReal[i] = sqrt(vReal[i]*vReal[i] + vImag[i]*vImag[i]);
	}
}

void Windowing(double *vData, uint16_t samples, uint8_t windowType, uint8_t dir)
{// Weighing factors are computed once before multiple use of FFT
// The weighing function is symetric; half the weighs are recorded
	double samplesMinusOne = (double(samples) - 1.0);
	for (uint16_t i = 0; i < (samples >> 1); i++) {
		double indexMinusOne = double(i);
		double ratio = (indexMinusOne / samplesMinusOne);
		double weighingFactor = 1.0;
		// Compute and record weighting factor
		switch (windowType) {
		case FFT_WIN_TYP_HAMMING: // hamming
			weighingFactor = 0.54 - (0.46 * cos(twoPi * ratio));
			break;
		}
		if (dir == FFT_FORWARD) {
			vData[i] *= weighingFactor;
			vData[samples - (i + 1)] *= weighingFactor;
		}
		else {
			vData[i] /= weighingFactor;
			vData[samples - (i + 1)] /= weighingFactor;
		}
	}
}


void MajorPeak(double *vD, uint16_t samples, double samplingFrequency, double *f, double *v)
{
	double maxY = 0;
	uint16_t IndexOfMaxY = 0;
	//If sampling_frequency = 2 * max_frequency in signal,
	//value would be stored at position samples/2
	for (uint16_t i = 1; i < ((samples >> 1) + 1); i++) {
		if ((vD[i - 1] < vD[i]) && (vD[i] > vD[i + 1])) {
			if (vD[i] > maxY) {
				maxY = vD[i];
				IndexOfMaxY = i;
			}
		}
	}
	double delta = 0.5 * ((vD[IndexOfMaxY - 1] - vD[IndexOfMaxY + 1]) / (vD[IndexOfMaxY - 1] - (2.0 * vD[IndexOfMaxY]) + vD[IndexOfMaxY + 1]));
	double interpolatedX = ((IndexOfMaxY + delta)  * samplingFrequency) / (samples - 1);
	//double popo =
	if (IndexOfMaxY == (samples >> 1)) //To improve calculation on edge values
		interpolatedX = ((IndexOfMaxY + delta)  * samplingFrequency) / (samples);
	// returned value: interpolated frequency peak apex
	*f = interpolatedX;
	*v = abs(vD[IndexOfMaxY - 1] - (2.0 * vD[IndexOfMaxY]) + vD[IndexOfMaxY + 1]);
}

uint8_t Exponent(uint16_t value)
{
	// Calculates the base 2 logarithm of a value
	uint8_t result = 0;
	while (((value >> result) & 1) != 1) result++;
	return(result);
}

// Private functions

void Swap(double *x, double *y)
{
	double temp = *x;
	*x = *y;
	*y = temp;
}

Some questions come to mind are:

  • Why is the buffer filling at this speed for the Particle E-Series vs Metro M0 Express
  • Is it possible to increase/decrease the sampling frequency of the device?

Any help would be appreciated.

Thank you.

Try changing the adc sample time for faster sampling.

However the bigger problem is that you are doing these samples one at a time per loop() call. loop() is interleaved with calls to the system firmware functionality, so there are sometimes significant delays. For loop to get called more accurately you would need to use SYSTEM_THREAD(ENABLED) at the top of your program to not wait on the system thread.

However, what you really want is probably to do the above, as well as use a software timer or ISR to fill a buffer with input values at a specified frequency. Otherwise your frequency will never be guaranteed.

You are sampling in the Loop so the sampling will be limited to the loop speed!

@justicefreed_amper beat me to this - I would do your sampling in a function into RAM and ensure that you avoid/disable interrupts whilst sampling. I use the ADC on a Photon to determine AC current and I found this requires precise voltage readings - perhaps at not as high a frequency!

This project is for the Photon, but it will work fine on the E Series though you won’t be able to upload the data on an Electron. You can run an FFT on it just fine, however, especially when using DMA to gather the samples so the main CPU can devote its cycles to doing the FFT.

Example 3 does the same thing, but is more experimental. It does all of the sampling in hardware using the ADC, hardware timer and DMA, storing the samples in RAM at precise intervals without using the main CPU. It’s very efficient and this example works at a 32000 Hz sample rate with 16-bit samples. The node.js server program saves the data to wav files.

Example 4 is like Example 3, except it uploads 6 channels of 44.1KHz 16-bit samples to a server. The Javascript can save one of the streams to a wav file.

Thank you “justicefreed_amper” and “armor” for your inputs!

After mentioning that the sampling in the Loop limits the speed, i changed it so that it samples it in a for loop which increased the sampling rate to 10,500! The FFT sampling now works properly. I also changed the “adc sample time” which also helped increase the speed.

Sounds like that may work well for you then. Just keep in mind that any long blocking code (like a loop within loop()) will impair the devices ability to respond to cloud requests, OTA updates, maintain it’s cloud connection, and other system tasks. If any of those are things that are important to you you will need to use an ISR, Software Timer, or SYSTEM_THREAD(ENABLED) with single threaded sections for your timing critical sections.

1 Like