Built-in project · Arduino Uno R3

Bench supply monitor on an Arduino Uno

An INA219 reads a 12 V supply’s volts, amps, watts and watt-hours on an LCD; an over-current knob trips a shutoff relay that latches, with a buzzer, until RESET (a low-voltage teaching model). The whole build, an Arduino Uno and 14 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.

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

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

The circuit runs here once the page has loaded.

What you'll learn

  • Sensors
  • Displays
  • Choosing and reading a resistor
  • Reading a button
  • How an LED lights and why it needs a resistor
  • Difficulty: Advanced
  • Time: About 45 minutes
  • Board: Arduino Uno R3
  • Parts: 16

How it works

An INA219 reads a 12 V supply’s volts, amps, watts and watt-hours on an LCD; an over-current knob trips a shutoff relay that latches, with a buzzer, until RESET (a low-voltage teaching model).

Arduino Uno R3 runs the sketch below. The circuit around it: Current monitor, INA219, DC voltage source, Relay module, Resistor, 33 Ω, Slide switch, Resistor, 15 Ω. pinMode sets PIN_RESET as an input with its pull-up on. digitalWrite drives PIN_LED_TRIP LOW. digitalRead reads whether PIN_RESET is HIGH or LOW. analogRead measures the voltage on PIN_LIMIT as a number. Serial.begin opens the serial port at 115200 baud.

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
Current monitor, INA219 pin SCLArduino Uno R3 pin SCL
Current monitor, INA219 pin SDAArduino Uno R3 pin SDA
DC voltage source pin V-Arduino Uno R3 pin GND
Buzzer pin 1Arduino Uno R3 pin 9
Relay module pin INArduino Uno R3 pin 7
Resistor, 220 Ω (2) pin 1Arduino Uno R3 pin 6
Resistor, 220 Ω (1) pin 1Arduino Uno R3 pin 5
Pushbutton pin 1aArduino Uno R3 pin 2
Current monitor, INA219 pin VCCArduino Uno R3 pin 5V
Relay module pin VCCArduino Uno R3 pin 5V
Character LCD with I2C backpack pin VCCArduino Uno R3 pin 5V
Potentiometer pin 3Arduino Uno R3 pin 5V
Current monitor, INA219 pin GNDArduino Uno R3 pin GND
Relay module pin GNDArduino Uno R3 pin GND
Resistor, 33 Ω pin 1Arduino Uno R3 pin GND
Resistor, 15 Ω pin 2Arduino Uno R3 pin GND
Character LCD with I2C backpack pin GNDArduino Uno R3 pin GND
Pushbutton pin 2aArduino Uno R3 pin GND
LED, red pin CArduino Uno R3 pin GND
LED, green pin CArduino Uno R3 pin GND
Buzzer pin 2Arduino Uno R3 pin GND
Potentiometer pin 1Arduino Uno R3 pin GND
Potentiometer pin 2Arduino Uno R3 pin A0
Character LCD with I2C backpack pin SDAArduino Uno R3 pin A4
Character LCD with I2C backpack pin SCLArduino Uno R3 pin A5
Relay module pin COMCurrent monitor, INA219 pin VIN-
DC voltage source pin V+Current monitor, INA219 pin VIN+
Resistor, 33 Ω pin 2Relay module pin NO
Slide switch pin 2Relay module pin NO
Resistor, 15 Ω pin 1Slide switch pin 3
LED, red pin AResistor, 220 Ω (1) pin 2
LED, green pin AResistor, 220 Ω (2) pin 2

The notes on the canvas

Word for word, as it sits beside the circuit in the editor.

Try it
1. Press Run. The output is OFF until you press RESET.
2. Press RESET: the relay closes and the LCD shows 12 V, about 0.36 A, 4.4 W, and the watt-hours climb.
3. Flip the load switch to add the second resistor: the current jumps to about 1.16 A.
4. Turn the limit knob down to 1 A with the heavy load on: after 100 ms the relay opens, the red LED lights and the buzzer beeps, latched until RESET.
5. RESET with the output on clears the watt-hours, min and max.

