raw Electronics
RAW Electronics Measurement Resistance Measurement

DIY Ohmmeter to Measure Resistor Values with Arduino

Robert Eisele

Measuring electrical resistance is essentially measuring how much a component resists current flow. A cheap multimeter can do this out of the box, but its resistance mode is often surprisingly inaccurate, so it is worth building a small Arduino-based ohmmeter yourself and, more importantly, understanding exactly how good — or how bad — its readings really are.

Components

Theory

The simplest way to measure resistance is to build a voltage divider where one resistor value is known and the other is the resistor we want to measure.

The output voltage which we measure with the integrated ADC behind the analog pins of our Arduino depends on the relationship of these two resistors. Since we know one resistor value and how much voltage drops across it, the value of the unknown resistor can be deduced from the relationship of the resistors:

\[\frac{V_\text{out}}{V_\text{in}} = \frac{R_2}{R_1+R_2}\]

Where \(V_\text{in}\) is the reference voltage we put into the voltage divider and \(V_\text{out}\) is the output of the voltage divider, which we measure with the Arduino.

When we read an integer \(x\in [0, 2^b-1]\) from the ADC, using \(b=10\) bit resolution, we can state the output voltage of the voltage divider as:

\[V_\text{out} = \frac{x}{2^b} V_\text{in}\]

We now have two choices, say \(R_1\) is the known resistor value and \(R_2\) the resistor we want to measure and vice versa.

\(R_1\) is the known resistor

\[ \begin{array}{rl} & \frac{V_\text{out}}{V_\text{in}} = \frac{R_2}{R_1+R_2}\\ \Leftrightarrow & V_\text{out} \cdot R_1 + V_\text{out} \cdot R_2 = V_\text{in} \cdot R_2\\ \Leftrightarrow & V_\text{out} \cdot R_1 = (V_\text{in} - V_\text{out}) \cdot R_2\\ \Leftrightarrow & R_2 = \frac{V_\text{out} \cdot R_1}{V_\text{in} - V_\text{out}}\\ \Leftrightarrow & R_2 = R_1\cdot\frac{x}{2^b - x}\\ \end{array} \]

\(R_2\) is the known resistor

\[\begin{array}{rl} & \frac{V_\text{out}}{V_\text{in}} = \frac{R_2}{R_1+R_2}\\ \Leftrightarrow & V_\text{out} \cdot (R_1 + R_2) = V_\text{in} \cdot R_2\\ \Leftrightarrow & R_1 + R_2 = \frac{V_\text{in}}{V_\text{out}}\cdot R_2\\ \Leftrightarrow & R_1 = \frac{V_\text{in}}{V_\text{out}}\cdot R_2 - R_2\\ \Leftrightarrow & R_1 = R_2\cdot\left(\frac{V_\text{in}}{V_\text{out}}-1\right)\\ \Leftrightarrow & R_1 = R_2\cdot\left(\frac{2^b}{x}-1\right)\\ \end{array}\]

So we have two solutions, depending on whether we decide to make \(R_1\) or \(R_2\) the unknown resistor we want to measure. Both cases have the advantage, that they don't rely on \(V_\text{in}\), which means that the accuracy of the measurement does not depend on the reference voltage!

How Much Error Should You Expect?

A number coming out of the serial monitor looks precise, but it does not tell you how far it can be trusted. Two independent error sources sit underneath every reading: the reference resistor is never exactly its printed value, and the ADC code \(x\) is an integer, so it can only resolve the voltage divider's output in discrete steps.

Writing \(R_\text{ref}\) for whichever resistor is fixed and \(N=2^b\) for the number of ADC codes (\(N=1024\) for the 10-bit converter on a classic Arduino Uno or Nano), the unknown resistor as a function of the code is

\[R = R_\text{ref}\cdot\frac{x}{N-x}.\]

Differentiating with respect to \(x\) shows exactly how sensitive a reading is to a one-code error:

\[\frac{\partial R}{\partial x} = \frac{R_\text{ref}\cdot N}{(N-x)^2}\]

For a small code error \(\Delta x\), this turns into an absolute and, more usefully, a relative error estimate:

\[|\Delta R_x|\approx\left|\frac{R_\text{ref}\cdot N}{(N-x)^2}\Delta x\right| \qquad\Rightarrow\qquad \left|\frac{\Delta R_x}{R}\right|\approx\left|\frac{N}{x\cdot(N-x)}\Delta x\right|\]

