Guides · 15 min read

Arduino Voltmeter: Measure 0–25 V with an LCD and Calibration

How to build an Arduino voltmeter: measure 0–25 V with a divider, pick resistors, add zener protection, calibrate, use an LCD, an INA219 and a battery tester.

An Arduino voltmeter is a measuring instrument you can build in an evening: two resistors, one analog input and a few lines of code are enough to measure 0–25 V accurately and show it on a display. Arduino's analog inputs can read at most 5 V, so to measure a higher voltage you first scale it down with a voltage divider and then scale it back up in code by the same ratio. Battery monitors, power supply readouts, solar panel loggers and battery testers all rely on this one idea.

In this guide you'll learn step by step how to pick resistor values for a given range, how a zener and series resistor protect the analog input from overvoltage, how the ready-made 0–25 V voltage sensor module works, and how setting the reference voltage and calibrating bring your readings close to a multimeter's. Then you'll show the value on an I2C LCD, build the same voltmeter on an ESP32 and a Raspberry Pi Pico, add current sensing with an INA219 to turn it into a wattmeter, and finally make a tester that measures a battery's real capacity in mAh.

Digital voltmeter
The voltmeter you build with Arduino can show the voltage to two decimal places, just like these panel meters.

How does an Arduino voltmeter work?

Arduino doesn't know voltage directly; its ADC (analog-to-digital converter) compares the voltage on the analog input with a reference voltage and returns the result as an integer. The Arduino Uno's ADC is 10-bit: it reads 0 for 0 V and 1023 for the reference voltage (5 V by default). The voltage at the pin is Vpin = reading × Vref / 1024. If there's a voltage divider at the input, the voltage you're actually measuring is Vin = Vpin × (R1 + R2) / R2. So the accuracy of the whole voltmeter depends on three things: the real value of Vref, the ratio of the two resistors and the ADC's own error.

BoardADCReference1 step at the pin1 step with a 0–25 V divider
Arduino Uno, Nano, Pro Mini10-bit (0–1023)5 V (AVcc)4.88 mV24.4 mV (30 kΩ + 7.5 kΩ)
ESP3212-bit (0–4095)About 3.3 V (11 dB attenuation)About 0.8 mVAbout 9 mV (100 kΩ + 10 kΩ)
Raspberry Pi Pico12-bit (0–65535 via read_u16)3.3 V (ADC_VREF)0.81 mV8.9 mV (100 kΩ + 10 kΩ)

The voltage divider and choosing resistors for 0–25 V

The voltage you're measuring enters through the top resistor R1, the midpoint where the two resistors meet goes to the analog pin, and the bottom resistor R2 goes to GND. When choosing resistors, first decide the highest voltage, then pick a ratio that leaves the pin a little below the reference at that voltage, for example about 4.5 V on an Uno. The 30 kΩ and 7.5 kΩ pair used in ready-made modules divides by exactly 5: 25 V at the input gives 5.00 V at the pin. Don't pick values at random either: if the total resistance is too low the divider wastes current from the source you're measuring, and if it's too high the sampling capacitor inside the ADC can't charge and the reading drifts. The ATmega328P datasheet recommends that the source driving an analog input be no more than 10 kΩ; for a divider that source resistance is R1 and R2 in parallel (6 kΩ for 30 kΩ and 7.5 kΩ). You'll find the formula in detail, plus loading effects, in the voltage divider article.

RangeArduino Uno (5 V): R1 + R2Pin at full rangeESP32 / Pico (3.3 V): R1 + R2Pin at full range
0–10 V12 kΩ + 10 kΩ4.55 V33 kΩ + 10 kΩ2.33 V
0–25 V30 kΩ + 7.5 kΩ (ready-made module)5.00 V100 kΩ + 10 kΩ2.27 V
0–30 V56 kΩ + 10 kΩ4.55 V120 kΩ + 10 kΩ2.31 V
0–50 V100 kΩ + 10 kΩ4.55 V220 kΩ + 10 kΩ2.17 V
Instead of working out the ratio by hand for your own range, you can use the voltage divider calculator. In the ESP32 and Pico columns we deliberately kept the pin below 2.5 V, because the ESP32's ADC loses linearity near the top. Prefer 1% metal film resistors; two 5% resistors can shift the ratio by roughly 10% in the worst case.

Protection: zener diode, series resistor and capacitor

Even with a correctly calculated divider, a wrong probe, a spike on the power supply or accidentally touching 48 V instead of 25 V can push the pin above 5 V. The ATmega328P's pins tolerate at most the supply plus 0.5 V. Inside the pin there are protection diodes to 5 V and GND, but they only handle small currents. The good news is that the divider's top resistor already limits the current. With a 30 kΩ R1, even touching 50 V lets at most about 1.5 mA flow into the pin. To reduce that current further and absorb the excess voltage safely, you can add the following layers.

  • Series resistor: Put a 4.7–10 kΩ resistor between the divider's midpoint and the analog pin. During overvoltage, the current into the pin's internal diodes drops to the microamp range.
  • 100 nF capacitor: Connect it between the analog pin and GND. It makes up for the higher source resistance caused by the series resistor, because the ADC takes the charge it needs while sampling from this capacitor. It also filters noise and short spikes.
  • 5.1 V zener diode: Connect a zener such as the 1N4733A or BZX55C5V1 with its cathode to the pin and its anode to GND. When the voltage tries to rise above 5.1 V, the zener conducts and dumps the excess to GND.
  • Watch the zener leakage: A zener starts drawing a small current a little below its breakdown voltage, and with a high-resistance divider that current pulls the reading down. If you use a zener, keep the pin's maximum around 4 V, or use a Schottky diode (BAT85) from the pin to the 5 V rail instead.
  • Reverse polarity: If you connect the probes backwards, a negative voltage reaches the pin. The series resistor limits the current; for extra safety you can add a second Schottky diode with its anode to GND and its cathode to the pin.
