Millivolt Meter


The Millivolt Meter Project

This is a DIY mil­li­volt meter that was orig­i­nal­ly designed by
Scullcom Hobby Electronics and pre­sent­ed on Youtube.
Definitely take the time to watch the videos (4 parts) as they are very well done and pre­sent­ed in a way that’s easy to fol­low and learn from.

I was in need of a meter that I could ded­i­cate to low volt­age pre­ci­sion read­ings, and could be done for a rea­son­able cost. So this project seemed like a per­fect fit. As usu­al after view­ing the video I saw many areas that the meter could be mod­i­fied and improved for my spe­cif­ic use, and quick­ly began design­ing my own ver­sion of it using Eagle CAD V7.5.

Some of the changes I made were to reduce the noise and add shield­ing to the input sec­tion. I also made the deci­sion to uti­lize a pre­ci­sion volt­age divider net­work instead of the less expen­sive indi­vid­ual resis­tors.
Another major depar­ture from the orig­i­nal design was to use a MCP23017 (16 input/output) I2C port expander board that I had designed sev­er­al years ago, for con­nect­ing the LCD dis­play and push-but­tons. This reduced the num­ber of pins used by the Arduino Pro mini, and made it eas­i­er to mount the RGB LCD dis­play. As orig­i­nal­ly designed there were no pins left for future options.
I added guard rings on the top and bot­tom of the cir­cuit board around the low lev­el input cir­cuit­ry to the op-amp, and also removed the sol­der­mask around all low lev­el traces.
Bottom of board with input sec­tion on top right.

Top of board with input sec­tion on top left.

Boards were made in USA using the OSHpark board ser­vice.
https://oshpark.com/shared_projects/qgv0fpKN

The volt­age divider resis­tor is a Caddock 1776-C6815 of which the 1K, 9K, and 90K sec­tions are used in series to make up a 100K leg of the divider along with the 900K sec­tion for the oth­er leg of the divider. The 9M sec­tion was not used.
Next to the LTC2400 24bit ADC, a set of sol­der pad jumpers were added to allow selec­tion of either the 50Hz or 60Hz notch fil­ter.
Top of board with sol­der paste and sur­face mount devices except C7 mounted.

Board after refow sol­der­ing using a Presto Liddle Griddle and man­u­al tem­per­a­ture con­trol uti­liz­ing an infrared non-con­tact ther­mome­ter.
An impor­tant step is to clean the traces and com­po­nents in the input sec­tion so that no con­t­a­m­i­nants such as flux, oils, sur­fac­tants, or fin­ger­prints remain.

Caddock divider net­work and Sparkfun 16 MHz 5 volt Arduino Pro Mini added.

Other com­po­nents and I2C head­er installed, ready for testing.

After some pre­lim­i­nary test­ing I found some insta­bil­i­ty in the out­put of the ADR4540B 4.096 volt ref­er­ence IC. I had clear­ly left out the required out­put fil­ter capac­i­tor shown in the orig­i­nal schemat­ics.
I was eas­i­ly able to bodge the required capac­i­tor from the 4.096 test pin to the ground pin of the tan­ta­lum capac­i­tor direct­ly below it. The new V 1.51 schemat­ic and board files now include it.
One thing I noticed dur­ing test­ing was that the main board and dis­play used less than 50 mA and the orig­i­nal spec­i­fied 5 volt TO220 pack­age reg­u­la­tor, along with my added heatsink was much more than is need­ed. Moving to a small­er foot­print 150 mA reg­u­la­tor would free up con­sid­er­able space. Another addi­tion to the design would be to add a 250 mA PTC resetable fuse on the 9V input.

Checking fit and clear­ances of cop­per shield.

Mounting main board with shield and input wires attached. Battery guide and hold­er also mount­ed in enclo­sure.
Battery assem­bly is com­prised of 6 AA 1.5 volt bat­ter­ies for 9 V nom­i­nal input to regulator.

Front pan­el with dis­play, switch­es, and jacks mounted.

The front pan­el was designed using “Front Panel Designer” to fit a stan­dard Hammond 1455N1601 extrud­ed box with met­al end plates 6.299″ L x 4.055″ W x 2.087 H. — Link to design file at end of page.
Design file was sent to Front Panel Express in Seattle, WA. USA and was shipped five days lat­er. The pan­el is made from “Medium bronze” anodized alu­minum and is 2 mm in thick­ness. With a rec­tan­gu­lar beveled cutout for the LCD dis­play, D‑holes for the two banana jacks, coun­ter­sunk holes for box mount­ing, and stan­dard holes for the three switches.

RGB LCD with I2C port expander board con­nect­ed to main board.

OpenEVSE dis­play board with a 16 IO I2C port expander and bat­tery back­up RTC (Real Time Clock) DS3231. The board also includes cur­rent lim­it­ing resis­tors for the three back­light RGB led’s, and a con­trast poten­tiome­ter. For the I2C bus there are two pullup resis­tors posi­tions pro­vid­ed along with address select sol­der pad jumpers for the port expander. Four I/O ports are bro­ken out and can be indi­vid­u­al­ly con­fig­ured as inputs or out­puts, along with a ground pin. The board is sized to match the stan­dard foot­print for many 2 X 16 LCD dis­plays.
https://oshpark.com/shared_projects/J6RW88kf

The code pro­vid­ed by Scullcom Hobby Electronics was mod­i­fied to use the I2C inter­face for the LCD and input push-but­tons. RGB back­light­ing on the LCD changes col­or depend­ing on which mode the meter is cur­rent­ly in.

During start­up the EEPROM stored Cal Level is displayed.

Pressing the Calibrate but­ton starts the cal­i­bra­tion mode prompt­ing the user to short the input leads.

After the cal­i­bra­tion is com­plet­ed the Adjust Factor that is writ­ten to EPROM is dis­played on the LCD briefly before return­ing to mea­sure­ment mode.

Measurement mode, cur­rent­ly dis­play­ing micro volts.

With all shield­ing in place and cal­i­bra­tions per­formed the meter fluc­tu­ates
±12 uV max­i­mum with the input leads short­ed and typ­i­cal­ly ± 5 uV.

Link to EagleCAD Millivolt meter V1.51 schemat­ic and board files ZIP
Link to EagleCAD I2C port expander dis­play V4.2 schemat­ic and board files ZIP
Link to Front Panel Designer V1.1 front pan­el file ZIP

Modified code Version 3.20 December 2018 using Paul Versteeg’s fil­ter, cal­i­bra­tion, and many oth­er code enhance­ments. For I2C RGB dis­play and I/O.

UPDATED Millivolt Meter Version 2.11 blog post

MilliVoltMeter320.zip firmware

/* SCULLCOM HOBBY ELECTRONICS
 * MILLIVOLT METER USING LTC2400 24bit ADC CHIP
 * Using the Version 1.51 PCB designed by Barbouri (Greg Christenson)
 * https://www.barbouri.com/2016/05/26/millivolt-meter/
 * 
 * Software version 3.20
 * 4.096 volt precision reference (ADR4540)
 * 
 * ****************************************************************
 * ****************************************************************
 * Items to modify in this code for your individual Millivolt Meter:
 * 
 * float v_ref - This is your actual voltage measured from the internal ADR4540 IC
 * float cal_2_1v_ref - This is the actual voltage of your 2.048V external reference for calibration if used
 * float cal_2_5v_ref - This is the actual voltage of your 2.500V external reference for calibration if used
 * float cal_5v_ref - This is the actual voltage of your 5.000V external reference for calibration if used
 * float cal_10v_ref - This is the actual voltage of your 10.000V external reference for calibration if used
 * float adc_ref_volts - This is the actual value of the external voltage reference for battery calibration
 * batt_voltage = 9.5 - If using battery voltage monitoring comment out this line and un-comment line above it
 * ****************************************************************
 * ****************************************************************
 *
 * Modified May 2016, December 2018 by Barbouri
 * for I2C display and pushbuttons 
 * Added includes Wire.h, Adafruit_MCP23017.h, Adafruit_RGBLCDShield.h 
 * https://github.com/adafruit/Adafruit-MCP23017-Arduino-Library 
 * remapped buttons from processor to I2C port expander
 * Compilled using Arduino 1.8.2
 * 
 * Changes by dbldutch: 
 *  - added code to display the sw version in the welcome screens 
 *  - added code to display a real micro symbol instead of uV 
 *  - added code to monitor the charging level of a NiCAD/NiMH cell 
 *  - changed code to display decimal digits based on volts measured. 
 *  
 *  - Version 2.00:
 *  - stripped the 4LSB in the Read_ADC, before averaging. They add no value here.
 *    Calibration uses a larger sample size. Added a loop counter to show progress.
 *    
 *  - Version 3.00 : 
 *  - Changed from average based sampling to an Infinite Input Response (IIR) filter design.
 *    Details found here: https://github.com/arduino/Arduino/issues/3934
 *    and here : https://en.m.wikipedia.org/wiki/Infinite_impulse_response
 *  - Created a dynamic ADC conversion delay for the LTC2400 to wring out some idling time.
 *  - Changed to EEPROM.put & .get to also store floats. Eliminated the previous functions.
 *  - With the IIR filtering, there is no need for reduced decimals. Eliminated the code.
 *  - Added a calibration to a voltage reference to tweak the accuracy.
 *  
 *  - Version 3.10:
 *  - Added a dynamic filter weight algorithm to the IIR filter.
 *  
 *  - Version 3.11:
 *  - Changed the conversion delay to (re)start at the end of the LCD update cycle, such that the LTC
 *    has 165 mSec of "quiet" time on the power and data lines to do the sampling.
 *  - Fixed a compounded rounding error of the IIR filter calculation.
 *  - Added the filter weight exponent multiplier to the display.
 *  
 *  - Version 3.12:
 *  - Added some tweaks and changes to allow a precise calibration of the reference voltage. This
 *    largely determines the linearity of the meter. 
 * 
 *   Software version:
 */
String SW_VERSION = " Version 3.20";

/*
 * Battery indicator level:
 * - Analog pin A0 (via a 20K:10K divider) [10K is minimum value for the A2D]
 */
 
#include <SPI.h>                         // include SPI library (Serial Peripheral Interface)
#include <Wire.h>                        // include I2C library
#include <Adafruit_MCP23017.h>           // include I2C for MCP23017 port expander
#include <Adafruit_RGBLCDShield.h>       // include Adafruit RGB I2C Display
                                         // 
#include <EEPROM.h>                      // include EEPROM library

