The Arduino DAQ Chronicles



Introduction

This page should really be called "Using the Arduino as a Really Inexpensive DAQ."  This is where we dive into the limits of the using the Arduino as a data collector.  Here you will find the information you need to know in order to determine when to move on to another board, or a dedicated DAQ.   The photo shows the Arduino Mega setup used for testing - a piece of bent acrylic with a section cut out for mounting a serial LCD and a couple of breadboards glued in place.  The Mega, using a 1280 chip, runs at the same speed as the Atmega based 328 Arduino boards, the only difference being some extra memory and more I/O ports, and of course a higher price tag ($37 to $39 on ebay).  Also note that the Mega does not have a replaceable chip, so this is probably not the board to use for experimentation.  That said, I am using the Mega (do as I say, not as I do), and the results are equally valid for any Atmel, 5 volt, 328 chip running at 16 MHz, i.e the common (for good reason) Arduino Uno.

Before you collect data, their are two things you need to know, sample rateresolution, and responsiveness.  I added responsiveness because of comments like this.  Are these comments valid?  We'll see.  If you have a drive shaft capable of spinning at 10,000 rpm (167 revolutions per second), it doesn't do any good to hook it up to a collector that can only sample at 100 times a second.  Likewise, suppose you have a sensor hooked up to that same shaft that measures the reflection from a thin white stripe drawn along the axis of the otherwise black background.  Let's say the voltage variation due to the increased light reflection of the white stripe passing by the sensor drives the voltage of the sensor from 0 volts to .05 volts.  We are merely wasting our time if our collector can only measure voltage changes of .1 volt; that's the importance of resolution.  So what is an Arduino capable of?


Resolution


First we'll look at the resolution issue.  The analog to digital converters (ADC) in the Arduino are considered to have 10 bit resolution.  The bits of resolution is an obfuscation that means 2^10, or 1024 divisions (0 to 1023), of the reference voltage,  If someone tries to sell you a DAQ with with 4 bit resolution, you'll probably want to pass, since this would divide the reference voltage into 2^4, or 16 parts (0 to 15 steps).  For an Arduino, the reference voltage is usually 5 volts, and that means the smallest detectable voltage variation is 5/1023 or .0049 volts (4.9 mV).  That's not too bad, and there are some easy ways to adjust the system for even better resolution.  The Arduino ADC ports are normally tied to a 5 volt reference, but this is adjustable.  Let's say you have a sensor that outputs in the millivolt (mV) range and thus 0.001 volt is significant.  We can easily use software to change the ADC port reference to 1.1 volts, and that gives us a resolution of 1.1/1023 or 0.0011.  Close but still not quite enough.  That same referenced page gives us another route, the AREF pin can be tied to and external voltage source, andl, let's say that source is 0.5 volts.  Now we can detect a voltage of 0.5/1023, or .00049 volts (0.49 mV).  Of course there are other issues involved such as circuit noise and stability, and I haven't really tried to measure down to these tolerances; but in theory, it should be possible, and note that this is closing in on 14 bit resolution at a non-adjustable 5 volt level.  

You can use a voltage divider to set a non-standard (not 5, 3.3, or 1.1) voltage.  The manual says to always place a voltage on the AREF pin using a 5K (5000) ohm, or better, resistance.  With that in mind, lets use one of the many voltage divider calculators on the web with 5000 ohms for R1, 0.5 volts for Vout, and 5.0 volts for Vin.  The calculator gives us


555 ohms.  Don't have a 555 ohm resistor in your kit?  Well, the ratio of the resistances is the important thing, and you probably do have a 10K ohm resistor and a 1000 ohm resistor, and that gives 0.455 volts (close enough).  Now you have a resolution of 0.455/1023 or 0.44 mV.  Remember, for less taxing jobs, you also have a 3.3 volt pin that can be tied to AREF directly through a 5K resistor (according to documentation) to give an instant resolution increase to 3.3/1023 or 3.2 mV.  What, you may ask, are the true reference voltages?  It does make a difference in terms of power sources used.  The voltages found on my board are summarized in the following table.

 3.3 v pin  5 v pin
 USB power only 3.41 v 4.78 v
 9 v battery 3.42 v 5.02 v
 9 v power supply 3.42 v 5.02 v
     
The moral here is to beware of running the board on USB power only.

Sample Rates