This voltmeter is only for DC and low voltages (a few tens of volts at most). Never try to measure the 230 V mains with two resistors: it's potentially lethal, and it ties the Arduino's GND to the mains. For mains voltage, use a galvanically isolated transformer module such as the ZMPT101B. Also, the negative of the source you're measuring must be connected to the Arduino's GND; without a common ground the reading is meaningless.

The ready-made voltage sensor module (0–25 V)

If you'd rather not solder, you can use the small red or blue module sold as a "voltage sensor". There's nothing magic inside: just a divider made of exactly a 30 kΩ and a 7.5 kΩ resistor, a screw terminal and a three-pin header. Because the ratio is 5, you can measure 0–25 V with a 5 V Arduino, but only up to 16.5 V with a 3.3 V board. On a 3.3 V board, touching 25 V puts 5 V on the pin and damages it, so for the ESP32 and Pico it's safer to build your own divider from the table above. The module has no protection diode, so the zener or Schottky measures from the previous section apply here too.

Module pinFunctionArduino UnoESP32 (your own divider)Raspberry Pi Pico (your own divider)
VCC (screw terminal)Positive of the source to measureSource +Top end of R1Top end of R1
GND (screw terminal)Negative of the source to measureSource −Source − and GNDSource − and GND
SDivided signalA0GPIO 34 (ADC1)GP26 (ADC0, pin 31)
+Not connected on most modulesLeave unconnected--
−Module GNDGNDGNDGND (e.g. pin 33)

Step by step: your first Arduino voltmeter

  1. Connect the module's S pin to A0 and its − pin to GND. If you're building your own divider, put the 30 kΩ on the source side and the 7.5 kΩ on the GND side, and run the midpoint to A0.
  2. Plug the Arduino into USB, measure between the 5V pin and GND with a multimeter and note the value. USB voltage is often anywhere between 4.7 and 5.1 V.
  3. Enter the value you measured in the VREF constant in the code below and upload it.
  4. Connect the source you want to measure to the module's screw terminal: positive to VCC, negative to GND. A 9 V battery or an adjustable bench supply is ideal for a first test.
  5. Open the Serial Monitor at 9600 baud. Read the voltage, and at the same time measure the source with your multimeter and compare the two values.
C++ (Arduino IDE)
1// Arduino voltmetre: 0-25 V gerilim sensörü modülü / Arduino voltmeter: 0-25 V voltage sensor module
2// Modül S -> A0, modül - -> GND / module S -> A0, module - -> GND
3// Ölçülen kaynak modülün VCC ve GND klemensine / measured source goes to the module's VCC and GND terminals
4const int SENSE_PIN = A0;
5const float VREF = 5.00;       // 5V pininde ölçtüğün gerilim / voltage you measured on the 5V pin
6const float R1 = 30000.0;      // üst direnç (ohm) / top resistor (ohms)
7const float R2 = 7500.0;       // alt direnç (ohm) / bottom resistor (ohms)
8const float CAL = 1.000;       // kalibrasyon: gerçek / okunan / calibration: real / measured
9const int SAMPLES = 32;        // ortalama için örnek sayısı / samples to average
10
11float readVoltage() {
12  analogRead(SENSE_PIN);                    // ilk okumayı at / discard the first reading
13  unsigned long sum = 0;
14  for (int i = 0; i < SAMPLES; i++) {
15    sum += analogRead(SENSE_PIN);
16    delayMicroseconds(500);
17  }
18  float adc = (float)sum / SAMPLES;         // 0-1023 arası ortalama / average between 0 and 1023
19  float vPin = adc * VREF / 1024.0;         // pindeki gerilim / voltage at the pin
20  return vPin * (R1 + R2) / R2 * CAL;       // bölücüden önceki gerilim / voltage before the divider
21}
22
23void setup() {
24  Serial.begin(9600);
25}
26
27void loop() {
28  float v = readVoltage();
29  if (v < 0.05) v = 0.0;                    // boşta gürültüyü sıfırla / zero the noise when idle
30  Serial.print(F("Gerilim / Voltage: "));
31  Serial.print(v, 2);
32  Serial.println(F(" V"));
33  delay(500);
34}