A teaching model at 12 V. The reverse-polarity and over-voltage notes (a series diode or a P-MOSFET, a TVS, a crowbar) are text in the sketch; this circuit has none of them.

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
// Bench supply monitor: an INA219 between a bench supply and its load reads
// the volts, amps and watts, adds up the watt-hours, and keeps the min and
// max. A knob sets an over-current limit; if the load draws more than that for
// a short time, a shutoff relay opens and STAYS open (latched), a buzzer
// sounds, and only the RESET button closes it again.
//
// THIS IS A TEACHING MODEL at 12 V with small resistors for a load. It is not
// a protection device. Never put it between a mains-powered supply, a car or
// a large battery and anything you care about.
//
//   INA219     VCC 5V  GND  SDA -> SDA  SCL -> SCL        (address 0x40)
//              VIN+ from the supply's plus, VIN- to the relay's COM
//   LCD 16x2   I2C backpack at 0x27 on the same SDA and SCL
//   relay      IN -> pin 7 (HIGH = energized = load connected)
//              COM from the INA219 VIN-, NO to the load; NC is not used
//   pin 2      RESET button to GND (the chip's pull-up)
//   A0         over-current limit knob, 0.10 to 3.00 A
//   pin 6      green LED: output on
//   pin 5      red LED: tripped
//   pin 9      buzzer
//   Every ground joins: the supply's minus, the load, the Uno, the modules.
//
// HOW THE READINGS WORK
//   The INA219 module has a 0.1 ohm shunt resistor in series with the load.
//   The chip reads the few millivolts across it for the current (10 uV a step
//   in the shunt register) and the voltage on VIN- against ground for the bus
//   voltage (4 mV a step). With the 32 V, 2 A calibration the library uses, a
//   current step is 0.1 mA and the chip tops out near 3.2 A. The sketch
//   multiplies volts by amps for the watts and adds watts times the time since
//   the last sample into watt-hours.
//   The relay sits on the load side of the shunt, so with the output off the
//   display still shows the supply voltage and 0 A.
//
// THE LIMIT, THE LATCH AND RESET
//   Over the limit for TRIP_MS (100 ms) trips it: a quick inrush spike shorter
//   than that is ignored. A trip opens the relay and holds it open until RESET
//   is pressed, even if the load goes away by itself. Power-up is the same:
//   the output stays OFF until RESET is pressed.
//   RESET rules:
//     output OFF at power-up   RESET turns the output on.
//     TRIPPED                  RESET turns the output back on (the latch
//                              clears). If the fault is still there it trips
//                              again after TRIP_MS. Watt-hours and min/max
//                              keep counting from before.
//     output ON                RESET clears the watt-hours, min and max.
//   Turning the knob below the present load's current while the output is on
//   trips it after TRIP_MS like any other overload.
//
// REVERSE POLARITY AND OVER-VOLTAGE (text only: this circuit has neither)
//   Reverse polarity: swapping the supply leads would push current backwards
//   through the INA219 and everything else. A diode in series with the plus
//   lead blocks that but drops 0.7 V (silicon) or about 0.3 V (Schottky) and
//   wastes that voltage times the current as heat. A P-channel MOSFET in the
//   plus lead does the same job with almost no loss: its gate goes to ground
//   and its body diode is the first to conduct with the right polarity,
//   after which the gate, well below the source, turns the channel fully
//   on, so the drop is only the current times the milliohms of RDS(on). With
//   the leads swapped the gate is not below the source, the channel stays off
//   and the body diode is reverse biased, so nothing flows.
//   Over-voltage: the INA219's bus input is good to 26 V, so a bench supply
//   turned up past that damages it. A TVS diode across the input clamps
//   spikes, and a crowbar (a thyristor that shorts the rail when it passes a
//   set voltage) forces a fuse or the supply's own current limit to act. This
//   sketch's relay is a series shutoff, not a crowbar: a crowbar shorts the
//   supply, a shutoff disconnects the load.

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

const int PIN_RESET = 2;
const int PIN_RELAY = 7;
const int PIN_LED_ON = 6;
const int PIN_LED_TRIP = 5;
const int PIN_BUZZER = 9;
const int PIN_LIMIT = A0;

const float MIN_LIMIT_A = 0.10;
const float MAX_LIMIT_A = 3.00;
const unsigned long TRIP_MS = 100;
const unsigned long SAMPLE_MS = 20;
const unsigned long LCD_MS = 250;
const unsigned long PAGE_MS = 3000;
const unsigned long BLANK_MS = 150;   // the relay's contact settles before min and max count

enum State { OUTPUT_OFF, OUTPUT_ON, TRIPPED };
const char *const STATE_NAME[] = {"OFF", "ON", "TRIPPED"};

Adafruit_INA219 ina;
LiquidCrystal_I2C lcd(0x27, 16, 2);

State state = OUTPUT_OFF;
float volts = 0, milliamps = 0, limitA = 1.0;
float milliWattHours = 0;
float minV = 0, maxV = 0, minMa = 0, maxMa = 0;
bool haveRange = false;
float peakMa = 0;
bool over = false;
unsigned long overSince = 0;
unsigned long onSince = 0;
unsigned long lastSample = 0, lastLcd = 0, lastStatus = 0;
bool buttonDown = false;
unsigned long buttonChange = 0;

