Experiment - split up analog read in 3 steps

AnalogRead() takes about 110 micros() on a UNO. During this time the call is blocking and only interrupts can be served.

As I did not like that I did an experiment to check if analogRead() can be split in 3 functions:

void AR_Start(uint8_t pin); // starting the conversion
bool AR_Ready(); // checking if conversion is done
int AR_Value()); // reading the value

Find below a first experiment in which I broke up the code of analogRead() in these 3 steps (removed some lines) and it seems to work. (using a floating line).

output: (count++, micros, value) except for 1st 2 lines

start: 
398
248	112	386
231	104	339
240	108	312
233	104	293
232	104	280
247	112	273
241	108	275
234	108	291
233	104	310
234	112	338
240	108	355
233	108	362

The output indicates that it takes 100++ micros() to do the analogRead(), so there is quite some time to do some computing between starting the analogRead() and reading its value. In that time I can:

  • increment an int 200+ times
  • do digitalWrite 20+ times
  • etc

The nice part is that in theory I can start an new analogRead directly after reading its value so the new value is (almost) ready when I need it - OK the reading might become outdated or other effects may arise, to be investigated.

(The highly experimental code - all disclaimers apply :wink:

//
//    FILE: asyncAnalogRead.pde
//  AUTHOR: Rob Tillaart
//    DATE: 09-jun-2012
//
// PUPROSE: experimental 
//

void setup()
{
  Serial.begin(115200);
  Serial.println("start: ");
  Serial.println(analogRead(0));  // traditional
}

void loop()
{
  unsigned long start=0;
  unsigned long duration = 0;
  int count=0;

  AR_Start(0);
  start = micros();
  while(!AR_Ready())
  {
    count++; // dummy behaviour
  }
  duration = micros() - start;

  Serial.print(count);
  Serial.print("\t");
  Serial.print(duration);
  Serial.print("\t");
  Serial.print(AR_Value());
  Serial.println();
  delay(200);
}

#include "wiring_private.h"
#include "pins_arduino.h"

// uint8_t analog_reference = DEFAULT;

void AR_Start(uint8_t pin)
{
#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
  if (pin >= 54) pin -= 54; // allow for channel or pin numbers
#else
  if (pin >= 14) pin -= 14; // allow for channel or pin numbers
#endif

#if defined(ADCSRB) && defined(MUX5)
  // the MUX5 bit of ADCSRB selects whether we're reading from channels
  // 0 to 7 (MUX5 low) or 8 to 15 (MUX5 high).
  ADCSRB = (ADCSRB & ~(1 << MUX5)) | (((pin >> 3) & 0x01) << MUX5);
#endif
  // set the analog reference (high two bits of ADMUX) and select the
  // channel (low 4 bits).  this also sets ADLAR (left-adjust result)
  // to 0 (the default).
#if defined(ADMUX)
  ADMUX = (DEFAULT << 6) | (pin & 0x07);
#endif

  // without a delay, we seem to read from the wrong channel
  //delay(1);
  sbi(ADCSRA, ADSC);
}

boolean AR_Ready()
{
  // ADSC is cleared when the conversion finishes
  return bit_is_set(ADCSRA, ADSC)==0;
}

int AR_Value()
{

  // we have to read ADCL first; doing so locks both ADCL
  // and ADCH until ADCH is read.  reading ADCL second would
  // cause the results of each conversion to be discarded,
  // as ADCL and ADCH would be locked when it completed.
  int low  = ADCL;
  int high = ADCH;
  // combine the two bytes
  return (high << 8) | low;
}

You can speed up the time it takes by adjusting the prescaler value. I played with it below and this didn't change the number of bits generated for me.

ADCSRA = ADCSRA & 252; // Speed up adc conversion by adjusting adc clock prescaler

The scaler can be further reduced providing even more of a speedup if less precision is needed. In the sketch I did this on, I was able to read (and write to SD card) about 12,000,000 bytes per day. With this prescalar change I got 40,000,000 bytes per day.

And this can be combined with your approach, if less processing is needed during the analog conversion.

unfortunately I cannot change the prescaler to get more precission (I need to oversample for that)

Why not to use default "Conversion completed" interruption?

The nice part is that in theory I can start an new analogRead directly after reading its value so the new value is (almost) ready when I need it

Usually I start analog measurements when I need a data at this specific point in time, not when I need a value that was taken "a while" ago. Start Conversion Timing for most application is critical, doesn't matter when data is ready, as soon as conversion completed before new measurements has to be done.

Usually I start analog measurements when I need a data at this specific point in time

Agree, Normally I do too but sometimes I want to do high speed sampling as possible.

My UNO makes about 8200 analog samples per second if I do not do any math with it, just a raw loop of analogRead().

An example, if I need to do typical thermistor math:

double Thermister(int RawADC) 
{
  double Temp = log(((10240000L/RawADC) - 10000));
  Temp = 1 / (0.001129148 + (0.000234125 + (0.0000000876741 * Temp * Temp ))* Temp );
  Temp = Temp - 273.15;            // Convert Kelvin to Celcius
  // Temp = (Temp * 9.0)/ 5.0 + 32.0; // Convert Celcius to Fahrenheit
  return Temp;
}

this number drops to about 3000 (IIRC) so a bit of float math makes me loose 2/3 of my samples.

Simple sum:
8200 samples = 1 sec ==> 1 sample = 110 usec

3000 Samples + 3000Math = 1 sec ==> (with the above) ==> 1 Math = 220 usec.

If I can bring the analog sample from 110 usec to 10 usec (as on average there is 100usec waiting time, see above) 1sample + 1 Math becomes 230 usec
That makes it possible to do 1sec/230 usec = 4000+ samples per second which is >30% more samples per second than traditional.

So this technique would bring the number of samples down from 8200 to 4000 (~1/2) iso to 3000 (~1/3).


Other way to look at it, if I make 8200 analog samples every second and I am polling until ready for 100usec per sample I have been busy waiting for 80% of the time.

Why not to use default "Conversion completed" interruption?

Never worked with it, do you have an example? or a link?

robtillaart:

Why not to use default "Conversion completed" interruption?

Never worked with it, do you have an example? or a link?

Section two on interrupt driven ADC may help, can't find any arduino examples...

http://www.avrfreaks.net/index.php?name=PNphpBB2&file=printview&t=56429&start=0

Thanks for the link, added to my TODO list :slight_smile:

wanderson:

Why not to use default "Conversion completed" interruption?

can't find any arduino examples

There are some examples for using Noise Reduction (which is sort of related). I can't find anything about asynchronous reads either.

@robtillaart: Would auto-triggering with a ring buffer be useful? Analog readings would be collected (and timestamped) continuously in the background. The application would pull readings from a shared ring buffer. I think managing multiple channels is the only tricky part.

Can't recollect if I seen any such examples, that working with interruption Conversion Complete. It looks like concept to get maximum quantity samples is not correct from the bases, even such software exist, and definitely would provide the biggest numbers, BUT. Continue my line of thoughts, that Timing Start Conversion is most critical part, ( quality of samples, not the quantity, BTW, why do I need to sample temperature 9.6 ksps?), it make more sense to define specific sampling frequency let say 8 kHz, than try to squize something that would be hard to predict. AFAIK, timing to serve ISR is not strictly defined, so application that provide max numbers of samples via ISR_ADC_vect would have unspecified sampling rate.
There is an other example, which has specific sampling rate, and not pulling ADC waiting results.
http://interface.khm.de/index.php/lab/experiments/arduino-realtime-audio-processing/

(related threads)