Now, we tackle sample rates, and this gets a bit tricky.  A DAQ does nothing but collect data at given time intervals.  If we tell a DAQ to sample temperature at 500 samples a second, you get 500 voltage values every second, and you can be assured that each sample was taken precisely at 0.002 seconds (2 ms) spacing.  There is no need to collect the time from a DAQ, since we know the time spacing between each sample.  This timing certainty doesn't hold with the Arduino.  The Arduino does not sample at precise intervals unless we force it to do so (with some difficulty).  If the Arduino is looping, or calculating, there is a certain amount of CPU time being dedicated to those tasks.  Data collection is not the primary focus of the Arduino, indeed it seems more like an afterthought.   So, the easiest way to collect data with an Arduino is to just collect a time stamp and then the voltage signal.  This is especially true if you are going to do calculations on the data.  

To demonstrate Arduino sam
pling from the simplest schemes, to the more complex, we'll use the following setup with an Analog Devices TMP36 temperature sensor.  Lady Ada has the definitive tutorial on using this sensor with the Arduino, and I won't even try to improve on her fantastic writeup with one exception.  Note the output of the sensor maxes out around 2 volts.  To increase our resolution, we'll tie the AREF terminal to the 3.3 volt terminal using a 1K resistor (I know I said 5K before, but 1K works).   A pushbutton switch has also been added that keeps digital pin 2 tied to ground until pushed and then it momentarily goes high.  Physically, the temperature sensor looks like a transistor (same case), and the +5 vdc goes to the left hand pin when looking at the flat side of the case with leads extended downward (red wire in the picture where it is shown upside down).
 
A 1000 ohm resistor is used to limit current.  The documentation talks about a 5K resistor, and I was surprise by the voltage drop across that value.  I then used a 10K variable resistor and dialed it down, ad saw that AREF was definitely pulling current because there was a significant voltage drop across the resistor.  With 1K, that drop was 0.12 volts which surprisingly enough pulled the 3.42 volt to 3.3v - just what we wanted using a commonly found resistor value..




















Now we'll write some simple code that will send temperature results to the serial monitor running at the snail's pace of 9600 baud.  You can leave out LED13 and the pushbutton, of course.


/*
Sampling with the Arduino
example 1 - 9600 baud
from http://sites.goggle.com/site/MeasuringStuff
*/

// the famous LED on digital pin 13
byte ledPin =  13;
// we'll use digital pin 2 to signal the start of data collection
byte letsStartPin = 2;
// read the temp sensor on analog pin 0
byte sensorPin = 0;
// time will be in millis (milliseconds)
unsigned long time;
// the raw temp data ranging from 0 to 1023
unsigned int analog0;
// tied the reference voltage AFREF to the 3.3 v pin using a 1K resistor
float arefVolts = 3.3;
// we'll let the arduino do the voltage calculation (arefVolts/1023)*analog0
float volts;
// ditto with the temp calc in C, 100*volts-50
float tempC;
// ditto for F, tempC*9/5+32
float tempF;

void setup() {
  // set the reference pin voltage to 3.3 volts (measured with voltmeter)
  analogReference(EXTERNAL);
  // initialize serial port:
  Serial.begin(9600);
  // signal ready to start by turning on LED 13
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, HIGH);
  pinMode(letsStartPin,INPUT);
  // endless loop until letsStartPin goes high;
  do { 
  }
  while (digitalRead(letsStartPin) == LOW); 
  digitalWrite(ledPin, LOW);  
}
void loop() {
  time=millis();
  analog0=analogRead(0);
  volts=(analog0/1023.0)*arefVolts;
  tempC=100*volts-50;
  tempF=tempC*9/5+32;
  Serial.print(time); 
  Serial.print(" , ");
  Serial.print(analog0);
  Serial.print(" , ");
  Serial.print(volts); 
  Serial.print(" , ");
  Serial.print(tempC); 
  Serial.print(" , ");
  Serial.println(tempF); 
}

Here is a snippet of the output gathered using ttylog (ttylog -b 9600 -/dev/ttyUSB0 >> data.txt), there are similar programs in Windows like TeraTerm:

75258 , 217 , 0.70 , 20.00 , 68.00
75295 , 217 , 0.70 , 20.00 , 68.00
75333 , 217 , 0.70 , 20.00 , 68.00
75370 , 217 , 0.70 , 20.00 , 68.00
75408 , 217 , 0.70 , 20.00 , 68.00
75445 , 217 , 0.70 , 20.00 , 68.00
75483 , 217 , 0.70 , 20.00 , 68.00
75521 , 216 , 0.70 , 19.68 , 67.42
75557 , 217 , 0.70 , 20.00 , 68.00
75595 , 217 , 0.70 , 20.00 , 68.00
75632 , 217 , 0.70 , 20.00 , 68.00
75670 , 216 , 0.70 , 19.68 , 67.42
75707 , 217 , 0.70 , 20.00 , 68.00
75745 , 217 , 0.70 , 20.00 , 68.00