The code averages 32 readings each time, so the ADC's ±1-step jitter largely disappears and the last digit settles down. The first reading is discarded because the ADC's sampling capacitor still holds charge from the previous channel. Rounding anything below 0.05 V to zero stops random values such as 0.02–0.04 V from appearing when nothing is connected. Remember that the resolution is 24.4 mV: even though you see two decimals, the last digit moves in steps of 0.02–0.03 V. If you need finer resolution, narrow the range; with a 0–10 V divider one step drops to 10.7 mV.

Reference voltage and calibration

People often say Arduino voltmeters "measure wrong", but the real source of the error is usually the reference. If the code says 5.00 V while the board gets 4.80 V from USB, every reading is 4% high: a 12.00 V battery shows as 12.50 V. This voltage changes from a desktop to a laptop and from one USB cable to another, and when you power the board from an external adapter its regulator takes over and the value is different again. There are three ways to fix this: measure the 5V pin and write it in the code, use a fixed internal reference, or recalculate the board's own supply voltage at every measurement.

MethodCodeAdvantageWatch out for
Default (AVcc)analogReference(DEFAULT)No setup, full 0–5 V rangeWhatever the supply does, the reading does too
Internal 1.1 V referenceanalogReference(INTERNAL)Stable and independent of the supplyVaries from 1.0 to 1.2 V between boards; the divider ratio must be about 23 (e.g. 220 kΩ + 10 kΩ)
External reference (AREF)analogReference(EXTERNAL)Best accuracy with a precision reference such as the LM4040This line must run before you apply a voltage to AREF, or it shorts against the internal reference
Supply measurement (readVcc)The code belowNo extra parts, stays accurate when USB or the battery changesOnly works on AVR boards (Uno, Nano, Pro Mini); calibrate the 1.1 V value once
C++ (Arduino IDE)
1// Kendi besleme gerilimini ölçen voltmetre (ATmega328P: Uno, Nano, Pro Mini)
2// Voltmeter that measures its own supply (ATmega328P: Uno, Nano, Pro Mini)
3const int SENSE_PIN = A0;
4const float R1 = 30000.0;      // üst direnç / top resistor
5const float R2 = 7500.0;       // alt direnç / bottom resistor
6long bandgapMv = 1100;         // iç referans (mV), kalibrasyonla düzelt / internal reference (mV), fix by calibration
7
8long readVccMv() {
9  // Referans AVcc, ölçülen kanal iç 1,1 V / reference AVcc, measured channel is the internal 1.1 V
10  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
11  delay(2);                                  // referans otursun / let the reference settle
12  ADCSRA |= _BV(ADSC);                       // dönüşümü başlat / start a conversion
13  while (bit_is_set(ADCSRA, ADSC)) {}        // bitmesini bekle / wait until it finishes
14  unsigned int raw = ADCW;                   // 10 bit sonuç / 10-bit result
15  return bandgapMv * 1024L / raw;            // Vcc = 1,1 V x 1024 / okunan / Vcc = 1.1 V x 1024 / reading
16}
17
18float readVoltage(float vcc) {
19  analogRead(SENSE_PIN);                     // kanal değişti, ilk okumayı at / channel changed, discard first
20  unsigned long sum = 0;
21  for (int i = 0; i < 32; i++) sum += analogRead(SENSE_PIN);
22  float vPin = (sum / 32.0) * vcc / 1024.0;
23  return vPin * (R1 + R2) / R2;
24}
25
26void setup() {
27  Serial.begin(9600);
28}
29
30void loop() {
31  float vcc = readVccMv() / 1000.0;
32  float v = readVoltage(vcc);
33  Serial.print(F("Vcc: "));
34  Serial.print(vcc, 3);
35  Serial.print(F(" V   Giris / Input: "));
36  Serial.print(v, 2);
37  Serial.println(F(" V"));
38  delay(500);
39}
  1. Calibrate the internal reference: Run the readVcc code and compare the Vcc value in the Serial Monitor with what your multimeter reads on the 5V pin. New bandgapMv = 1100 × measured / displayed. For example, if the board shows 5.12 V and the multimeter reads 4.98 V, enter 1100 × 4.98 / 5.12 ≈ 1070.
  2. Single-point calibration: Connect a known voltage near the top of the range (e.g. 12 V). CAL = multimeter value / Arduino value. This factor also corrects the ratio error from resistor tolerance.
  3. Two-point calibration: If you see a constant offset at low voltages, measure at two points, one low (e.g. 2 V) and one high (e.g. 20 V). Slope = (M2 − M1) / (A2 − A1), offset = M1 − slope × A1; the result is real = slope × reading + offset (M is the multimeter value, A the Arduino value).
  4. Verify: After calibrating, check the result with a third voltage in the middle of the range. A carefully built Uno voltmeter measures 0–25 V with less than 1% error.

A digital voltmeter with an LCD

I2C 16x2 LCD display
Thanks to the I2C backpack, the 16x2 LCD connects with just four wires: GND, VCC, SDA and SCL.

To make the voltmeter independent of the computer, simply print the value on an I2C LCD. Connect the LCD's GND to GND, VCC to 5V, SDA to A4 and SCL to A5. Install the LiquidCrystal I2C library (Frank de Brabander) from the Library Manager. The display's address is usually 0x27, or 0x3F on some modules. The code below shows the live voltage on the top line and the lowest and highest values seen since power-up on the bottom line, so you can also catch the voltage dip on a battery while a motor is running.

