February 1, 2015

Fast sampling from analog input

The first part of the OScope project is to implement the Arduino sketch to read the input values from an analog pin. In this article will describe how to achieve a reliable sampling of analog signals up to 615 KHz using some advanced techniques.

Arduino provides an convenient way to read analog input this using the analogRead() function. Without going into much details the analogRead() function takes 100 microseconds leading to a theoretical sampling rate of 9600 Hz. You can read more about this topic here.

The following piece of code takes 1000 samples using the analogRead() calculates some statistics.


void setup()
{
  Serial.begin(115200);
  pinMode(A0, INPUT);
}

void loop()
{
  long t0, t;

  t0 = micros();
  for(int i=0; i<1000; i++) {
    analogRead(A0);
  }
  t = micros()-t0;  // calculate elapsed time

  Serial.print("Time per sample: ");
  Serial.println((float)t/1000);
  Serial.print("Frequency: ");
  Serial.println((float)1000*1000000/t);
  Serial.println();
  delay(2000);
}

This code gives 112us per sample for a 8928 Hz sampling rate.

So how can we increase sampling rate?

Speedup the analogRead() function


We now need a little more details. The ADC clock is 16 MHz divided by a 'prescale factor'. The prescale is set by default to 128 which leads to 16MHz/128 = 125 KHz ADC clock. Since a conversion takes 13 ADC clocks, the default sample rate is about 9600 Hz (125KHz/13).
Adding few lines of code in the setup() function we can set an ADC prescale to 16 to have a clock of 1 MHz and a sample rate of 76.8KHz.

#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

void setup()
{
  sbi(ADCSRA, ADPS2);
  cbi(ADCSRA, ADPS1);
  cbi(ADCSRA, ADPS0);
  ...

The real frequency measured with the test program is 17us per sample for a 58.6 KHz sampling rate.

The following table shows prescale values with registers values and theoretical sample rates. Note that prescale values below 16 are not recommended because the ADC clock is rated.

Prescale ADPS2 ADPS1 ADPS0 Clock freq (MHz) Sampling rate (KHz)
2 0 0 1 8 615
4 0 1 0 4 307
8 0 1 1 2 153
16 1 0 0 1 76.8
32 1 0 1 0.5 38.4
64 1 1 0 0.25 19.2
128 1 1 1 0.125 9.6


Interrupts


A better strategy is to avoid calling the analogRead() function and use the 'ADC Free Running mode'. This is a mode in which the ADC continuously converts the input and throws an interrupt at the end of each conversion. This approach has two major advantages:
  1. Do not waste time waiting for the next sample allowing to execute additional logic in the loop function.
  2. Improve accuracy of sampling reducing jitter.
In this new test program I set the prescale to 16 as the example above getting a 76.8 KHz sampling rate.

int numSamples=0;
long t, t0;

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

  ADCSRA = 0;             // clear ADCSRA register
  ADCSRB = 0;             // clear ADCSRB register
  ADMUX |= (0 & 0x07);    // set A0 analog input pin
  ADMUX |= (1 << REFS0);  // set reference voltage
  ADMUX |= (1 << ADLAR);  // left align ADC value to 8 bits from ADCH register

  // sampling rate is [ADC clock] / [prescaler] / [conversion clock cycles]
  // for Arduino Uno ADC clock is 16 MHz and a conversion takes 13 clock cycles
  //ADCSRA |= (1 << ADPS2) | (1 << ADPS0);    // 32 prescaler for 38.5 KHz
  ADCSRA |= (1 << ADPS2);                     // 16 prescaler for 76.9 KHz
  //ADCSRA |= (1 << ADPS1) | (1 << ADPS0);    // 8 prescaler for 153.8 KHz

  ADCSRA |= (1 << ADATE); // enable auto trigger
  ADCSRA |= (1 << ADIE);  // enable interrupts when measurement complete
  ADCSRA |= (1 << ADEN);  // enable ADC
  ADCSRA |= (1 << ADSC);  // start ADC measurements
}

ISR(ADC_vect)
{
  byte x = ADCH;  // read 8 bit value from ADC
  numSamples++;
}
  
void loop()
{
  if (numSamples>=1000)
  {
    t = micros()-t0;  // calculate elapsed time

    Serial.print("Sampling frequency: ");
    Serial.print((float)1000000/t);
    Serial.println(" KHz");
    delay(2000);
    
    // restart
    t0 = micros();
    numSamples=0;
  }
}


If you want to learn more on ADC Free Running mode and tweaking ADC register you can look at the following pages.

‎AVR Guide - Analog Inputs
Instructables - Girino - Fast Arduino Oscilloscope
Instructables - Arduino Audio Input
Arduino Forum - Faster Analog Read

16 comments:

  1. How are you transferring the samples to your computer ?
    I'm using serial for that and that's seriously affecting the sample rate.

    ReplyDelete
    Replies
    1. You can set a higher bitrate for the serial port.
      Try - Serial.begin(115200);
      I have changed the example.

      Delete
    2. Hi how can i transfer live data from arduino to matlab in PC and make live plot

      Delete
  2. Thanks for your prompt reply. I made the following changes to your code to achieve the desired result.
    (1)
    ADMUX |= (1 << REFS0); // set reference voltage to internal
    ADMUX |= (1 << REFS1);

    (2)
    ISR(ADC_vect)
    {
    byte x = ADCH; // read 8 bit value from ADC
    numSamples++;
    }

    The result of this is that I'm getting constant value(255) through the serial even though different voltages is being applied through the serial. What I'm thinking is that maybe the ADC is putting out samples to fast for the Serial to handle or maybe there is some other problem.
    My main objective is to get the samples to my laptop as soon as they are put into the register by ADC. If there is any alternative way to do that please suggest. If not, please suggest changes in the code to fix the serial problem.
    Thanks!

    ReplyDelete
    Replies
    1. Edit:
      (2)
      ISR(ADC_vect)
      {
      byte x = ADCH; // read 8 bit value from ADC
      Serial.println(x);
      numSamples++;
      }

      The result of this is that I'm getting constant value(255) through the serial even though different voltages is being applied to the ANALOG A0 pin

      Delete
    2. I have found the problem.
      Replace
      ADMUX |= (A0 & 0x07);
      to
      ADMUX |= (0 & 0x07);

      Delete
    3. I have also updated my OScope project (http://yaab-arduino.blogspot.it/p/oscope.html)
      Can you try to download the new version and tell me what do you think?
      https://drive.google.com/file/d/0B6PxDhwK5tSJaTFka254WUk1NjA/view?usp=sharing

      Delete
    4. Thank you for your help. It worked! I used your code to sample data for the myograph me and my team created as a part of course project. I appreciate your help.
      Sure! I'll give it a try.

      Delete
  3. How could we adapt the code for A0 and A1

    ReplyDelete
  4. Hello Bruno, Very interesting and useful article! Complimenti!
    Do you think that it's possible to sample the data at 8khz and save all on the sd card?
    Thanks
    Stefano

    ReplyDelete
  5. I don't have an SD card reader so I cannot try.
    I think it should be possible but I don't think you can sample 'exactly' at 8KHz.
    You can use the free running mode to sample at 9.6Khz and write to SD when a new sample is available.

    ReplyDelete
  6. How to change ADMUX |= (0 & 0x07); if i want to read from all 6 analog pins?,thank you

    ReplyDelete
  7. Why is is x local?
    And shouldn't numsamples be declared volatile?

    ReplyDelete
  8. hi bruno, how can u acces the analog variable?

    ReplyDelete
  9. How I use this in an arduino due?

    ReplyDelete