Skip to content
Buy 10+ on select items — save 10% auto-applied
Free US shipping on orders $35+
Order by 3pm ET — ships same-day from the US
Skip to main content

Arduino Nano Relay: Measure 18650 Capacity in mAh | ShillehTek

September 13, 2026 9 views

Arduino Nano Relay: Measure 18650 Capacity in mAh | ShillehTek
Project

Build an Arduino Nano 18650 capacity tester using a relay and power resistor to log real mAh and mWh with LCD cutoff at 3.0 V from ShillehTek.

1 hr Intermediate8 parts

Project Overview

18650 Battery Capacity Tester: Build an Arduino Nano battery tester that uses a 5V relay module and a power resistor to discharge an 18650 cell, then calculates real capacity in mAh and mWh on a 16x2 LCD.

The number printed on a lithium cell is a promise; the number this tester prints is the truth. It discharges a charged 18650 through a known resistor, reads the voltage every half second, integrates current over time, and cuts the load off with a relay the moment the cell reaches 3.0 V, leaving you with the real milliamp-hours (and milliwatt-hours) on a 16x2 LCD. It is the tool that sorts a bag of salvaged laptop cells into keepers and recyclers.

  • Time: ~1 hour (plus 2 to 3 hours per test)
  • Skill level: Intermediate
  • What you will build: A push-to-start constant-resistance discharge tester with live voltage, current and elapsed time, an automatic low-voltage cutoff, and a final mAh/mWh result.
Arduino Nano 18650 battery capacity tester showing measured mAh and mWh on a 16x2 LCD during discharge
Discharge, integrate, cut off: the real capacity in mAh.

Parts List

From ShillehTek

External

  • An 18650 cell holder with leads
  • A 4Ω power resistor rated 10 W (or two 8.2Ω 5 W resistors in parallel); it dissipates over 4 W and gets hot
  • A multimeter to measure the resistor's real value and your board's 5 V rail

Note: lithium cells are unforgiving. Never discharge below 2.8 V (this tester stops at 3.0 V), never test a swollen, dented or leaking cell, keep the resistor on a ceramic tile away from anything that melts, and never leave the first run unattended. The relay is wired so a dead Arduino means an open circuit.

Step-by-Step Guide

Step 1 - Wire the Discharge Path

Goal: Battery to relay to resistor and back to the battery.

What to do: Battery holder + to relay COM. Relay NO to one end of the 4Ω resistor. Other end of the resistor to battery holder -, and battery - to Arduino GND (shared ground is essential).

Battery + to A0 as well, so the Nano reads the cell voltage directly (a full cell is 4.2 V, safely under the 5 V ADC limit). Relay module: VCC to 5V, GND to GND, IN to D10.

Arduino Nano wired on a breadboard to a 5V relay module and 4 ohm power resistor for 18650 discharge testing with A0 measuring battery voltage
The relay sits between the cell and the load; A0 watches the cell.

Expected result: With the relay off, nothing flows. With it on, the cell drives about 1 A through the resistor.

Step 2 - Wire LCD and Button

Goal: Readout and a start button.

What to do: I2C LCD: SDA to A4, SCL to A5, VCC to 5V, GND to GND. Button between D9 and GND (internal pull-up).

Expected result: Backlight on, ready for code.

Step 3 - Calibrate Two Numbers

Goal: Improve accuracy by measuring instead of assuming.

What to do: With the multimeter, measure the resistor's actual resistance (a "4Ω" part may read 3.8 to 4.3Ω) and the Nano's 5V pin voltage while powered as it will be during the test (USB 5 V is often 4.8 to 5.1 V). Enter both as R_LOAD and VREF in the sketch. This is the difference between plus or minus 10% and plus or minus 2% results.

Expected result: Two constants that match your hardware.

Step 4 - Upload the Sketch

Goal: Run the discharge test, integrate current over time, and stop automatically at cutoff voltage.

Code:

#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);

const int RELAY = 10, BTN = 9, VBAT = A0;
const bool  RELAY_ON = LOW;      // most 1-channel modules are active-LOW; flip if yours clicks the wrong way
const float VREF     = 5.00;     // measured 5V pin voltage
const float R_LOAD   = 4.0;      // measured load resistance in ohms
const float V_CUTOFF = 3.0;      // stop here (never below 2.8 V)

