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.
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.
Parts list
11 parts, plus the jumper wires. They retail for about $40; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × DC jack and adapter
- 2 × Current sensor, ACS712
- 1 × Relay moduleIn Halloween Prop Kit.
- 1 × Filament lamp
- 1 × Resistor, 100 ΩIn Starter Kit, Claw Machine Kit, Halloween Prop Kit.
- 1 × Slide switchIn Logic Lab Kit, Spaceship Panel Kit.
- 2 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab Kit.
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 pin | Connects to |
|---|---|
| Relay module pin IN | Arduino Uno R3 pin 7 |
| Pushbutton (2) pin 1a | Arduino Uno R3 pin 3 |
| Pushbutton (1) pin 1a | Arduino Uno R3 pin 2 |
| Current sensor, ACS712 (1) pin VCC | Arduino Uno R3 pin 5V |
| Relay module pin VCC | Arduino Uno R3 pin 5V |
| Current sensor, ACS712 (2) pin VCC | Arduino Uno R3 pin 5V |
| DC jack and adapter pin V- | Arduino Uno R3 pin GND |
| Current sensor, ACS712 (1) pin GND | Arduino Uno R3 pin GND |
| Relay module pin GND | Arduino Uno R3 pin GND |
| Current sensor, ACS712 (2) pin IP- | Arduino Uno R3 pin GND |
| Current sensor, ACS712 (2) pin GND | Arduino Uno R3 pin GND |
| Slide switch pin 3 | Arduino Uno R3 pin GND |
| Pushbutton (1) pin 2a | Arduino Uno R3 pin GND |
| Pushbutton (2) pin 2a | Arduino Uno R3 pin GND |
| Current sensor, ACS712 (1) pin OUT | Arduino Uno R3 pin A0 |
| Current sensor, ACS712 (2) pin OUT | Arduino Uno R3 pin A1 |
| Current sensor, ACS712 (1) pin IP+ | DC jack and adapter pin V+ |
| Relay module pin COM | Current sensor, ACS712 (1) pin IP- |
| Filament lamp pin 1 | Relay module pin NO |
| Resistor, 100 Ω pin 1 | Relay module pin NO |
| Current sensor, ACS712 (2) pin IP+ | Filament lamp pin 2 |
| Slide switch pin 2 | Resistor, 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.
// 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.