Community project

Multi-Channel Battery Monitor

Arduino
Photo of Multi-Channel Battery Monitor
Generated with AI

Meherab Hoshain Apu

Published October 3, 2026

This project builds a smart charging system that monitors and manages up to three lithium-ion batteries simultaneously. Using an Arduino Uno, relay modules, and TP4056 charger boards, the system automatically cycles through charging phases while measuring voltage across each battery and displaying real-time status on a 16x2 LCD screen.

The guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions to connect the barrel jack power input, buck converter, relay-controlled chargers, voltage divider networks, and I2C LCD display. The included firmware handles charge cycle timing, voltage sampling, battery detection, and multi-page display output to track charging progress and battery health.

Wiring diagram

Wiring diagram for Multi-Channel Battery Monitor

Gather all the parts

QtyComponent
1

12V Barrel-Jack Adapter

12 V, 2 A

12 V / 2 A wall adapter with a 5.5 mm / 2.1 mm barrel jack. Used to power motor drivers, LED strips, or boards that need a higher rail.

1

24v Buck Converter

Set output to 5.0 V

LM2596-based adjustable step-down buck converter module. Commonly used to regulate a higher battery rail, such as a 2S 18650 pack, down to 5V for Arduino logic. It is a regulator, not a charger or battery protection board.

1

TP4056 Li-Ion/LiPo charger module with protection

1 A maximum; set lower for small cells

TP4056 single-cell Li-Ion/LiPo linear charger module, 5V USB input, 1A charge current (programmable). Common variants ship with DW01 protection. Pair with battery_lipo_storage for the cell.

1

TP4056 Li-Ion/LiPo charger module with protection

1 A maximum; set lower for small cells

TP4056 single-cell Li-Ion/LiPo linear charger module, 5V USB input, 1A charge current (programmable). Common variants ship with DW01 protection. Pair with battery_lipo_storage for the cell.

1

TP4056 Li-Ion/LiPo charger module with protection

1 A maximum; set lower for small cells

TP4056 single-cell Li-Ion/LiPo linear charger module, 5V USB input, 1A charge current (programmable). Common variants ship with DW01 protection. Pair with battery_lipo_storage for the cell.

1

Lithium Ion Battery - 3.7V 2000mAh

3.7 V nominal, 2000 mAh

3.7V 2000mAh lithium-ion polymer battery with JST-PH connector and protection circuitry. Output ranges from 4.2 V fully charged to 3.0 V cutoff.

1

Lithium Ion Battery - 3.7V 2000mAh

3.7 V nominal, 2000 mAh

3.7V 2000mAh lithium-ion polymer battery with JST-PH connector and protection circuitry. Output ranges from 4.2 V fully charged to 3.0 V cutoff.

1

Lithium Ion Battery - 3.7V 2000mAh

3.7 V nominal, 2000 mAh

3.7V 2000mAh lithium-ion polymer battery with JST-PH connector and protection circuitry. Output ranges from 4.2 V fully charged to 3.0 V cutoff.

1

LCD 16x2 I2C

16x2 character LCD display with I2C backpack

1

10kΩ Resistor

10 kΩ, 1/4 W

The upper resistor that makes the channel one battery voltage safe for the Arduino input.

1

10kΩ Resistor

10 kΩ, 1/4 W

The lower resistor that completes the channel one battery-voltage measuring pair.

1

10kΩ Resistor

10 kΩ, 1/4 W

The upper resistor that makes the channel two battery voltage safe for the Arduino input.

1

10kΩ Resistor

10 kΩ, 1/4 W

The lower resistor that completes the channel two battery-voltage measuring pair.

1

10kΩ Resistor

10 kΩ, 1/4 W

The upper resistor that makes the channel three battery voltage safe for the Arduino input.

1

10kΩ Resistor

10 kΩ, 1/4 W

The lower resistor that completes the channel three battery-voltage measuring pair.

1

5V 2-Channel Relay Module

A preassembled two-relay board that lets the Arduino switch two separate charger power lines.

1

5V 2-Channel Relay Module

A preassembled two-relay board that lets the Arduino switch two separate charger power lines.