The datasheet of the ATmega328P used on most classic Arduino boards specifies the ADC's absolute accuracy as typically \(\pm 2\) codes, so \(\Delta x=2\) is a reasonable starting point unless you have characterised your own board.

The reference resistor contributes its own tolerance directly and proportionally, since \(R\) scales linearly with \(R_\text{ref}\):

\[\left|\frac{\Delta R_r}{R}\right|\approx\left|\frac{\Delta R_\text{ref}}{R_\text{ref}}\right|\]

Adding both contributions gives the overall relative error you should expect from a single reading:

\[\left|\frac{\Delta R}{R}\right|\approx\left|\frac{N}{x\cdot(N-x)}\Delta x\right| + \left|\frac{\Delta R_\text{ref}}{R_\text{ref}}\right|\]

Estimated relative error over the full ADC code range for \(\Delta x=2\), with a 0.1% (blue) and a 1% (red) reference resistor.

Two things stand out. The error blows up near the edges of the code range, when \(x\) is close to \(0\) or \(N\), because \(x\cdot(N-x)\) becomes small exactly there — which is what happens when the unknown resistor is much smaller or much larger than the reference. Away from the edges, though, the reference resistor's own tolerance is usually the larger of the two terms, not the ADC's quantisation. In other words: a good reference resistor buys you more accuracy than a better ADC.

Choosing the Best Reference Resistor

If you already know the range you care about, from \(R_\text{min}\) to \(R_\text{max}\), you can pick \(R_\text{ref}\) so the quantisation error is balanced at both ends instead of being much worse at one of them. Error is lowest around the middle code \(N/2\) and grows the further \(x\) drifts away from it, so the optimum is reached when \(R_\text{min}\) and \(R_\text{max}\) sit symmetrically around that middle code:

\[\frac{N}{2}-x_\text{min} = x_\text{max}-\frac{N}{2}\quad\Leftrightarrow\quad N = x_\text{max}+x_\text{min}\]

Substituting \(x=\dfrac{N}{1+R_\text{ref}/R}\) for both boundaries and solving for \(R_\text{ref}\) collapses to a clean result:

\[R_\text{ref} = \sqrt{R_\text{min}\cdot R_\text{max}}\]

the geometric mean of the two extremes. This is exactly why a practical ohmmeter design often uses a rotary switch between a handful of reference values — for example \(220\,\Omega\), \(1\,\text{k}\Omega\), \(10\,\text{k}\Omega\) and \(100\,\text{k}\Omega\) — instead of a single fixed resistor: every decade needs roughly its own geometric-mean reference to stay accurate.

The reverse question is just as useful in practice: given a fixed \(R_\text{ref}\) and a target relative error, which resistance values can you actually trust? Solving the combined error formula for the boundary codes gives

\[x_\text{max,min} = \frac{N}{2}\pm\sqrt{\left(\frac{N}{2}\right)^2 - \frac{N\cdot\Delta x} {\left|\Delta R/R\right|-\left|\Delta R_\text{ref}/R_\text{ref}\right|}}\]

and converting the codes back into resistances,

\[R_\text{max,min} = \frac{R_\text{ref}}{\dfrac{N}{x_\text{max,min}}-1}\]

gives the resistance interval that a given reference resistor can measure within your chosen error budget. Both directions of this calculation — picking \(R_\text{ref}\) for a range, and finding the usable range for a fixed \(R_\text{ref}\) — are wrapped up in the calculator below, so you do not have to juggle the algebra by hand every time you change a resistor.

Enter the ADC and reference resistor you are working with, then either a target measurement range or a single resistor to check.

Your Setup

Best Reference Resistor for a Range

Enter your range and press Calculate.
Recommended reference (√(Rmin·Rmax))-
Error at smallest resistor-
Error at largest resistor-
Usable range at your target error-

Check a Single Reading

ADC code x-
Quantisation error-
Total relative error-
Absolute error-

Implementation

As shown in the theory part, it depends on whether \(R_1\) or \(R_2\) is kept constant but in both cases a simple voltage divider can be set up:

In the sketch, an optional capacitor with \(C=10\,\text{nF}\) is used to reduce dynamic loading effects and noise of the ADC input. Let's start with a helper function to pretty-print the resulting resistance:

static void printResistance(float r) {

  if (r >= 1e6) {
    Serial.print(r / 1e6);
    Serial.println("MΩ");
  } else if (r >= 1e3) {
    Serial.print(r / 1e3);
    Serial.println("kΩ");
  } else {
    Serial.print(r);
    Serial.println("Ω");
  }
}

