Skip to content

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.

Wiring

Every pin here is read out of BRUHpumpcontrollerV6.ino, which is the authority.

FunctionArduino pinGoes to
Pump start / stopD4DB25 pin 15
NeoPixel dataD524-pixel ring
Pump speed setD6Boost module → DB25 pin 1
Display CLKD7MAX7219
Display CSD8MAX7219
Display DIND9MAX7219
Encoder channel AD10KY-040 DT
Encoder switchD10KY-040 SW — see below
Encoder channel BD12KY-040 CLK
Pump directionD13DB25 pin 16
Left footswitchA0Pedal to ground
Right footswitchA1Pedal to ground
Pump feedbackA3Voltage divider from the pump
Relay IN1A42-channel relay
Relay IN2A52-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:

OpeningSizeWhere
Ring aperture66.3 mm outside, 51 mm inside270 mm from the left edge, 70 mm up
Encoder shaft7.2 mmDead centre of the ring aperture
Display window60.8 × 14.3 mmDirectly above the ring, on the same centreline
Two round openings29.9 mm, centres 62.4 mm apartRight-hand end
Rectangular cutout50 × 27 mmLeft of centre
Round holes9.5 mm and 11.9 mmBottom edge, centres 54 mm apart
Corner squares5 × 5 mm, ×4Joinery

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. Bring it up on the bench with DEBUG on 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.

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.

Every file for this project on GitHub →