C++ (Arduino IDE)
1// LCD ekranlı dijital voltmetre (0-25 V, min/maks) / Digital voltmeter with LCD (0-25 V, min/max)
2// LCD: SDA -> A4, SCL -> A5, VCC -> 5V, GND -> GND; sensör S -> A0 / sensor S -> A0
3#include <Wire.h>
4#include <LiquidCrystal_I2C.h>
5
6LiquidCrystal_I2C lcd(0x27, 16, 2);   // adres 0x27 ya da 0x3F / address 0x27 or 0x3F
7
8const int SENSE_PIN = A0;
9const float VREF = 5.00;              // 5V pininde ölçülen / measured on the 5V pin
10const float RATIO = (30000.0 + 7500.0) / 7500.0;   // = 5,0
11const float CAL = 1.000;              // kalibrasyon katsayısı / calibration factor
12
13float vMin = 99.99;
14float vMax = 0.0;
15
16float readVoltage() {
17  analogRead(SENSE_PIN);
18  unsigned long sum = 0;
19  for (int i = 0; i < 32; i++) sum += analogRead(SENSE_PIN);
20  float v = (sum / 32.0) * VREF / 1024.0 * RATIO * CAL;
21  return v < 0.05 ? 0.0 : v;
22}
23
24void printField(float value) {
25  char buf[8];
26  dtostrf(value, 5, 2, buf);          // sabit genişlik: " 9.87" / fixed width: " 9.87"
27  lcd.print(buf);
28}
29
30void setup() {
31  lcd.init();
32  lcd.backlight();
33  lcd.print(F("Voltmetre 0-25V"));
34  delay(1000);
35  lcd.clear();
36}
37
38void loop() {
39  float v = readVoltage();
40  if (v > 0.5) {                      // boştayken min/maks güncelleme / don't update min/max when idle
41    if (v < vMin) vMin = v;
42    if (v > vMax) vMax = v;
43  }
44
45  lcd.setCursor(0, 0);
46  lcd.print(F("V: "));
47  printField(v);
48  lcd.print(F(" V      "));
49
50  lcd.setCursor(0, 1);
51  lcd.print(F("L"));
52  printField(vMin > 99.0 ? 0.0 : vMin);
53  lcd.print(F("  H"));
54  printField(vMax);
55  lcd.print(F("  "));
56  delay(300);
57}

Building the voltmeter with an ESP32

The ESP32 runs on 3.3 V and its pins can't take more than 3.6 V, so use a 100 kΩ + 10 kΩ divider for 0–25 V: 25 V at the input gives 2.27 V at the pin. Connect the divider to one of the ADC1 pins (GPIO 32–39); ADC2 pins don't work while Wi-Fi is on. The ESP32's ADC is 12-bit but not linear at the ends: it barely sees anything below 0.15 V, and its error grows above 2.5 V. The analogReadMilliVolts() function uses calibration data written into the chip at the factory and gives a far more accurate millivolt result than the raw value. To remove the remaining offset, the code applies the two-point calibration from the previous section with the GAIN and OFFSET constants.

C++ (Arduino IDE)
1// ESP32 voltmetre: 100k + 10k bölücü ile 0-25 V / ESP32 voltmeter: 0-25 V with a 100k + 10k divider
2// Kaynak + -> 100k -> GPIO 34 -> 10k -> GND, kaynak - -> GND / source + -> 100k -> GPIO 34 -> 10k -> GND
3const int SENSE_PIN = 34;                 // ADC1_CH6, Wi-Fi açıkken de çalışır / works with Wi-Fi on
4const float RATIO = (100000.0 + 10000.0) / 10000.0;   // = 11,0
5const float GAIN = 1.000;                 // iki noktalı kalibrasyon: eğim / two-point calibration: slope
6const float OFFSET = 0.00;                // iki noktalı kalibrasyon: kayma (V) / two-point calibration: offset (V)
7
8float readVoltage() {
9  uint32_t mv = 0;
10  for (int i = 0; i < 64; i++) {
11    mv += analogReadMilliVolts(SENSE_PIN);   // fabrika kalibrasyonlu mV / factory-calibrated mV
12  }
13  float vPin = mv / 64.0 / 1000.0;           // pindeki gerilim (V) / voltage at the pin (V)
14  float v = vPin * RATIO * GAIN + OFFSET;
15  return v < 0.10 ? 0.0 : v;
16}
17
18void setup() {
19  Serial.begin(115200);
20  analogReadResolution(12);                  // 0-4095
21}
22
23void loop() {
24  float v = readVoltage();
25  Serial.printf("Gerilim / Voltage: %.2f V\n", v);
26  delay(500);
27}

Building the voltmeter with a Raspberry Pi Pico (MicroPython)

