Milliohm meter

Ordinary multimeters use a voltage divider to compare the resistor under test with a known reference resistor. This works well for large resistance values, but the resistance of the test leads makes it impractical to measure small resistance values (typically 1ohm or less).

A milliohm meter gets around this limitation by using a 4 wire measurement technique, where 2 wires are used to drive a current through the resistor and 2 wires are used to read the voltage across the resistor.

After watching Louis Scully’s attempts at making a milliohm meter (https://www.youtube.com/watch?v=anE0jDeBuxo) I was convinced that there had to be a better way of designing one.

Hardware

All milliohm meters consist of a current source and a way to measure voltage (an ADC, a multimeter, or even an analog voltmeter). I started off the design by creating a simple constant current source using an op-amp and a transistor

In this configuration, the op-amp changes its output until the voltage measured across the sense resistor (Rsense) is the same as the reference voltage (Vref). The transistor or MOSFET acts as a variable resistor and dissipates the excess power as heat.

The first issue that I ran into was changing the sense resistor to measure different ranges. The obvious method of using a MOSFET to switch the resistors fails because the MOSFET will be in series with the sense resistor and the Rds of the MOSFET will impact the accuracy of the current source.

After a bit of searching, I found the solution on an EEVblog youtube video (https://www.youtube.com/watch?v=xSEYPP5Xsi0). This solution places the MOSFETs outside of the op-amp’s feedback loop:

This way by activating the left-most MOSFET, all 3 resistors are put in series giving a total of 90+9+1 = 100 ohms sense resistor. Activating the next MOSFET will result in 9+1 = 10 ohms sense resistor and the final MOSFET will result in a 1-ohm sense resistor. To overcome the fact that 9 is not a standard E24 value, I used two 18 ohm resistors in parallel (180 ohms to make 90, etc.). For the final design, I ended up with 6 ranges (1 ohm to 100k ohm)

For Vref I decided to go with the LM4140 which is a 1.024V precision reference. Combined with the sense resistors, this gave me 6 current ranges going from 1A to 10μA.

I then created a development board to test different op-amps, MOSFETs, and transistors:

I finally stelled on the MAX4238 for the op-amp and a D2PAK Darlington pair transistor (BUB941ZT). The reason for this choice was the limited voltage of the op-amp (5V max) and lack of logic level MOSFETs with enough safe DC operating area. Darlington pair transistors require a base current rather than gate voltage and typically have a much better DC operating area.

The next step was choosing an ADC and designing a complete PCB. After many prototypes, I went with the ADS1219 which is a 24-bit delta-sigma ADC. The final Schematic and PCB can be found at: https://oshwlab.com/theepicn008/milliohm-meter-v3-0

This design has a 10μΩ accuracy and the raw bill of material is around 25£ (30$).

Software

The software was written poorly during the development stages, a newer version is coming soon which solves all the issues and bugs with the development version. However, if you want to build your own and test it, you can find the development version here:

#include <Wire.h>
#include <ADS1219.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define drdy 2
#define mult 2.048/pow(2,23)
#define pin 10 
#define samples 20
#define SettleTime 30000

ADS1219 ads(drdy);
//float    Rshunt[6] = {0.9912,9.976,99.98,999.9,10000.0,99970.0};
float    Rshunt[6] = {1,10,100,1000,10000,100000};
uint16_t Tdelay[6] = {5000,700,200,200,200,200};
int32_t  Vsense;
int32_t  Vshunt;
uint8_t  Gain=1;

Adafruit_SSD1306 oled(128, 64, &Wire);

char prefixes[4] = "m K";
int powers[3] = {3,0,-3};

static const unsigned char PROGMEM ohm[] = {
  0x00, 0x1f, 0x00, 0x00, 0x00, 0xff, 0xe0, 0x00, 0x03, 0xff, 0xf8, 0x00, 0x07, 0xc0, 0xfc, 0x00, 
  0x0f, 0x00, 0x3e, 0x00, 0x1e, 0x00, 0x1e, 0x00, 0x3c, 0x00, 0x0f, 0x00, 0x3c, 0x00, 0x0f, 0x00, 
  0x78, 0x00, 0x07, 0x80, 0x78, 0x00, 0x07, 0x80, 0x70, 0x00, 0x03, 0x80, 0x70, 0x00, 0x03, 0x80, 
  0x70, 0x00, 0x03, 0x80, 0x70, 0x00, 0x03, 0x80, 0x70, 0x00, 0x03, 0x80, 0x70, 0x00, 0x03, 0x80, 
  0x70, 0x00, 0x03, 0x80, 0x70, 0x00, 0x03, 0x80, 0x78, 0x00, 0x07, 0x80, 0x38, 0x00, 0x07, 0x00, 
  0x38, 0x00, 0x07, 0x00, 0x1c, 0x00, 0x0e, 0x00, 0x1e, 0x00, 0x0e, 0x00, 0x0e, 0x00, 0x1c, 0x00, 
  0x07, 0x00, 0x38, 0x00, 0x03, 0x80, 0x70, 0x00, 0xff, 0xc0, 0xff, 0xc0, 0xff, 0xc0, 0xff, 0xc0
};

void setup() {
  pinMode(drdy,INPUT_PULLUP);
  Wire.begin();
  Wire.setClock(3400000L);
  Serial.begin(9600);
  for (int i=5; i<11; i++){
    pinMode(i,OUTPUT);
    digitalWrite(i,0);
  }
  ads.setDataRate(90);
  digitalWrite(pin,1);
  oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  //oled.setRotation(2);
  oled.clearDisplay();
  oled.display();
  oled.setTextColor(WHITE);
  oled.setTextSize(4);
}

bool loadConnected (){
  return (abs(ads.readDifferential_2_3())<200);
}

int detectRange() {
  int i=10;
  float v = abs(ads.readDifferential_0_1()*mult);
  while(v < 0.2048 && i> 5 ){
    digitalWrite(i,0);
    i--;
    digitalWrite(i,1);
    delay(10);
    v = abs((ads.readDifferential_0_1())*mult);
  }
  digitalWrite(i,0);
  return i-5;
}

void takeSamples(int range){
  digitalWrite(range+5,1);
  delayMicroseconds(SettleTime);
  if(abs(ads.readDifferential_0_1()) < 2048000){
    Gain = 4;
    ads.setGain(GAIN_FOUR);
    for (int j=0; j < samples; j++){
      Vsense += abs(ads.readDifferential_0_1());
    }
    ads.setGain(GAIN_ONE);
    for (int j=0; j < samples; j++){
      Vshunt += ads.readSingleEnded(2);
    }
  } else {
    Gain=1;
    ads.setGain(GAIN_ONE);
    for (int j=0; j < samples; j++){
      Vsense += abs(ads.readDifferential_0_1());
      Vshunt += ads.readSingleEnded(2);
    }
  }
  digitalWrite(range+5,0);
}

int prefix(int x){
  return (x < 0) ? 0 : (x/3)+1;
}

int decimalPlaces(int x){
  return (x < 0) ? abs(x) : 3-(int(x)%3);
}

void loop() {
  if (!loadConnected()){
    oled.clearDisplay();
    oled.setCursor(6,2);
    oled.println("NO");
    oled.println("LOAD!");
    oled.display();
  }
  while(!loadConnected());
  Vsense = 0;
  Vshunt = 0;
  long start = micros();
  int range=detectRange();
  long finish = micros()-start;
  Serial.print("Ranging  took: ");
  Serial.println(finish);
  Serial.print("Range: ");
  Serial.println(range);
  start = micros();
  takeSamples(range);
  finish = micros()-start;
  Serial.print("Sampling took: ");
  Serial.println(finish);
  Serial.println("######################");
  float result = (Vsense*Rshunt[range])/(Vshunt*Gain);
  int lg = floor(log10(result));
  uint8_t pref = prefix(lg);
  uint8_t dp = decimalPlaces(lg);
  oled.clearDisplay();
  oled.setCursor(6, 2);
  oled.println(result*pow(10,powers[pref]),dp);
  oled.setCursor(66, 34);
  oled.print(prefixes[pref]);
  oled.drawBitmap(96,34,ohm,26,28,1);
  oled.display();
  delay(Tdelay[range]);
  digitalWrite(10,1);
}

Earlier prototypes and failed designs

I have gone through many iterations and design changes that didn’t make it to the final version, but I thought it’d be a good idea to include the fails as well.

Leave a comment

Design a site like this with WordPress.com
Get started