//---- initialize the i2c/LCD library
Adafruit_RGBLCDShield lcd = Adafruit_RGBLCDShield();

// These #defines make it easy to set the backlight color
#define RED 0x1
#define YELLOW 0x3
#define GREEN 0x2
#define TEAL 0x6
#define BLUE 0x4
#define VIOLET 0x5
#define WHITE 0x7

//---- LT 2400 ADC convertor
const int LTC_CS = 10;                   // set ADC chip select pin !CS (!SS) to Arduino pin D10
                                         // SPI SLCK is D13, SDO (MISO) is D12
long adcread;                            // reading from the ADC (LTC2400)
int ct = 165;                            // ADC converstion time is 160mS typical, +/-3.2 mS (data sheet)
unsigned long ct_start;                  // seed the conversion start timestamp
unsigned long ct_chk;                    // the entry timestamp, used to dynamically create the delay
float volt;                              // voltage reading from ADC

                                         // Following measurements * were made after a warm up
                                         // This is a critical measurement because it determines the linearity of the meter
                                         
float v_ref = 4.09674;                   // ADR4540B Reference Voltage measured with a 6.5 digit calibrated multimeter *

//---- IIR Low Pass Filtering with a dynamic filter weight algorithm
float average;                           // holds the result of the filter
int filterWeight = 32;                   // from 4..64 Higher numbers = heavier filtering
int fw_multiplier = 1;                   // multiplier for the filterWeight calculation
int noise_level = 96;                    // 96 is +/- 234 uV input voltage differential

//---- Zero offset Calibration
int CalSetup = 0;                        // calibration check
int DecPlaces = 0;                       // limit to the number of decimal places on display
long zero_cal;                           // calibration adjustment factor
long zc_address = 0L;                    // set EEPROM memory start address location 0..3
const int cal_adj_samples = 127;         // number of samples

//---- Calibration Factor
float cal_factor = 1.0;                  // calibration factor to adjust for the linearity errors
float cal_avg;                           // the resulting value of the filter
float cal_2_1v_ref = 2.04808;            // 2.048 V ref actual voltage to 5 places *
float cal_2_5v_ref = 2.50011;            // 2.500 V ref actual voltage to 5 places *
float cal_5v_ref = 5.00002;              // 5.000 V ref actual voltage to 5 places *
float cal_10v_ref = 9.99987;             // 10.00 V ref actual voltage to 5 places *
long cf_address = 5L;                    // EEPROM address 5..8

//---- Display the result to the LCD
String v;                                // string to hold the V, mV or uV string data for the display
String micro;                            // string to hold the real micro character
int dec_adj = 0;                         // decimal places adjustment [0..6], set manually by the button
int dec_digit = 6;                       // integer that holds number of decimal places displayed on LCD
int dTV = 5;                             // default is 10.00000 V for 10 Volt and above
int dV = 6;                              // default is 1.000000 V for 1-9 Volt range
int dmV = 4;                             // default is 100,0000 mV for MilliVolt range
int duV = 0;                             // default is 1000000 uV for MicroVolt range

//---- Arduino ADC Battery check
int batt = A0;                           // the 9V NiMH or NiCAD battery
int check_v = 0;                         // check the battery voltage
int loop_cnt = 365;                      // check it approx. every minute (loop time is 0.165 Sec)
float adc_cal = 0.0;                     // ADC calibration value
float adc_ref_volts = 5.14;              // external volt reference (VCC) -> measured
float adc_res = 1024;                    // 10 bit ADC resolution
const int adc_samples = 4;               // set number of sample readings to average the result

// Create a set of new symbols that can be displayed on the LCD
byte batt_full[8] =                      // full battery symbol, displayed at 9V
  {
  B01110,B11111,B11111,B11111,B11111,B11111,B11111,B11111
  };
byte batt_8_7[8] =                       // 8.7V level
  {
  B00000,B01110,B11111,B11111,B11111,B11111,B11111,B11111
  };
byte batt_8_3[8] =                       // 8.3V level
  {
  B00000,B00000,B01110,B11111,B11111,B11111,B11111,B11111
  };
byte batt_8_0[8] =                       // 8.0V level
  {
  B00000,B00000,B00000,B01110,B11111,B11111,B11111,B11111
  };
byte batt_7_7[8] =                       // 7.7V level
  {
  B00000,B00000,B00000,B00000,B01110,B11111,B11111,B11111
  };
byte batt_7_5[8] =                       // 7.5V level
  {
  B00000,B00000,B00000,B00000,B00000,B01110,B11111,B11111
  };
byte batt_empty[8] =                     // <7.3V Empty
  {
  B00100,B00100,B00100,B00000,B00100,B00000,B01110,B11111
  };
byte batt_charging[8] =                  // >10V Charging symbol
  {
  B00010,B00100,B01000,B11111,B00010,B10100,B11000,B11100
  };


// ******** Initialization routine, runs only at start or reboot ********
void setup() {
  // Serial.begin(9600); // ==>> activate for debug and test only

  micro = char(228);                     // real micro symbol is char(b11100100)

  pinMode (LTC_CS, OUTPUT);              // set LTC_CS (pin D10 on Arduino Nano) to OUTPUT mode
  digitalWrite(LTC_CS, HIGH);            // set LCT2400 chip select pin HIGH to disable
                                         // initialize digital pin LED_BUILTIN as an output.
  SPI.begin();                           // initialise SPI bus
  SPI.setBitOrder(MSBFIRST);             // Sets the order of bits shifted out and in to SPI bus, MSBFIRST (most-significant bit first)
  SPI.setDataMode(SPI_MODE0);            // set SPI to Mode 0 (MOSI read on rising edge (CPLI=0) and SCK idle low (CPOL=0))
  SPI.setClockDivider(SPI_CLOCK_DIV16);  // divide Arduino clock by 16 to gave a 1 MHz SPI clock

  lcd.begin(16, 2);                      // set up the LCD's number of columns and rows
  lcd.setBacklight(GREEN);               // set LCD backlight color to Green
  lcd.setCursor(0,0);                    // set LCD cursor to column 0, row O (start of first line)
  lcd.print("Millivolt Meter ");
  lcd.setCursor(0,1);
  lcd.print(SW_VERSION);                 // print software version to display
  delay(2500);
  lcd.clear();                           // clear dislay
  lcd.setCursor(0,0);                    // set LCD cursor to column 0, row O (start of first line)

//  ==>> only for testing!
/*
  zero_cal = 0;                          // Start with a clean slate
  Serial.println(zero_cal);
  EEPROM.put(zc_address,zero_cal);       // store calibration factor in EEPROM
  EEPROM.get(zc_address,zero_cal);       // retrieve calibration factor in EEPROM 
  Serial.println(zero_cal); 
  cal_factor = 1.0;
  Serial.println(cal_factor, 6); 
  EEPROM.put(cf_address,cal_factor);     // store calibration factor in EEPROM
  EEPROM.get(cf_address,cal_factor);
  Serial.println(cal_factor, 6);
*/

  EEPROM.get(zc_address, zero_cal);      // get the zero cal factor from EEPROM
  EEPROM.get(cf_address, cal_factor);    // get the cal factor from EEPROM

  lcd.clear();                           // clear dislay
  lcd.setBacklight(YELLOW);              // set LCD backlight color to Yellow
  lcd.setCursor(0,0);                    // set LCD cursor to column 0, row O (start of first line)
  lcd.print("Zero Cal Factor:");
  lcd.setCursor(0,1);
  lcd.print(zero_cal);                   // Briefly show calibration factor stored in EEPROM at switch on
  delay(2000);
  lcd.clear();                           // clear dislay
  lcd.setCursor(0,0);                    // set LCD cursor to column 0, row O (start of first line)
  lcd.print("V-Cal Factor:");
  lcd.setCursor(0,1);
  lcd.print(cal_factor, 6);              // Briefly show calibration factor stored in EEPROM at switch on
  delay(2000);
  lcd.clear();
  lcd.setBacklight(GREEN);               // set LCD backlight color to Green
  lcd.setCursor(0,0);                    // set LCD cursor to column 0, row O (start of first line)
  lcd.print("Millivolt Meter  ");        // print Millivolt Meter to display and clear the rest
  
  // create a set of special symbols from the battery monitor definitions above
  lcd.createChar (0, batt_full);         // >9V
  lcd.createChar (1, batt_8_7);          // 8.7V
  lcd.createChar (2, batt_8_3);          // 8.3V
  lcd.createChar (3, batt_8_0);          // 8.0V
  lcd.createChar (4, batt_7_7);          // 7.7V
  lcd.createChar (5, batt_7_5);          // 7.5V
  lcd.createChar (6, batt_empty);        // <7.3V measurements can be inaccurate!
  lcd.createChar (7, batt_charging);     // > 10V charging and connected to wall-wart
  analogReference(DEFAULT);              // not needed here, use the external or default (=internal) A2D reference

  Monitor_batt();                        // get the battery level and show it on the display
  
  ct_start = millis();                   // seed the conversion start timestamp for the LTC2400
  for (int i=0;i<5;i++) {                // disregard the first five readings as they seem unstable
    average = Spi_Read();                // and also seed the IIR filter
  }
}


/**************************************************************************************
 * Routine to read the data from the LTC2400 A2D convertor through the SPI interface
 */
long Spi_Read(void){                     // SPI(Serial Peripheral Interface) read sub-routine to read data form the LTC2400 ADC
                                         // and transfer 8 bits (1 byte) at a time - total of 4 bytes.
  long result = 0L;                      // result represents rolling total of the bytes transferred
  long b;                                // b is result of reading ADC output bytes

  //calculate the minimum conversion delay dynamically
  ct_chk = millis();
  unsigned int ct_delay = ct_chk - ct_start; // use the time already spent and factor that in
  if (ct_delay < ct){
    delay(ct - ct_delay);                // use the adjusted conversion delay if needed
  }
  
  digitalWrite(LTC_CS, LOW);             // LTC2400 chip select pin taken low to wake up the ADC and enable the SDO (MOSI) output
  delayMicroseconds(1);                  // timing delay but is not really required

  if (!(PINB & (1 << 4))) {              // check for a low !EOC on the MOSI pin D12, if the ADC is ready to transmit new data
                                         // if not, try again later -> this will reduce the number of readings to average
    b = SPI.transfer(0xff);              // transfer first byte most significant bits first.
    b &= 0x0f;                           // discard first 4 status bits (bits 31 to 25) mask received data with binary 00001111
    result = b;                          // result after removing first 4 bits (replacing them with 0's)
    result <<= 8;                        // shift first byte left by 8 places
    b = SPI.transfer(0xff);              // transfer second byte most significant bits first.
    result |= b;                         // add second byte to first byte by using the OR function (now 12 bits)
    result = result << 8;                // shift result left by 8 places
    b = SPI.transfer(0xff);              // transfer third byte most significant bits first.
    result |= b;                         // add third byte to result by using the OR function (now 20 bits)
    result = result << 8;                // shift result left by 8 places
    b = SPI.transfer(0xff);              // transfer fourth byte most significant bits first.
    result |= b;                         // add fourth byte to result by using the OR function (now 28 bits)
    result = result >> 4;                // get rid of the 4 LSB bits, they don't add any value in this application
  
    digitalWrite(LTC_CS, HIGH);          // LTC2400 chip enters low power (sleep mode) and disables the ADC output.
    ct_start = millis();                 // start the conversion delay timer (restarted at the end of the main loop)
    return(result);                      // return with result as the 24 bit data representing the voltage
  }
}