enum { IDLE, RUNNING, DONE } state = IDLE;
float mAh = 0, mWh = 0;
unsigned long t0, lastStep;

float readV() {                                  // average 32 samples for a steady reading
  long s = 0; for (int i = 0; i < 32; i++) s += analogRead(VBAT);
  return s / 32.0 * VREF / 1023.0;
}

void setup() {
  pinMode(RELAY, OUTPUT); digitalWrite(RELAY, !RELAY_ON);   // load OFF
  pinMode(BTN, INPUT_PULLUP);
  lcd.init(); lcd.backlight();
}

void loop() {
  float v = readV();

  if (state == IDLE) {
    lcd.setCursor(0, 0); lcd.print("Batt "); lcd.print(v, 2); lcd.print("V      ");
    lcd.setCursor(0, 1); lcd.print(v > 3.3 ? "Press to start  " : "Charge battery  ");
    if (digitalRead(BTN) == LOW && v > 3.3) {
      mAh = 0; mWh = 0; t0 = lastStep = millis();
      digitalWrite(RELAY, RELAY_ON);             // load ON
      state = RUNNING; delay(300);
    }

  } else if (state == RUNNING) {
    unsigned long now = millis();
    if (now - lastStep >= 500) {
      float dtH = (now - lastStep) / 3600000.0;  // elapsed hours since last step
      lastStep = now;
      float i = v / R_LOAD;                      // amps through the load
      mAh += i * 1000.0 * dtH;                   // integrate current  -> mAh
      mWh += v * i * 1000.0 * dtH;               // integrate power    -> mWh
      lcd.setCursor(0, 0);
      lcd.print(v, 2); lcd.print("V "); lcd.print(i, 2); lcd.print("A ");
      lcd.print((now - t0) / 60000); lcd.print("m ");
      lcd.setCursor(0, 1);
      lcd.print((int)mAh); lcd.print("mAh "); lcd.print((int)mWh); lcd.print("mWh ");
      if (v <= V_CUTOFF) { digitalWrite(RELAY, !RELAY_ON); state = DONE; }   // load OFF
    }

  } else {                                       // DONE
    lcd.setCursor(0, 0); lcd.print("Done: "); lcd.print((int)mAh); lcd.print(" mAh   ");
    lcd.setCursor(0, 1); lcd.print((int)mWh); lcd.print(" mWh - reset ");
    if (digitalRead(BTN) == LOW) { state = IDLE; delay(300); }
  }
}

What to do: Install LiquidCrystal_I2C, upload, insert a fully charged cell, press the button.

Expected result: The relay clicks, the top line shows something like "4.08V 1.02A 0m", and the mAh count climbs. Two to three hours later the voltage reaches 3.00 V, the relay clicks off, and the LCD shows the capacity, typically 1,800 to 2,800 mAh for a healthy salvaged cell and far less for a tired one.

Step 5 - Read the Result Honestly

Goal: Understand what the capacity number represents.

What to do: Manufacturers rate cells at a gentle 0.2 C discharge down to 2.5 to 2.75 V; our ~1 A load to 3.0 V reads about 5 to 10% lower than the label on a perfect cell. Compare cells against each other with the same rig rather than against the label.

Cells that show under 60% of their rating, get warm, or drop voltage quickly under load are best recycled. Recharge keepers on the TP4056 and mark the measured mAh on the wrapper.

Expected result: A sorted, labeled set of cells you can trust in a pack.

Conclusion

This Arduino Nano relay-based 18650 capacity tester discharges a cell through a known resistor, tracks voltage over time, integrates current, and shuts off at 3.0 V to report real mAh and mWh on a 16x2 LCD. It is a practical way to evaluate and match salvaged cells using a consistent test method.

Want the exact parts used in this build? Grab them from ShillehTek.com. If you want help customizing this project or building something for your product, check out our IoT consulting services.

Photo credit and reference: Mirko Pavleski (mircemk) on Hackster.io.

Parts for this build

Everything used in this tutorial. Uncheck what you already have.

All 8 in stock
0 parts selected $0.00