On the Raspberry Pi Pico the analog inputs are GP26, GP27 and GP28. Connect the same 100 kΩ + 10 kΩ divider as on the ESP32 to GP26. In MicroPython, read_u16() returns the 12-bit result scaled to 0–65535. The Pico's ADC reference is filtered from the 3.3 V rail; for better accuracy, measure between ADC_VREF (pin 35) and GND with a multimeter and enter the value in VREF. There's a bonus in the code too: on the standard Pico, GP29 (ADC3) is wired to an internal divider that divides the board's VSYS supply input by 3. So without any extra parts you can also read the voltage of the battery powering the Pico. On the Pico W and Pico 2 W, GP29 is shared with the wireless chip, so this line doesn't work directly on those boards.

MicroPython
1# Raspberry Pi Pico voltmetre (MicroPython) / Raspberry Pi Pico voltmeter (MicroPython)
2# Kaynak + -> 100k -> GP26 -> 10k -> GND / source + -> 100k -> GP26 -> 10k -> GND
3from machine import ADC
4import time
5
6sense = ADC(26)            # GP26 = ADC0 (pin 31)
7vsys = ADC(29)             # GP29 = VSYS / 3 (yalnız standart Pico / standard Pico only)
8VREF = 3.3                 # ADC_VREF; multimetreyle ölçüp düzelt / measure and correct with a multimeter
9RATIO = (100000 + 10000) / 10000    # = 11.0
10GAIN = 1.000               # iki noktalı kalibrasyon: eğim / two-point calibration: slope
11OFFSET = 0.00              # iki noktalı kalibrasyon: kayma (V) / two-point calibration: offset (V)
12
13
14def pin_voltage(adc, samples=64):
15    total = 0
16    for _ in range(samples):
17        total += adc.read_u16()            # 0-65535
18    return total / samples * VREF / 65535  # pindeki gerilim / voltage at the pin
19
20
21while True:
22    v_in = pin_voltage(sense) * RATIO * GAIN + OFFSET
23    if v_in < 0.05:
24        v_in = 0.0
25    v_sys = pin_voltage(vsys) * 3          # VSYS = GP29 x 3
26    print("Giris / Input: {:.2f} V   VSYS: {:.2f} V".format(v_in, v_sys))
27    time.sleep(0.5)

Adding current to make a wattmeter: ACS712 or INA219

ACS712 current sensor module
The ACS712 measures current using the Hall effect and keeps the measured circuit electrically isolated from the Arduino.

When you add current measurement to a voltmeter, you also get power: P = V × I. If a 12 V battery supplies 1.5 A, the power is 18 W. Add the power up over time and you get energy: an 18 W load running for 2 hours uses 36 Wh. There are two common options. The ACS712 has an analog output, also measures AC current and provides isolation; it simply becomes a second analog channel on your existing voltmeter. The INA219 is a DC power monitor that measures voltage and current in the same chip with 12-bit precision and talks over I2C; on its own it's a ready-made wattmeter.

FeatureVoltage module + ACS712 (5A)INA219 module
Voltage range0–25 V (set by the divider)0–26 V (inside the chip)
Current range±5 A±3.2 A (with a 0.1 Ω shunt)
Current resolutionAbout 26 mA on an Uno0.1 mA
AC currentYesNo
ConnectionTwo analog pinsI2C (SDA, SCL), address 0x40
Best forMotors, mains-side current and where isolation is neededSmall DC power measurements such as batteries, solar panels and USB devices
C++ (Arduino IDE)
1// INA219 + I2C LCD ile wattmetre: V, A, W ve Wh / Wattmeter with INA219 + I2C LCD: V, A, W and Wh
2// Kaynak + -> INA219 VIN+ ; INA219 VIN- -> yük + ; yük - -> kaynak - -> Arduino GND
3// Source + -> INA219 VIN+ ; INA219 VIN- -> load + ; load - -> source - -> Arduino GND
4#include <Wire.h>
5#include <Adafruit_INA219.h>
6#include <LiquidCrystal_I2C.h>
7
8Adafruit_INA219 ina219;               // varsayılan adres 0x40 / default address 0x40
9LiquidCrystal_I2C lcd(0x27, 16, 2);
10
11float energyWh = 0.0;
12unsigned long lastMs = 0;
13
14void printField(float value, int width, int decimals) {
15  char buf[10];
16  dtostrf(value, width, decimals, buf);
17  lcd.print(buf);
18}
19
20void setup() {
21  Serial.begin(9600);
22  lcd.init();
23  lcd.backlight();
24  if (!ina219.begin()) {              // modül bulunamadı / module not found
25    lcd.print(F("INA219 yok!"));
26    while (true) delay(10);
27  }
28  // ina219.setCalibration_16V_400mA();  // küçük akımlar için daha hassas / finer for small currents
29  lastMs = millis();
30}
31
32void loop() {
33  float busV = ina219.getBusVoltage_V();           // VIN- ile GND arası / between VIN- and GND
34  float shuntMv = ina219.getShuntVoltage_mV();     // şönt üzerindeki düşüm / drop across the shunt
35  float loadV = busV + shuntMv / 1000.0;           // kaynağın gerilimi / source voltage
36  float currentA = ina219.getCurrent_mA() / 1000.0;
37  float powerW = loadV * currentA;
38
39  unsigned long now = millis();
40  energyWh += powerW * (now - lastMs) / 3600000.0; // W x saat = Wh / W x hours = Wh
41  lastMs = now;
42
43  lcd.setCursor(0, 0);
44  printField(loadV, 5, 2);  lcd.print(F("V "));
45  printField(currentA, 6, 3); lcd.print(F("A  "));
46  lcd.setCursor(0, 1);
47  printField(powerW, 6, 2); lcd.print(F("W "));
48  printField(energyWh, 6, 2); lcd.print(F("Wh"));
49
50  Serial.print(loadV, 2);    Serial.print(F(" V, "));
51  Serial.print(currentA, 3); Serial.print(F(" A, "));
52  Serial.print(powerW, 2);   Serial.print(F(" W, "));
53  Serial.print(energyWh, 3); Serial.println(F(" Wh"));
54  delay(500);
55}