/**************************************************************************************
 * Routine to run a zero offset calibration.
 * We'll calculate the input offset by manually shortening the input.
 */
void Zero_Cal_Adjust() {
  lcd.clear();
  lcd.setBacklight(RED);                // set LCD backlight to Red
  lcd.setCursor(0,0);
  lcd.print("=Zero Calibrate");
  lcd.setCursor(0,1);
  lcd.print("Short input");
  delay(3000);

  int cal_counter = cal_adj_samples;    // total number of readings
  average = Spi_Read();                 // seed the IIR filter

  for (int i=0; i < cal_adj_samples; i++) {  // create a moving average with an IIR filter
    average = average + (Spi_Read() - average) / 64; // maximum filterweight
    // show the progress
    lcd.setCursor(14,1);
    if (cal_counter < 10){              // get rid of the first decimal number               
      lcd.print(" ");
    }
    lcd.print(cal_counter);
    cal_counter--;
  }
  zero_cal = round(average);            // round to the next integer value
  EEPROM.put(zc_address, zero_cal);     // store binary offset calibration factor in EEPROM
  
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("Zero Cal Adjust:");
  lcd.setCursor(0,1);
  lcd.print(zero_cal);
  delay(3000);
  lcd.clear();
  lcd.setBacklight(GREEN);               // set LCD backlight to Green
  lcd.setCursor(0,0);
  lcd.print("Millivolt Meter  ");        // print Millivolt Meter to display and clear the rest
  DecPlaces = 0;                         // if decimal places got changed, reset it
  Monitor_batt();                        // the batt level display got erased, get it back 
}


 /**************************************************************************************
 * Routine to run the voltage calibration against a voltage reference.
 * The calibration factor is creatad by connecting a 2.500 reference 
 * to the input. This will enhance the linearity of the conversion.
 */
void Ref_Cal_Adjust2() {
  lcd.clear();
  lcd.setBacklight(BLUE);                  // set LCD backlight to Blue
  lcd.setCursor(0,0);
  lcd.print("=2.5V-Ref Cal");
  lcd.setCursor(0,1);
  lcd.print("Connect V-Ref");
  delay(3000);

  int cal_counter = cal_adj_samples;        // total number of readings
  cal_avg = Spi_Read();                     // seed the IIR filter

  for (int i=0; i < cal_adj_samples; i++) { // create a moving average with an IIR filter
    cal_avg = cal_avg + (Spi_Read() - cal_avg) / 64;  // maximum filter weight
    // show the progress
    lcd.setCursor(14,1);
    if (cal_counter < 10){                  // get rid of the first decimal number               
      lcd.print(" ");
    }
    lcd.print(cal_counter);
    cal_counter--;
  }
  // convert filtered result to volt and include the reference, the input divider
  // and the zero cal. The zero cal must be done before the reference cal.
  float ref_volt = (cal_avg - zero_cal) * v_ref / 16777216 * 10; 
  cal_factor = (cal_2_5v_ref - ref_volt);  // absolute difference
  cal_factor = 1 + (cal_2_5v_ref - ref_volt)/cal_2_5v_ref; // multiplication cal factor per volt
  EEPROM.put(cf_address, cal_factor);    // store the calibration factor in EEPROM
  
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("VRef Cal Factor:");
  lcd.setCursor(0,1);
  lcd.print(cal_factor, 6);
  delay(3000);
  lcd.clear();
  lcd.setBacklight(GREEN);               // set LCD backlight to Green
  lcd.setCursor(0,0);
  lcd.print("Millivolt Meter  ");        // print Millivolt Meter to display and clear the rest
  DecPlaces = 0;                         // if decimal places got changed, reset it
  Monitor_batt();                        // the batt level display got erased, put it back
}


 /**************************************************************************************
 * Routine to run the voltage calibration against a voltage reference.
 * The calibration factor is creatad by connecting a 5.0V reference 
 * to the input. This will enhance the linearity of the conversion.
 */
void Ref_Cal_Adjust5() {
  lcd.clear();
  lcd.setBacklight(BLUE);                  // set LCD backlight to Blue
  lcd.setCursor(0,0);
  lcd.print("=5.0V-Ref Cal");
  lcd.setCursor(0,1);
  lcd.print("Connect V-Ref");
  delay(3000);

  int cal_counter = cal_adj_samples;        // total number of readings
  cal_avg = Spi_Read();                     // seed the IIR filter

  for (int i=0; i < cal_adj_samples; i++) { //create a moving average with an IIR filter
    cal_avg = cal_avg + (Spi_Read() - cal_avg) / 64;  // maximum filter weight
    // show the progress
    lcd.setCursor(14,1);
    if (cal_counter < 10){                  // get rid of the first decimal number               
      lcd.print(" ");
    }
    lcd.print(cal_counter);
    cal_counter--;
  }
  // convert filtered result to volt and include the reference, the input divider
  // and the zero cal. The zero cal must be done before the reference cal.
  float ref_volt = (cal_avg - zero_cal) * v_ref / 16777216 * 10; 
  cal_factor = (cal_5v_ref - ref_volt);  // absolute difference
  cal_factor = 1 + (cal_5v_ref - ref_volt)/cal_5v_ref; // multiplication cal factor per volt
  EEPROM.put(cf_address, cal_factor);    // store the calibration factor in EEPROM
  
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("VRef Cal Factor:");
  lcd.setCursor(0,1);
  lcd.print(cal_factor, 6);
  delay(3000);
  lcd.clear();
  lcd.setBacklight(GREEN);               // set LCD backlight to Green
  lcd.setCursor(0,0);
  lcd.print("Millivolt Meter  ");        // print Millivolt Meter to display and clear the rest
  DecPlaces = 0;                         // if decimal places got changed, reset it
  Monitor_batt();                        // the batt level display got erased, put it back
}

 /**************************************************************************************
 * Routine to run the voltage calibration against a voltage reference.
 * The calibration factor is creatad by connecting a 2.048V reference 
 * to the input. This will enhance the linearity of the conversion.
 * This cal was 7.5V but since I have a 2.048 reference it was changed. 
 */
void Ref_Cal_Adjust2_1() {
  lcd.clear();
  lcd.setBacklight(BLUE);                  // set LCD backlight to Blue
  lcd.setCursor(0,0);
  lcd.print("=2.048V-Ref Cal");
  lcd.setCursor(0,1);
  lcd.print("Connect V-Ref");
  delay(3000);

  int cal_counter = cal_adj_samples;        // total number of readings
  cal_avg = Spi_Read();                     // seed the IIR filter

  for (int i=0; i < cal_adj_samples; i++) { // create a moving average with an IIR filter
    cal_avg = cal_avg + (Spi_Read() - cal_avg) / 64;  // maximum filter weight
    // show the progress
    lcd.setCursor(14,1);
    if (cal_counter < 10){                  // get rid of the first decimal number               
      lcd.print(" ");
    }
    lcd.print(cal_counter);
    cal_counter--;
  }
  // convert filtered result to volt and include the reference, the input divider
  // and the zero cal. The zero cal must be done before the reference cal.
  float ref_volt = (cal_avg - zero_cal) * v_ref / 16777216 * 10; 
  cal_factor = (cal_2_1v_ref - ref_volt);  // absolute difference
  cal_factor = 1 + (cal_2_1v_ref - ref_volt)/cal_2_1v_ref; // multiplication cal factor per volt
  EEPROM.put(cf_address, cal_factor);    // store the calibration factor in EEPROM
  
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("VRef Cal Factor:");
  lcd.setCursor(0,1);
  lcd.print(cal_factor, 6);
  delay(3000);
  lcd.clear();
  lcd.setBacklight(GREEN);               // set LCD backlight to Green
  lcd.setCursor(0,0);
  lcd.print("Millivolt Meter  ");        // print Millivolt Meter to display and clear the rest
  DecPlaces = 0;                         // if decimal places got changed, reset it
  Monitor_batt();                        // the batt level display got erased, put it back
}

 /**************************************************************************************
 * Routine to run the voltage calibration against a voltage reference.
 * The calibration factor is creatad by connecting a 10.000V reference 
 * to the input. This will enhance the linearity of the conversion.
 */
void Ref_Cal_Adjust10() {
  lcd.clear();
  lcd.setBacklight(BLUE);                 // set LCD backlight to Blue
  lcd.setCursor(0,0);
  lcd.print("=10.0V-Ref Cal");
  lcd.setCursor(0,1);
  lcd.print("Connect V-Ref");
  delay(3000);

  int cal_counter = cal_adj_samples;        // total number of readings
  cal_avg = Spi_Read();                     // seed the IIR filter

  for (int i=0; i < cal_adj_samples; i++) { //create a moving average with an IIR filter
    cal_avg = cal_avg + (Spi_Read() - cal_avg) / 64;  // maximum filter weight
    // show the progress
    lcd.setCursor(14,1);
    if (cal_counter < 10){                  //get rid of the first decimal number               
      lcd.print(" ");
    }
    lcd.print(cal_counter);
    cal_counter--;
  }
  // convert filtered result to volt and include the reference, the input divider
  // and the zero cal. The zero cal must be done before the reference cal.
  float ref_volt = (cal_avg - zero_cal) * v_ref / 16777216 * 10; 
  cal_factor = (cal_10v_ref - ref_volt);  // absolute difference
  cal_factor = 1 + (cal_10v_ref - ref_volt)/cal_10v_ref; // multiplication cal factor per volt
  EEPROM.put(cf_address, cal_factor);    // store the calibration factor in EEPROM
  
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("VRef Cal Factor:");
  lcd.setCursor(0,1);
  lcd.print(cal_factor, 6);
  delay(3000);
  lcd.clear();
  lcd.setBacklight(GREEN);               // set LCD backlight to Green
  lcd.setCursor(0,0);
  lcd.print("Millivolt Meter  ");        // print Millivolt Meter to display and clear the rest
  DecPlaces = 0;                         // if decimal places got changed, reset it
  Monitor_batt();                        // the batt level display got erased, put it back
}