void clearCounters() {
  milliWattHours = 0;
  haveRange = false;
}

void setOutput(bool on) {
  digitalWrite(PIN_RELAY, on ? HIGH : LOW);
  digitalWrite(PIN_LED_ON, on ? HIGH : LOW);
}

void pressReset() {
  if (state == OUTPUT_ON) {
    clearCounters();
    onSince = millis();
    Serial.println("RESET: watt-hours, min and max cleared");
    return;
  }
  bool wasTripped = state == TRIPPED;
  state = OUTPUT_ON;
  over = false;
  haveRange = wasTripped ? haveRange : false;
  noTone(PIN_BUZZER);
  digitalWrite(PIN_LED_TRIP, LOW);
  setOutput(true);
  onSince = millis();
  Serial.println(wasTripped ? "RESET: latch cleared, output ON" : "RESET: output ON");
}

void checkButton() {
  bool down = digitalRead(PIN_RESET) == LOW;
  if (down != buttonDown && millis() - buttonChange >= 30) {
    buttonDown = down;
    buttonChange = millis();
    if (down) pressReset();
  }
}

void sample() {
  unsigned long now = millis();
  unsigned long dt = now - lastSample;
  lastSample = now;
  volts = ina.getBusVoltage_V();
  milliamps = ina.getCurrent_mA();
  if (state != OUTPUT_ON) return;

  milliWattHours += volts * milliamps * dt / 3600000.0;
  if (now - onSince >= BLANK_MS) {
    if (!haveRange) {
      minV = maxV = volts;
      minMa = maxMa = milliamps;
      haveRange = true;
    }
    if (volts < minV) minV = volts;
    if (volts > maxV) maxV = volts;
    if (milliamps < minMa) minMa = milliamps;
    if (milliamps > maxMa) maxMa = milliamps;
  }

  if (milliamps > limitA * 1000.0) {
    if (!over) {
      over = true;
      overSince = now;
    } else if (now - overSince >= TRIP_MS) {
      state = TRIPPED;
      peakMa = milliamps;
      setOutput(false);
      digitalWrite(PIN_LED_TRIP, HIGH);
      Serial.print("TRIP: ");
      Serial.print((long)(milliamps + 0.5));
      Serial.print(" mA over the ");
      Serial.print((long)(limitA * 1000.0 + 0.5));
      Serial.println(" mA limit, output OFF, latched until RESET");
    }
  } else {
    over = false;
  }
}

void showLcd() {
  char top[17];
  char bottom[17];
  int v100 = (int)(volts * 100.0 + 0.5);
  int mA = (int)(milliamps + (milliamps < 0 ? -0.5 : 0.5));
  if (state == OUTPUT_OFF) {
    snprintf(top, sizeof top, "OFF  %2d.%02dV     ", v100 / 100, v100 % 100);
    snprintf(bottom, sizeof bottom, "press RESET     ");
  } else if (state == TRIPPED) {
    snprintf(top, sizeof top, "OVER-CURRENT!   ");
    snprintf(bottom, sizeof bottom, "peak %d.%02dA RESET", (int)peakMa / 1000, ((int)peakMa % 1000) / 10);
  } else if ((millis() / PAGE_MS) % 2 == 0) {
    int mw = (int)(volts * milliamps / 10.0);
    int mwh = (int)milliWattHours;
    snprintf(top, sizeof top, "%2d.%02dV  %d.%03dA   ", v100 / 100, v100 % 100, mA / 1000, mA % 1000);
    snprintf(bottom, sizeof bottom, "%d.%02dW %d.%03dWh   ", mw / 100, mw % 100, mwh / 1000, mwh % 1000);
  } else {
    int lo = (int)minMa, hi = (int)maxMa;
    int v1 = (int)(minV * 100.0), v2 = (int)(maxV * 100.0);
    snprintf(top, sizeof top, "I %d.%03d-%d.%03dA ", lo / 1000, lo % 1000, hi / 1000, hi % 1000);
    snprintf(bottom, sizeof bottom, "V %d.%02d-%d.%02dV ", v1 / 100, v1 % 100, v2 / 100, v2 % 100);
  }
  lcd.setCursor(0, 0);
  lcd.print(top);
  lcd.setCursor(0, 1);
  lcd.print(bottom);
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN_RESET, INPUT_PULLUP);
  pinMode(PIN_RELAY, OUTPUT);
  pinMode(PIN_LED_ON, OUTPUT);
  pinMode(PIN_LED_TRIP, OUTPUT);
  pinMode(PIN_BUZZER, OUTPUT);
  setOutput(false);
  Serial.println("Mokxi Arduino Uno: bench supply monitor (teaching model, 12 V)");
  lcd.init();
  lcd.backlight();
  if (!ina.begin()) {
    Serial.println("FAULT: no INA219 at 0x40, check SDA, SCL and power");
    lcd.print("No INA219 0x40");
    while (1);
  }
  Serial.println("output OFF until RESET is pressed");
}