Now with \(R_1:= 1000\) being the constant resistor, the following Arduino code is enough to measure \(R_2\):

void setup() {
  Serial.begin(9600);
}

void loop() {

  uint16_t x = analogRead(A0);

  float R1 = 1000; // Ω
  float R2 = R1 * x / (1024.0 - x);

  printResistance(R2);
  delay(500);
}

Or equivalently with \(R_2:= 1000\) being the constant resistor:

void setup() {
  Serial.begin(9600);
}

void loop() {

  uint16_t x = analogRead(A0);

  float R2 = 1000; // Ω
  float R1 = R2 * (1024.0 / x - 1);

  printResistance(R1);
  delay(500);
}

Both implementations use a hard-coded reference resistance of \(1\,\text{k}\Omega\). In practice, I used a rotary switch to change the value between \(220\,\Omega\), \(1\,\text{k}\Omega\), \(10\,\text{k}\Omega\) and \(100\,\text{k}\Omega\) to keep it close to the resistor being measured — following exactly the geometric-mean reasoning above — since otherwise the error increases dramatically.

The total resistance \(R_1 + R_2\) between the reference voltage of 5 V and GND should not be lower than \(125\,\Omega\), because otherwise more than \(I = \frac{U}{R} = \frac{5\text{V}}{125\,\Omega} = 40\,\text{mA}\) would flow, which exceeds the power rating of the pin.

Averaging Noisy Readings

A single ADC sample jitters a little from read to read, mostly from ADC and supply noise rather than from the systematic error sources discussed above. Averaging several samples smooths that jitter out and gives a much steadier number on the serial monitor — but it does not touch the quantisation or reference-tolerance error, since averaging cannot correct a systematic bias such as an inaccurate reference resistor. The two error sources are simply independent problems.

#define R_REF 10000.0
#define AVERAGES 10    // range: 1 to 100

void setup() {
  analogReference(DEFAULT);
  Serial.begin(9600);
}

void loop()
{
  float resistance;
  uint16_t x;
  static float cumulativeResistance = 0;
  static uint8_t i = 0;

  x=analogRead(A0);
  resistance = R_REF*x/(1024.0-x);
  cumulativeResistance = cumulativeResistance + resistance;
  i++;
  if (i==AVERAGES)
  {
    Serial.println(cumulativeResistance/AVERAGES);
    cumulativeResistance = 0;
    i = 0;
  }
  delay(500/AVERAGES); // about two measurements per second
}

With AVERAGES set to 10, this sketch reports a fresh average about twice a second. Raising it reduces jitter further but also slows down the update rate, so treat it as a knob rather than a fixed constant. A \(10\,\text{k}\Omega\), 0.1% reference resistor is a solid default for measuring resistors from roughly \(1\, \text{k}\Omega\) to \(100\,\text{k}\Omega\), and it is what was used to validate the error formulas above against real hardware.

Snapping to Known Values

If all you need is quickly testing your E-series resistors, you can avoid adding any extra ADC error by simply searching for the closest known resistor instead of trusting the raw reading:

static float nearestResistor(float v) {

  const float Rs[] = {
    1.5,
    4.7,
    10,
    47,
    100,
    220,
    330,
    470,
    680,
    1000,
    2200,
    3300,
    4700,
    10000,
    22000,
    47000,
    100000,
    330000,
    1000000
  };

  float minErr = INFINITY;
  float ret = v;

  for (uint8_t i = 0; i < sizeof(Rs) / sizeof(float); i++) {

    float err = abs(Rs[i] - v);

    if (err < minErr) {
      minErr = err;
      ret = Rs[i];
    }
  }
  return ret;
}

Please note that since an ADC never works perfectly and the output \(x\) is an integer, several error sources come together, as derived above. To reduce the overall error, use a reference resistor with a low tolerance, preferably 1% or 0.1%. Furthermore, the USB port should be avoided as the power supply, since it is quite noisy and unstable — an external supply is strongly recommended for any ratiometric measurement like this one.

Switching between Ω, kΩ and MΩ, as printResistance() does above, is a common enough problem to be worth a small dedicated library rather than reinventing it in every sketch — UnitFormat.js does exactly this kind of human-readable unit scaling in JavaScript, if you are logging or plotting the readings on a computer instead of the serial monitor.