/**************************************************************************************
 * routine to check if the button was pressed, and depending on the length, decide what action to take
 */
void Button_press() {
   uint8_t buttons = lcd.readButtons();
                                          // Buttons availible on I2C LCD header: A0=SELECT, A1=RIGHT,
      if (buttons) {                      //  A2=DOWN, A3=UP, A4=LEFT - Positions: GND,A0,A1,A2,A3,A4
        
        lcd.setCursor(15, 1);             // placeholder for the button press ack
        lcd.print("0");                   // back to default

        if (buttons & BUTTON_SELECT) {    // 0.000 V Cal Input Shorted Position A0
          Zero_Cal_Adjust();
        }
        if (buttons & BUTTON_RIGHT) {     // 2.500 V Cal Position A1
          Ref_Cal_Adjust2();
        }
        if (buttons & BUTTON_DOWN) {      // 5.000 V Cal Position A2
          Ref_Cal_Adjust5();
        }
        if (buttons & BUTTON_UP) {        // 10.000 V Cal Position A3
          Ref_Cal_Adjust10();
        }
        if (buttons & BUTTON_LEFT) {      // Extra Button Position A4 - Not Used
          Zero_Cal_Adjust(); 
        }
      }  
}


/**************************************************************************************
 * The battery level monitor routine
 */
void Monitor_batt() {
  int i;
  long sum = 0;
  int sensorValue; 
  for (i=0; i<(adc_samples); i++) {
    sensorValue = analogRead(batt) + adc_cal; // read from A0 and add the calibration factor
    delay(100);
    sum += sensorValue;
  }
  sum = sum / adc_samples;

  float batt_voltage = sum;
  // Convert the analog reading (which goes from 0 - 1023) to a voltage 0 - 12V:
 
  // batt_voltage = (sensorValue * (adc_ref_volts / adc_res) * 3) + adc_cal;
  batt_voltage = 9.5;  // ****** UN-COMMENT line above, and COMMENT out this line to use BATTERY MONITOR ******
  
  /*  note that during the test with a potmeter connected to 5V, the resulting maximum volt level is just below
   *  20V, if you connect to a PC with a USB cable for power.
   *  With the power coming from the 78L12, the maximum level will be 20V, unless you create a voltage divider to
   *  limit the maximum voltage coming from the potmeter to 3V, which is equal to the 20K:10K divider to the cell.
   */

  // print out the "battery" voltage level at the right-hand fields of the first line
  lcd.setCursor(15, 0);                  // start of the batt level field (line 1, last position)

  if (batt_voltage < 7.3) {              // critical batt level for the reference and the ADC is < 7.3V
    lcd.print(char(6));                  // battery is empty!
    } else if (batt_voltage < 7.7 && batt_voltage > 7.3){  // batt getting too low, connect to mains or stop
      lcd.print(char(5));                // stop measuring
    } else if (batt_voltage < 8.0 && batt_voltage > 7.7){  // batt charge is getting critical
      lcd.print(char(4));                //
    } else if (batt_voltage < 8.3 && batt_voltage > 8.0){  // batt charge is OK
      lcd.print(char(3));                //
    } else if (batt_voltage < 8.7 && batt_voltage > 8.3){  // batt charge is OK
      lcd.print(char(2));                //
    } else if (batt_voltage < 8.9 && batt_voltage > 8.7){  // batt charge is OK
      lcd.print(char(1));                //
    } else if (batt_voltage < 10 && batt_voltage > 8.9){   // batt is full
      lcd.print(char(0));
    } else if (batt_voltage > 10){       // batt is charging
      lcd.print(char(7));                // charging
    }
}


/**************************************************************************************
 * The main routine
 */
void loop() {
  
  // The minimum (also normal) looptime is 166 mSec, determined by the aquisition delay of the LTC2400
  
  Button_press();                        // check if the button was pressed

  // Check the battery level approx. every minute
  if (check_v > loop_cnt) {
      check_v = 0;
      Monitor_batt();                    // checking the battery level takes about 810 mSec
  } else {
    check_v ++;
  }

  // Take a new raw LTC2400 reading
  adcread = Spi_Read();
  
  // Check if the new reading is outside the noise level band of the filtered result
  // and dynamically adjust the filter weight accordingly.
  if ((adcread > average + noise_level)||(adcread < average - noise_level)){
                                        // reading is outside the noise band
    fw_multiplier--;                    // scale the filterWeight down with powers of 2
    if (fw_multiplier < 1){             // lower limit to 2<<1 = 4
      fw_multiplier = 1;                // bottom-out
      average = adcread;                // and reset the filter
      } 
  }else{                                // the reading is inside the noise band
      fw_multiplier++;                  // scale the filterWeight up with powers of 2
      if (fw_multiplier > 6){           // upper limit is 2<<5 = 128 
          fw_multiplier = 6;
      }
  }
  // update the filter weight; ranges from 4..128
  filterWeight = 2 << fw_multiplier;

  /*  Run the quisition through the IIR filter and take the new reading with a
   *  grain of filterWeight salt. ie. divide the new reading by the filterWeight factor (from 4..64)
   *  Note: average must be a float, otherwise there will be a compounded rounding error in the result.
   */
  average = average + (adcread - average) / filterWeight;

  /* 
   *  Convert the filtered result to volts.
   *  16777216 = 2^24, the maximum number with 24 bits.
   *  Multiply by 10 because of the voltage divider at the inputConvert the filtered result to volts.
   *  Subtract the zero calibration factor and apply the cal_factor.
   *  The cal_factor is calculated against a known voltage reference.
   *  Cast the result to a floating point variable to get decimals.
   */
  volt = (average - zero_cal) * v_ref / 16777216 * 10 * cal_factor;
  
  // prepare for the display of the data
  
  // ==>> for debugging & Testing, to be analyzed with MS-Excel:
  /*
  Serial.print(adcread);
  Serial.print("\t");
  Serial.print(average);
  Serial.print("\t");
  Serial.println(filterWeight);
  */
  //  ==>> for testing purposes only, read and display the 9V cell instead
  //  volt = (analogRead(A0) * (adc_ref_volts / adc_res) * 2) + adc_cal;

  if (volt <0.001) {                     // check if voltage reading is below 1 milli-Volt    
    volt = volt * 1000000;               // if so multiply reading by 1.000.000 and display as micro-Volt
    v = micro + "V  " ;                  // use uV on display after voltage reading
    dec_digit = duV;                     // set display to 0 decimal places (1000000 uV)
  
  } else if (volt < 1){                  // check if voltage reading is below 1 volt
    volt = volt * 1000;                  // if below 1 volt multiply by 1.000 and display as Millivolt
    v = "mV  ";                          // use mV on display after voltage reading
    dec_digit = dmV;                     // set display to 4 decimal places (100.0000 mV)

  } else if (volt < 10){                 // check if voltage reading is below 10 volt
    v = "V  ";                           // use V on display after voltage reading
    dec_digit = dV;                      // set display to 6 decimal places (1.000000 V)
   
  } else {                               // volt is > 10V
    v = "V  ";                           // if 10 volt or higher use letter V on display after voltage reading
    dec_digit = dTV;                     // set display to 5 decimal places (10.00000 V)
  }

  lcd.setBacklight(GREEN);               // set LCD backlight to Green
  lcd.setCursor(0, 1);                   // set LCD cursor to Column 0 and Row 1 (second row of LCD, first column)
  lcd.print(volt, dec_digit);            // print voltage as floating number with x decimal places
  lcd.print(" ");                        // add one blank space after voltage reading
  lcd.print(v);                          // print either uV, mV or V to LCD display
  lcd.setCursor(12,1);
  lcd.print("fw=");
  lcd.setCursor(15,1);
  lcd.print(fw_multiplier);              // show the filterweight exponent multiplier
  
  ct_start = millis();                   // After this noisy display intermezzo, reset the LTC2400 conversion time counter
                                         // such that the ADC has the full 165 mSec to sample a new acquisition
                                         // during a quiet period.
}

 

Meter running version 3.20 software with updated code by Paul Versteeg
Millivolt Meter run­ning ver­sion 3.20 soft­ware with updat­ed code by Paul Versteeg

 