The first column is the time in milliseconds.  Note the rate is 37 to 38 ms per iteration.  That's a sample rate of 26 samples per second.  Works for a slow moving temperature scan, but not much else.  So lets try running the same thing at 115200 baud.  At that speed the average time interval between readings is 4 ms, giving a sample rate of 250 samples per second.  So speeding up the serial transfer rate to the maximum (and a bit unstable) speed of 115200 gives a 10 fold increase.  Can we do better?  How about if we strip it down to bare bones.

/*
Sampling with the Arduino
example 3 - 115200 baud
from http://sites.goggle.com/site/MeasuringStuff
*/

// the famous LED on digital pin 13
byte ledPin =  13;
// we'll use digital pin 2 to signal the start of data collection
byte letsStartPin = 2;

void setup() {
  // initialize both serial ports:
  Serial.begin(115200);
  // signal ready to start by turning on LED 13
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, HIGH);
  pinMode(letsStartPin,INPUT);
  // endless loop until letsStartPin goes high;
  do { 
  }
  while (digitalRead(letsStartPin) == LOW); 
  digitalWrite(ledPin, LOW);  
}
void loop() {
  Serial.println(micros()); 
  Serial.println(analogRead(0));
}

The output looks something like:

11278308
147
11280232
146
11282164
147
11284088
146
11286020
147
11287952
146
11289880
147

Now the time between reads is wavering between 1924 and 1932 microseconds, giving a sample rate per second of 517.  Still painfully low.  Let's bypass the serial bus and do a short burst in memory.  This technique won't be suitable for continuous sampling because the 8K memory (or 2K on an Atmega 328) is going to fill up real fast (remember sram is where variables are kept not the 128K/32K flash which is program storage space), especially with an unsigned long integer, but here goes.  This code will give us the maximum sample rate allowable without resorting to interrupts and assembly code (we may come back to the interrupts though).  If we have to do that, it's time to look at a low cost DAQ. 

 /*
Sampling with the Arduino
example 4 - 9600 baud
from http://sites.goggle.com/site/MeasuringStuff
*/

// the famous LED on digital pin 13
byte ledPin =  13;
// we'll use digital pin 2 to signal the start of data collection
byte letsStartPin = 2;
// our array size
int size=10;
unsigned long time[10];
unsigned int rawdata[10];

void setup() {
  // initialize both serial ports:
  Serial.begin(9600);
  // signal ready to start by turning on LED 13
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, HIGH);
  pinMode(letsStartPin,INPUT);
  // endless loop until letsStartPin goes high;
  do { 
  }
  while (digitalRead(letsStartPin) == LOW); 
  digitalWrite(ledPin, LOW);  
}
void loop() {
  // avoid an internal loop structure - just do it
  time[0]=micros();
  rawdata[0]=analogRead(0);
  time[1]=micros();
  rawdata[1]=analogRead(0);
  time[2]=micros();
  rawdata[2]=analogRead(0);
  time[3]=micros();
  rawdata[3]=analogRead(0);
  time[4]=micros();
  rawdata[4]=analogRead(0);
  time[5]=micros();
  rawdata[5]=analogRead(0);
  time[6]=micros();
  rawdata[6]=analogRead(0);
  time[7]=micros();
  rawdata[7]=analogRead(0);
  time[8]=micros();
  rawdata[8]=analogRead(0);
  time[9]=micros();
  rawdata[9]=analogRead(0);
  for (int i=0;i<size;i++) {
    Serial.print(time[i]);
    Serial.print(" , ");
    Serial.println(rawdata[i]);
  }
  delay(5000);
}


The output for one iteration is:

23172108 , 149
23172220 , 149
23172332 , 149
23172452 , 149
23172572 , 149
23172692 , 149
23172812 , 149
23172932 , 149
23173052 , 149
23173172 , 149

Now, the time between samples is down to 120 microseconds, and that gives a sample rate of 8300 samples a second, being a decent rate for an inexpensive DAQ.  The trick will be trying to get a continuous sample from the system at rates above 500 samples per second.  

ADC Responsiveness


