Built-in project · Arduino Uno R3

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.

The circuit runs here once the page has loaded.

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.

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
Resistor, 220 Ω pin 1Arduino Uno R3 pin 12
Buzzer pin 1Arduino Uno R3 pin 8
Relay module pin INArduino Uno R3 pin 7
Pushbutton pin 1aArduino Uno R3 pin 4
Float switch, ZP4510 (2) pin 1Arduino Uno R3 pin 3
Float switch, ZP4510 (1) pin 1Arduino Uno R3 pin 2
Current sensor, ACS712 pin VCCArduino Uno R3 pin 5V
Relay module pin VCCArduino Uno R3 pin 5V
Float switch, ZP4510 (1) pin 2Arduino Uno R3 pin GND
Float switch, ZP4510 (2) pin 2Arduino Uno R3 pin GND
Current sensor, ACS712 pin GNDArduino Uno R3 pin GND
Relay module pin GNDArduino Uno R3 pin GND
Mini water pump pin V-Arduino Uno R3 pin GND
Pushbutton pin 2aArduino Uno R3 pin GND
Buzzer pin 2Arduino Uno R3 pin GND
LED, red pin CArduino Uno R3 pin GND
Current sensor, ACS712 pin OUTArduino Uno R3 pin A0
Relay module pin NOCurrent sensor, ACS712 pin IP+
Slide switch pin 1Current sensor, ACS712 pin IP-
Relay module pin COM6 V
Slide switch pin 2Mini water pump pin V+
LED, red pin AResistor, 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.

sketch.ino · Arduino Uno R3
// 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 editor

Troubleshooting

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.