66 Replies to “Millivolt Meter”

  1. This looks like an amaz­ing project; great work, to both you and Scullcom.

    Would you con­sid­er putting togeth­er these parts into a kit (con­tain­ing a PCB, com­po­nents, enclo­sure, pan­el)? I’d read­i­ly buy one or two.

    1. Hi Simon,
      Currently with my work com­mit­ments, I am unable to put togeth­er a kit for sale. I some­times have an extra bare board that I can sell though. I will try to look into some kits lat­er this Winter.

      Thanks,
      Barbouri

  2. Hi Barbouri,

    I already post­ed a sim­i­lar request with one of your oth­er designs with the same top­ic. Do you have a BOM to share for this project?

    Many thanks in advance,

    Paul

  3. I also tried to get to a BOM or this project, by extract­ing the part­slist from Eagle.

    I noticed a dis­crep­an­cy between the board lay­out (that has R5) and the schemat­ic (that does not), although both car­ry the cor­rect ver­sion num­ber. Maybe you can update the schematic?

    Here is my attempt to cre­ate a BOM, I hope you can either cor­rect on com­ment on my pro­pos­al partnumbers:
    Part Value Package Supplier Partnumber

    C1 1000uF/25V CPOL-RADIAL-1000UF-25V 5mm UVR1E102MPD
    C2 100uF/25V CPOL-RADIAL-100UF-25V lead spac­ing 2mm? UVK1E101MDD
    C3 0.1uF MLCC 594-K104K20X7RH5TL2
    C4 3.3uF/25V tan­ta­lum “TAP335K010SCS or TAP335K010SRW or T356A335K010AS


    C5 0.1uF MLCC 594-K104K20X7RH5TL2
    C6 0.1uF MLCC 594-K104K20X7RH5TL2
    C7 0.1uf 805-CAP SMD 08055G104ZAT2A
    C8 3.3uF/25V tan­ta­lum “TAP335K010SCS or TAP335K010SRW or T356A335K010AS


    C9 10uF/25V tan­ta­lum TAP106K025SCS
    C10 220pF 0402-CAP SMDC0402X5R1C221K020BC or C0402X7R1A221K020BC

    C11 0.1uF MLCC 594-K104K20X7RH5TL2
    C12 0.1uf MLCC 594-K104K20X7RH5TL2
    IC1 7805T (1Amp) TO220H LM7805CT or NCP7805TG
    IC1 78L05 (100mA) TO92 can be used alter­na­tive­ly LM78L05ACZ
    R3 10K 0603-RES SMD AC0603JR-0710KL
    R4 10K 0603-RES SMD AC0603JR-0710KL
    R5 10K 0603-RES SMD AC0603JR-0710KL
    RN1 1776-C6815 Caddock net­work resis­tor 1776-C6815
    U$1 ARDUINO_PRO_MINI ARDUINO_PRO_MINI Sparkfun Arduino Pro Mini 328 — 5V/16MHz
    U1 LTC2400IS8 SOIC127P600X175-8N DigiKey LTC2400IS8#PBF
    U2 AD8628 SOIC127P600X175-8N AD8628ARZ
    U3 ADR4540B SOIC127P600X175-8N ADR4540BRZ

    Many thanks in advance,
    Paul

    1. Hi Paul,
      Here is a link to my Digikey shared parts list.
      http://www.digikey.com/short/393771
      R5 was only used in the V1.5 board as an extra pull-up for the reset pin.
      In V1.51 and above it is not used as there is already a pull-up includ­ed on the pro-mini board.
      Most of the pic­tures in the post show the V1.50 pro­to­type board, and do not include the impor­tant C12 capacitor.
      Thanks,
      Barbouri

  4. I for­got, sorry.
    There is anoth­er dis­crep­an­cy between the cir­cuit dia­gram, the silkscreen and pic­ture of your stuffed board.
    You have installed a 1000uF/25V capac­i­tor for C1, and a 100uF/25V capac­i­tor for C2.

    I also added a 78L05 TO-92 (100mA) on the BOM because that can be used instead of the 7805 TO-220 (1A), right?

    Do you have a source or infor­ma­tion (thick­ness, lay­out) for the cop­per(?) shield you built?

    Sincerely,
    Paul

    1. Hi Paul,
      I used the 1000 / 100 uF capac­i­tors (C1 / C2) because that is what I had in stock, but the designed capac­i­tor val­ues 220 / 10 uF will work great. I had rec­om­mend­ed a 150 mA reg­u­la­tor in the post, but the 100 mA one should work as long as an effi­cient dis­play is used.

      Barbouri

  5. I worked with the hot­line from DigiKey and I now have access to the list. I will work on that and see if I can coor­di­nate what I’m doing with Louis Scully, he is hope­ful­ly doing the same thing as he did with the mill-ohm meter. Maybe I can help.

    Thanks,
    Paul

  6. Hey Greg, I final­ly fin­ished my built of the volt­meters, I actu­al­ly built two. I believe that there are some inter­est­ing enhance­ments to the design, so I here is a link to my blog that details it all.

    I must say that I am very impressed with the accu­ra­cy of the meter, they are much bet­ter than I antic­i­pat­ed and a very wel­come addi­tion to my toolk­it. Thank you again for doing this all!

    Paul

    http://www.paulvdiyblogs.net/2016/09/building-6-digit-digital-milli-voltmeter.html

    1. Hi Paul, I have been fol­low­ing your blog since your ear­li­er post.
      Your meters look great. You might be inter­est­ed in the cal­i­bra­tion code used in the Programmable Voltage Reference project as it might be used as a basis for set­ting up a mul­ti-point cal­i­bra­tion scheme for the Millivolt Meter.
      Greg (Barbouri)

      1. Thanks for point­ing this out. I saw it ear­li­er when I looked at some of your oth­er projects. As a mat­ter of fact, I had been con­tem­plat­ing doing some­thing sim­i­lar for the volt­meter as soon as I saw the lin­ear­i­ty deviation.

        Compensating for lin­ear­i­ty devi­a­tions and the likes is a tech­nique I read about some time ago, but I have nev­er used it myself. I will need to do some soul and Google search­ing to get my arms around it, for now I have a few too many oth­er projects to fin­ish. One of which is the mil­li-ohm meter. The PCB is done and works great, I now need to order some more parts to fit it in the hous­ing I have in mind. When done I’ll report back on that project too.

        Thank you for your contributions!
        Paul

  7. Greg,

    There is a nasty spike intro­duced to the main 5V and also the 4.096 ref­er­ence volt­age, which is caused by the switch­ing of D10, the LTC_CS sig­nal. I used a 4n7 decou­pling capac­i­tor on the Arduino PCB, but you may want to con­sid­er adding some room on the main PCB when you do anoth­er turn. Look at my blog for more details.

  8. Greg, I think we may have a poten­tial bug in the LTC code that will com­pro­mise the aver­ag­ing of the mul­ti­ple readings.

    I assume that Louis got his orig­i­nal code from Martin Nawrath, (Academy of Media Arts Cologne, Germany) it looks a lot like that, but Martin uses no aver­ag­ing in his code.

    In the main loop, we use a for-next loop that incre­ments the num­ber of sam­ples we get from the Spi_Read func­tion and then aver­age the result by div­ing the result with the num­ber of samples.

    However, in the Spi_Read func­tion, we skip the com­plete read­ing of the ADC if it is not ready.
    Here:
    if (!(PINB & (1 « 4))) {

    }
    This means that the aver­ag­ing can be off. If I’m right, the Spi_Read func­tion should have a flag to sig­nal a cor­rect read­ing back to the main loop.

    I will put a log­ic ana­lyz­er on the pins to see if this is hap­pen­ing, and how often.

    What is your take on this?

    Regards,

    Paul

  9. I could not find instances of this hap­pen­ing, but I did fix the code. I also made some mea­sure­ments with my Logic Analyzer and put that on my blog also. (address some­where in anoth­er post)

  10. hi Paul did­n’t see my ques­tion? I think I get the answer : this is a DC mil­li­volt meter since there is a ref­er­ence volt­age ‚baut I think we can turn this device into AC mil­li­volt­meter and make it more use­full in elec­tron­ics . do you agree?

  11. Paul,
    Ik heb een bor­d­je gekocht op Ebay van een Finse aanbieder.
    Daar heb ik na veel exper­i­menteren goede resul­tat­en bereikt.
    De ingangss­pan­nings del­er is direct op de ingang van de LTC2410 aangesloten.
    Er is dus geen buffer­ver­sterk­er aan de ingang toegepast.
    Het bor­d­je heeft de vol­gende specificaties;
    (I bought a board on Ebay from a Finnish provider.
    I’ve achieved good results after a lot of experimenting.
    The input volt­age divider is direct­ly con­nect­ed to the input of the LTC2410.
    Thus, there is no buffer ampli­fi­er applied to the input.
    The board has the fol­low­ing specifications;)GoogleTranslate

    24bit LTC2410 ADC Module designed for Arduino or oth­er embed­ded sys­tems to acheive high res­o­lu­tion analoge to dig­i­tal con­ver­sion. The mod­ule use LTC2410 24bit ADC IC and MAX6126 2.048 volt ultra high pre­ci­sion, ultra low noise reference.
    A p‑p noise of 3uV can be achieved when mea­sur­ing the mod­ule Voltage ref­er­ence with a volt­age divider using a USB pow­er source.
    LTC2410 24bit ful­ly dif­fer­en­tial Analog to dig­i­tal converter
    2 ppm INL, No miss­ing code
    2.5 ppm Full Scale Error
    0.1 ppm Offset
    0.16 ppm Noise
    Maxim MAX6126 ultra high pre­ci­sion, ultra low noise reference.
    Reference volt­age out­put 2.048 volt
    ±0.02% accuracy
    Ultra low 3ppm/C max tem­per­a­ture coefficient
    Ultra Low 1.3uVp.p noise
    High sta­bil­i­ty Voltage divider for volt­ages up to MAX 32 volt.
    Vishay UXB0207 1Mohm 0.1% 5ppm
    Vishay UXB0207 33K2 0.1% 2ppm
    Measurement max­i­mum voltage
    Voltage V1 – ½ Vref (-1.024 volt) to ½ Vref (+1.024 volt)
    Voltage V2 – Maximum 32 volt. (resis­tor divider,

    Na enige aan­passin­gen aan het bor­d­je (koel­ing van IC’s) en de soft­ware, heb ik de vol­gende resul­tat­en bereikt.
    (After some adjust­ments to the sign (cool­ing IC) and soft­ware, I have achieved the fol­low­ing results.)GoogleTranslate

    Calibratie mod­ule Gemeten LTC2410 Deviatie
    2,501144 2,501142 ‑2,0 uV
    5,00241 5,002386 ‑24,0 uV
    7,50235 7,502346 ‑4,0 uV
    10,00313 10,003146 16,0 uV

    Het enige prob­leem wat ik nog heb is dat de off­set nog wat zwab­bert. Ongeveer zo’n 15 uVolt. (Loopt langza­am op en neer — ongeveer een paar minuten).
    Bedankt voor je inbreng op het ontwerp van SCULLYCOM.
    Ik ga ook dit ontwerp bouwen en heb inmid­dels de print en onderde­len besteld.
    (The only prob­lem I still have is that the off­set some wob­bles. Approximately about 15 uVolt. (Runs slow­ly up and down — about a cou­ple of minutes).
    Thanks for your input on the design of SCULLYCOM.
    I’m going to build this design and have already ordered the PCB and components.)GoogleTranslate

  12. I’ve been look­ing over the design for this board, and one thing trou­bles me a lit­tle. The AD8628 input op-amp does­n’t have a neg­a­tive sup­ply input, it’s only using dig­i­tal ground. This means the input can’t quite go down to true 0V, because the out­put of that chip is lim­it­ed to around 1mV above its V- sup­ply rail. Given the 10:1 divider net­work on the input, that lim­its it to an input volt­age of 10mV.

    Would this be improved if the input op-amp had a neg­a­tive sup­ply rail too — e.g. a lit­tle charge pump chip like the ICL7660 to pro­vide it some­thing below-0V to give it true to-ground headroom?

    1. Paul,
      The input of the AD8628 can go down 0V, and even down to ‑0.3 volts. It is the out­put which is lim­it­ed, even being a rail to rail op amp. The AD8628 is also tied to the ana­log ground sec­tion, not the dig­i­tal ground. So the input divider has no lim­it­ing effect on the input voltage.
      I have done some tests with input volt­ages in the 1 to 10 mV range with good track­ing. I do see a lot more noise at these low­er mV ranges, but for me this is acceptable.
      The cir­cuit would ben­e­fit from a +/- pow­er sup­ply, but would be lim­it­ed to 6 volts total for the AD8628.

      If some­one is inter­est­ed in devel­op­ing a work­ing pro­to­type using a dual sup­ply, I would be inter­est­ed in putting togeth­er a final cir­cuit board for it.
      Greg (Barbouri)

      1. I’m intend­ing to build mine in two halves, with a total­ly dif­fer­ent dig­i­tal sec­tion on a sep­a­rate board, con­nect­ed to your board con­tain­ing the ADC via an iso­la­tor chip (I have a spare ADU1402 from a dif­fer­ent project). This dis­con­nects the dig­i­tal (dis­play, but­tons, ser­i­al port?) half from the mea­sure­ment fron­tend. An iso­lat­ed DC-DC con­vert­er will pow­er it, let­ting the mea­sure­ment half float inde­pen­dent­ly of bench pow­er — I’m not intend­ing to use bat­ter­ies in mine.

        Because of this, it occurs to me one adap­ta­tion I could make is to move the “ana­log ground”, the lev­el that’s at the end of the resis­tive divider and the “- IN” ter­mi­nal, by split­ting the sup­ply 5V via an op-amp of some sort. If I did that, then the range is extend­ed into part­ly neg­a­tive val­ues, by reduc­ing the pos­i­tive end of the range. Of course now I’d have to sub­tract the off­set from the ADC’s read­ing in soft­ware, and this would require some frag­ile cal­i­bra­tion. I don’t know how that would drift over time though. It would how­ev­er, give me a bipo­lar mea­sure­ment ability.

          1. Hi Vincenzo,
            Currently there is not a revi­sion with the TI isolator.
            But I was just think­ing about updat­ing this project this morn­ing, and one of the updates would be the isolator.
            I most like­ly will not get to this until August, after com­ple­tion of the DC Load project.
            Thanks,
            Greg (Barbouri)

  13. i’m con­fused about the ana­log ground. how does this work when you only have a sin­gle-end­ed ADC? sure­ly if the ADC is expect­ing the ref­er­ence volt­age to be ref­er­enced to its ground, then the ADC and the volt­age ref­er­ence must be using the same ground? i would think that hav­ing a sep­a­rate ana­log ground would only make sense if you were using a dif­fer­en­tial ADC?

  14. Hi Barbouri,

    I have built this project and I have a ques­tion regard­ing the enclo­sure. Would it be pos­si­ble to post a pic­ture of the mechan­ics involved in mount­ing the LCD dis­play to the front pan­el. I did order the front pan­el and Hammond enclo­sure. The cir­cuit is work­ing fine, I just need some assis­tance in the enclo­sure mount­ing. Thanks to you and Louis for all the hard work.

    1. Hi Rob,
      In the pho­to with the cap­tion “Mounting main board with shield and input wires attached” there are four black plas­tic machine screws with nuts attached to the cor­ners of the dis­play. After cen­ter­ing the dis­play, these can either be epox­ied or attached with hot-melt glue to the back of the front pan­el. After attach­ment I used the nuts to adjust the clear­ance of the dis­play for a light con­tact fit.

  15. Anyone have any spare V1.51 PC boards that I could pur­chase ? I thought that I’d give it a try before I put in an order to board house. I’m in the US and could Paypal you.

  16. This is a great project, but I don’t like the drift of the last dec­i­mals. So I am in the process to rewrite the adc part by inte­grat­ing the results over time, pret­ty much the same prin­ci­ple as over­sam­pling. So the last dig­its get way more sta­ble and the result imho gets clos­er to my 6.5 dig­it bench meter.
    In the moment I try to devel­op an algo­rithm to fig­ure out when to start a new mesure­ment instead of over­sam­pling the old one.

    Once I am hap­py with the result I will share my code if you are interested.

    Cheers
    Rubi

  17. Hi Borbouri,
    Nice project, i’m look­ing to build one or an off­shoot depend­ing on what I want to add on/improve. I noticed Paul Versteeg had issues with the CS pin caus­ing noise on the volt­age rail. Is this per­haps caused by no lim­it­ing resis­tors on D10, D12 and D13?

  18. Hello again Borbouri,
    Looking at mak­ing an off­shoot of the V1.5 Board with 3x 300K and 1x 100K 0.05% resis­tors in place of the Caddock. I’m also think­ing of push­ing it to 100x100mm and adding addi­tion­al con­nec­tors for more but­tons or rotary encoder and the banana jack con­nec­tions right to the PCB. Any thoughts?

    https://www.digikey.com/product-detail/en/susumu/RG2012N-304-W-T1/RG20N300KWCT-ND/600935

    https://www.digikey.com/product-detail/en/susumu/RG2012N-104-W-T1/RG20N100KWCT-ND/600924

    1. Hi Vincenzo,
      Yes, indi­rect­ly. The cop­per shield is con­nect­ed to the Digital Ground plane. The Digital Ground plane is con­nect­ed to the Analog Ground thru a sin­gle point.

      Greg (Barbouri)

    1. Hi Vincenzo,
      I used .5 mm / 24 awg hob­by cop­per sheet for the shield, but any­thing in that range will work.
      Some have even used cop­per clad cir­cuit board mate­r­i­al, sol­dered togeth­er to form a box.

      Greg (Barbouri)

    1. Hi Bstrag,
      As it is designed the Millivolt meter is a DC only sys­tem. The update rate of the LTC2400 ADC is less than 10 Hz so you would need to add some­thing like an AD8436ARQZ true RMS-to-DC con­vert­er to mea­sure an AC signal.

      Greg (Barbouri)

  19. Hi Barbouri, I can see you are short­ing the leds to cal­i­brate the meter. Scullcom hob­by use a 5v ref­er­ence to cal­i­brate it. Have you changed the code, or can it be cal­i­brat­ed both ways? (:

    Rasmus.

    1. Hi Rasmus,
      I am only short­ing the leads for 0 volt cal­i­bra­tion and have one vari­able that was pro­grammed in soft­ware that gives me the best lin­ear­i­ty for the entire range.
      The soft­ware vari­able will be dif­fer­ent for each meter depend­ing on ADC, volt­age divider resis­tors, and volt­age reference.

      Greg (Barbouri)

  20. Kudos on the board Greg. I bought six through OSHPark, and have built up four … very close to test­ing (maybe tonight). One small issue that I’ve not­ed is that pins con­nect­ed to the ground plane are very dif­fi­cult to sol­der. I know Kicad does ther­mal relief for flood fills. Is this avail­able in Eagle? Regards

    1. Hi Mike,
      Eagle does have the option of using “ther­mal iso­la­tion” and the OSH Park boards Ver. 1.51 do include ther­mal iso­la­tion for the ground planes.
      But because of the large ground planes on both sides of the board, even with ther­mal iso­la­tion a lot of heat does get pulled from the ground connections.
      I get the best results when using a flat-blade sol­der tip, adding extra flux, and increas­ing the tem­per­a­ture by 20 deg C when sol­der­ing the ground connections.

      Currently I am work­ing on an updat­ed Millivolt Meter board that uses a Coto 9002–05-11 relay for auto­mat­i­cal­ly select­ing the low­est range (0 — 4.6 volts) with the same max range of 46 volts as the orig­i­nal V 1.51 board.
      Looking for 1 to 10 uV usable res­o­lu­tion. Hopefully I will com­plete the design lat­er this summer.

      1. Thanks Greg. I look for­ward to see­ing your new design.
        On a side note, I have been read­ing through the LT2400 data sheet, ad note that the input con­ver­sion range is ‑12.5% Vref to 112.5% Vref, and that there is an out­put sign bit. I have yet to think through the impli­ca­tions for the input stage, but maybe you have already done so! How far ‑ve do you think the design can safe­ly go? I’m think­ing there might be some scope to use the mil­li­volt­meter not only for absolute DC mea­sure­ment of a pre­ci­sion volt­age ref­er­ence, but also as a null meter. (I should also men­tion that I am in the ear­ly stages of lay­ing out Conrad Hoffman’s MML null meter design in KiCAD.) Regards

  21. So, I have just had a quick look at the AD8628 datasheet (front end oper­a­tional ampli­fi­er) used in the mil­li­volt­meter. The input spec­i­fi­ca­tion is strict­ly zero to 5V, although there is some wrig­gle room giv­en the absolute rat­ing is ‑0.3V (diode clamp). Who knows what hap­pens for a neg­a­tive input 🙂 !
    I’ve also had a cur­so­ry look at the three ver­sions of soft­ware that seem to be com­mon­ly avail­able. Louis’s V33 from his site, an ear­li­er sim­pler ver­sion V7 from github, and PaulV’s ver­sion from his web­site (http://www.paulvdiyblogs.net/2016/09/building-6-digit-digital-milli-voltmeter.html). None rec­og­nize the LTC2400 neg­a­tive bit. I think V7 cal­i­brates at 0V, V33 at 5V, and PaulV’s at zero *and* a num­ber of ref­er­ence voltages.
    With a ground­ed input, any noise enter­ing the sys­tem after the AD8626 will make a pos­i­tive con­tri­bu­tion (even if neg­a­tive). There is also a 1uV off­set volt­age for this device.
    In short, the cal­i­bra­tion looks like it can be improved. I have my first board up and run­ning, and using V33, I get a 2 mil­li­volt read­ing with input ground­ed. No cop­per shield yet, and the board was cleaned, but before the Caddock was sol­dered in.
    Greg, when you revise the cir­cuit :-), that small neg­a­tive input range for the LTC2400 (actu­al­ly ‑0.3V because of the diode clamp) sure looks entic­ing. If it could be car­ried through the front end, it would allow a true zero cal.

    1. Hi Mike,
      I will look at some options for replac­ing the AD8628 with an op-amp that sup­ports a small neg­a­tive input voltage. 

      Greg (Barbouri)

  22. Greg, you are a absolute champion!
    I have been think­ing a lot about the mil­li­volt­meter, and the gen­er­al “hob­by­ist” through volt-nut appli­ca­tion of the LTC2400. I’m sure you’re aware of the var­i­ous threads on EEVBlog et al. Notably, no-one has yet pro­duced a com­pa­ra­ble easy to imple­ment project for the hob­by­ist with rea­son­able skills, or made any use­ful soft­ware public.
    Although I seem to be devel­op­ing volt-nut aspi­ra­tions, I real­ly don’t want to own an Agilant (HP) 3458, nor do I want to be respon­si­ble for keep­ing one in cal­i­bra­tion. I would how­ev­er, like to have a pre­ci­sion low volt­age DC mea­sur­ing tool, and the Scullcom/Barbouri design ticks a lot of box­es for me.
    I was already work­ing on the Chinese knock off of the Pro-Mini plat­form, and espe­cial­ly the low pow­er angle. Using the RocketScream library and remov­ing the reg­u­la­tor and pow­er LED from the board allows the pro-mini to sleep and use only 6uA. Outstanding, giv­en it is an Arduino for a few bucks. It was an *excel­lent* choice for the millivoltmeter.
    The LTC2400 also uses a miser­ly amount of pow­er when not sampling.
    What wor­ries me most about the design (as I’ve men­tioned before) is the accu­ra­cy of the cal­i­bra­tion. The exist­ing soft­ware does­n’t impress me.
    So, here is where I am propos­ing to go. (It’s most­ly soft­ware … you might like to think about it in terms of the hard­ware design you are contemplating) :
    — a 4 cell LiIon bat­tery sup­ply, charged with a bal­ance charg­er. (Hence low noise.) LT1763‑5 LDO reg­u­la­tor for the 5V sup­ply. For the most part, the instru­ment is always pow­ered on, with the inter­nal volt­age ref­er­ence get­ting more sta­ble with age. The pro-mini, LCD, and LTC2400 in pow­er down when not active­ly tak­ing a measurement.
    — cal­i­bra­tion with a ground­ed input, and using an exter­nal pre­ci­sion volt­age ref­er­ence to cor­rect for off­set, and gain errors (I’m pret­ty naive in my under­stand­ing, which at present comes from the Atmel appli­ca­tion note AVR120 “Calibration and Characterization of the ADC on the AVR”). Silly me, I got trapped into think­ing I would need a pre­ci­sion 5.0000 ref­er­ence to cal­i­brate. I was pur­su­ing the AD586LQ which is a pret­ty fine 5V buried zen­er ref­er­ence, but now am think­ing that 5V is not rel­e­vant at all. I have a friend of a neigh­bour with an HP34401 in cal, so I can send a portable ref­er­ence (always pow­ered on) to him, and it comes back say­ing say 6.950432V. Given the caveats of tem­per­a­ture coef­fi­cient, and long term sta­bil­i­ty, I can take that fig­ure, and hard code it into my Arduino soft­ware, then cal­i­brate to that pre­cise ref­er­ence val­ue. The instru­ment should then be very sta­ble until the tem­per­a­ture changes, at which point … recal. (I have in mind to soon hack togeth­er a pro­gram to see if I’m on the right track).
    — Don’t lose the neg­a­tive sign bit from the LTC2400! Reject those val­ues on the zero cal, because they’re not help­ing. Flag over­volt­age on the dis­play, although I don’t pro­pose to get any­where near 50V.
    — put the pro-mini to sleep when the LTC2400 is acquir­ing a sam­ple, and wake from inter­rupt on SPI, hence again, reduced noise.
    — Here is the most impor­tant part. I can’t com­pete with Agilant/HP/Keithley with Kalman fil­ter­ing that responds imme­di­ate­ly to changes to the input. So I need to change the phi­los­o­phy around the way that the instru­ment oper­ates. I tell the instru­ment when I’m ready to take a mea­sure­ment by push­ing a “go” but­ton, let it go off and take say 100 sam­ples, then 16.5 sec­onds lat­er, it presents an aver­age volt­age read­ing, AND the stan­dard devi­a­tion. Isn’t 3sd a 99% con­fi­dence inter­val, and basi­cal­ly a mea­sure­ment of the noise fig­ure of the mea­sure­ment? Again, the 16 x 2 LCD hav­ing two lines was a great choice to present the infor­ma­tion on. Now, I don’t know how many sam­ples gives 3sd approach­ing the noise floor of the hard­ware. Some smar­ty­pants prob­a­bly does, but say it was 1000 sam­ples. Can I wait for 165 sec­onds? Sure. Also, hav­ing I2C allows me to eas­i­ly con­nect a real time clock, EEPROM for stor­ing mea­sure­ments, temperature/humidity sens­ing etc. 

    So, lots of work to do. Should be back with some results in six months or so :-).

    I hope you don’t mind me putting these thoughts up, tacked onto your board design. (Again, con­grat­u­la­tions on the board. It’s tak­en the whole con­cept streaks ahead). Say the word, and I’ll stop :-).
    Regards

  23. Quick update. I’m wrong. I have test­ed some code look­ing for a neg­a­tive input to the LTC2400. I nev­er see a neg­a­tive flag, so I guess the op amp has a slight pos­i­tive offset?

  24. Hi again Greg,
    Firstly, let me apol­o­gize for mak­ing this so mes­sage so long. I have attached 200 lines of code below for the Millivoltmeter which I think you will find valu­able. May I sug­gest that you back in your dis­play and but­ton changes from the basic ScullcomV33 ver­sion of the code (so that it runs on your hard­ware) have a play, and then post the code with maybe a review as a new blog entry?
    So, as promised, I have imple­ment­ed off­set, and gain cor­rec­tion, with stan­dard devi­a­tion cal­cu­lat­ed for mea­sure­ments. With my build of the hard­ware (in an alu­mini­um box, pow­ered via USB, board not prop­er­ly cleaned, and no cop­per shield yet) and tak­ing 30 sam­ples I am get­ting stan­dard devi­a­tion of around 40uV. So effec­tive­ly I think I have a 5 1/2 dig­it instrument.
    From here, apart from work­ing on the pow­er sup­ply and shield­ing, I’d like to try putting the Arduino into low pow­er sleep while the ADC is con­vert­ing. I hope that will improve the noise levels.
    Regards,

    —– cut —–
    //
    // mvm.ino — Six Digit Millivoltmeter Software
    //
    // © Mike, 12 July 2018
    // Released under GPL2 or later.
    //
    // Derived from Scullcom V33/Barbouri code
    //
    // This ver­sion uses the “new” SPI library inter­face, refer https://www.arduino.cc/en/Reference/SPI, accessed June 2018.
    // Standard devi­a­tion pro­vid­ed by Rob Tillarts “Statistic” library.
    //
    // Major inno­va­tion in this soft­ware over pre­vi­ous ver­sions comes from the Atmel appli­ca­tion note “AVR120:
    // Characterization and Calibration of the ADC on the AVR”. In par­tic­u­lar, see Section 2.1 “Fixed-Point Arithmetic for
    // Offset and Gain Error Compensation”. Note that we use a scal­ing fac­tor of 2^24 (16777216).
    //
    // This ver­sion is con­trolled through three but­tons: Left, Centre, and Right.
    //
    // Shorting the input, then press­ing the left but­ton sets the “off­set” (zeroes the instru­ment). Do this first.
    // Enter a val­ue for your most accu­rate ref­er­ence volt­age below using the EXTERNAL_VOLTAGE_REFERENCE define. Connect
    // your exter­nal ref­er­ence, and press the right but­ton. This sets the instru­ment “gain”. Do this second.
    // The cen­tre but­ton takes a num­ber of sam­ples (num_samples), and dis­plays the aver­age, and stan­dard devi­a­tion. The
    // stan­dard devi­a­tion should be a noise mea­sure for your setup.
    // Offset cal­i­bra­tion clears the gain. The cal­i­bra­tion order is important.

    #include // Serial Peripheral Interface Library used to com­mu­ni­cate with LTC2400 (MISO, MOSI and SCK)
    #include
    #include
    #include

    //
    // Hardware Definition
    //

    //LTC2400 with CS on pin 10, SDO on pin 12, and SCK on pin 13.
    #define LTC2400_CS 10 

    #define BUTTON_Left 4
    #define BUTTON_Center 3
    #define BUTTON_Right 2 

    #define INTERNAL_VOLTAGE_REFERENCE 4.096 // Nominal ADR4540 ref­er­ence volt­age for LTC2400
    #define EXTERNAL_VOLTAGE_REFERENCE 4.99870 // External ref­er­ence volt­age used for calibration

    // 16 x 2 back­lit LCD mod­ule with I2C backpack
    // Address is gen­er­al­ly 0x27, or 0x3F. Change if the LCD does not respond.
    LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);

    //
    // Software Parameters
    //

    // To avoid loss of pre­ci­sion in the care­ful­ly craft­ed inte­ger arith­metic, Statistic is only used to keep the
    // stan­dard devi­a­tion. A side project is to rewrite the pack­age using int64_t.
    Statistic stats;

    con­st uint8_t num_samples = 30;

    int64_t ADC_offset = 0LL;
    uint8_t num_bad_reads = 0;

    con­st int64_t scaled_half_LSB = 0x7FFFFFLL; // 0.5
    int64_t scaled_gain_factor = 0xFFFFFFLL; // ini­tial­ly 1.0

    int64_t ScaledReadADC(void) {

    bool error_flag;

    SPI.beginTransaction (SPISettings (1000000, MSBFIRST, SPI_MODE0)); // 1 MHz clock, MSB first, mode 0
    digitalWrite(LTC2400_CS, LOW); // LTC2400 chip select pin tak­en low enables ADC conversion
    delayMicroseconds(10); // tim­ing delay but may not be required

    while ((PINB & (1 « 4))) { } //check to see if ADC is ready by test­ing EOC — wait while con­ver­sion completed

    int64_t sam­ple = 0;
    for (int i = 0; i < 4; ++i) { // Read 4 bytes (32 bits) from the ADC
    sam­ple <>= 4; // Discard 4 LSBs (noise)

    digitalWrite(LTC2400_CS, HIGH); // LTC2400 chip select pin tak­en high dis­ables ADC output
    SPI.endTransaction(); // SPI trans­ac­tion completed

    int64_t scaled_result = sam­ple * scaled_gain_factor + scaled_half_LSB — ADC_offset * scaled_gain_factor;

    return(scaled_result);
    }

    int64_t ScaledReadMultipleADC(void){
    stats.clear();

    // Throw away first sam­ple to clear ADC.
    // Note that on my hard­ware, when the input volt­age is changed from ground to +5V (and the reverse), the first
    // sam­ple read seems to be in error. Could be that the 220pF capac­i­tor at the Op Amp input is not
    // cor­rect­ly sol­dered on the board (it is tiny!). (Or some­thing else?) Check lat­er by comparison
    // between hard­ware builds.
    ScaledReadADC(); 

    int64_t sum_samples = 0LL;
    for (int i = 0; i > 24; // ADC off­set is not scaled

    lcd.setCursor(0, 1);
    ShowReading(ConvertToVoltage(ADC_offset <> 24);
    return(descaled_ADC_sample * 10.0 * INTERNAL_VOLTAGE_REFERENCE / 16777216.0);
    }

    void ShowReading(float x) {

    uint8_t decimal_places = 6;

    char pre­fix = 0;
    if (x < 0.001) {
    x *= 1000000;
    pre­fix = ‘u’;
    decimal_places = 0;
    } else if (x < 1) {
    x *= 1000;
    pre­fix = ‘m’;
    decimal_places = 3;
    }

    lcd.print(x, decimal_places); // Print volt­age as float­ing num­ber with the right num­ber of dec­i­mal places
    lcd.print(“ ”); // Add one blank space after volt­age reading

    if (pre­fix)
    lcd.print(prefix);
    lcd.print(“V ”); // Extra spaces to clean up when volt­ages go from large to small (8 spaces)
    }

    void DisplayMeasurement(float aver­age, float standard_deviation, uint8_t bad_reads) {
    lcd.setCursor(0, 0);
    ShowReading(average);

    lcd.setCursor(15, 0);
    lcd.print(bad_reads);

    lcd.setCursor(0, 1);
    ShowReading(standard_deviation);
    }

    // end

    1. Hi Mike,
      Thanks for the code. It will be sev­er­al weeks before I have some time to work with the Millivolt meter.
      I am cur­rent­ly trav­el­ling, which is the rea­son for the delay in respond­ing to you.

      Thanks again.
      Greg

  25. This might help!

    //
    // mvm.ino -- Six Digit Millivoltmeter Software
    //
    // (C) Mike, 12 July 2018
    // Released under GPL2 or later.
    //
    // Derived from Scullcom V33/Barbouri code
    //
    // This version uses the "new" SPI library interface, refer https://www.arduino.cc/en/Reference/SPI, accessed June 2018.
    // Standard deviation provided by Rob Tillarts "Statistic" library.
    //
    // Major innovation in this software over previous versions comes from the Atmel application note "AVR120:
    // Characterization and Calibration of the ADC on the AVR". In particular, see Section 2.1 "Fixed-Point Arithmetic for
    // Offset and Gain Error Compensation". Note that we use a scaling factor of 2^24 (16777216).
    //
    // This version is controlled through three buttons: Left, Centre, and Right.
    //
    // Shorting the input, then pressing the left button sets the "offset" (zeroes the instrument). Do this first.
    // Enter a value for your most accurate reference voltage below using the EXTERNAL_VOLTAGE_REFERENCE define. Connect
    // your external reference, and press the right button. This sets the instrument "gain". Do this second.
    // The centre button takes a number of samples (num_samples), and displays the average, and standard deviation. The
    // standard deviation should be a noise measure for your setup.
    // Offset calibration clears the gain. The calibration order is important.

    #include // Serial Peripheral Interface Library used to com­mu­ni­cate with LTC2400 (MISO, MOSI and SCK)
    #include
    #include
    #include

    //
    // Hardware Definition
    //

    //LTC2400 with CS on pin 10, SDO on pin 12, and SCK on pin 13.
    #define LTC2400_CS 10 

    #define BUTTON_Left 4
    #define BUTTON_Center 3
    #define BUTTON_Right 2 

    #define INTERNAL_VOLTAGE_REFERENCE 4.096 // Nominal ADR4540 ref­er­ence volt­age for LTC2400
    #define EXTERNAL_VOLTAGE_REFERENCE 4.99870 // External ref­er­ence volt­age used for calibration

    // 16 x 2 back­lit LCD mod­ule with I2C backpack
    // Address is gen­er­al­ly 0x27, or 0x3F. Change if the LCD does not respond.
    LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);

    //
    // Software Parameters
    //

    // To avoid loss of pre­ci­sion in the care­ful­ly craft­ed inte­ger arith­metic, Statistic is only used to keep the
    // stan­dard devi­a­tion. A side project is to rewrite the pack­age using int64_t.
    Statistic stats;

    con­st uint8_t num_samples = 30;

    int64_t ADC_offset = 0LL;
    uint8_t num_bad_reads = 0;

    con­st int64_t scaled_half_LSB = 0x7FFFFFLL; // 0.5
    int64_t scaled_gain_factor = 0xFFFFFFLL; // ini­tial­ly 1.0

    int64_t ScaledReadADC(void) {

    bool error_flag;

    SPI.beginTransaction (SPISettings (1000000, MSBFIRST, SPI_MODE0)); // 1 MHz clock, MSB first, mode 0
    digitalWrite(LTC2400_CS, LOW); // LTC2400 chip select pin tak­en low enables ADC conversion
    delayMicroseconds(10); // tim­ing delay but may not be required

    while ((PINB & (1 « 4))) { } //check to see if ADC is ready by test­ing EOC — wait while con­ver­sion completed

    int64_t sam­ple = 0;
    for (int i = 0; i < 4; ++i) { // Read 4 bytes (32 bits) from the ADC
    sam­ple <>= 4; // Discard 4 LSBs (noise)

    digitalWrite(LTC2400_CS, HIGH); // LTC2400 chip select pin tak­en high dis­ables ADC output
    SPI.endTransaction(); // SPI trans­ac­tion completed

    int64_t scaled_result = sam­ple * scaled_gain_factor + scaled_half_LSB — ADC_offset * scaled_gain_factor;

    return(scaled_result);
    }

    int64_t ScaledReadMultipleADC(void){
    stats.clear();

    // Throw away first sam­ple to clear ADC.
    // Note that on my hard­ware, when the input volt­age is changed from ground to +5V (and the reverse), the first
    // sam­ple read seems to be in error. Could be that the 220pF capac­i­tor at the Op Amp input is not
    // cor­rect­ly sol­dered on the board (it is tiny!). (Or some­thing else?) Check lat­er by comparison
    // between hard­ware builds.
    ScaledReadADC(); 

    int64_t sum_samples = 0LL;
    for (int i = 0; i > 24; // ADC off­set is not scaled

    lcd.setCursor(0, 1);
    ShowReading(ConvertToVoltage(ADC_offset <> 24);
    return(descaled_ADC_sample * 10.0 * INTERNAL_VOLTAGE_REFERENCE / 16777216.0);
    }

    void ShowReading(float x) {

    uint8_t decimal_places = 6;

    char pre­fix = 0;
    if (x < 0.001) {
    x *= 1000000;
    pre­fix = ‘u’;
    decimal_places = 0;
    } else if (x < 1) {
    x *= 1000;
    pre­fix = ‘m’;
    decimal_places = 3;
    }

    lcd.print(x, decimal_places); // Print volt­age as float­ing num­ber with the right num­ber of dec­i­mal places
    lcd.print(“ ”); // Add one blank space after volt­age reading

    if (pre­fix)
    lcd.print(prefix);
    lcd.print(“V ”); // Extra spaces to clean up when volt­ages go from large to small (8 spaces)
    }

    void DisplayMeasurement(float aver­age, float standard_deviation, uint8_t bad_reads) {
    lcd.setCursor(0, 0);
    ShowReading(average);

    lcd.setCursor(15, 0);
    lcd.print(bad_reads);

    lcd.setCursor(0, 1);
    ShowReading(standard_deviation);
    }

    // end

  26. Nope. Sorry. Ok, looks like you do have some seri­ous edit­ing to do.
    If you can’t recov­er the code (at least the read ADC func­tion is miss­ing bits as well as the includes), PM me, and I’ll send you a zipped file.

  27. Hi thought i would give this a go! I can sol­der!! But do you have a parts list for the two types of board I ordered? with digi key or similar

  28. Hi Greg,

    How accu­rate your Millivoltmeter please? I can see on the low­er end, for exam­ple, mea­sur­ing 1mV is giv­ing me around 500uV, which is 50% error. I think lin­ear­i­ty error is high­er clos­er to a LTC2400 lim­its. Accuracy improve clos­er to 10mV (Millivoltmeter is read­ing 9.5mV). I have prop­er shield­ing in place. Also using dif­fer­ent volt­age stan­dards (I tried 2.5V, 5V and 10V) to cal­i­brate the meter is giv­ing me slight­ly dif­fer­ent results, but as a rule of thumb if I cal­i­brate it at 10V accu­ra­cy is the best at 10V. If I cal­i­brate it at 2.5V, around 2.5V accu­ra­cy is the best. High res­o­lu­tion is good, but how do I achieve a good accu­ra­cy on the 0–10V scale please?

    1. Hi Alex,
      My V1.5 Millivolt meter is with­in 25 uV at 700 mV, and with­in 50 uV at 100 mV using a 5 volt cal.
      Something that I am work­ing on with the new pro­to­types, is the abil­i­ty to cal­i­brate at mul­ti­ple points and have the soft­ware use the most appro­pri­ate cal for the volt­age measured.
      I have also imple­ment­ed a dual input range of 0 — 4V and 0 — 40V on the most recent pro­to­type which should improve the low end res­o­lu­tion and accu­ra­cy below 4V.

      Greg (Barbouri)

  29. Thanks Greg,

    That’s pret­ty much my prob­lem. Either it is more accu­rate around 5V using 5V cal or around 10V using 10V cal. Like in Ian Johnston’s vref (or in fact in your vari­able vref), there has to be a mul­ti­ple points calibration.
    Looking for­ward to your new prototype. 

    Regards
    Alex

  30. Hi Greg,

    This looks like a great build and design. I was plan­ning on build­ing this (and the MiliOhm project too). Looking through the com­ments I see that a while back you were work­ing on a dual range ver­sion with a Coto relay. Was there any fur­ther progress in this — I am sure I would be pret­ty pleased if I can get the same sort of res­o­lu­tion as you have achieved but would be good to try an updat­ed ver­sion of you had made fur­ther modifications .

    1. Hi John,
      Next blog post will be on the Millivolt Meter ver­sion 2.11
      The blog post is most­ly done, just fin­ish­ing up all the documentation.

      Thanks,
      Greg (Barbouri)

Leave a Reply to Vincenzo Cancel reply

Your email address will not be published. Required fields are marked *