Built-in project · Arduino Uno R3

Bunker battery gauge on an Arduino Uno

An INA219 measures a battery under load. A lookup table turns volts into a charge percent, a moving average smooths the current, and a traffic light shows how much is left. The whole build, an Arduino Uno and 5 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate About 20 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $32. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Sensors
  • Lights
  • Sound
  • Choosing and reading a resistor
  • Difficulty: Intermediate
  • Time: About 20 minutes
  • Board: Arduino Uno R3
  • Parts: 7

How it works

An INA219 measures a battery under load. A lookup table turns volts into a charge percent, a moving average smooths the current, and a traffic light shows how much is left.

Arduino Uno R3 runs the sketch below. The circuit around it: Battery: AA, coin cell, LiPo, Current monitor, INA219, Resistor, 33 Ω, Piezo speaker, Traffic light module. pinMode sets PIEZO as an output. Serial.begin opens the serial port at 115200 baud. tone plays a note on PIEZO. millis reads the time since power-up, so the sketch can time things without stopping.

Parts list

7 parts, plus the jumper wires. They retail for about $32; on Mokxi they're free.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Piezo speaker pin 1Arduino Uno R3 pin 9
Traffic light module pin GArduino Uno R3 pin A0
Traffic light module pin YArduino Uno R3 pin A1
Traffic light module pin RArduino Uno R3 pin A2
Current monitor, INA219 pin SDAArduino Uno R3 pin A4
Current monitor, INA219 pin SCLArduino Uno R3 pin A5
Current monitor, INA219 pin VIN+Battery: AA, coin cell, LiPo pin V+
Battery: AA, coin cell, LiPo pin V-Ground
Current monitor, INA219 pin VCC5 V
Current monitor, INA219 pin GNDGround
Resistor, 33 Ω pin 1Current monitor, INA219 pin VIN-
Resistor, 33 Ω pin 2Ground
Piezo speaker pin 2Ground
Traffic light module pin GNDGround

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Bunker battery gauge: how much is left in the battery, and how long will it
// last at this load? An INA219, a traffic light and a piezo on an Uno.
//
//   INA219    VIN+ <- battery +, VIN- -> the load -> battery -, on I2C (A4, A5)
//   traffic light   green -> A0, yellow -> A1, red -> A2
//   piezo     pin 9 and GND
//
// The traffic light is the gauge you read from across the room: green above
// 50 percent, yellow from 20 to 49, red below 20. The Serial Monitor has the
// numbers, once a second.
//
// The battery's minus, the INA219's GND and the Uno's GND are the same wire.
// The load stands in for the bunker's lamps and radio. Slide the battery's
// charge down in the properties panel and watch the percent and the time left
// fall; make the load heavier and the time left shortens faster than the
// percent does.
//
// # Volts are not a fuel gauge, but they are close enough
//
// A lithium cell sits at 4.2 V full and 3.0 V empty, but not in a straight
// line: it drops quickly at the top, lies almost flat through the middle and
// falls off a cliff at the bottom. So the sketch keeps a table of eleven
// points, one every ten percent, finds the two the reading is between and
// slides along the line joining them. That is linear interpolation, and it is
// how most battery gauges that have no coulomb counter work.
//
// The reading is taken under load, which pulls the battery's terminal volts
// down a little, so the gauge reads slightly low while the lights are on.
// That is honest: the battery really is working harder.
//
// # A smoothed current
//
// Loads flicker. Dividing the charge left by a current that jumps about gives
// a time left that jumps about as well. The sketch keeps an exponential moving
// average: each second the average moves one eighth of the way to the new
// reading. One line, no array, and a single noisy sample barely shows.

#include <Wire.h>
#include <Adafruit_INA219.h>

const uint8_t PIEZO = 9;
const uint8_t GREEN_LED = A0;
const uint8_t YELLOW_LED = A1;
const uint8_t RED_LED = A2;
const int CAPACITY_MAH = 2000;  // the battery's label
const int LOW_PERCENT = 20;

// Millivolts at 0, 10, 20 ... 100 percent for one lithium cell.
const int CURVE[11] = {3000, 3300, 3450, 3550, 3620, 3680, 3750, 3830, 3930, 4050, 4200};

Adafruit_INA219 ina;

float averageMa = 0;
bool firstReading = true;
unsigned long lastRead = 0;
unsigned long lastBeep = 0;
bool warned = false;

int percentFor(int mv) {
  if (mv <= CURVE[0]) return 0;
  if (mv >= CURVE[10]) return 100;
  int i = 0;
  while (mv >= CURVE[i + 1]) i++;
  // how far between point i and point i + 1, in tenths of a step
  long into = (long)(mv - CURVE[i]) * 10 / (CURVE[i + 1] - CURVE[i]);
  return i * 10 + (int)into;
}

void setup() {
  Serial.begin(115200);
  pinMode(PIEZO, OUTPUT);
  pinMode(GREEN_LED, OUTPUT);
  pinMode(YELLOW_LED, OUTPUT);
  pinMode(RED_LED, OUTPUT);
  ina.begin();
  Serial.println("bunker battery gauge ready");
}