Assemble it in 8 steps

1. Turn off the power first

Unplug the 12 V adapter and unplug the Uno USB cable before moving any wire. This prevents a loose wire from touching the wrong place while you work.

  • Do not connect a lithium cell backwards; reversed battery wires can damage the TP4056 board or the cell.

2. Set the 5 V converter

Connect the 12 V adapter to the buck converter VIN+ and VIN−. Use a multimeter to adjust its output to exactly 5.00 V before connecting the Uno, relay boards, LCD, or TP4056 boards.

  • An output above 5 V can damage the Uno, LCD, relay boards, and TP4056 charger boards.

3. Make one common ground

Join buck converter VOUT− to Uno GND, both relay-board GND pins, all three TP4056 IN− pins, all three TP4056 OUT− pins, and the LCD GND. These black or negative wires give the Arduino voltage readings a correct reference.

  • Keep the ground wires short and secure.

4. Wire the charger power relays

Connect buck VOUT+ to the Uno 5V pin (power), both relay-board VCC pins (power), LCD VCC (power), and the relay COM terminals (charger power). Connect relay board 1 NO1 to charger 1 IN+ (charger 1 power), relay board 1 NO2 to charger 2 IN+ (charger 2 power), and relay board 2 NO1 to charger 3 IN+ (charger 3 power). Connect relay board 1 IN1 to Uno D2 (control), IN2 to D3 (control), and relay board 2 IN1 to D4 (control).

  • Use the NO terminal, not NC, so a charger loses power when the Uno is off.
  • Leave the unused IN2, COM2, and NO2 terminals on relay board 2 disconnected.

5. Connect each battery to its own charger

Connect Battery 1 positive to charger 1 B+ (battery power) and negative to B− (battery ground). Repeat for Battery 2 with charger 2 and Battery 3 with charger 3. Do not join the three batteries together.

  • Do not use a cell that reads about 1.2 V. A normal single lithium-ion cell is normally about 3.0 V to 4.2 V; a 1.2 V reading can mean the cell is damaged or is not the correct battery type.

6. Wire the three safe voltage measurements

For each charger, connect one 10 kΩ resistor from B+ to a junction. Connect the other 10 kΩ resistor from that same junction to GND. Connect charger 1’s junction to Uno A0 (voltage reading), charger 2’s junction to A1 (voltage reading), and charger 3’s junction to A2 (voltage reading). Do not connect B+ directly to an Arduino pin.

  • B+ measures the cell itself. OUT+ can be disconnected by the TP4056 protection circuit and can give a misleading low-battery reading.

7. Connect the 16 by 2 LCD display

Connect LCD VCC to regulated 5 V (power), LCD GND to common GND (ground), LCD SDA to Uno A4 (data), and LCD SCL to Uno A5 (clock).

  • Make sure VCC and GND are not swapped — swapped power can damage the display.

8. Power and check the display cycle

Check every connection once more, then connect the Uno by USB and the 12 V adapter. The LCD shows voltage for 30 seconds, estimated percentage for 30 seconds, then charging state for 30 seconds. It charges for two minutes, turns all three relays off for three seconds while it averages A0, A1, and A2, then starts the next two-minute charge period.

  • Compare each displayed voltage with a multimeter between the same TP4056 B+ and B− terminals.
  • A channel becomes FULL at 4.20 V, or after eight minutes without at least a 0.01 V rise across successive two-minute checks.

Review all connections

1. Connections between "supply_12v" and "Arduino"

Functionsupply_12vArduino
power+12V → 24v Buck Converter VIN+EXT
groundGND → 24v Buck Converter VIN-EXT

2. Connections between "buck_5v" and "Arduino"

Functionbuck_5vArduino
powerVOUT+5V
groundVOUT-GND

3. Connections between "charger_1" and "Arduino"

Functioncharger_1Arduino
groundIN-GND
powerB+ → Lithium Ion Battery - 3.7V 2000mAh BAT+EXT
groundB- → Lithium Ion Battery - 3.7V 2000mAh BAT-EXT
groundOUT-GND
powerOUT+ → No external load connected; leave this protected output terminal unusedEXT