The INA219 works out the current from the small voltage drop across the 0.1 Ω shunt resistor between VIN+ and VIN−, so the module goes in series on the positive side of the source (high side). Because the bus voltage is measured between VIN− and GND, the code adds the shunt drop to get the source's real voltage. The default calibration is for 32 V and 2 A; if you'll only measure a few hundred milliamps, enable the setCalibration_16V_400mA() line to narrow the range to 400 mA and improve accuracy at small currents. Energy is found by adding up the current power multiplied by the time elapsed in each loop; it resets when you switch the Arduino off, so to keep it, write the value to EEPROM at intervals. For code that does the same calculation with the ACS712, see the power and energy section of the ACS712 article.

Battery tester: a voltmeter that measures capacity (mAh)

18650 Li-ion cell
The capacity printed on an 18650 cell and its real capacity often differ; the tester reveals the truth.

A battery's voltage doesn't tell you exactly how much energy is left in it; a full but worn-out cell can also show 4.1 V. The way to find the real capacity is to discharge the battery through a known load and add up the current over time: mAh = Σ (current × time). In this circuit a MOSFET switches the load on and off, and the Arduino measures the battery voltage and the voltage at the bottom of the load resistor once a second. The difference between the two, divided by the load resistance, gives the current. When the battery reaches its cut-off voltage the MOSFET turns off and the result is printed. At the start, the code also compares the battery's unloaded and loaded voltage to calculate its internal resistance; the higher the internal resistance, the more the battery sags at high current.

PartConnectionNote
Battery +To A0 through 10 kΩ; also the top of the load resistorPut 100 nF between A0 and GND
4.7 Ω load resistorBetween battery + and the MOSFET drainAt least 5 W (preferably 10 W) cement resistor; about 0.9 A flows with an 18650 and it gets hot
MOSFET drainBottom of the load resistor; to A1 through 10 kΩPut 100 nF between A1 and GND as well
MOSFET sourceGNDThe battery negative connects here too
MOSFET gateTo D9 through 220 Ω; to GND through 100 kΩUse a logic-level MOSFET such as the IRLZ44N
C++ (Arduino IDE)
1// Pil kapasite test cihazı: mAh, Wh ve iç direnç / Battery capacity tester: mAh, Wh and internal resistance
2// Pil + -> 10k -> A0 ; pil + -> 4,7 ohm yük -> MOSFET drain ; drain -> 10k -> A1
3// Battery + -> 10k -> A0 ; battery + -> 4.7 ohm load -> MOSFET drain ; drain -> 10k -> A1
4// MOSFET source -> GND ; gate <- D9 (220 ohm), gate -> 100k -> GND ; pil - / battery - -> GND
5const int BAT_PIN = A0;
6const int DRAIN_PIN = A1;
7const int LOAD_PIN = 9;
8const float VREF = 5.00;        // 5V pininde ölçülen / measured on the 5V pin
9const float R_LOAD = 4.7;       // yük direnci, multimetreyle ölç / load resistor, measure it
10const float CUTOFF_V = 3.00;    // Li-ion 3,00 V ; alkalin AA / alkaline AA 0,90 V
11
12float mAh = 0.0;
13float wh = 0.0;
14unsigned long lastMs = 0;
15unsigned long startMs = 0;
16bool running = false;
17
18float readPin(int pin) {
19  analogRead(pin);                          // kanal değişti, ilk okumayı at / discard first reading
20  unsigned long sum = 0;
21  for (int i = 0; i < 32; i++) sum += analogRead(pin);
22  return (sum / 32.0) * VREF / 1024.0;
23}
24
25float loadCurrent(float vBat) {
26  return (vBat - readPin(DRAIN_PIN)) / R_LOAD;   // dirençteki gerilim / R / voltage across R / R
27}
28
29void setup() {
30  Serial.begin(9600);
31  pinMode(LOAD_PIN, OUTPUT);
32  digitalWrite(LOAD_PIN, LOW);              // yük kapalı / load off
33  delay(1000);
34
35  float vOpen = readPin(BAT_PIN);           // yüksüz gerilim / open-circuit voltage
36  if (vOpen < CUTOFF_V + 0.1) {
37    Serial.println(F("Pil zayif ya da takili degil / Battery low or missing"));
38    return;
39  }
40  digitalWrite(LOAD_PIN, HIGH);             // yükü aç / load on
41  delay(1000);
42  float vLoad = readPin(BAT_PIN);
43  float amps = loadCurrent(vLoad);
44  Serial.print(F("Bos / Open: "));
45  Serial.print(vOpen, 3);
46  Serial.print(F(" V  Yuklu / Loaded: "));
47  Serial.print(vLoad, 3);
48  Serial.print(F(" V  Ic direnc / Internal R: "));
49  Serial.print((vOpen - vLoad) / amps * 1000.0, 0);
50  Serial.println(F(" mOhm"));
51
52  running = true;
53  startMs = lastMs = millis();
54}
55
56void loop() {
57  if (!running) return;
58  unsigned long now = millis();
59  if (now - lastMs < 1000) return;          // saniyede bir ölç / measure once per second
60
61  float hours = (now - lastMs) / 3600000.0;
62  lastMs = now;
63  float vBat = readPin(BAT_PIN);
64  float amps = loadCurrent(vBat);
65  mAh += amps * 1000.0 * hours;             // akım x süre / current x time
66  wh += vBat * amps * hours;
67
68  Serial.print((now - startMs) / 1000);
69  Serial.print(F(" s  "));
70  Serial.print(vBat, 3);
71  Serial.print(F(" V  "));
72  Serial.print(amps, 3);
73  Serial.print(F(" A  "));
74  Serial.print(mAh, 0);
75  Serial.println(F(" mAh"));
76
77  if (vBat <= CUTOFF_V) {                   // kesme gerilimi / cut-off voltage
78    digitalWrite(LOAD_PIN, LOW);
79    running = false;
80    Serial.print(F("Bitti / Done: "));
81    Serial.print(mAh, 0);
82    Serial.print(F(" mAh, "));
83    Serial.print(wh, 2);
84    Serial.println(F(" Wh"));
85  }
86}