void loop() {
  unsigned long now = millis();
  if (now - lastRead < 1000 && !firstReading) return;
  lastRead = now;

  int mv = (int)((ina.getBusVoltage_V() + ina.getShuntVoltage_mV() / 1000.0) * 1000.0);
  float ma = ina.getCurrent_mA();
  if (ma < 0) ma = -ma;
  if (firstReading) {
    averageMa = ma;
    firstReading = false;
  } else {
    averageMa += (ma - averageMa) / 8.0;
  }

  int percent = percentFor(mv);
  long leftMah = (long)CAPACITY_MAH * percent / 100;
  long minutes = averageMa > 1 ? (long)(leftMah * 60.0 / averageMa) : 0;

  Serial.print("battery ");
  Serial.print(percent);
  Serial.print("% ");
  Serial.print(mv);
  Serial.print(" mV ");
  Serial.print((int)averageMa);
  Serial.print(" mA ");
  Serial.print(minutes / 60);
  Serial.print("h");
  Serial.print(minutes % 60);
  Serial.println("m left");

  bool low = percent < LOW_PERCENT;
  digitalWrite(GREEN_LED, percent >= 50);
  digitalWrite(YELLOW_LED, percent >= LOW_PERCENT && percent < 50);
  digitalWrite(RED_LED, low);
  if (low && !warned) {
    warned = true;
    Serial.println("LOW BATTERY: save the lights for the night");
  }
  if (!low) warned = false;
  if (low && now - lastBeep > 5000) {
    lastBeep = now;
    tone(PIEZO, 880, 200);
  }
}

Code walkthrough

The key functions of the sketch, and what each one does.

setup sketch.ino

Runs once when the board starts. It gets the pins and libraries ready.

void setup() {
  Serial.begin(115200);
  pinMode(PIEZO, OUTPUT);
  pinMode(GREEN_LED, OUTPUT);
  pinMode(YELLOW_LED, OUTPUT);
  pinMode(RED_LED, OUTPUT);
  ina.begin();
  Serial.println("bunker battery gauge ready");
}

loop sketch.ino

Runs over and over for as long as the board has power. This is where the project does its work.

void loop() {
  unsigned long now = millis();
  if (now - lastRead < 1000 && !firstReading) return;
  lastRead = now;

  int mv = (int)((ina.getBusVoltage_V() + ina.getShuntVoltage_mV() / 1000.0) * 1000.0);
  float ma = ina.getCurrent_mA();
  if (ma < 0) ma = -ma;
  if (firstReading) {
    averageMa = ma;
    firstReading = false;
  } else {
    averageMa += (ma - averageMa) / 8.0;
  }

  int percent = percentFor(mv);
  long leftMah = (long)CAPACITY_MAH * percent / 100;
  long minutes = averageMa > 1 ? (long)(leftMah * 60.0 / averageMa) : 0;

  Serial.print("battery ");
  Serial.print(percent);
  Serial.print("% ");
  Serial.print(mv);
  Serial.print(" mV ");
  Serial.print((int)averageMa);
  Serial.print(" mA ");
  Serial.print(minutes / 60);
  Serial.print("h");
  // ... 17 more lines in the editor

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The battery is wired backwards, or the voltage is too low or too high for the circuit. A fresh AA is about 1.5 V, a LiPo is 3.7 V nominal and 4.2 V full. Battery: AA
    Match the plus and minus marks. Check the voltage your parts need. Two AAs make 3 V, four make 6 V. Never feed a LiPo to a 3.3 V chip directly at 4.2 V.
  • The circuit resets or browns out when a motor or servo starts. A battery has internal resistance, so the voltage sags under load. Battery: AA
    Use a fresh pack with enough current rating. Add a 100 uF or larger capacitor near the load. Power motors from their own pack and join the grounds.
  • The current reads zero or negative. Current monitor
    Put the shunt in series with the load, not across the supply. The load current must flow from VIN+ to VIN-. Swap them if the sign is reversed.
  • The reading is off by a scale factor, or the part is not found. Current monitor
    The address is 0x40 by default (A0 and A1 change it). Set the calibration for your shunt with the library. Keep bus voltage under 26 V.
  • The LED is dim, burns out, or the pin reads wrong because the resistor value is off. Resistor
    Read the color bands again. A 220 ohm to 330 ohm resistor suits a typical LED on 5 V. 10k is a common pull-up or pull-down value. Check with a multimeter if unsure.
  • A pull-up or pull-down resistor is missing, so the input floats and reads random values. Resistor
    Tie the input to 5 V or ground through about 10k. Or use INPUT_PULLUP and wire the switch to ground.
  • A passive piezo stays silent when you just set the pin HIGH. Piezo speaker
    A passive piezo needs a changing signal. Use tone(pin, frequency) or a PWM wave. Around 2 to 4 kHz is loudest. A buzzer with a built in oscillator makes sound from plain DC instead.
  • Sound is very quiet or the pin seems overloaded. Piezo speaker
    Drive the piezo from a pin directly, or through a 100 ohm resistor. For a bigger buzzer, switch it with a transistor. Check that you wired the right two legs and joined grounds.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.