4. Connections between "charger_2" and "Arduino"

Functioncharger_2Arduino
groundIN-GND
powerB+ → Lithium Ion Battery - 3.7V 2000mAh BAT+EXT
groundB- → Lithium Ion Battery - 3.7V 2000mAh BAT-EXT
groundOUT-GND
powerOUT+ → No external load connected; leave this protected output terminal unusedEXT

5. Connections between "charger_3" and "Arduino"

Functioncharger_3Arduino
groundIN-GND
powerB+ → Lithium Ion Battery - 3.7V 2000mAh BAT+EXT
groundB- → Lithium Ion Battery - 3.7V 2000mAh BAT-EXT
groundOUT-GND
powerOUT+ → No external load connected; leave this protected output terminal unusedEXT

6. Connections between "divider_1_top" and "Arduino"

Functiondivider_1_topArduino
analogEnd B → 10kΩ Resistor End AEXT
analogEnd A → TP4056 Li-Ion/LiPo charger module with protection B+EXT

7. Connections between "divider_1_bottom" and "Arduino"

Functiondivider_1_bottomArduino
analogEnd AGPIO 14
groundEnd BGND

8. Connections between "divider_2_top" and "Arduino"

Functiondivider_2_topArduino
analogEnd B → 10kΩ Resistor End AEXT
analogEnd A → TP4056 Li-Ion/LiPo charger module with protection B+EXT

9. Connections between "divider_2_bottom" and "Arduino"

Functiondivider_2_bottomArduino
analogEnd AGPIO 15
groundEnd BGND

10. Connections between "divider_3_top" and "Arduino"

Functiondivider_3_topArduino
analogEnd B → 10kΩ Resistor End AEXT
analogEnd A → TP4056 Li-Ion/LiPo charger module with protection B+EXT

11. Connections between "divider_3_bottom" and "Arduino"

Functiondivider_3_bottomArduino
analogEnd AGPIO 16
groundEnd BGND

12. Connections between "lcd_1" and "Arduino"

Functionlcd_1Arduino
powerVCC5V
groundGNDGND
i2cSDAGPIO 18
i2cSCLGPIO 19

13. Connections between "relay_board_1" and "Arduino"

Functionrelay_board_1Arduino
powerVCC5V
groundGNDGND
digitalIN1GPIO 2
digitalIN2GPIO 3
powerCOM15V
powerNO1 → TP4056 Li-Ion/LiPo charger module with protection IN+EXT
powerCOM25V
powerNO2 → TP4056 Li-Ion/LiPo charger module with protection IN+EXT

14. Connections between "relay_board_2" and "Arduino"

Functionrelay_board_2Arduino
powerVCC5V
groundGNDGND
digitalIN1GPIO 4
powerCOM15V
powerNO1 → TP4056 Li-Ion/LiPo charger module with protection IN+EXT

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <stdio.h>

const uint8_t RELAY_1_PIN = 2;
const uint8_t RELAY_2_PIN = 3;
const uint8_t RELAY_3_PIN = 4;
const uint8_t BATTERY_1_PIN = A0;
const uint8_t BATTERY_2_PIN = A1;
const uint8_t BATTERY_3_PIN = A2;

// Measure the Uno 5 V pin with a multimeter and adjust this value if needed.
const float ADC_REFERENCE_VOLTS = 5.00f;
const float DIVIDER_RATIO = 2.0f;       // two equal 10 kΩ resistors
const float FULL_VOLTAGE = 4.20f;
const float DISCONNECTED_VOLTAGE = 0.20f;
const float MEANINGFUL_RISE_VOLTS = 0.01f;
const unsigned long CHARGE_TIME_MS = 120000UL;
const unsigned long REST_AND_SAMPLE_TIME_MS = 3000UL;
const unsigned long NO_RISE_FULL_TIME_MS = 480000UL;
const unsigned long SAMPLE_GAP_MS = 100UL;
const unsigned long DISPLAY_UPDATE_MS = 500UL;
const unsigned long PAGE_TIME_MS = 30000UL;

