Batch mixer on an Arduino Uno
Pick a recipe on a keypad and a relay board runs it: fill A to a level, fill B for a time, mix, then drain. The recipes are a table of numbers, a fill that stops rising is a fault, and * aborts safely by draining the tank. The whole build, an Arduino Uno and 9 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 $51. On Mokxi they're free.
What you'll learn
- Controllers
- Motors
- Displays
- Making sound with a buzzer
- Reading a knob with an analog pin
- Difficulty: Intermediate
- Time: About 35 minutes
- Board: Arduino Uno R3
- Parts: 18
How it works
Pick a recipe on a keypad and a relay board runs it: fill A to a level, fill B for a time, mix, then drain. The recipes are a table of numbers, a fill that stops rising is a fault, and * aborts safely by draining the tank.
Arduino Uno R3 runs the sketch below. The circuit around it: Keypad, 4x3 telephone, Relay board, four channels, Solenoid, DC motor, Buzzer, Potentiometer. pinMode sets VALVE_A_PIN as an output. digitalWrite drives BUZZER_PIN HIGH. analogRead measures the voltage on LEVEL_PIN as a number. delay waits 5 milliseconds. Serial.begin opens the serial port at 9600 baud.
Parts list
18 parts, plus the jumper wires. They retail for about $51; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × Keypad, 4x3 telephone
- 1 × Relay board, four channels
- 7 × Power, 5 V
- 3 × Solenoid
- 1 × DC motor
- 1 × BuzzerIn Starter Kit, Rocket Flight Computer Kit.
- 1 × PotentiometerIn Halloween Prop Kit, Starter Projects Kit.
- 1 × Character LCD with I2C backpackIn Claw Machine Kit, Vending Machine Kit, Rhythm Game 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 |
|---|---|
| Keypad, 4x3 telephone pin R1 | Arduino Uno R3 pin 13 |
| Keypad, 4x3 telephone pin R2 | Arduino Uno R3 pin 12 |
| Keypad, 4x3 telephone pin R3 | Arduino Uno R3 pin 11 |
| Keypad, 4x3 telephone pin R4 | Arduino Uno R3 pin 10 |
| Keypad, 4x3 telephone pin C1 | Arduino Uno R3 pin 9 |
| Keypad, 4x3 telephone pin C2 | Arduino Uno R3 pin 8 |
| Keypad, 4x3 telephone pin C3 | Arduino Uno R3 pin 7 |
| Relay board, four channels pin IN1 | Arduino Uno R3 pin 6 |
| Relay board, four channels pin IN2 | Arduino Uno R3 pin 5 |
| Relay board, four channels pin IN3 | Arduino Uno R3 pin 4 |
| Relay board, four channels pin IN4 | Arduino Uno R3 pin 3 |
| Buzzer pin 1 | Arduino Uno R3 pin 2 |
| Potentiometer pin 2 | Arduino Uno R3 pin A0 |
| Character LCD with I2C backpack pin SDA | Arduino Uno R3 pin A4 |
| Character LCD with I2C backpack pin SCL | Arduino Uno R3 pin A5 |
| Relay board, four channels pin GND | Ground |
| Relay board, four channels pin VCC | 5 V |
| Solenoid (1) pin 1 | Relay board, four channels pin NO1 |
| Relay board, four channels pin COM1 | 5 V |
| Solenoid (2) pin 1 | Relay board, four channels pin NO2 |
| Relay board, four channels pin COM2 | 5 V |
| DC motor pin 1 | Relay board, four channels pin NO3 |
| Relay board, four channels pin COM3 | 5 V |
| Solenoid (3) pin 1 | Relay board, four channels pin NO4 |
| Relay board, four channels pin COM4 | 5 V |
| Solenoid (1) pin 2 | Ground |
| Solenoid (2) pin 2 | Ground |
| DC motor pin 2 | Ground |
| Solenoid (3) pin 2 | Ground |
| Potentiometer pin 3 | 5 V |
| Buzzer pin 2 | Ground |
| Potentiometer pin 1 | Ground |
| Character LCD with I2C backpack pin GND | Ground |
| Character LCD with I2C backpack pin VCC | 5 V |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Batch mixer: a tank that follows a recipe, picked on a keypad, one step at a
// time.
//
// 13 12 11 10 keypad rows R1 to R4
// 9 8 7 keypad columns C1 to C3
// 6 relay 1, valve A (ingredient A in)
// 5 relay 2, valve B (ingredient B in)
// 4 relay 3, the mixer motor
// 3 relay 4, the drain valve
// 2 buzzer, + to the pin and - to GND
// A0 the level sensor (a knob stands in for it: ends on 5 V and
// GND, the middle to A0)
// LCD SDA -> A4 SCL -> A5 VCC -> 5 V GND -> GND
//
// A batch plant does not have one program per product. It has one sequence
// and a table of recipes, and the recipe is just numbers the sequence reads:
//
// READY press 1, 2 or 3 to pick a recipe, then # to start.
// FILL A valve A open until the level reaches the recipe's target. This
// step watches a sensor. If the level stops rising for
// STALL_MS, something is wrong (a leak, a stuck valve), so it faults.
// FILL B valve B open for the recipe's time. This step only watches the
// clock: the flow is known, so time measures the amount.
// MIX the mixer runs for the recipe's time.
// DRAIN the drain opens until the tank is below EMPTY_PCT.
// DONE a beep. Pick the next recipe.
// FAULT everything closed and an alarm. * clears it.
//
// * aborts a running batch at any step: valves shut, the mixer stops and the
// tank drains, because an empty tank is the safe one. Every output is set in
// one place, enter(), so no step can forget to close a valve.
//
// Press Run, press 1 then #, and turn the knob up slowly to fill the tank.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
const int VALVE_A_PIN = 6;
const int VALVE_B_PIN = 5;
const int MIXER_PIN = 4;
const int DRAIN_PIN = 3;
const int BUZZER_PIN = 2;
const int LEVEL_PIN = A0;
// The relay board switches on a low input.
const int ON = LOW;
const int OFF = HIGH;
// The recipes are data, not code. Times are scaled down to seconds so a
// batch fits in a minute on the bench.
struct Recipe {
int levelA; // percent of the tank to fill with A
unsigned long fillBMs; // how long valve B stays open
unsigned long mixMs; // how long the mixer runs
};
const Recipe RECIPES[] = {
{40, 3000, 5000},
{60, 2000, 8000},
{25, 5000, 4000},
};
const int RECIPE_COUNT = 3;
const unsigned long STALL_MS = 5000; // fill A must keep rising inside this
const int RISE_PCT = 2; // a rise this big counts as progress
const int EMPTY_PCT = 5; // below this the tank is empty
const unsigned long DONE_BEEP_MS = 1000;
const byte ROWS = 4;
const byte COLS = 3;
char keys[ROWS][COLS] = {
{'1', '2', '3'},
{'4', '5', '6'},
{'7', '8', '9'},
{'*', '0', '#'},
};
byte rowPins[ROWS] = {13, 12, 11, 10};
byte colPins[COLS] = {9, 8, 7};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
enum State { READY, FILL_A, FILL_B, MIX, DRAIN, DONE, FAULT };
const char* const NAMES[] = {"READY", "FILL A", "FILL B", "MIX", "DRAIN", "DONE", "FAULT"};
LiquidCrystal_I2C lcd(0x27, 16, 2);
State state = READY;
unsigned long enteredAt = 0;
int recipe = 0; // 1..3, 0 for none picked
bool aborted = false;
int bestLevel = 0; // the highest level seen in FILL A
unsigned long risingAt = 0;
int shownLevel = -1; // the level on the LCD, to redraw only on a change
unsigned long beepUntil = 0;
int levelPct() {
return (long)analogRead(LEVEL_PIN) * 100 / 1023;
}
// An LCD over I2C is slow, so only the number is redrawn, and only when it
// changes: the loop has to keep reading the keypad.
void showLevel(int level) {
if (level == shownLevel) return;
shownLevel = level;
lcd.setCursor(6, 1);
lcd.print(level);
lcd.print("% ");
}
void show() {
lcd.setCursor(0, 0);
lcd.print("R");
if (recipe) lcd.print(recipe);
else lcd.print("-");
lcd.print(" ");
lcd.print(NAMES[state]);
lcd.print(" ");
lcd.setCursor(0, 1);
lcd.print("Level ");
shownLevel = -1;
showLevel(levelPct());
}
void beep(unsigned long ms) {
beepUntil = millis() + ms;
digitalWrite(BUZZER_PIN, HIGH);
}
void enter(State next) {
state = next;
enteredAt = millis();
digitalWrite(VALVE_A_PIN, next == FILL_A ? ON : OFF);
digitalWrite(VALVE_B_PIN, next == FILL_B ? ON : OFF);
digitalWrite(MIXER_PIN, next == MIX ? ON : OFF);
digitalWrite(DRAIN_PIN, next == DRAIN ? ON : OFF);
Serial.print("state ");
Serial.println(NAMES[next]);
show();
}
void setup() {
pinMode(VALVE_A_PIN, OUTPUT);
pinMode(VALVE_B_PIN, OUTPUT);
pinMode(MIXER_PIN, OUTPUT);
pinMode(DRAIN_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(VALVE_A_PIN, OFF);
digitalWrite(VALVE_B_PIN, OFF);
digitalWrite(MIXER_PIN, OFF);
digitalWrite(DRAIN_PIN, OFF);
Serial.begin(9600);
Serial.println("Batch mixer");
lcd.init();
lcd.backlight();
enter(READY);
}
void handleKey(char key) {
bool running = state == FILL_A || state == FILL_B || state == MIX;
if (key == '*') {
if (running) {
Serial.println("abort");
aborted = true;
enter(DRAIN);
} else if (state == FAULT) {
enter(READY);
}
return;
}
if (state != READY && state != DONE) return;
if (key >= '1' && key <= '0' + RECIPE_COUNT) {
recipe = key - '0';
Serial.print("recipe ");
Serial.println(recipe);
enter(READY);
} else if (key == '#' && recipe) {
Serial.print("start recipe ");
Serial.println(recipe);
aborted = false;
bestLevel = levelPct();
risingAt = millis();
enter(FILL_A);
}
}
void loop() {
char key = keypad.getKey();
if (key) handleKey(key);
unsigned long now = millis();
int level = levelPct();
const Recipe& r = RECIPES[recipe > 0 ? recipe - 1 : 0];
switch (state) {
case FILL_A:
// Level control: the sensor says when to stop.
if (level >= r.levelA) {
enter(FILL_B);
} else {
// The watchdog: a fill that stops rising is a fault, not a wait.
if (level >= bestLevel + RISE_PCT) {
bestLevel = level;
risingAt = now;
}
if (now - risingAt >= STALL_MS) {
Serial.println("fault: level not rising");
beep(DONE_BEEP_MS);
enter(FAULT);
}
}
break;
case FILL_B:
// Time control: the clock says when to stop.
if (now - enteredAt >= r.fillBMs) enter(MIX);
break;
case MIX:
if (now - enteredAt >= r.mixMs) enter(DRAIN);
break;
case DRAIN:
if (level < EMPTY_PCT) {
if (aborted) {
Serial.println("aborted, tank empty");
enter(READY);
} else {
Serial.print("batch done, recipe ");
Serial.println(recipe);
beep(DONE_BEEP_MS);
enter(DONE);
}
}
break;
default:
break;
}
if (beepUntil && now >= beepUntil) {
beepUntil = 0;
digitalWrite(BUZZER_PIN, LOW);
}
showLevel(level);
delay(5);
}
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() {
pinMode(VALVE_A_PIN, OUTPUT);
pinMode(VALVE_B_PIN, OUTPUT);
pinMode(MIXER_PIN, OUTPUT);
pinMode(DRAIN_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(VALVE_A_PIN, OFF);
digitalWrite(VALVE_B_PIN, OFF);
digitalWrite(MIXER_PIN, OFF);
digitalWrite(DRAIN_PIN, OFF);
Serial.begin(9600);
Serial.println("Batch mixer");
lcd.init();
lcd.backlight();
enter(READY);
}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() {
char key = keypad.getKey();
if (key) handleKey(key);
unsigned long now = millis();
int level = levelPct();
const Recipe& r = RECIPES[recipe > 0 ? recipe - 1 : 0];
switch (state) {
case FILL_A:
// Level control: the sensor says when to stop.
if (level >= r.levelA) {
enter(FILL_B);
} else {
// The watchdog: a fill that stops rising is a fault, not a wait.
if (level >= bestLevel + RISE_PCT) {
bestLevel = level;
risingAt = now;
}
if (now - risingAt >= STALL_MS) {
Serial.println("fault: level not rising");
beep(DONE_BEEP_MS);
enter(FAULT);
}
}
break;
case FILL_B:
// Time control: the clock says when to stop.
// ... 29 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- Pressing one key reports several keys or the wrong key. Keypad
Keep the row and column pin order right. Scan by driving one column low and reading the rows with pull-ups. Use the Keypad library with the correct pin lists, and match the pin map to your wiring. - Keys do not register or float. Keypad
Rows need pull-ups, either INPUT_PULLUP or external resistors. Give each key press a few milliseconds of debounce and check for loose wires on all seven pins. - The relay clicks on at startup, or does the opposite of what the code says. Relay board
These boards are active low: LOW turns the relay on and HIGH turns it off. Set the pin HIGH before pinMode OUTPUT so the relay does not pulse on at boot. - The board resets or relays chatter when they switch. Relay board
Power the relay coils from a separate 5 V supply that can handle all four (about 70 mA each), and join the grounds. Mains loads are dangerous, so use low voltage for learning. - The board resets, or the pin burns out, when the solenoid is wired straight to a pin. Solenoid
A solenoid draws far too much for a pin. Switch it with a MOSFET or transistor from its own supply (such as 12 V), with the grounds joined. - The transistor or the controller fails after some switching. Solenoid
A coil produces a voltage spike when current stops. Put a diode across the coil, cathode on the supply side, such as a 1N4007 or 1N5819, to absorb the kick. - The motor does not spin, or the pin gets hot or the board resets when it is wired straight to a GPIO pin. DC motor
A pin gives about 20 mA and a motor needs far more. Drive it through a transistor or motor driver, with its own supply and a shared ground. - The transistor or pin is damaged by spikes when the motor stops. DC motor
Put a flyback diode across the motor, cathode to the positive side. A motor is a coil and kicks back voltage when switched off. Add a 0.1 uF capacitor across the motor terminals to cut noise.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.