Another limitation that has not been discussed so far is the ability of the ADC to recover.  Are we fooling ourselves in thinking that an 8K sampling rate is truly obtainable?  The temperature sensor was hardly fluctuating, so it doesn't clue us in on the ADC limitations in terms of responsiveness.  So, before going any further, the next test needs to be a sample of a fast fluctuating zero to five volt signal.  We'll use a sine wave generator operating at 1000 Hz and 0 to 5 volts amplitude.  If we truly have a responsive system at an approximate 8K sample rate, we should get eight representative samples along each sine cycle.  This ought to be interesting.  Here's the code:

 /*
Sampling with the Arduino
example 5 - 9600 baud
from http://sites.goggle.com/site/MeasuringStuff
*/

// the famous LED on digital pin 13
byte ledPin =  13;
// we'll use digital pin 2 to signal the start of data collection
byte letsStartPin = 2;
// our array size
int i=0;
int size=100;
unsigned long time[100];
unsigned int rawdata[100];

void setup() {
  // initialize both serial ports:
  Serial.begin(9600);
  // signal ready to start by turning on LED 13
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, HIGH);
  pinMode(letsStartPin,INPUT);
  // endless loop until letsStartPin goes high;
  do { 
  }
  while (digitalRead(letsStartPin) == LOW); 
  digitalWrite(ledPin, LOW);  
}

void loop() {
  // avoid an internal loop structure - just do it
  for (i=0 ;i<size;i++){
  time[i]=micros();
  rawdata[i]=analogRead(0); 
  }
  for (i=0;i<size;i++) {
    Serial.print(time[i]);
    Serial.print(" , ");
    Serial.println(rawdata[i]);
  }
  delay(5000);
}

And, here is a graph of the output and a snippet of the 100 samples where the first column is time in microseconds, the second column is the rawdata in the form of 0 to 1023 corresponding to 0 to 5.02 volts, and the last column was added in a spreadsheet, and it is the time in microseconds between entries Lines of data).
















23198936 99
23199148 12 212
23199268 207 120
23199388 574 120
23199508 902 120
23199628 1010 120
23199752 830 124
23199868 473 116
23199988 132 120
23200108 8 120
23200228 169 120
23200348 525 120
23200468 869 120
23200588 1014 120
23200708 867 120
23200828 522 120
23200948 167 120
23201068 8 120
23201188 133 120
23201308 476 120
23201428 833 120
23201548 1011 120
23201668 900 120
23201788 545 120
23201916 185 128
23202036 9 120
23202156 117 120
23202276 452 120
23202396 814 120
23202516 1008 120

There doesn't appear to be a problem with ADC responsiveness at 8.33K samples per second.  Also, note that the use of the FOR loop did not cause any slowdown in collection EXCEPT for the initial sample.  So now, it's just a matter of figuring out the right tricks to use to make the Arduino an easily programmed inexpensive DAQ.  So, we know that when using the Arduino IDE (instead of native assembly language), with a 16 MHz chip,we are limited to just over 8000 samples per second, and while we always have 10 bit resolution, that resolution can be adjusted to match the input voltage - a real plus over fixed reference voltages.

Digging Deeper - Speeding It Up


This section kicks off with this very interesting thread:


By default, the successive approximation circuitry requires an input clock frequency [ADC clock] between 50 kHz and 200 kHz
to get maximum resolution. If a lower resolution than 10 bits is needed, the input clock frequency to the ADC can be higher
than 200 kHz to get a higher sample rate.

The ADC clock is 16 MHz divided by a prescale factor. The prescale is set to 128 (16MHz/128 = 125 KHz) in wiring.c. Since a
conversion takes 13 ADC clocks, the sample rate is about 125KHz/13 or 9600 Hz.

So anyway, setting the prescale to, say, 16, would give a sample rate of 77 KHz. Not sure what kind of resolution you would
get though!

EDIT:

Found this reference:

http://www.atmel.com/dyn/resources/prod_documents/DOC2559.PDF

The ADC accuracy also depends on the ADC clock. The recommended maximum ADC clock frequency is limited by the internal DAC
in the conversion circuitry. For optimum performance, the ADC clock should not exceed 200 kHz. However, frequencies up to
1 MHz do not reduce the ADC resolution significantly. Operating the ADC with frequencies greater than 1 MHz is not
characterized.

So looks like using a prescale of 16 as above would give an ADC clock of 1 MHz and a sample rate of ~77KHz without much loss
in resolution. 

Hmmm, 77 kHz you say?  Definitely going to look into this!  