With a 4.7 Ω load, an 18650 cell discharges at a current that starts around 0.9 A and falls towards 0.65 A; testing a healthy 2500 mAh cell takes about three hours. For Li-ion, never set the cut-off below 3.0 V, or the cell gets damaged. If you're testing an alkaline AA battery, set CUTOFF_V to 0.9 V; it draws about 0.3 A with the same load. You can compare the result with the label and with the battery life calculator. A new 18650's internal resistance measured this way, including the holder and wires, is usually below 0.15 Ω; a cell above 0.3 Ω shouldn't be used for high-current jobs. To understand capacity units, have a look at the mAh and battery capacity article.

Never short-circuit Li-ion cells, and don't place the load resistor on a flammable surface; a resistor dissipating around 4 W can reach 80–100 °C. Don't leave the test unattended, and never test swollen or dented cells.

Project ideas with an Arduino voltmeter

  • Battery monitor and low-voltage alarm: Watch the 12 V battery in a camper or solar setup and sound a buzzer when it drops below 11.8 V.
  • Solar panel logger: Write the panel's voltage, and its current via the INA219, to an SD card every minute and work out the daily yield in Wh.
  • Bench supply readout: Add a voltmeter and an INA219 to the output of an LM2596 module to turn it into a bench power supply.
  • Remote battery monitoring: Send the voltage you measure with an ESP32 to Home Assistant over MQTT, or get a Telegram message when it's low.
  • 18650 pack check: Test the capacity of the cells you charge with a TP4056, then match cells of similar capacity to build a balanced pack.
  • Big-digit display: Show the value on a 7-segment display or an OLED display instead of the LCD, large enough to read from across the room.

Common mistakes and how to fix them

SymptomLikely causeFix
Reading is always 2–5% high or lowVREF is 5.00 V in the code but the board runs on a different voltageMeasure the 5V pin and enter it in VREF, or use the readVcc method
Value keeps jumping aroundThe source negative isn't tied to the Arduino GND, or the line is noisyConnect a common GND, average readings, add 100 nF at the pin
Stuck at the same value at high voltageThe pin reached the reference and the ADC saturated at 1023Widen the range by increasing the divider ratio
Reading bends near the top and isn't linearZener leakage, or the top end of the ESP32 ADCKeep the pin below 4 V (2.5 V on the ESP32) or use a Schottky
On the ESP32 the reading is 0 or frozen once Wi-Fi startsThe divider is on an ADC2 pinUse one of the ADC1 pins, GPIO 32–39
INA219 shows negative currentVIN+ and VIN− are swappedSource positive goes to VIN+, load positive to VIN−
LCD is lit but shows no textWrong I2C address or contrast settingFind the address with an I2C scanner and turn the trimmer on the back
Before connecting a newly built divider to the Arduino, switch on the source and measure the midpoint voltage with a multimeter. If it's close to what you calculated and below the reference, it's safe to connect it to the pin. This ten-second check stops two swapped resistors from frying your board.

An Arduino voltmeter is one of the best ways to learn how measurement works: with two resistors, one formula and a little calibration you get results close to a multimeter's; add a current sensor and it becomes a wattmeter, add a MOSFET and it becomes a battery tester. In the BlueGrays app, the "Digital Voltmeter with LCD" project lets you build the same circuit step by step with a wiring diagram and ready-made code, and from there you can move on to battery monitoring and power measurement projects.

Frequently asked questions

How many volts can an Arduino measure?

