Built-in project · Arduino Uno R3

GFCI concept demo (not a real GFCI) on an Arduino Uno

Two ACS712 sensors compare the hot and neutral current of a 12 V lamp and a relay latches open on a 60 mA imbalance: a teaching model, NOT a real GFCI, which trips at 4 to 6 mA. The whole build, an Arduino Uno and 9 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 30 minutes to build and run. Runs in your browser. Free, and no account needed.

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

The circuit runs here once the page has loaded.

What you'll learn

  • Sensors
  • School
  • Choosing and reading a resistor
  • Reading a button
  • Difficulty: Intermediate
  • Time: About 30 minutes
  • Board: Arduino Uno R3
  • Parts: 11

How it works

Two ACS712 sensors compare the hot and neutral current of a 12 V lamp and a relay latches open on a 60 mA imbalance: a teaching model, NOT a real GFCI, which trips at 4 to 6 mA.

Arduino Uno R3 runs the sketch below. The circuit around it: DC jack and adapter, Current sensor, ACS712, Relay module, Filament lamp, Resistor, 100 Ω, Slide switch. pinMode sets RELAY_PIN as an output. digitalWrite drives TRIPPED_LED HIGH. digitalRead reads whether TEST_PIN is HIGH or LOW. analogRead measures the voltage on HOT_PIN 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
Relay module pin INArduino Uno R3 pin 7
Pushbutton (2) pin 1aArduino Uno R3 pin 3
Pushbutton (1) pin 1aArduino Uno R3 pin 2
Current sensor, ACS712 (1) pin VCCArduino Uno R3 pin 5V
Relay module pin VCCArduino Uno R3 pin 5V
Current sensor, ACS712 (2) pin VCCArduino Uno R3 pin 5V
DC jack and adapter pin V-Arduino Uno R3 pin GND
Current sensor, ACS712 (1) pin GNDArduino Uno R3 pin GND
Relay module pin GNDArduino Uno R3 pin GND
Current sensor, ACS712 (2) pin IP-Arduino Uno R3 pin GND
Current sensor, ACS712 (2) pin GNDArduino Uno R3 pin GND
Slide switch pin 3Arduino Uno R3 pin GND
Pushbutton (1) pin 2aArduino Uno R3 pin GND
Pushbutton (2) pin 2aArduino Uno R3 pin GND
Current sensor, ACS712 (1) pin OUTArduino Uno R3 pin A0
Current sensor, ACS712 (2) pin OUTArduino Uno R3 pin A1
Current sensor, ACS712 (1) pin IP+DC jack and adapter pin V+
Relay module pin COMCurrent sensor, ACS712 (1) pin IP-
Filament lamp pin 1Relay module pin NO
Resistor, 100 Ω pin 1Relay module pin NO
Current sensor, ACS712 (2) pin IP+Filament lamp pin 2
Slide switch pin 2Resistor, 100 Ω pin 2

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
// GFCI concept demo: a teaching model of ground-fault detection on a 12 V DC
// lamp. THIS IS NOT A REAL GFCI. A real GFCI trips at 4 to 6 mA of imbalance
// within about 25 ms. The ACS712 cannot see milliamps (one converter step is
// 26 mA, and its noise is a few steps), so this stand-in trips at a much
// larger imbalance: 60 mA. A hobby board is never a substitute for a listed
// GFCI device, and nothing here is safe to connect to mains or to a person.
//
//   A0      ACS712 "hot" sensor OUT   (VCC on 5 V, GND on GND)
//   A1      ACS712 "neutral" sensor OUT
//   pin 7   relay module IN (high = contacts closed, lamp on)
//   pin 2   TEST button to GND (chip pull-up)
//   pin 3   RESET button to GND (chip pull-up)
//   pin 13  the Uno's own LED: lit while tripped
//
// The load circuit (12 V, never touches the Uno):
//   supply plus -> hot sensor -> relay COM, NO -> lamp -> neutral sensor -> supply minus
// The leak switch adds a 100 ohm path from the lamp's hot side straight to
// ground. That current passes through the hot sensor but never comes back
// through the neutral sensor, like current going through a person or a damp
// wire to ground instead of home on the neutral.
//
// Why it works: with no fault, the current out on the hot wire equals the
// current back on the neutral, so hot minus neutral is zero. Any current that
// finds another way home shows up as the difference. A real GFCI does the
// subtraction inside one toroid coil; this model does it with two sensors.
//
// Both sensors are zeroed at power on, while the relay is still open and no
// current flows. Each reading is an average of 32 samples. Two windows in a
// row over the limit (under 25 ms in the simulator) trip the relay, and it stays open
// (latched) until RESET. TEST adds a 100 mA make-believe imbalance while held,
// like a real GFCI's TEST button. With the contacts open nothing flows, so the
// sensors cannot see a leak: RESET closes the relay for a short trial and
// opens it again at once if the imbalance is still there.

