DEV Community

DownyPaws
DownyPaws

Posted on

A Simple PIR-and-Interval State Machine for a Small Water Pump

Disclosure: I work with DownyPaws, a UK shop that sells a cordless cat water fountain. This article is a general electronics tutorial. It is not a teardown, not a product review, and not a claim that the commercial device uses this exact circuit or firmware.

Small circulating pumps show up in pet fountains, desk-top water features, and compact hydroponic setups. Two control modes come up again and again: run the pump for a few seconds when something moves nearby, or run it on a fixed interval so the water does not sit still for too long. This article walks through a practical way to combine those modes on a microcontroller, with a complete Arduino-style sketch you can flash and test on a bench.

The goal is a controller that is boring in the best sense. It should start predictably, ignore noisy motion edges, avoid running the pump dry if you later add a level check, and keep the high-current path off the microcontroller pin.

What you are building

The firmware has three states:

  • Idle. The pump is off. The controller watches a PIR (passive infrared) sensor and a millis()-based interval timer.
  • Motion run. A valid motion edge starts the pump and keeps it on for a fixed hold time, for example 20 seconds. Extra motion during the hold can optionally refresh the timer.
  • Interval run. If no motion has run the pump for a chosen period, for example 15 minutes, the controller starts the pump for the same hold time.

That is enough to keep water moving in a quiet room without running a pump all day. It is also a clear example of a one-shot hold timer plus a watchdog interval, a pattern you can reuse for fans, humidifiers, or aquarium aerators.

You will need:

  • A 5 V USB power source, or a USB-C breakout that can supply a stable 5 V rail. Check the current rating against your pump.
  • A small submersible or recirculating pump rated for the same voltage as your switch. Many bench pumps are 5 V or 12 V.
  • A logic-level N-channel MOSFET (for example IRLZ44N is overkill; a small logic-level part such as IRLZ34, IRLML2502, or AO3400 is more appropriate for a 5 V pump) or a module that already includes the MOSFET and a flyback diode.
  • A flyback diode across the pump motor if the module does not already provide one.
  • A PIR module with a digital output, such as an HC-SR501 or a quieter modern equivalent.
  • An Arduino Nano, Uno, or an ESP32 if you prefer. The sketch below uses Arduino-style APIs only.
  • A breadboard, jumper wires, and a common ground between the sensor, the MCU, and the MOSFET.

Do not power a pump directly from a microcontroller GPIO. Pump stall current can exceed what a pin or the onboard regulator can safely deliver, and the inductive kick on turn-off will stress the pin.

Hardware notes that actually matter

Treat the pump as an inductive load. Place the MOSFET where it can switch the pump's low side (ground side) and put a diode across the motor, cathode to the pump positive rail, anode to the switched node. A 1N5819 or a comparable Schottky is fine for a small 5 V pump; use a diode whose current rating is above the pump's stall current if you have that figure.

Keep a common ground. The PIR output is only meaningful if the sensor and the MCU share ground. If you later run the pump from a separate pack, still join the grounds unless you move to an isolated driver.

Watch the inrush. A cheap pump can pull a brief spike when it starts. A 100 µF electrolytic plus a 100 nF ceramic near the pump supply often reduces brown-outs on a weak USB port. If the MCU resets every time the pump starts, this is the first thing to check.

Keep water and electronics apart. The controller, battery or USB connector, and MOSFET belong in a dry volume. If the project lives near a tank, use a cable gland, keep connectors above the fill line, and do not rely on a rubber dust cap as if it were a waterproof rating. Charge or program the board away from the water.

The PIR module usually has two trimmers: sensitivity and time delay. For a firmware-controlled hold, turn the module's own time delay down to its minimum so the firmware owns the 20-second window. If you leave the module's delay long, you will think your code is retriggering when the sensor output is simply staying high.

The timing model

Use millis() rather than delay(). You need to keep reading the PIR during a run, and you need the interval timer to keep advancing while other work happens.

Define four intervals in one place:

  • HOLD_MS — how long the pump stays on after a trigger. Twenty seconds is a reasonable starting point.
  • INTERVAL_MS — the maximum quiet time before the interval mode runs the pump. Fifteen minutes is a common starting point.
  • MOTION_LOCKOUT_MS — ignore new motion edges for a short period after a falling edge, so a noisy PIR does not chatter.
  • MIN_ON_MS — optional, a floor so a bounce cannot produce a 30 ms pump pulse.

Store two timestamps: runUntil (when the current pump run should end) and nextIntervalDue (when the interval mode should fire if nothing else has run the pump). Every time a run starts, set runUntil = now + HOLD_MS and push nextIntervalDue = now + INTERVAL_MS. That last assignment is important. A motion run should also postpone the interval run, otherwise the pump may start again a few seconds later because the interval timer was already close to expiry.

Example sketch

