Dosing pump on an Arduino Uno
A peristaltic pump on an encoder gear motor and an L9110S: press the button and it pumps exactly 5 ml, counting the encoder's pulses and slowing for the last turn. The whole build, an Arduino Uno and 4 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 30 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $43. On Mokxi they're free.
What you'll learn
- Counting encoder pulses with an interrupt
- Turning turns of a pump into milliliters
- Controlling a motor with an L9110S and PWM
- Slowing down before the end to stop accurately
- Powering a motor from its own battery with a shared ground
- Difficulty: Intermediate
- Time: About 30 minutes
- Board: Arduino Uno R3
- Parts: 6
How it works
A peristaltic pump moves the same amount of liquid every turn, so a dose is just a number of turns. The encoder on the gear motor counts 330 pulses per pump turn. This pump moves 0.5 ml a turn, so 5 ml is 10 turns, or 3300 pulses.
Press the button and the pump runs at full speed. One turn before the end it slows to a crawl. A few pulses early it brakes, and the count keeps going while the motor coasts. The Serial Monitor then prints the dose that actually went through.
Parts list
6 parts, plus the jumper wires. They retail for about $43; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab Kit.
- 1 × Motor driver, L9110S
- 1 × Gear motor with encoder
- 1 × Battery: AA, coin cell, LiPoIn Handheld Console Kit, Laser Sword Kit, Power Bank 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 |
|---|---|
| Motor driver, L9110S pin AIB | Arduino Uno R3 pin 6 |
| Motor driver, L9110S pin AIA | Arduino Uno R3 pin 5 |
| Pushbutton pin 1a | Arduino Uno R3 pin 4 |
| Gear motor with encoder pin B | Arduino Uno R3 pin 3 |
| Gear motor with encoder pin A | Arduino Uno R3 pin 2 |
| Gear motor with encoder pin VCC | Arduino Uno R3 pin 5V |
| Pushbutton pin 2a | Arduino Uno R3 pin GND |
| Motor driver, L9110S pin GND | Arduino Uno R3 pin GND |
| Gear motor with encoder pin GND | Arduino Uno R3 pin GND |
| Battery: AA, coin cell, LiPo pin V- | Arduino Uno R3 pin GND |
| Battery: AA, coin cell, LiPo pin V+ | Motor driver, L9110S pin VCC |
| Gear motor with encoder pin M1 | Motor driver, L9110S pin OA1 |
| Gear motor with encoder pin M2 | Motor driver, L9110S pin OA2 |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Dosing pump: a peristaltic pump head on a gear motor with a Hall encoder,
// driven by an L9110S, that pumps a measured dose at the press of a button.
// The aquarium fertilizer doser, the plant feeder and the cocktail machine
// are all this.
//
// pin 5 the L9110S's A-IA (speed, PWM) pin 6 its A-IB
// pin 2 the encoder's A (an interrupt) pin 3 its B
// pin 4 the button to GND (INPUT_PULLUP)
// the pack's + to the L9110S's VCC; the encoder's VCC to 5 V; grounds joined
//
// A peristaltic pump moves the same volume every turn of its rotor, so the
// dose is a number of turns, and the encoder counts them. It gives 11 pulses
// a motor turn through a 30:1 gearbox: 330 pulses on A for each turn of the
// pump. This head moves 0.5 ml a turn (measure yours: pump 20 turns into a
// measuring cylinder and divide), so 5 ml is 10 turns, 3300 pulses.
//
// # Stopping on the mark
//
// A motor does not stop the instant it is told. So the pump runs at full
// speed until it is one turn short, slows to a crawl for the last turn, and
// brakes (both inputs high) a little early, at 8 pulses to go, for the few
// it coasts through: at a crawl, shorted, with its gearbox dragging, a gear
// motor stops within a few degrees of the pump head. The count goes on after
// the brake, so the dose printed is what actually went through.
const int IA = 5;
const int IB = 6;
const int ENC_A = 2;
const int ENC_B = 3;
const int BUTTON = 4;
const long PULSES_PER_TURN = 330;
// Hundredths of a ml per turn of the pump head.
const long CENTI_ML_PER_TURN = 50;
const long DOSE_CENTI_ML = 500;
const long DOSE_PULSES = DOSE_CENTI_ML * PULSES_PER_TURN / CENTI_ML_PER_TURN;
const long SLOW_AT = DOSE_PULSES - PULSES_PER_TURN;
const long BRAKE_AT = DOSE_PULSES - 8;
const int FULL = 255;
const int CRAWL = 90;
volatile long pulses = 0;
void onA() {
// B low on A's rising edge is forward.
if (digitalRead(ENC_B) == LOW) {
pulses++;
} else {
pulses--;
}
}
long count() {
noInterrupts();
long n = pulses;
interrupts();
return n;
}
void run(int speed) {
analogWrite(IA, speed);
digitalWrite(IB, LOW);
}
void brakeMotor() {
digitalWrite(IA, HIGH);
digitalWrite(IB, HIGH);
}
void printMl(long p) {
long centi = p * CENTI_ML_PER_TURN / PULSES_PER_TURN;
Serial.print(centi / 100);
Serial.print('.');
if (centi % 100 < 10) Serial.print('0');
Serial.print(centi % 100);
Serial.print(" ml");
}
void setup() {
Serial.begin(115200);
Serial.println("Mokxi Arduino Uno: dosing pump");
pinMode(IA, OUTPUT);
pinMode(IB, OUTPUT);
pinMode(ENC_A, INPUT);
pinMode(ENC_B, INPUT);
pinMode(BUTTON, INPUT_PULLUP);
brakeMotor();
attachInterrupt(digitalPinToInterrupt(ENC_A), onA, RISING);
Serial.print("press for a dose of ");
printMl(DOSE_PULSES);
Serial.println();
}
void dose() {
noInterrupts();
pulses = 0;
interrupts();
Serial.println("pumping");
run(FULL);
bool slow = false;
while (count() < BRAKE_AT) {
if (!slow && count() >= SLOW_AT) {
run(CRAWL);
slow = true;
}
}
brakeMotor();
delay(500);
long p = count();
Serial.print("dosed ");
printMl(p);
Serial.print(" (");
Serial.print(p);
Serial.println(" pulses)");
}
void loop() {
if (digitalRead(BUTTON) == LOW) {
dose();
while (digitalRead(BUTTON) == LOW) delay(10);
delay(50);
}
}
Code walkthrough
The key functions of the sketch, and what each one does.
onA sketch.ino
Runs every time encoder channel A rises. It looks at channel B at that moment. If B is low the motor is turning forward and pulses goes up, otherwise it goes down. Because it is an interrupt, no pulse is missed while the rest of the sketch is busy.
void onA() {
// B low on A's rising edge is forward.
if (digitalRead(ENC_B) == LOW) {
pulses++;
} else {
pulses--;
}
}dose sketch.ino
Resets the count and starts the pump at FULL. It then waits in a loop. At SLOW_AT it drops to CRAWL, and at BRAKE_AT it leaves the loop and brakes. After half a second it prints the final dose in ml and in pulses.
void dose() {
noInterrupts();
pulses = 0;
interrupts();
Serial.println("pumping");
run(FULL);
bool slow = false;
while (count() < BRAKE_AT) {
if (!slow && count() >= SLOW_AT) {
run(CRAWL);
slow = true;
}
}
brakeMotor();
delay(500);
long p = count();
Serial.print("dosed ");
printMl(p);
Serial.print(" (");
Serial.print(p);
Serial.println(" pulses)");
}brakeMotor sketch.ino
Sets both L9110S inputs high, which shorts the motor and stops it quickly. setup also calls it first, so the pump starts stopped.
void brakeMotor() {
digitalWrite(IA, HIGH);
digitalWrite(IB, HIGH);
}Troubleshooting
The usual mistakes with this circuit, and the fix for each.
- The dose is too big or too small This project
Measure your pump. Run 20 turns into a measuring cylinder, divide by 20, and changeCENTI_ML_PER_TURN(hundredths of a ml per turn). - The motor never moves or the count stays at 0 This project
The motor driver needs its own battery on VCC, with its ground joined to the Arduino ground. Also check encoder A on pin 2 and B on pin 3. - The input reads random HIGH and LOW when the button is not pressed. Pushbutton
A floating pin picks up noise. Add a 10 k pull-down or pull-up resistor, or use INPUT_PULLUP and wire the button between the pin and GND. Then pressed reads LOW. - The button seems always pressed, or never changes. Pushbutton
A 4-leg tactile switch has legs joined in pairs. Place it across the breadboard center gap so the pairs sit on different rows. Wired along one side, the legs are already connected. - The motor does not turn or stops quickly when both inputs are the same. Motor driver
Each output follows its input. Set one input high and the other low to turn, swap them to reverse. Equal inputs brake the motor rather than coast. Use PWM on one input for speed. - The driver gets hot or the board resets. Motor driver
Power the motors from a separate supply of about 2.5 to 12 V, within the chip's rating, and share ground with the controller. Small motors only: stalled or large motors exceed its roughly 800 mA limit. - The motor does not move, or the board resets. Gear motor with encoder
The motor draws more than a pin gives, so use a driver such as an L298N or a MOSFET. Use a separate supply for the motor and join the grounds. Add a flyback diode if you use a transistor. - The count is wrong or the direction is reversed. Gear motor with encoder
Power the encoder from its own VCC (often 3.3 V to 5 V) and ground. Use interrupts on channel A and read channel B for direction. Swap A and B if the count goes the wrong way.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.