const int HOT_PIN = A0;
const int NEUTRAL_PIN = A1;
const int RELAY_PIN = 7;
const int TEST_PIN = 2;
const int RESET_PIN = 3;
const int TRIPPED_LED = 13;

const float TRIP_MA = 60.0;           // imbalance that trips (a real GFCI: 4 to 6 mA)
const float TEST_INJECT_MA = 100.0;   // the make-believe imbalance while TEST is held
const float MA_PER_COUNT = 26.42;     // ACS712-05B: 5000 mV / 1023 / 0.185 V per A
const int WINDOW_SAMPLES = 32;        // readings averaged per sensor per window
const int WINDOWS_TO_TRIP = 2;        // windows in a row over the limit
const unsigned long REPORT_MS = 500;
const unsigned long TRIAL_SETTLE_MS = 25;  // relay pull-in time before a RESET trial reads

float hotZero = 512;
float neutralZero = 512;
bool tripped = false;
int over = 0;
float hotMa = 0;
float neutralMa = 0;
bool lastReset = false;
unsigned long lastReport = 0;

void relayClosed(bool closed) {
  digitalWrite(RELAY_PIN, closed ? HIGH : LOW);
}

bool testHeld() {
  return digitalRead(TEST_PIN) == LOW;
}

// One window: average both sensors together and return hot minus neutral in mA.
float measure() {
  long hot = 0;
  long neutral = 0;
  for (int i = 0; i < WINDOW_SAMPLES; i++) {
    hot += analogRead(HOT_PIN);
    neutral += analogRead(NEUTRAL_PIN);
  }
  hotMa = (hot / (float)WINDOW_SAMPLES - hotZero) * MA_PER_COUNT;
  neutralMa = (neutral / (float)WINDOW_SAMPLES - neutralZero) * MA_PER_COUNT;
  float diff = hotMa - neutralMa;
  if (testHeld()) diff += TEST_INJECT_MA;
  return diff;
}

void zeroSensors() {
  long hot = 0;
  long neutral = 0;
  for (int i = 0; i < 256; i++) {
    hot += analogRead(HOT_PIN);
    neutral += analogRead(NEUTRAL_PIN);
  }
  hotZero = hot / 256.0;
  neutralZero = neutral / 256.0;
}

void trip(float diff) {
  relayClosed(false);
  tripped = true;
  over = 0;
  digitalWrite(TRIPPED_LED, HIGH);
  Serial.print(testHeld() ? "TRIP (TEST button): imbalance " : "TRIP: imbalance ");
  Serial.print((int)diff);
  Serial.print(" mA over the ");
  Serial.print((int)TRIP_MA);
  Serial.println(" mA limit, relay open, latched");
}

void tryReset() {
  if (!tripped) {
    Serial.println("RESET: nothing to reset");
    return;
  }
  if (testHeld()) {
    Serial.println("RESET refused: release TEST first");
    return;
  }
  // Trial: close the relay, let it settle, then look for the imbalance.
  relayClosed(true);
  unsigned long began = millis();
  while (millis() - began < TRIAL_SETTLE_MS) {
  }
  float first = measure();
  float second = measure();
  if (first > TRIP_MA && second > TRIP_MA) {
    relayClosed(false);
    Serial.print("RESET refused: leak still present, imbalance ");
    Serial.print((int)second);
    Serial.println(" mA, relay open");
    return;
  }
  tripped = false;
  over = 0;
  digitalWrite(TRIPPED_LED, LOW);
  Serial.println("RESET accepted: relay closed, lamp on");
}