The sketch below is complete enough to flash. Change the pin numbers to match your board. PUMP_PIN must be a PWM-capable pin only if you later add a soft start; a digital pin is enough for on/off control.

const int PIR_PIN = 2;
const int PUMP_PIN = 3;
const int LED_PIN = 13;

const unsigned long HOLD_MS = 20000UL;
const unsigned long INTERVAL_MS = 15UL * 60UL * 1000UL;
const unsigned long MOTION_LOCKOUT_MS = 400UL;

unsigned long runUntil = 0;
unsigned long nextIntervalDue = 0;
unsigned long motionLockoutUntil = 0;
int lastPir = LOW;

void setup() {
  pinMode(PIR_PIN, INPUT);
  pinMode(PUMP_PIN, OUTPUT);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(PUMP_PIN, LOW);
  digitalWrite(LED_PIN, LOW);

  unsigned long now = millis();
  nextIntervalDue = now + INTERVAL_MS;
}

void startRun(unsigned long now) {
  runUntil = now + HOLD_MS;
  nextIntervalDue = now + INTERVAL_MS;
  digitalWrite(PUMP_PIN, HIGH);
  digitalWrite(LED_PIN, HIGH);
}

void loop() {
  unsigned long now = millis();
  int pir = digitalRead(PIR_PIN);

  // Rising edge, after a short lockout, starts or refreshes a motion run.
  if (pir == HIGH && lastPir == LOW && now >= motionLockoutUntil) {
    startRun(now);
  }

  if (pir == LOW && lastPir == HIGH) {
    motionLockoutUntil = now + MOTION_LOCKOUT_MS;
  }
  lastPir = pir;

  // Interval mode only fires when the pump is currently off.
  if (now >= nextIntervalDue && now >= runUntil) {
    startRun(now);
  }

  if (now >= runUntil) {
    digitalWrite(PUMP_PIN, LOW);
    digitalWrite(LED_PIN, LOW);
  }
}
Enter fullscreen mode Exit fullscreen mode

millis() overflow is not a special case if you only compare with subtraction or with now >= deadline and you set deadlines with now + interval using unsigned long arithmetic. After about 49 days the counter wraps, and unsigned comparison still works as long as the intervals themselves are much shorter than the wrap period.

If you want motion during a run to refresh the hold, that already happens: a new rising edge calls startRun() again. If you would rather have a strict 20-second shot that cannot be extended, gate the motion branch with now >= runUntil.

Adding a dry-run interlock

A pump that runs against air will wear sooner and can heat up. If your tank can be emptied, add a simple level check before startRun() turns the MOSFET on.

A contact level sensor, a float switch, or two stainless probes with a high-value pull-up can all work. In firmware, require the "water present" input to stay stable for a few hundred milliseconds before you trust it. If the level input is low, skip startRun(), keep the pump off, and postpone nextIntervalDue by a shorter retry interval, for example 60 seconds, so the controller can recover after a refill without waiting a full 15 minutes.

Do not detect a dry pump only by current unless you have measured that pump. Stall and dry-run currents vary widely, and a cheap module may not give you a clean analog signal.

How to test it on the bench

Test in this order, with the pump in a cup of water, not in a finished enclosure:

  1. Leave the PIR covered or pointed at a quiet wall. Confirm the pump starts once after INTERVAL_MS. For bench work, temporarily set INTERVAL_MS to 30 seconds so you are not waiting 15 minutes.
  2. Trigger the PIR with a hand wave. Confirm a single rising edge runs the pump for about HOLD_MS, and that a second wave during the hold refreshes or is ignored, depending on the policy you chose.
  3. Confirm that a motion run pushes the next interval out. Wave, wait until the pump stops, then confirm it does not start again until the full interval has passed.
  4. Unplug the PIR output (or hold the pin low) and confirm the interval mode still works on its own.
  5. If you added a level interlock, lift the sensor out of the water and confirm the pump will not start in either mode.

If the pump chatters, look at the PIR module delay, the lockout constant, and the supply voltage at the MCU during pump start. If the MOSFET stays warm at rest, you may have a gate that is not being driven fully to the logic-high voltage, or you may have chosen a part that is not logic-level.

Why this pattern stays useful

A lot of small water appliances only need these two behaviours: respond to presence, and do not stay idle for too long. You can later replace the PIR with a time-of-flight sensor, add a soft-start PWM ramp in the first 200 ms of a run, or log run counts over serial. The state machine does not have to change much.

If you want to see a finished commercial fountain that offers motion and timer modes in a household setting, I work with the shop at https://downypaws.co.uk/ and write about everyday pet-care gadgets there. The sketch in this article is a bench pattern, not a copy of any commercial firmware.

Once the timing feels right, move the electronics into a dry box, strain-relief the pump cable, and only then put the pump into its tank. The firmware is the easy part. Keeping water where it belongs is the part that decides whether the project still works a month later.

Top comments (0)