You can connect at most 5 V (3.3 V on the ESP32 and Pico) directly to an analog pin. With a voltage divider you can raise that limit as far as you like: 30 kΩ + 7.5 kΩ measures 25 V and 100 kΩ + 10 kΩ measures 50 V. Above a few tens of volts, however, safety and insulation become important, and mains voltage must never be measured this way.

Why is my Arduino voltmeter reading wrong?

The most common reason is the reference voltage: the code says 5.00 V but the board gets 4.7–4.9 V from USB, so every reading shifts by the same ratio. Other causes are 5% resistors, a missing common GND and a divider with too much resistance. Measuring the 5V pin and entering it in the code, or using the readVcc method, and then setting the CAL factor with a known voltage fixes it.

Can I use the 0–25 V voltage sensor module with an ESP32?

You can, but carefully. Because the module's ratio is 5, an ESP32 can only measure up to 16.5 V with it; touch 25 V and 5 V reaches the pin, damaging the ESP32. Building your own 100 kΩ + 10 kΩ divider for the ESP32 is both safer and more accurate, since it keeps the ADC in its linear region.

Can an Arduino measure 230 V AC mains voltage?

Never with a voltage divider: it's potentially lethal and connects the Arduino to the mains. For mains voltage, use a module such as the ZMPT101B, which contains a small transformer and provides galvanic isolation. Its output is AC, so the code calculates the RMS value.

Can an Arduino measure negative voltage?

Not directly; an analog pin can't read below GND and may be damaged. The fix is to shift the signal up towards 5 V with resistors: for example, two equal resistors between the signal and 5 V create a midpoint that maps −5 V to +5 V onto 0–5 V. For more precise work, an op-amp level shifter is used.

Can an Arduino voltmeter be as accurate as a multimeter?

A calibrated Uno voltmeter measures 0–25 V with less than 1% error, which is enough for most hobby work and close to a cheap multimeter. Because of the 10-bit ADC, resolution is limited to about 24 mV on a 25 V range. For better results you can use a 16-bit external ADC such as the ADS1115, 0.1% resistors and a precision reference.

See all projects
Measuring Light Intensity with an LDR
#11Beginner

Measuring Light Intensity with an LDR

Want to see how bright your room is as a number? The LDR, a small part that reacts to light, measures the light around it, and the Arduino writes the value to your computer screen every half second. C

4.6(81)
139
Reading Light Intensity with the ESP32 LDR Module
#113Beginner

Reading Light Intensity with the ESP32 LDR Module

Real-time monitoring of the LDR module's raw ADC value and relative light percentage calculated from dark and light reference values in the Serial Monitor. This percentage is not a lux measurement. Teaches the module’s calibration logic. It is ideal to complete this before Project 10, as it is used to determine the threshold value.

4.6(36)
55
Digital Voltmeter with LCD Display
#56Intermediate
Premium

Digital Voltmeter with LCD Display

Find out how much voltage a battery or power adapter gives with a meter you built yourself. A divider made of two resistors brings high voltage down to a level the board can read, and the Arduino calc

4.5(27)
36
Temperature Measurement with NTC and 2x16 Lcd Display
#44Beginner
Premium

Temperature Measurement with NTC and 2x16 Lcd Display

Measure the temperature around you with a sensor that looks like a tiny bead. This part, called an NTC, lets electricity through more easily as it warms up; the Arduino reads that change, turns it int

4.5(33)
44
Using LCD Screen with I2C
#32Beginner
Premium

Using LCD Screen with I2C

Give your Arduino its own screen and make it write its first message. The two-line LCD screen hooks up with just a few wires; a greeting appears on the top line, then a second message on the bottom li

4.8(53)
70
ESP32 I2C LCD Display (16x2)
#119Intermediate
Premium

ESP32 I2C LCD Display (16x2)

In this project, we display text by connecting a 16x2 character LCD screen to the ESP32 via the I2C protocol. Thanks to I2C, we can control the screen using just two data pins (SDA and SCL), eliminating the waste of 6+ pins associated with a classic parallel connection. The code prints a fixed message on the first line and a counter that increments every second on the second line. Additionally, we run an I2C scanner at the start to print the display’s address to the Serial Monitor (typically 0x2

4.9(23)
40
Measuring Distance Using LCD Display
#31Advanced
Premium

Measuring Distance Using LCD Display

A tool that measures distance without a ruler: move your hand back and forth in front of the sensor and watch the number change. The HC-SR04 sends out a sound wave and listens for its echo, and the Ar

4.6(45)
59
ESP32 LED Brightness Control with a Potentiometer (Analog → PWM)
#115Intermediate
Premium

ESP32 LED Brightness Control with a Potentiometer (Analog → PWM)

In this project, we control the brightness of an LED analogically using a potentiometer. The ESP32's ADC pin reads the 0-3.3V voltage from the potentiometer, and this value is then converted into a PWM (Pulse Width Modulation) signal and sent to the LED. As a result, as you turn the potentiometer, the LED gradually brightens and dims. With Arduino Core 3.0+, analogWrite() can be used directly; on the ESP32-S2/S3, the ADC resolution is fixed at 12-bit.

4.5(19)
32