void setup() {
  Serial.begin(115200);
  Serial.println("Mokxi Arduino Uno: GFCI concept demo, NOT a real GFCI");
  Serial.println("A real GFCI trips at 4 to 6 mA within about 25 ms.");
  Serial.println("The ACS712 cannot see milliamps, so this stand-in trips at 60 mA.");
  Serial.println("Never a substitute for a listed GFCI device.");
  pinMode(RELAY_PIN, OUTPUT);
  pinMode(TRIPPED_LED, OUTPUT);
  pinMode(TEST_PIN, INPUT_PULLUP);
  pinMode(RESET_PIN, INPUT_PULLUP);
  relayClosed(false);
  zeroSensors();
  Serial.println("Sensors zeroed with the relay open");
  relayClosed(true);
  Serial.println("Relay closed, lamp on");
}

void loop() {
  float diff = measure();
  if (!tripped) {
    if (diff > TRIP_MA) {
      over++;
      if (over >= WINDOWS_TO_TRIP) trip(diff);
    } else {
      over = 0;
    }
  }

  bool reset = digitalRead(RESET_PIN) == LOW;
  if (reset && !lastReset) tryReset();
  lastReset = reset;

  if (millis() - lastReport >= REPORT_MS) {
    lastReport = millis();
    Serial.print("hot ");
    Serial.print((int)hotMa);
    Serial.print(" mA, neutral ");
    Serial.print((int)neutralMa);
    Serial.print(" mA, diff ");
    Serial.print((int)diff);
    Serial.print(" mA, ");
    Serial.println(tripped ? "TRIPPED" : "ON");
  }
}

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);
  Serial.println("Mokxi Arduino Uno: GFCI concept demo, NOT a real GFCI");
  Serial.println("A real GFCI trips at 4 to 6 mA within about 25 ms.");
  Serial.println("The ACS712 cannot see milliamps, so this stand-in trips at 60 mA.");
  Serial.println("Never a substitute for a listed GFCI device.");
  pinMode(RELAY_PIN, OUTPUT);
  pinMode(TRIPPED_LED, OUTPUT);
  pinMode(TEST_PIN, INPUT_PULLUP);
  pinMode(RESET_PIN, INPUT_PULLUP);
  relayClosed(false);
  zeroSensors();
  Serial.println("Sensors zeroed with the relay open");
  relayClosed(true);
  Serial.println("Relay closed, lamp on");
}

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() {
  float diff = measure();
  if (!tripped) {
    if (diff > TRIP_MA) {
      over++;
      if (over >= WINDOWS_TO_TRIP) trip(diff);
    } else {
      over = 0;
    }
  }

  bool reset = digitalRead(RESET_PIN) == LOW;
  if (reset && !lastReset) tryReset();
  lastReset = reset;

  if (millis() - lastReport >= REPORT_MS) {
    lastReport = millis();
    Serial.print("hot ");
    Serial.print((int)hotMa);
    Serial.print(" mA, neutral ");
    Serial.print((int)neutralMa);
    Serial.print(" mA, diff ");
    Serial.print((int)diff);
    Serial.print(" mA, ");
    Serial.println(tripped ? "TRIPPED" : "ON");
  }
}

Troubleshooting

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

  • Board gets too much voltage and overheats or dies. DC jack and adapter
    Check the adapter voltage before plugging in. Boards like the Uno want 7 to 12 V on the jack. A 5 V part wired to a 12 V supply can be destroyed.
  • Nothing powers on, or the polarity is backward. DC jack and adapter
    Most barrel jacks are center positive, but some are not. Check the label on the adapter, and make sure the adapter can supply enough current for the load.
  • The reading does not sit at zero amps. The output is about half of VCC at no current and moves only 185, 100 or 66 mV per amp, depending on the 5A, 20A or 30A part. Current sensor
    Read the idle value at startup and subtract it. Pick the version that fits your current and use the matching mV per amp. Average many samples to cut the noise.
  • The load is wired in parallel instead of in series, or a 3.3 V board reads wrong. The sensor needs 5 V and its output can be above 3.3 V. Current sensor
    Break the wire and run the load current through the IP+ and IP- terminals. Power the sensor from 5 V, and scale the output with a divider for 3.3 V ADCs.
  • 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 lamp is dim or does not light when driven from a board pin. Filament lamp
    A lamp needs far more current than a pin gives (a pin is limited to about 20 mA). Switch it with a transistor or MOSFET from its own supply that matches the lamp's voltage rating.
  • The lamp's brightness lags or it burns out the switch when it first turns on. Filament lamp
    A cold filament draws several times its running current at first. Size the switch for that surge, not just the steady current.

Change it and keep it

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