Peristaltic Dosing Pump Controller
Lab peristaltic pumps have a DB25 remote-control port on the back that almost nobody uses, because the vendor’s foot pedal and dosing controller cost more than the pump did. This is that controller: an Arduino, a rotary encoder sitting in the middle of a 24-pixel ring, an eight-digit display and two footswitches, behind a laser-cut acrylic panel. Dial a volume, tap a pedal, and keep both hands on what you are actually doing.
This drives an existing pump through its remote port. It is not a pump — the head, the motor and the tubing are the pump you already own. Check yours has a DB25 remote connector, and get its manual out, because you will need to know what it expects on pins 1, 15 and 16.
What it does
The encoder sets a target volume between 100 and 4000 in 50-unit steps, and comes up at 2000. The display carries both numbers at once: dose-so-far on the right four digits, target on the left four. Tap the left footswitch and D4 pulls DB25 pin 15 to start the pump; tap it again and the pump stops. The right footswitch flips direction on DB25 pin 16, and only while the pump is stopped, which is the right way round. Speed is a separate number, 150 to 2900, that leaves D6 as PWM and reaches the pump’s speed input on DB25 pin 1 through a boost module.
Volume is in mL — the sketch’s own commented-out debug line says so — and the counter integrates the speed number as volume per minute, so speed is mL/min. A target of 2000 is two litres and the 2900 ceiling is a fast pump.
The volume counter is fed by the pump, not by a clock. A3 reads the pump’s speed feedback through a divider, the sketch maps 0–405 ADC counts onto 0–2900 mL/min, and integrates that ten times a second. That is the whole reason this thing doses accurately instead of guessing, and it is also the first thing that bites you: with A3 unwired the measured speed is zero, the volume never climbs, and the pump runs until you tap the pedal again.
Speed comes up at 2500, and because of the pin collision described under Wiring it stays there — the sketch as shipped writes a fixed PWM of 219 out of 255 to D6 and never changes it. Everything you set with the knob is volume.
Parts
Some links below are Amazon affiliate links. As an Amazon Associate I earn from qualifying purchases — at no extra cost to you. It helps keep these guides free. Outside the US, they should send you to your own Amazon store; if one doesn't, searching the part number there will find it.
- Arduino Nano (ATmega328P) — the sketch uses A0, A1, A3, A4, A5 and most of D4 through D13, which leaves a Nano with exactly one digital pin spare. A Uno works and is bigger than it needs to be.
- Nano screw-terminal adapter, 3-pack — fifteen wires land on this board. Dropping the Nano into a terminal shield turns every one of them into a screwdriver job you can redo inside the box.
- KY-040 rotary encoder module, 5-pack
— buy the module, not a bare encoder. The sketch sets the two encoder channels
to plain
INPUTwith no pull-up, so it needs a board that brings its own. The knob caps come in the bag. A bare EC11, 10-pack is cheaper and has the same 7 mm threaded bushing the panel is cut for, but then the two 10 kΩ pull-ups on the channels are yours to add. - MAX7219 8-digit seven-segment display, 2-pack — this exact module. The panel’s display window is 60.8 × 14.3 mm, which is this board’s eight digits and no border either side.
- 24-pixel RGBW LED ring — the panel aperture is 66 mm outside and 51 mm inside, so it wants a 24-pixel ring and no other size. A 12-pixel ring is 37 mm across and rattles around in the hole.
- 2-channel relay module, 4-pack — low-level trigger, which is what the sketch assumes: it writes HIGH to leave a valve shut. These drive pinch valves on the lines, not the pump.
- Momentary foot pedals
×2 — any normally-open pedal. Both inputs are
INPUT_PULLUPand read a press as a short to ground, so there is nothing to get wrong but the polarity of a changeover switch. - DB25 male solder-cup connector and hood — only three pins and a ground actually go anywhere.
- XL6009 boost module, 5-pack — this is what gets the speed signal above the Arduino’s 5 V ceiling. Skip it if your pump’s speed input tops out at 5 V.
- Voltage sensor / divider module — for the feedback line into A3. See Calibration for the ratio the sketch expects.
- 12 V 4 A supply, MP1495 buck module, 5-pack and 5.5 × 2.5 mm panel jacks — nothing in the box actually wants 12 V. The buck is there because a barrel-jack brick is the easy thing to buy and the ring needs a stiffer 5 V than a Nano’s regulator will give it. Match the jack to the brick’s 2.5 mm plug; a 2.1 mm jack will not take it.
- White opaque acrylic, 12 × 36 in, 1/4 in — one sheet holds the 470 mm panel with room left over. The second, larger panel set in the folder needs a sheet of its own; see The panel.
- 24 AWG silicone hookup wire and Dupont jumpers, or perfboard, headers and terminal blocks if you would rather solder it once.
- M3 screw and nut kit and nylon standoffs — the boards all mount on M3, and the nylon ones keep the relay board’s underside off an acrylic panel you have just wired.
Wiring
Every pin here is read out of BRUHpumpcontrollerV6.ino, which is the authority.
| Function | Arduino pin | Goes to |
|---|---|---|
| Pump start / stop | D4 | DB25 pin 15 |
| NeoPixel data | D5 | 24-pixel ring |
| Pump speed set | D6 | Boost module → DB25 pin 1 |
| Display CLK | D7 | MAX7219 |
| Display CS | D8 | MAX7219 |
| Display DIN | D9 | MAX7219 |
| Encoder channel A | D10 | KY-040 DT |
| Encoder switch | D10 | KY-040 SW — see below |
| Encoder channel B | D12 | KY-040 CLK |
| Pump direction | D13 | DB25 pin 16 |
| Left footswitch | A0 | Pedal to ground |
| Right footswitch | A1 | Pedal to ground |
| Pump feedback | A3 | Voltage divider from the pump |
| Relay IN1 | A4 | 2-channel relay |
| Relay IN2 | A5 | 2-channel relay |
D11 and A2 are the only pins left. D2 and D3 are declared at the top of the
sketch as the encoder pins and then never read — outputA is sampled once in
setup() into a variable nothing looks at again, and outputB is not used at
all. Leave them empty.
The encoder switch and encoder channel A are both on D10. EncoderSwitch is
10 and encoder0PinA is 10, and setup() sets pin 10 to INPUT_PULLUP and then
immediately to plain INPUT, so the pull-up loses. In practice the sketch reads
the same pin as “is the button held” and “did channel A just go high”, the
button always looks released, and the encoder only ever adjusts volume — speed
mode is unreachable.
The fix is one character: change int EncoderSwitch = 10; to 11 and wire the
encoder’s SW to D11, which is the free pin. Do it before you cut wire to length.
The display comment lies. Above the constructor the sketch says “7 to DIN,
4 to CS, 5 to CLK”, but the constructor is DigitLedDisplay(9, 8, 7) — DIN on
9, CS on 8, CLK on 7. The constructor is what runs; the comment is left over
from an earlier revision. Wire the table.
D13 is also the Nano’s onboard LED, so the board tells you which way the pump is set to turn without you opening the box. That is worth keeping rather than moving. The other half of that deal is the bootloader, which flashes the same pin for a second or so after every reset — and opening the serial monitor resets the board. Your pump’s direction input sees those pulses. Power the controller up first and the pump second, and don’t plug USB in mid-run.
The ring and the relays are switched off
loop() calls ReadFootswitch(), ReadEncoder(), ControlPump() and
UpdateLCD(). The fifth line is //UpdateLEDS(); — commented out. Build the
sketch as shipped and the pump doses correctly, the display counts, and the ring
stays dark forever.
That matters more than it sounds, because the relay logic lives inside
UpdateLEDS() too, under a heading that has nothing to do with LEDs. With that
call commented out the valves sit in whatever state setup() left them —
both closed — and never flop with direction. Uncomment the line to get either
one, and expect to get both.
While you are in there, UpdateLEDS() writes setPixelColor(NUM_LEDS, ...) in
its stopped-and-blinking branch. On a 24-pixel ring the valid indices are 0 to
23, so that branch lights nothing. Change it to i if you want the red blink.
The panel
The case is laser-cut acrylic, not printed, and the folder holds two designs
that are not two views of the same box. Cut pump_controller_housing_v4.
That one is a 470 × 165 mm panel with a 5 mm square out of each corner, and everything the sketch drives has an opening on it. Measured off the DXF:
| Opening | Size | Where |
|---|---|---|
| Ring aperture | 66.3 mm outside, 51 mm inside | 270 mm from the left edge, 70 mm up |
| Encoder shaft | 7.2 mm | Dead centre of the ring aperture |
| Display window | 60.8 × 14.3 mm | Directly above the ring, on the same centreline |
| Two round openings | 29.9 mm, centres 62.4 mm apart | Right-hand end |
| Rectangular cutout | 50 × 27 mm | Left of centre |
| Round holes | 9.5 mm and 11.9 mm | Bottom edge, centres 54 mm apart |
| Corner squares | 5 × 5 mm, ×4 | Joinery |
The encoder sits in the middle of the ring, which is the whole idea of the layout: the knob you are turning is surrounded by the bar graph of the dose it is running, and the display sits on the same vertical centreline above both. A 7.2 mm hole is the threaded bushing of an EC11-bodied encoder, which is what a KY-040 module has on it.
Nothing in the sketch needs a 29.9 mm hole. Two of them at the end of the panel is cable-gland or panel-connector territory, and that is the one part of this layout worth resizing to what you are actually bringing through the acrylic before you send the job.
There are no finger joints anywhere in these files — the panels are rectangles with corner squares removed, and they bolt together. That means material thickness is yours to choose: nothing in the cut geometry is sized to a particular sheet. 1/4 in is stiff enough that a footswitch cable tugging on the panel does not flex the display out of its window.
Two further rectangles, 113.8 × 98.9 mm and 99.9 × 99.9 mm, sit over the bottom
edge of pump_controller_housing_v4 and overhang the panel outline. Separate
them in your laser software before you send the job.
pump_controller_front is the older design, not a re-nest of the current one:
290 × 315 mm and portrait rather than landscape, 7.9 mm corner squares instead
of 5 mm, one 29.9 mm opening and one 30.4 mm one instead of the matched pair,
and no 9.5 mm hole. The ring, encoder, display and 50 × 27 mm cutout are the
same. It is there for reference; cut housing_v4.
The rest of the folder is a second, larger panel set for a taller box, and it
wants its own sheet — 560 × 253 mm will not share a 12 × 36 in sheet with the
470 mm panel. front_final is four 140 × 253 mm panels nested on one sheet,
each with 8 mm corner squares, and only the fourth one is drilled.
front_only_final is that drilled panel on its own: two 99.9 × 108.9 mm
windows, a 16.4 mm hole and a 7.4 mm hole — another encoder shaft. Sides is a
plain 132 × 245 mm rectangle with no openings at all, and those numbers are
exactly 140 − 8 and 253 − 8, so a side panel is one corner-notch smaller than
the front in both directions. That relationship is how the set goes together,
and it is worth checking on a cardboard mock-up before you cut acrylic.
swirl_final is a 371 × 260 mm spiral of some sixteen thousand entities. It is
artwork, not a cut path. Send it to the engrave layer or you will spend an hour
cutting a spiral out of a sheet you needed.
Check the scale on import. Every file here is in millimetres, and some laser software silently rescales an imported DXF to whatever it feels like. Measure the 60.8 mm display window on screen before you fire anything.
Assembly
-
Cut the panel and dry-fit the display, the encoder and the ring into their openings while everything is still flat and reachable. Acrylic kerf varies by machine; a 7.2 mm shaft hole is the one that most often needs a pass with a round file.
-
Wire the panel hardware first — display, encoder, ring — with the panel on the bench in front of you, and leave enough tail to lift it away from the box later.
-
Set the buck module to 5 V before you connect it to anything. Put a meter on its output, turn the trimmer, then wire it in. These ship at whatever the last person on the production line left them at.
-
Solder the DB25 last. It is the connector you least want to redo inside a closed box, and only pins 1, 15, 16 and a ground go anywhere.
-
Bring it up on the bench with
DEBUGon and the pump disconnected. The serial log prints every volume and speed change, so you can confirm the encoder counts in the right direction before anything moves liquid.
Firmware
The sketch is BRUHpumpcontrollerV6.ino, in the download section below. It needs
two libraries, both from the Arduino IDE’s Library Manager:
- Adafruit NeoPixel for the ring
- DigitLedDisplay for the MAX7219
Keep the .ino inside a folder of the same name, which is how the IDE expects
to find a sketch.
The ring is declared NEO_GRBW — RGBW, with a real white LED in each pixel. If
you buy a plain RGB WS2812B ring instead, change that to NEO_GRB or every
colour comes out shifted. BRIGHTNESS is 50 of 255, which is what keeps 24 RGBW
pixels under half an amp; turn it up and size your 5 V rail accordingly.
There is a #define DEBUG at the top. Leave it while you are bringing the board
up and comment it out when the box is closed.
Calibration
Two separate things need calibrating, and they are not related.
The feedback divider. currentPumpSpeed = map(voltage, 0, 405, 0, 2900) — so
the sketch expects the pump’s full-scale speed feedback to arrive at A3 as 405
ADC counts, which on a 5 V reference is about 1.98 V. Set your divider so that
your pump running flat out puts roughly 2 V on A3. Get this wrong and every dose
is wrong by the same ratio, silently, because nothing else in the sketch
measures anything.
Volume per revolution. That depends on your tubing bore and your pump head, so the numbers on the display mean nothing until you check them. Pump into a tared vessel on a balance, weigh what came out, and trim the divider until the display agrees with the scale. Redo it whenever the tubing changes — peristaltic tubing takes a set as it ages, and an old line delivers less per turn than a new one.
The speed line. D6 is PWM, not a voltage. Whatever you put between D6 and DB25 pin 1 has to smooth it to DC first, and the boost module is only in the chain because some pumps want 0–10 V there and an Arduino cannot reach it. Check your pump’s manual for what pin 1 expects, then set the module so full-scale PWM lands at full-scale speed and not above it.
Troubleshooting
The pump starts and never stops. A3 is not seeing the feedback line, so measured speed is zero and the accumulated volume never reaches the target. Check the divider before you check anything else.
The ring is dark and the valves never move. UpdateLEDS() is commented out
in loop(). See above.
Turning the knob only ever changes volume, never speed. The D10 collision. Move the encoder switch to D11.
The right pedal stops responding when you are holding the left one.
ReadFootswitch() gates the right switch on Left_Footswitch_Status_Previous
instead of the right one’s. It is a copy-paste slip in the condition; change it
to Right_Footswitch_Status_Previous and both pedals become independent.
The display shows garbage or nothing. DIN, CS and CLK are 9, 8 and 7 — not what the comment above the constructor says.
The pump twitches when you plug in USB. That is the bootloader blinking D13, which is also the direction line. Nothing is wrong; don’t do it while a dose is running.
Files and downloads
Laser-cut files
Vector cut paths. Check the scale against the drawing before you cut — some laser software re-scales an imported DXF on open.
- front_final.dxf
- front_final.svg
- front_only_final.dxf
- front_only_final.svg
- pump_controller_front.dxf
- pump_controller_housing_v4.dxf
- pump_controller_housing_v4.svg
- Sides.dxf
- Sides.svg
- swirl_final.dxf
- swirl_final.svg
Firmware
The sketch that runs on the microcontroller. Open the .ino in the
Arduino IDE — keep it in a folder of the same name, which is how the IDE expects
to find a sketch.