Sump pump controller on an Arduino Uno
A teaching model of a sump controller: two floats run a relay pump, an ACS712 checks that current really flows, and alarms catch a dead pump, an overlong run and high water. The whole build, an Arduino Uno and 10 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 35 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.
What you'll learn
- Sensors
- Motors
- Reading a button
- Making sound with a buzzer
- Choosing and reading a resistor
- Difficulty: Intermediate
- Time: About 35 minutes
- Board: Arduino Uno R3
- Parts: 13
How it works
A teaching model of a sump controller: two floats run a relay pump, an ACS712 checks that current really flows, and alarms catch a dead pump, an overlong run and high water.
Arduino Uno R3 runs the sketch below. The circuit around it: Float switch, ZP4510, Current sensor, ACS712, Relay module, Mini water pump, Slide switch, Pushbutton. pinMode sets PUMP_FLOAT as an input with its pull-up on. digitalWrite drives RELAY LOW. digitalRead reads whether PUMP_FLOAT is HIGH or LOW. analogRead measures the voltage on SENSOR as a number. Serial.begin opens the serial port at 115200 baud.
Parts list
13 parts, plus the jumper wires. They retail for about $41; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Full-size breadboard
- 2 × Float switch, ZP4510
- 1 × Current sensor, ACS712
- 1 × Relay moduleIn Halloween Prop Kit.
- 1 × Mini water pump
- 1 × Power, 6 V
- 1 × Slide switchIn Logic Lab Kit, Spaceship Panel Kit.
- 1 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab Kit.
- 1 × BuzzerIn Starter Kit, Rocket Flight Computer Kit.
- 1 × Resistor, 220 ΩIn Starter Kit, Claw Machine Kit, Halloween Prop Kit.
- 1 × LED, redIn Starter Kit, Halloween Prop 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 |
|---|---|
| Resistor, 220 Ω pin 1 | Arduino Uno R3 pin 12 |
| Buzzer pin 1 | Arduino Uno R3 pin 8 |
| Relay module pin IN | Arduino Uno R3 pin 7 |
| Pushbutton pin 1a | Arduino Uno R3 pin 4 |
| Float switch, ZP4510 (2) pin 1 | Arduino Uno R3 pin 3 |
| Float switch, ZP4510 (1) pin 1 | Arduino Uno R3 pin 2 |
| Current sensor, ACS712 pin VCC | Arduino Uno R3 pin 5V |
| Relay module pin VCC | Arduino Uno R3 pin 5V |
| Float switch, ZP4510 (1) pin 2 | Arduino Uno R3 pin GND |
| Float switch, ZP4510 (2) pin 2 | Arduino Uno R3 pin GND |
| Current sensor, ACS712 pin GND | Arduino Uno R3 pin GND |
| Relay module pin GND | Arduino Uno R3 pin GND |
| Mini water pump pin V- | Arduino Uno R3 pin GND |
| Pushbutton pin 2a | Arduino Uno R3 pin GND |
| Buzzer pin 2 | Arduino Uno R3 pin GND |
| LED, red pin C | Arduino Uno R3 pin GND |
| Current sensor, ACS712 pin OUT | Arduino Uno R3 pin A0 |
| Relay module pin NO | Current sensor, ACS712 pin IP+ |
| Slide switch pin 1 | Current sensor, ACS712 pin IP- |
| Relay module pin COM | 6 V |
| Slide switch pin 2 | Mini water pump pin V+ |
| LED, red pin A | Resistor, 220 Ω pin 2 |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Sump pump controller: two floats, a relay, a current sensor and an alarm.
//
// A TEACHING MODEL with low-voltage stand-ins. A signal float switch only
// switches the relay's coil here. A real sump pump runs on line voltage, on
// its own GFCI-protected circuit, with wiring and a switch rated for it. Never
// connect this board to a building's pump or panel, and do not count on it to
// keep a basement dry. A battery backup pump is advised for the day the power
// fails in the same storm that fills the pit.
//
// pin 2 PUMP float (its other wire to GND): closed when the water is up
// pin 3 HIGH float, mounted above it (to GND): closed when the water is
// above the pump's normal range
// pin 4 ACK button to GND
// pin 7 the relay module's IN (high trips it); its COM is the pump's
// 6 V stand-in supply, its NO goes to the ACS712
// A0 the ACS712's OUT (VCC on 5 V, GND on GND); the pump's current
// flows through its IP+ and IP-
// pin 8 an active buzzer (its other leg to GND), beeped four times a second
// pin 12 the red alarm LED, through 220 ohms
//
// The rules.
// 1. PUMP float up for half a second: relay on. Pump off again once that
// float has been down for OFF_DELAY_MS, so each cycle pumps the pit a
// little below the float instead of switching on and off at one level.
// 2. HIGH float up for a third of a second: relay on, whatever the other
// float says, and the HIGH WATER alarm. The pump did not keep up.
// 3. Current check. Half a second after the relay pulls in the ACS712 must
// see at least RUN_MIN_MA. None means a dead pump, a stuck or burnt relay
// or no power: the NO CURRENT alarm.
// 4. Run time check. A cycle longer than MAX_RUN_MS is a stuck float, a
// clogged discharge or more water coming in than the pump can move: the
// LONG RUN alarm. The pump is left running, because the water is still
// there. (A pump that runs dry for long cooks itself: a real one has its
// own thermal cutout.) With no current the pump is not running, so this
// alarm waits for rule 3 to say so.
// NO CURRENT and LONG RUN stay until you press ACK; HIGH WATER clears by
// itself three seconds after the HIGH float falls. ACK also silences the
// buzzer. The LED stays lit while any alarm is set.
//
// Time scale. A real cycle is half a minute to a couple of minutes and a long
// run is a few minutes. MAX_RUN_MS is 20 seconds here and stands for 5
// minutes (15 to 1), so the simulator can reach it quickly. The cycles per
// hour figure is not scaled: it is the rate the last cycles imply, so it reads
// high when the simulator cycles every few seconds.
//
// The ACS712 5 A board reads DC here (185 mV an amp, 2.5 V at zero), and the
// zero is measured with the relay off at power on. A real AC pump needs an RMS
// reading (see the home energy monitor), or a current transformer.
const int PUMP_FLOAT = 2;
const int HIGH_FLOAT = 3;
const int ACK_BUTTON = 4;
const int RELAY = 7;
const int BUZZER = 8;
const int LED = 12;
const int SENSOR = A0;
const unsigned long OFF_DELAY_MS = 3000; // keep pumping this long after the float drops
const unsigned long SPINUP_MS = 500; // time to see current after the relay pulls in
const unsigned long MAX_RUN_MS = 20000; // stands for 5 minutes
const unsigned long RUN_MIN_MA = 100; // below this the pump is not drawing current
const unsigned long HIGH_CLEAR_MS = 3000;
const byte NOCURRENT = 1;
const byte LONGRUN = 2;
const byte HIGHWATER = 4;
struct Contact {
int pin;
bool raw;
bool stable;
unsigned long changed;
};
Contact pumpFloat = {PUMP_FLOAT, false, false, 0};
Contact highFloat = {HIGH_FLOAT, false, false, 0};
int midpoint = 512;
bool pumpOn = false;
unsigned long onAt = 0;
unsigned long windowAt = 0; // when the checks last started counting: pump on, or ACK
unsigned long floatDownAt = 0;
unsigned long cycles = 0;
unsigned long starts[5];
byte startCount = 0;
unsigned long lastRunS = 0;
unsigned long highDownAt = 0;
byte alarms = 0;
bool muted = false;
bool ackLast = false;
unsigned long mA = 0;
unsigned long lastRead = 0;
unsigned long lastReport = 0;
// A contact must hold a new state for `hold` ms before it is believed.
void track(Contact &c, bool upNow, unsigned long hold) {
if (upNow != c.raw) {
c.raw = upNow;
c.changed = millis();
}
if (c.raw != c.stable && millis() - c.changed >= hold) c.stable = c.raw;
}
unsigned long readMilliamps() {
long sum = 0;
for (int i = 0; i < 32; i++) sum += analogRead(SENSOR);
long diff = sum / 32 - midpoint;
if (diff < 0) diff = -diff;
// one step is 4.9 mV, and 185 mV is an amp: 26.4 mA a step
return (unsigned long)diff * 264 / 10;
}
void raise(byte which, const char *text) {
if (alarms & which) return;
alarms |= which;
muted = false;
Serial.print("ALARM: ");
Serial.println(text);
}
void setup() {
Serial.begin(115200);
Serial.println("Mokxi Arduino Uno: sump pump controller");
pinMode(PUMP_FLOAT, INPUT_PULLUP);
pinMode(HIGH_FLOAT, INPUT_PULLUP);
pinMode(ACK_BUTTON, INPUT_PULLUP);
pinMode(RELAY, OUTPUT);
pinMode(BUZZER, OUTPUT);
pinMode(LED, OUTPUT);
digitalWrite(RELAY, LOW);
// The pump is off: whatever the sensor reads now is its zero.
long sum = 0;
for (int i = 0; i < 256; i++) sum += analogRead(SENSOR);
midpoint = sum / 256;
Serial.print("zero ");
Serial.println(midpoint);
}
void setPump(bool on) {
pumpOn = on;
digitalWrite(RELAY, on ? HIGH : LOW);
if (on) {
onAt = millis();
windowAt = onAt;
cycles++;
for (int i = 4; i > 0; i--) starts[i] = starts[i - 1];
starts[0] = millis();
if (startCount < 5) startCount++;
Serial.println("pump on");
} else {
lastRunS = (millis() - onAt) / 1000;
Serial.print("pump off after ");
Serial.print(lastRunS);
Serial.println(" s");
}
}
void report() {
Serial.print("pump ");
Serial.print(pumpOn ? "ON " : "OFF ");
Serial.print(mA);
Serial.print(" mA run ");
Serial.print(pumpOn ? (millis() - onAt) / 1000 : 0);
Serial.print(" s cycles ");
Serial.print(cycles);
Serial.print(" per hour ");
// the rate the last few cycle starts imply
unsigned long perHour = 0;
if (startCount >= 2 && starts[0] > starts[startCount - 1])
perHour = (startCount - 1) * 3600000UL / (starts[0] - starts[startCount - 1]);
Serial.print(perHour);
Serial.print(" pump float ");
Serial.print(pumpFloat.stable ? "UP" : "down");
Serial.print(" high float ");
Serial.print(highFloat.stable ? "UP" : "down");
Serial.print(" alarm ");
if (!alarms) Serial.print("none");
if (alarms & NOCURRENT) Serial.print("NOCURRENT ");
if (alarms & LONGRUN) Serial.print("LONGRUN ");
if (alarms & HIGHWATER) Serial.print("HIGHWATER ");
Serial.println();
}
void loop() {
unsigned long now = millis();
track(pumpFloat, digitalRead(PUMP_FLOAT) == LOW, 500);
track(highFloat, digitalRead(HIGH_FLOAT) == LOW, 300);
if (now - lastRead >= 100) {
lastRead = now;
mA = readMilliamps();
}
// The pump follows the floats. The high float forces it on.
if (pumpFloat.stable || highFloat.stable) floatDownAt = now;
bool want = pumpOn ? (now - floatDownAt < OFF_DELAY_MS) : (pumpFloat.stable || highFloat.stable);
if (want != pumpOn) setPump(want);
now = millis(); // setPump() may have just stamped a later time
if (highFloat.stable) {
raise(HIGHWATER, "high water, pump forced on");
highDownAt = now;
} else if ((alarms & HIGHWATER) && now - highDownAt >= HIGH_CLEAR_MS) {
alarms &= ~HIGHWATER;
Serial.println("OK: high water cleared");
}
if (pumpOn) {
if (now - windowAt >= SPINUP_MS && mA < RUN_MIN_MA) raise(NOCURRENT, "no pump current");
// A pump that draws nothing is not running long; that is the other alarm.
if (now - windowAt >= MAX_RUN_MS && mA >= RUN_MIN_MA) raise(LONGRUN, "pump run too long");
}
// ACK: silence the buzzer and clear the faults that latch. They come back
// by themselves if the cause is still there.
bool down = digitalRead(ACK_BUTTON) == LOW;
if (down != ackLast) {
ackLast = down;
if (down) {
muted = true;
if (alarms & (NOCURRENT | LONGRUN)) Serial.println("alarm acknowledged");
alarms &= ~(NOCURRENT | LONGRUN);
if (pumpOn) windowAt = now;
}
}
digitalWrite(LED, alarms ? HIGH : LOW);
digitalWrite(BUZZER, alarms && !muted && (now / 250) % 2 ? HIGH : LOW);
if (now - lastReport >= 1000) {
lastReport = now;
report();
}
}
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: sump pump controller");
pinMode(PUMP_FLOAT, INPUT_PULLUP);
pinMode(HIGH_FLOAT, INPUT_PULLUP);
pinMode(ACK_BUTTON, INPUT_PULLUP);
pinMode(RELAY, OUTPUT);
pinMode(BUZZER, OUTPUT);
pinMode(LED, OUTPUT);
digitalWrite(RELAY, LOW);
// The pump is off: whatever the sensor reads now is its zero.
long sum = 0;
for (int i = 0; i < 256; i++) sum += analogRead(SENSOR);
midpoint = sum / 256;
Serial.print("zero ");
Serial.println(midpoint);
}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();
track(pumpFloat, digitalRead(PUMP_FLOAT) == LOW, 500);
track(highFloat, digitalRead(HIGH_FLOAT) == LOW, 300);
if (now - lastRead >= 100) {
lastRead = now;
mA = readMilliamps();
}
// The pump follows the floats. The high float forces it on.
if (pumpFloat.stable || highFloat.stable) floatDownAt = now;
bool want = pumpOn ? (now - floatDownAt < OFF_DELAY_MS) : (pumpFloat.stable || highFloat.stable);
if (want != pumpOn) setPump(want);
now = millis(); // setPump() may have just stamped a later time
if (highFloat.stable) {
raise(HIGHWATER, "high water, pump forced on");
highDownAt = now;
} else if ((alarms & HIGHWATER) && now - highDownAt >= HIGH_CLEAR_MS) {
alarms &= ~HIGHWATER;
Serial.println("OK: high water cleared");
}
if (pumpOn) {
if (now - windowAt >= SPINUP_MS && mA < RUN_MIN_MA) raise(NOCURRENT, "no pump current");
// A pump that draws nothing is not running long; that is the other alarm.
if (now - windowAt >= MAX_RUN_MS && mA >= RUN_MIN_MA) raise(LONGRUN, "pump run too long");
// ... 23 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- The switch acts opposite to what you expected: closed when dry, open when wet. Float switch
The float can be mounted either way up. Turn the float over on the stem to change whether the reed closes or opens when the water rises. Test with a meter. - The input reads random values or the pump chatters at the water level. Float switch
Wire it between the pin and ground with INPUT_PULLUP. Waves make the reed flicker, so add a delay or a short debounce time and keep the float in calm water. - 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. - A pump wired straight to a microcontroller pin does not run or resets the board. It draws far more than a pin can give. Mini water pump
Switch it with a logic level MOSFET or a transistor with a base resistor, from its own supply of 3 to 6 V. Join the grounds. - The motor spikes damage the driver, or the pump runs dry and burns out. A coil load makes voltage kicks when it stops. Mini water pump
Add a flyback diode across the pump, stripe to the positive side. Keep the pump under water, since a dry run overheats it and wears the seal.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.