Well here are the results (only the first column is from the Arduino - the rest was calculated using gnumeric) of the samples collected from the analog port of a 1000 Hz signal.  Not 77K, because that's theoretical, and the actual program has to collect and store samples in memory, but how's 56K?  There is no such thing as a 56K sample per second commercial DAQ for under $100, but we now have one for less than $30!


726 1 3.61
679 2 3.38
630 3 3.13
576 4 2.87
523 5 2.60
470 6 2.34
415 7 2.06
364 8 1.81
312 9 1.55
263 10 1.31
218 11 1.08
176 12 0.88
137 13 0.68
103 14 0.51
73 15 0.36
50 16 0.25
35 17 0.17
25 18 0.12
23 19 0.11
24 20 0.12
28 21 0.14
39 22 0.19
57 23 0.28
83 24 0.41
115 25 0.57
152 26 0.76
192 27 0.96
236 28 1.17
283 29 1.41
332 30 1.65
384 31 1.91
436 32 2.17
491 33 2.44
543 34 2.70
598 35 2.98
649 36 3.23
700 37 3.48
753 38 3.75
798 39 3.97
839 40 4.17
876 41 4.36
908 42 4.52
936 43 4.66
958 44 4.77
974 45 4.85
984 46 4.90
987 47 4.91
984 48 4.90
975 49 4.85
960 50 4.78
939 51 4.67
911 52 4.53
879 53 4.37
842 54 4.19
801 55 3.99
758 56 3.77
711 57 3.54
660 58 3.28
608 59 3.03
555 60 2.76
502 61 2.50
448 62 2.23
395 63 1.97
343 64 1.71
292 65 1.45
246 66 1.22
200 67 1.00
159 68 0.79
123 69 0.61
90 70 0.45
63 71 0.31
43 72 0.21
30 73 0.15
24 74 0.12
23 75 0.11
24 76 0.12
31 77 0.15
46 78 0.23
67 79 0.33
95 80 0.47
129 81 0.64
167 82 0.83
208 83 1.03
254 84 1.26
302 85 1.50
352 86 1.75
405 87 2.02
458 88 2.28
512 89 2.55
566 90 2.82
620 91 3.08
670 92 3.33
719 93 3.58
768 94 3.82
811 95 4.04
849 96 4.22
886 97 4.41
918 98 4.57
944 99 4.70
963 100 4.79

/*
  Analog Input with prescale change (thanks to jmknapp)
  http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1208715493/11
  Reading a 1 kHz sine wave, 0 to 5 volts
  Using analog 0
  Results stored in memory for highest speed
 */

#define FASTADC 1
// defines for setting and clearing register bits
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

int value[100];   // variable to store the value coming from the sensor
int i=0;

void setup() 
{
 Serial.begin(9600) ;
 int i ;
  
#if FASTADC
  // set prescale to 16
  sbi(ADCSRA,ADPS2) ;
  cbi(ADCSRA,ADPS1) ;
  cbi(ADCSRA,ADPS0) ;
#endif
}

void loop() 
 for (i=0;i<100;i++)
{
  value[i]=analogRead(0);
for (i=0;i<100;i++)
{
  Serial.println(value[i]);
Serial.println();
Serial.println();
Serial.println();
delay(5000);
  
}
 
And from the Atmel data sheet:


Yeah, Well, One Hundred Samples Seems a Bit Limiting, and That Timing Thing...


Yep, you're correct.  There are still a couple of problems to iron out.  First, we've got to be able to move data out at the same rate it's being collected or we're limited to just a few hundred samples.  Serial, even at 115,200 baud, is not going to do it, but once again the Arduino forums bail us out:


i2c can run at 400kbits, providing a transfer rate of 50K bytes per second.  So, we can reduce the resolution and go with a map to convert integers to bytes and get a 50K sample per second stream, or be reduced to a 25K sample rate for full resolution.  This setup is relatively painless, and there are memory chips that "talk" i2c.  But there is an even better, if more complicated solution, and that is using the SPI communications interface which runs at a whooping 4 Mbits.  This is what most SD/MMC card interfaces use, and it looks like the route to take.  Information can be found here, 


and here,


and with many other postings.

According to the main Arduino site there are soon to be official Megas with SD memory cards installed and officially supported.  When these show up, we'll continue the chronicles. I'm betting we have an awesome, programmable DAQ with deep logging capabilities for about $60.

To be continued...

In the meantime, here is an interesting project that moves around a bunch of data, and of course, LadyAda is always in the mix...



 

    
            
Comments