// Most 5 V relay boards are active LOW. Reverse these two values only if your
// particular board turns a relay on when its IN pin is HIGH.
const uint8_t RELAY_ON = LOW;
const uint8_t RELAY_OFF = HIGH;

// Standard 16x2 I2C backpack address for the selected LCD.
LiquidCrystal_I2C lcd(0x27, 16, 2);
const uint8_t relayPins[3] = {RELAY_1_PIN, RELAY_2_PIN, RELAY_3_PIN};
const uint8_t batteryPins[3] = {BATTERY_1_PIN, BATTERY_2_PIN, BATTERY_3_PIN};

enum CyclePhase { CHARGING, RESTING_AND_SAMPLING };
enum DisplayPage { VOLTAGE_PAGE, PERCENT_PAGE, STATUS_PAGE };
CyclePhase phase = CHARGING;
DisplayPage displayPage = VOLTAGE_PAGE;
bool chargeComplete[3] = {false, false, false};
bool batteryMissing[3] = {false, false, false};
bool haveBaseline[3] = {false, false, false};
float batteryVolts[3] = {0.0f, 0.0f, 0.0f};
float lastRisingVoltage[3] = {0.0f, 0.0f, 0.0f};
float sampleSum[3] = {0.0f, 0.0f, 0.0f};
uint16_t sampleCount = 0;
unsigned long phaseStartedMs = 0;
unsigned long pageStartedMs = 0;
unsigned long lastSampleMs = 0;
unsigned long lastRiseMs[3] = {0UL, 0UL, 0UL};
unsigned long lastDisplayMs = 0;

float readBatteryVoltage(uint8_t analogPin) {
  analogRead(analogPin); // discard the first reading after changing channels
  unsigned long total = 0;
  for (uint8_t i = 0; i < 8; ++i) total += analogRead(analogPin);
  return ((total / 8.0f) * ADC_REFERENCE_VOLTS / 1023.0f) * DIVIDER_RATIO;
}

void setRelay(uint8_t channel, bool enableCharging) {
  digitalWrite(relayPins[channel], enableCharging ? RELAY_ON : RELAY_OFF);
}

uint8_t batteryPercent(float volts) {
  if (volts < 3.00f) return 0;
  if (volts >= FULL_VOLTAGE) return 100;
  if (volts >= 4.10f) return (uint8_t)(90.0f + (volts - 4.10f) * 100.0f);
  if (volts >= 4.00f) return (uint8_t)(75.0f + (volts - 4.00f) * 150.0f);
  if (volts >= 3.90f) return (uint8_t)(55.0f + (volts - 3.90f) * 200.0f);
  if (volts >= 3.80f) return (uint8_t)(35.0f + (volts - 3.80f) * 200.0f);
  if (volts >= 3.70f) return (uint8_t)(15.0f + (volts - 3.70f) * 200.0f);
  if (volts >= 3.50f) return (uint8_t)((volts - 3.50f) * 75.0f);
  return 0;
}

void writeLine(uint8_t row, const char *text) {
  lcd.setCursor(0, row);
  lcd.print(text);
  for (uint8_t i = strlen(text); i < 16; ++i) lcd.print(' ');
}

const char *statusText(uint8_t channel) {
  if (batteryMissing[channel]) return "NULL";
  if (chargeComplete[channel]) return "FULL";
  return "CHG";
}

