Author Topic: Arduino Tutorial #5 - Digital Voltmeter, Arduino Analog to Digital Converter  (Read 26305 times)

MJLorton

  • Administrator
  • Hero Member
  • *****
  • Posts: 817
The original video is here: https://www.youtube.com/watch?v=y-_Pkw_GQ-c

In this tutorial we look at a digital voltmeter project and how it used the Arduino analog input. This involves understanding ADC or analog to digital converters and how they work. We also troubleshoot why the output reading on the LCD display is noisy.
We also gave a quick peek at the Arduino DUE: http://arduino.cc/en/Main/arduinoBoardDue

Thanks to Clarence of "Clarence's Wicked Mind" for the original code that I used and updated for this project: http://www.clarenceho.net:8123/blog/articles/search?q=voltmeter

Also read Measuring Stuff - The Arduino DAQ Chronicles: https://sites.google.com/site/measuringstuff/the-arduino

The Arduino Due is a microcontroller board based on the Atmel SAM3X8E ARM Cortex-M3 CPU (datasheet). It is the first Arduino board based on a 32-bit ARM core microcontroller. It has 54 digital input/output pins (of which 12 can be used as PWM outputs), 12 analog inputs, 4 UARTs (hardware serial ports), a 84 MHz clock, an USB OTG capable connection, 2 DAC (digital to analog), 2 TWI, a power jack, an SPI header, a JTAG header, a reset button and an erase button.

Buy the Arduino DUE here:http://astore.amazon.com/m0711-20/detail/B00A6C3JN2

The sketch / project code for the digital voltmeter:

/*
  Voltmeter
  Voltmeter base on voltage divider concept.

  by Clarence's Wicked Mind - news from Clarence, stuff that doesn't matter
  Code based on: http://www.clarenceho.net:8123/blog/articles/2009/05/17/arduino-test-voltmeter
  Coded by: arduinoprojects101.com
 
  Updated by Martin Lorton (http://mjlorton.com and http://www.youtube.com/mjlorton )for use in
  Arduino tutorial series: https://www.youtube.com/playlist?list=PLF86F263013F106C0
*/
#include <SoftwareSerial.h>
#define txPin 2
SoftwareSerial LCD = SoftwareSerial(0, txPin);
// since the LCD does not send data back to the Arduino, we should only define the txPin
  const int LCDdelay=10;  // conservative, 2 actually works
  int analogInput = 1;
  int LEDpin = 13;
  int prev = LOW;
  int refresh = 2000;
  long TotalTime = 0;
  int Samples = 0;
  long TimeRun = 0;
  float vout = 0.0;
  float vin = 0.0;
  float R1 = 4739.0;    // !! resistance of R1 !!
  float R2 = 988.0;     // !! resistance of R2 !!
  float vMax = 0.0;
  float vMin = 10000.0;
  float vAcc = 0.0;
  float vAve = 0.0;
  int value = 0;
  int LCDpos = 0;
  int vMaxFlag = 0;
  int vMinFlag = 0;
  int TMin = 0;
  int TSec = 0;

void clearLCD(){
  LCD.write(0xFE);   //command flag
  LCD.write(0x01);   //clear command.
  delay(LCDdelay);
}
void backlightOn() {  //turns on the backlight
  LCD.write(0x7C);   //command flag for backlight stuff
  LCD.write(157);    //light level.
  delay(LCDdelay);
}
void backlightOff(){  //turns off the backlight
  LCD.write(0x7C);   //command flag for backlight stuff
  LCD.write(128);     //light level for off.
   delay(LCDdelay);
}
void serCommand(){   //a general function to call the command flag for issuing all other commands   
  LCD.write(0xFE);
}

void setup()
{
 // declaration of pin modes
  pinMode(analogInput, INPUT);
  pinMode(LEDpin, OUTPUT);
  pinMode(txPin, OUTPUT);
 // begin sending over serial port
  Serial.begin(9600);
  LCD.begin(9600);
  delay(LCDdelay);
  backlightOn();
}

void loop()
{
   // read the value on analog input
  value = analogRead(analogInput);

  if (value >= 1023) {
    Serial.println("MAX!!");
    delay(refresh);
    return;
  }
  else if (value <= 0) {
    Serial.println("MIN!!");
    delay(refresh);
    return;
  }

  Samples = Samples + 1; 
  TotalTime = TotalTime + refresh;
  vout = (value * 5.0) / 1024.0;
  vin = vout / (R2/(R1+R2));
  vMinFlag = 0;
  vMaxFlag = 0;
  if (vin > vMax) {digitalWrite(LEDpin, HIGH);}
  if (vin > vMax) {vMaxFlag=1;}
  if (vin > vMax) {vMax=vin;}
  if (vin < vMin) {digitalWrite(LEDpin, HIGH);}
  if (vin < vMin) {vMinFlag=1;}
  if (vin < vMin) {vMin=vin;}
  vAcc = vin + vAcc;
  vAve = vAcc / Samples;
  TimeRun = TotalTime / 1000;
  // Serial output to computer terminal
  Serial.print(vin);
  Serial.print(" volt * ");
  Serial.print(vMax);
  Serial.print(" volt Max * ");
  Serial.print(vMin);
  Serial.print(" volt Min * ");
  Serial.print(vAve);
  Serial.print(" volt Ave * ");
  Serial.print(TimeRun);
  Serial.println(" Time (Sec)");
 
  // Martin Adding LCD Stuff
  clearLCD();
  serCommand();
  if (vin < 10) {LCD.write(129);}   
  else {LCD.write(128);}
  LCD.print(vin);
  serCommand();
  LCD.write(134);
  LCD.print("Volts");
  serCommand();
  if (vMax < 10) {LCD.write(193);}   
  else {LCD.write(192);}
  LCD.print(vMax);
  serCommand();
  LCD.write(198);
  if (vMaxFlag==1) {LCD.print("Max Volts *");}
  else {LCD.print("Max Volts");}
  LCD.write(0xFE);
  if (vMin < 10) {LCD.write(149);}   
  else {LCD.write(148);}
  LCD.print(vMin);
  serCommand();
  LCD.write(154);
  if (vMinFlag==1) {LCD.print("Min Volts *");}
  else {LCD.print("Min Volts");}
  serCommand();
  if (vAve < 10) {LCD.write(213);}   
  else {LCD.write(212);}
  LCD.print(vAve);
  serCommand();
  LCD.write(218);
  LCD.print("Ave Volts ");
  LCD.print(value); 
  serCommand();
  LCD.write(141);
  LCD.print(TimeRun);
  LCD.print(" Sec");
  // sleep...
  delay(refresh);
  digitalWrite(LEDpin, LOW);   
}
Play, discover, learn and enjoy! (and don't be scared to make mistakes along the way!)

ttyz

  • Newbie
  • *
  • Posts: 18
Martin,

I just wanted to share my last experience with arduino analog inputs. I have also had this issue with the noise when Arduino is powered by USB, but I found another solution that allows you to user an external voltage reference using AREF pin, it is actually very handy sometimes. You can read more about it here: http://arduino.cc/en/Reference/AnalogReference?from=Reference.AREF

Also do you have a GitHub account? Or maybe you consider creating one? It might get you some improvements to your code by the members of this community.

Thanks for another great tutorial.
Cheers,
Evgeny

mariush

  • Newbie
  • *
  • Posts: 42
If you're going to use this code in a future video, my suggestions would be to use some #define commands at the top to replace the numbers to something meaningful.

For example, instead of saying LCD.write(198); you could say LCD.write(LCD_CLEAR); and  put #define LCD_CLEAR 198  at the top.

Also, even though it makes the code longer, it makes code more readable if you arrange it differently... like so, for example:

  if (vMax < 10) {LCD.write(193);}   
  else {LCD.write(192);}

if (vMax < 10) {
   LCD.write(193);
 } else {
   LCD.write(192);
}

It's longer but it makes it easier to follow the code.
This is especially important because if there's a single instruction, the brackets aren't required:    if (vMax < 10) LCD.write(193);

tmm

  • Newbie
  • *
  • Posts: 7
You can try a shunt voltage reference to drive AREF. This should give a pretty stable reference even with noise on the powersupply. Just hook it up to the 5v rail with a series current limiting resistor the same way you would hook up an LED (check the datasheet, 1-2mA is the norm), then connect the anode to AREF. The ATMega processor will get upset if you use a reference higher than VCC and you will need a supply higher than 5v to power a 5v reference, so it's probably best to stick with a 4.096V/4.1V reference rather than 5V.
Just make sure you set the reference to external in your code (link in above post)

Another thing you can do is dither the ADC. You can do this on Arduino by setting the ADC prescaler to increase the ADC clock. This will make the ADC noisy, but if you take a large number of samples and average them the noise will average out and you can achieve resolution beyond the quantisation levels. On an Arduino Mega powered by USB and using the internal reference i could achieve around 12bit accuracy from the Arduino's 10bit ADC.

Put this at the top of your sketch (outside of setup() and loop())
Code: [Select]
//defines for setting and clearing ADC prescaler 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

put this inside setup().
Code: [Select]
  //set ADC prescaler to 2
  sbi(ADCSRA,ADPS2);
  sbi(ADCSRA,ADPS1);
  cbi(ADCSRA,ADPS0);

make a function to average a large number of acquisitions
Code: [Select]
float analogReadDither(int pin)
{
  float result; //to store the final value
  int num = 500; //the number of readings to take
  float fnum = float(num); //cast number of readings to floating point
 
  //get [num] readings and obtain the mean value
  for(int i=0; i<num; i++)
  {
      result += float(analogRead(pin))/fnum;
  }
  return result;
}

Then use analogReadDither(pin) instead of analogRead(pin). Just make sure your code won't cast the returned floating point number back to integer. You can reduce the number of acquisitions from 500 to make it run faster, or increase it for better accuracy.

The above won't work for the ARM based Arduino Due. Only for ATMega based boards (Uno, Duemilanove, Mega, Nano, etc).
« Last Edit: October 01, 2013, 08:57:21 AM by tmm »

SeanB

  • Administrator
  • Hero Member
  • *****
  • Posts: 1017
A few notes as to the ADC inputs.

ADC inputs are anything but resistive, they are in most cases a switched capacitor sample and hold. Especially on a MCU with multiple inputs and a single ADC with a mux for them. This leads to the inputs being a high impedance most of the time but drawing a small pulse of current to charge ( or source a small current if the input is dropping) an internal sample and hold cap in the ADC. This can lead to both scale errors and to noise on the converted value, as well as the values becoming dependant on the other ADC inputs if you are using multiple inputs. There are 2 methods to mitigate this, you can use a unity gain opamp buffer on the pin to give a low impedance drive or you can use a RC filter to the pin. This was always used on the 7106/7 with a 1M and 47n to 100n film capacitor across the input, the 1M resistor gave less then 1 LSB of error on the device due to the 100M plus input impedance ( aside from those current spikes as the internal switches operated of course, which is why the capacitor was there to provide a current buffer that swamped the charge).

To not use the RC you must have a higher current through any voltage divider, generally 1mA or thereabouts at full scale will work as a ballpark figure. Will be increased dissipation in the resistors and cause some thermal drift, but will give a slightly lower input noise than high value resistors.

As seen the reference needs to be stable, using a 4.096V LDO reference and driving the internal reference input with this gives about the best full scale range, but you need to have the LDO well decoupled and the output needs a filter as well to reduce the noise that the MCU will place on it. There have been many applications that run the whole MCU on a precision 5V reference ( providing the current drawn is low and essentially constant, and you use bus buffers on the input and output digital pins supplied from a separate logic 5V supply) to get a stable internal operating environment.

Another noise reduction technique is to initiate the ADC conversion and then stop the MCU and wake it when the conversion is complete. This reduces internal noise from changing data buses and voltage noise across the power and ground bonds and the substrate from influencing the result. With this the only thing running is a clock oscillator driving the ADC.

Another method after all the above is to use oversampling to average out the noise, and this does work well, though you do have the drawback of long sampling times and long update rates.

Hope this gives some ideas.

GTronix

  • Newbie
  • *
  • Posts: 1
oversampling works pretty good! I am able to get a stable reading down to .001 and I am impressed by this capabilty!

However the internal reference on the arduino drifts by millivolts so it is kind of useless. An external reference is a must if you need better accuracy.

Here is a link to the best oversampling code that I have found thus far. It includes a library. I am using it for measuring temperature accurately down to 0.1c resolution. My arduino clone can't run it higher than 17 bit. It runs best for me at 16 bit with 5 samples. It runs a little slow and works hard to calculate everything but it is perfect for my little hotbox project.

http://electricrcaircraftguy.blogspot.com/2014/05/using-arduino-unos-built-in-16-bit-adc.html#.VOJwbC4oLus

kardacz

  • Newbie
  • *
  • Posts: 5
Good evening everyone! I look for the scheme laboratory supply TTI EX752M: someone could you help me ?? Thanks a lot

MJLorton

  • Administrator
  • Hero Member
  • *****
  • Posts: 817
oversampling works pretty good! I am able to get a stable reading down to .001 and I am impressed by this capabilty!

However the internal reference on the arduino drifts by millivolts so it is kind of useless. An external reference is a must if you need better accuracy.

Here is a link to the best oversampling code that I have found thus far. It includes a library. I am using it for measuring temperature accurately down to 0.1c resolution. My arduino clone can't run it higher than 17 bit. It runs best for me at 16 bit with 5 samples. It runs a little slow and works hard to calculate everything but it is perfect for my little hotbox project.

http://electricrcaircraftguy.blogspot.com/2014/05/using-arduino-unos-built-in-16-bit-adc.html#.VOJwbC4oLus
Thanks for the post. Great to hear about your results using this method. One of the topics I want to cover soon is ADCs and this blog on oversampling is very interesting.
Cheers,
Martin.
Play, discover, learn and enjoy! (and don't be scared to make mistakes along the way!)