void loop() {
  unsigned long now = millis();
  checkButton();
  limitA = MIN_LIMIT_A + (MAX_LIMIT_A - MIN_LIMIT_A) * analogRead(PIN_LIMIT) / 1023.0;
  limitA = (int)(limitA * 100.0 + 0.5) / 100.0;

  if (now - lastSample >= SAMPLE_MS) sample();

  if (state == TRIPPED) {
    // A beep every third of a second.
    if ((now / 150) % 2 == 0) tone(PIN_BUZZER, 2400);
    else noTone(PIN_BUZZER);
  }

  if (now - lastLcd >= LCD_MS) {
    lastLcd = now;
    showLcd();
  }

  if (now - lastStatus >= 1000) {
    lastStatus = now;
    Serial.print("V ");
    Serial.print(volts, 2);
    Serial.print("  A ");
    Serial.print(milliamps / 1000.0, 3);
    Serial.print("  W ");
    Serial.print(volts * milliamps / 1000.0, 2);
    Serial.print("  Wh ");
    Serial.print(milliWattHours / 1000.0, 3);
    Serial.print("  limit ");
    Serial.print(limitA, 2);
    Serial.print("  ");
    Serial.print(STATE_NAME[state]);
    if (haveRange && state != OUTPUT_OFF) {
      Serial.print("  min ");
      Serial.print(minMa / 1000.0, 3);
      Serial.print(" max ");
      Serial.print(maxMa / 1000.0, 3);
      Serial.print("  Vmin ");
      Serial.print(minV, 2);
      Serial.print(" Vmax ");
      Serial.print(maxV, 2);
    }
    Serial.println();
  }
}

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(PIN_RESET, INPUT_PULLUP);
  pinMode(PIN_RELAY, OUTPUT);
  pinMode(PIN_LED_ON, OUTPUT);
  pinMode(PIN_LED_TRIP, OUTPUT);
  pinMode(PIN_BUZZER, OUTPUT);
  setOutput(false);
  Serial.println("Mokxi Arduino Uno: bench supply monitor (teaching model, 12 V)");
  lcd.init();
  lcd.backlight();
  if (!ina.begin()) {
    Serial.println("FAULT: no INA219 at 0x40, check SDA, SCL and power");
    lcd.print("No INA219 0x40");
    while (1);
  }
  Serial.println("output OFF until RESET is pressed");
}

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();
  checkButton();
  limitA = MIN_LIMIT_A + (MAX_LIMIT_A - MIN_LIMIT_A) * analogRead(PIN_LIMIT) / 1023.0;
  limitA = (int)(limitA * 100.0 + 0.5) / 100.0;

  if (now - lastSample >= SAMPLE_MS) sample();

  if (state == TRIPPED) {
    // A beep every third of a second.
    if ((now / 150) % 2 == 0) tone(PIN_BUZZER, 2400);
    else noTone(PIN_BUZZER);
  }

  if (now - lastLcd >= LCD_MS) {
    lastLcd = now;
    showLcd();
  }

  if (now - lastStatus >= 1000) {
    lastStatus = now;
    Serial.print("V ");
    Serial.print(volts, 2);
    Serial.print("  A ");
    Serial.print(milliamps / 1000.0, 3);
    Serial.print("  W ");
    Serial.print(volts * milliamps / 1000.0, 2);
    Serial.print("  Wh ");
  // ... 18 more lines in the editor

Troubleshooting

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

  • 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 circuit sees no voltage or the wrong amount. DC voltage source
    Make sure V+ and V- go to the right nodes. The source floats, so tie one side to your circuit ground. Check the set voltage matches what your parts allow.
  • A part gets too much voltage. DC voltage source
    Check the rating of every part before raising the supply. Use series resistors with LEDs, and do not connect the source straight across a short.
  • The relay does not click, or the board resets when it switches. Relay module
    A relay coil needs more current than a pin can give. Use a module with a transistor driver, and power VCC from 5 V. Share ground between the module and the Arduino.
  • The relay works backwards: on when you expect off. Relay module
    Many modules are active LOW: the input must go LOW to turn the relay on. Set the pin HIGH at startup so the load does not switch on during boot.
  • 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.

Change it and keep it

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