Calibration, or Just a Good Reference Resistor?

It's tempting to calibrate the ohmmeter against a "better" instrument, but that is harder than it sounds. Cheap digital multimeters can easily have more than 1% error in resistance mode themselves, and instruments that reliably beat that are expensive and need periodic recalibration. A simple two-point calibration removes offset and gain errors but not the ADC's nonlinearity, and correcting that properly would mean calibrating every single code individually — not a reasonable amount of work for a hobby instrument.

In practice it is both cheaper and more honest to buy resistors with a specified tolerance instead of trying to calibrate the instrument itself, and to simply plug that tolerance into the error formula above. 1% resistors cost a few cents each in most webshops, 0.1% parts a little more. If you are missing an odd value, you can often build it from more common ones — a \(3\,\text{k}\Omega\) reference, for example, is just three \(1\,\text{k}\Omega\), 1% resistors wired in series. Whatever you end up using, it's worth testing the finished ohmmeter against a handful of known resistors to confirm it behaves consistently before trusting it for real measurements.

Putting It to the Test

The error formula above is only a model; it is worth checking how closely it tracks a real build. Reference measurements taken with a laboratory-grade meter (accurate to 0.01%) on eight resistors, compared against four different Arduino-compatible boards using a shared \(10\,\text{k}\Omega\), 0.1% reference resistor, show the same overall shape as the formula predicts [1]:

Reference value (Ω) Calculated max. error Average measured error (4 boards)
508.2 4.34% 5.14%
994.8 2.47% 2.52%
1990.3 1.51% 1.43%
5733.4 0.94% 0.64%
9940 0.88% 0.40%
19942.6 0.98% 0.09%
47038 1.45% 0.27%
99795 2.46% 0.35%

Two takeaways: the measured error stays consistent across boards from different manufacturers, which suggests the ADC itself is not the limiting factor once you use an external supply; and the measured error is frequently below the calculated bound, especially away from the extremes, since real 1% and 0.1% resistors are often quite a bit closer to nominal than their worst-case tolerance allows.

A second experiment, sweeping the reference resistor itself across four decades (\(1\,\text{k}\Omega\), \(10\,\text{k}\Omega\), \(100\,\text{k}\Omega\), \(1\,\text{M}\Omega\), all 1%) while measuring resistors at 0.3, 1.0 and 3.0 times the reference value, makes the geometric-mean argument from earlier very concrete:

\(R/R_\text{ref}\) Measured error, \(R_\text{ref}=1\,\text{k}\Omega\) Measured error, \(R_\text{ref}=10\,\text{k}\Omega\) Measured error, \(R_\text{ref}=100\,\text{k}\Omega\) Measured error, \(R_\text{ref}=1\,\text{M}\Omega\)
0.3 1.43% 0.72% 1.10% 0.72%
1.0 0.58% 0.39% 0.39% 0.27%
3.0 0.00% 0.47% 1.05% 0.05%

The error stays low and fairly flat right around \(R=R_\text{ref}\), exactly where the geometric-mean rule points you, and grows a bit toward the edges of each decade — the same pattern the formula predicts, just with real resistor tolerances usually pulling the actual numbers below the worst case.

Build Your Own and Compare

The best way to internalise all of this is to build the circuit and try it yourself. A good exercise is to measure a full decade sweep with a single \(10\,\text{k}\Omega\), 1% reference resistor: \(1\,\text{k}\Omega\), \(3\,\text{k}\Omega\), \(10\,\text{k}\Omega\), \(30\,\text{k}\Omega\) and \(100\,\text{k}\Omega\), all 1% tolerance. If you don't have an exact \(3\,\text{k}\Omega\) part on hand, three \(1\,\text{k}\Omega\) resistors in series work just as well. Compare what you measure against the estimate from the accuracy calculator above — the readings near \(10\,\text{k}\Omega\) should be noticeably tighter than the ones near \(1\,\text{k}\Omega\) or \(100\,\text{k}\Omega\), exactly as the theory predicts.

Researchers at the University of Szeged run an ongoing citizen-science collection of exactly this kind of data from ohmmeters built by students and hobbyists worldwide, using the same 1 kΩ/3 kΩ/10 kΩ/30 kΩ/100 kΩ sweep with a shared 10 kΩ reference. If you build the circuit, their submission form is a fun way to contribute your numbers and see how your build compares with everyone else's, and the aggregated results are public.

Further Reading