void drawDisplay() {
  char line[17];
  if (displayPage == VOLTAGE_PAGE) {
    if (batteryMissing[0] && batteryMissing[1]) {
      snprintf(line, sizeof(line), "B1:NULL B2:NULL");
    } else if (batteryMissing[0]) {
      snprintf(line, sizeof(line), "B1:NULL B2:%.2f", batteryVolts[1]);
    } else if (batteryMissing[1]) {
      snprintf(line, sizeof(line), "B1:%.2f B2:NULL", batteryVolts[0]);
    } else {
      snprintf(line, sizeof(line), "B1:%.2f B2:%.2f", batteryVolts[0], batteryVolts[1]);
    }
    writeLine(0, line);
    if (batteryMissing[2]) snprintf(line, sizeof(line), "B3:NULL");
    else snprintf(line, sizeof(line), "B3:%.2fV", batteryVolts[2]);
    writeLine(1, line);
  } else if (displayPage == PERCENT_PAGE) {
    snprintf(line, sizeof(line), "B1:%3u%% B2:%3u%%",
             batteryMissing[0] ? 0 : batteryPercent(batteryVolts[0]),
             batteryMissing[1] ? 0 : batteryPercent(batteryVolts[1]));
    writeLine(0, line);
    if (batteryMissing[2]) snprintf(line, sizeof(line), "B3:NULL");
    else snprintf(line, sizeof(line), "B3:%3u%%", batteryPercent(batteryVolts[2]));
    writeLine(1, line);
  } else {
    snprintf(line, sizeof(line), "B1:%s B2:%s", statusText(0), statusText(1));
    writeLine(0, line);
    snprintf(line, sizeof(line), "B3:%s", statusText(2));
    writeLine(1, line);
  }
}

void beginRestAndSampling(unsigned long now) {
  phase = RESTING_AND_SAMPLING;
  phaseStartedMs = now;
  lastSampleMs = 0;
  sampleCount = 0;
  for (uint8_t channel = 0; channel < 3; ++channel) {
    sampleSum[channel] = 0.0f;
    setRelay(channel, false); // all chargers are off for the 3-second reading
  }
}

void finishSampling(unsigned long now) {
  if (sampleCount > 0) {
    for (uint8_t channel = 0; channel < 3; ++channel) {
      batteryVolts[channel] = sampleSum[channel] / sampleCount;
      batteryMissing[channel] = batteryVolts[channel] < DISCONNECTED_VOLTAGE;

      if (batteryMissing[channel]) {
        chargeComplete[channel] = false;
        haveBaseline[channel] = false;
      } else if (batteryVolts[channel] >= FULL_VOLTAGE) {
        chargeComplete[channel] = true;
      } else if (!haveBaseline[channel]) {
        haveBaseline[channel] = true;
        lastRisingVoltage[channel] = batteryVolts[channel];
        lastRiseMs[channel] = now;
      } else if (batteryVolts[channel] >= lastRisingVoltage[channel] + MEANINGFUL_RISE_VOLTS) {
        lastRisingVoltage[channel] = batteryVolts[channel];
        lastRiseMs[channel] = now;
      } else if (now - lastRiseMs[channel] >= NO_RISE_FULL_TIME_MS) {
        // Eight minutes without a 10 mV rise means this channel is treated as full.
        chargeComplete[channel] = true;
      }
    }
  }

  phase = CHARGING;
  phaseStartedMs = now;
  for (uint8_t channel = 0; channel < 3; ++channel) {
    setRelay(channel, !batteryMissing[channel] && !chargeComplete[channel]);
  }
}

void setup() {
  for (uint8_t channel = 0; channel < 3; ++channel) {
    pinMode(relayPins[channel], OUTPUT);
    setRelay(channel, true);
  }
  Wire.begin();
  lcd.init();
  lcd.backlight();
  lcd.clear();
  phaseStartedMs = millis();
  pageStartedMs = phaseStartedMs;
}

void loop() {
  const unsigned long now = millis();

  if (phase == CHARGING && now - phaseStartedMs >= CHARGE_TIME_MS) {
    beginRestAndSampling(now);
  }

  if (phase == RESTING_AND_SAMPLING) {
    if (now - lastSampleMs >= SAMPLE_GAP_MS) {
      lastSampleMs = now;
      for (uint8_t channel = 0; channel < 3; ++channel) {
        sampleSum[channel] += readBatteryVoltage(batteryPins[channel]);
      }
      ++sampleCount;
    }
    if (now - phaseStartedMs >= REST_AND_SAMPLE_TIME_MS) {
      finishSampling(now);
    }
  }

  if (now - pageStartedMs >= PAGE_TIME_MS) {
    pageStartedMs = now;
    displayPage = (DisplayPage)((displayPage + 1) % 3);
    lcd.clear();
  }

  if (now - lastDisplayMs >= DISPLAY_UPDATE_MS) {
    lastDisplayMs = now;
    drawDisplay();
  }
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik