Pick and place on an Arduino Uno
Two steppers home to their switches, then each START moves a part from the pick point to the place point: nozzle down, vacuum on, up, across, down, vacuum off, up, and back home to count it. 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.
Advanced About 35 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $46. On Mokxi they're free.
What you'll learn
- Motors
- Controllers
- Moving a servo to an angle
- Reading a button
- Difficulty: Advanced
- Time: About 35 minutes
- Board: Arduino Uno R3
- Parts: 17
How it works
Two steppers home to their switches, then each START moves a part from the pick point to the place point: nozzle down, vacuum on, up, across, down, vacuum off, up, and back home to count it.
Arduino Uno R3 runs the sketch below. The circuit around it: Relay module, DC motor, Hobby servo, Stepper motor, 28BYJ-48 with ULN2003, Limit switch, lever microswitch, Pushbutton. pinMode sets X_HOME_PIN as an input with its pull-up on. digitalRead reads whether switchPin is HIGH or LOW. Serial.begin opens the serial port at 9600 baud. millis reads the time since power-up, so the sketch can time things without stopping.
Parts list
17 parts, plus the jumper wires. They retail for about $46; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Relay moduleIn Halloween Prop Kit.
- 6 × Power, 5 V
- 1 × DC motor
- 1 × Hobby servoIn Halloween Prop Kit, Christmas Lights Kit, Starter Projects Kit.
- 2 × Stepper motor, 28BYJ-48 with ULN2003
- 2 × Limit switch, lever microswitchIn Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab 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 |
|---|---|
| Hobby servo pin PWM | Arduino Uno R3 pin 13 |
| Relay module pin IN | Arduino Uno R3 pin 12 |
| Stepper motor, 28BYJ-48 with ULN2003 (1) pin IN1 | Arduino Uno R3 pin 11 |
| Stepper motor, 28BYJ-48 with ULN2003 (1) pin IN2 | Arduino Uno R3 pin 10 |
| Stepper motor, 28BYJ-48 with ULN2003 (1) pin IN3 | Arduino Uno R3 pin 9 |
| Stepper motor, 28BYJ-48 with ULN2003 (1) pin IN4 | Arduino Uno R3 pin 8 |
| Stepper motor, 28BYJ-48 with ULN2003 (2) pin IN1 | Arduino Uno R3 pin 7 |
| Stepper motor, 28BYJ-48 with ULN2003 (2) pin IN2 | Arduino Uno R3 pin 6 |
| Stepper motor, 28BYJ-48 with ULN2003 (2) pin IN3 | Arduino Uno R3 pin 5 |
| Stepper motor, 28BYJ-48 with ULN2003 (2) pin IN4 | Arduino Uno R3 pin 4 |
| Limit switch, lever microswitch (1) pin NO | Arduino Uno R3 pin 3 |
| Limit switch, lever microswitch (2) pin NO | Arduino Uno R3 pin 2 |
| Pushbutton pin 2a | 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 module pin GND | Ground |
| Relay module pin VCC | 5 V |
| Relay module pin NO | 5 V |
| DC motor pin 1 | Relay module pin COM |
| DC motor pin 2 | Ground |
| Hobby servo pin GND | Ground |
| Hobby servo pin VCC | 5 V |
| Stepper motor, 28BYJ-48 with ULN2003 (1) pin VCC | 5 V |
| Stepper motor, 28BYJ-48 with ULN2003 (1) pin GND | Ground |
| Stepper motor, 28BYJ-48 with ULN2003 (2) pin VCC | 5 V |
| Stepper motor, 28BYJ-48 with ULN2003 (2) pin GND | Ground |
| Limit switch, lever microswitch (1) pin COM | Ground |
| Limit switch, lever microswitch (2) pin COM | Ground |
| Pushbutton pin 1a | 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.
// Pick and place: a two-axis gantry that homes itself, then moves a part
// from a pick point to a place point each time START is pressed.
//
// 7 6 5 4 X axis, a 28BYJ-48 on its ULN2003 board (IN1 to IN4)
// 11 10 9 8 Y axis, a second 28BYJ-48 (IN1 to IN4)
// 12 relay, the vacuum pump (the board switches on a low input)
// 13 Z axis, a servo that lowers the nozzle (red on 5 V, brown on GND)
// 2 X home switch, COM to GND (pressed = low)
// 3 Y home switch, COM to GND (pressed = low)
// A0 START button to GND (pressed = low)
// LCD SDA -> A4 SCL -> A5 VCC -> 5 V GND -> GND
//
// A stepper only knows how many steps it has taken, not where it is. So at
// power up the machine homes: each axis creeps back until its switch clicks,
// and that spot becomes step 0. From then on every position is a step count.
//
// One cycle is a list of states, and each one waits for the last to finish:
//
// TO PICK X runs to the pick point, then Y does (one axis at a time)
// PICK DOWN Z lowers the nozzle onto the part
// GRIP vacuum on, only now that the nozzle is down
// PICK UP Z lifts the part
// TO PLACE X, then Y, to the place point
// PLACE DOWN Z lowers the part
// RELEASE vacuum off, so the part stays put
// PLACE UP Z lifts the empty nozzle
// RETURN X, then Y, back to 0; the part is counted
//
// The order is the lesson: vacuum before the nozzle is down grabs air, and
// vacuum off before it is down drops the part. START while a cycle runs is
// ignored. The loop never waits with delay(); it checks the clock, so the
// steppers, the servo and the button are all looked after every pass.
//
// Press Run, click the X and then the Y home switch, then press START.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include "mokxi_servo.h"
const int X_HOME_PIN = 2;
const int Y_HOME_PIN = 3;
const int VACUUM_PIN = 12;
const int Z_PIN = 13;
const int START_PIN = A0;
const int X_PINS[4] = {7, 6, 5, 4}; // IN1 to IN4
const int Y_PINS[4] = {11, 10, 9, 8}; // IN1 to IN4
// Positions in half-steps: 4096 make one turn of a 28BYJ-48's shaft.
const long PICK_X = 512;
const long PICK_Y = 256;
const long PLACE_X = 1536;
const long PLACE_Y = 768;
const unsigned long STEP_US = 2000; // one half-step every 2 ms
const unsigned long Z_MS = 500; // time for the nozzle to go down or up
const unsigned long GRIP_MS = 400; // vacuum building up
const unsigned long RELEASE_MS = 400; // vacuum dying away
const unsigned long DEBOUNCE_MS = 30;
const int Z_UP = 20;
const int Z_DOWN = 110;
// The relay board switches on a low input.
const int ON = LOW;
const int OFF = HIGH;
// The eight-step half-step pattern, IN1 in bit 0.
const byte HALF_STEPS[8] = {0b0001, 0b0011, 0b0010, 0b0110, 0b0100, 0b1100, 0b1000, 0b1001};
enum State {
HOME_X, HOME_Y, IDLE, TO_PICK, PICK_DOWN, GRIP, PICK_UP,
TO_PLACE, PLACE_DOWN, RELEASE, PLACE_UP, RETURN
};
const char* const NAMES[] = {
"HOME X", "HOME Y", "IDLE", "TO PICK", "PICK DOWN", "GRIP", "PICK UP",
"TO PLACE", "PLACE DOWN", "RELEASE", "PLACE UP", "RETURN"
};
struct Axis {
const int* pins;
long pos; // half-steps from home
byte phase; // where in HALF_STEPS the coils are
};
Axis xAxis = {X_PINS, 0, 0};
Axis yAxis = {Y_PINS, 0, 0};
LiquidCrystal_I2C lcd(0x27, 16, 2);
Servo z(Z_PIN);
State state = HOME_X;
unsigned long enteredAt = 0;
unsigned long lastStep = 0; // micros() of the last half-step
unsigned long cycles = 0;
int zAngle = Z_UP;
long targetX = 0;
long targetY = 0;
bool startWas = false;
unsigned long startChanged = 0;
bool startStable = false;
void coils(Axis& a, byte pattern) {
for (int i = 0; i < 4; i++) digitalWrite(a.pins[i], (pattern >> i) & 1);
}
// One half-step forward (+1) or back (-1).
void step(Axis& a, int dir) {
a.phase = (a.phase + 8 + dir) % 8;
a.pos += dir;
coils(a, HALF_STEPS[a.phase]);
}
// Coils off when an axis is not moving: it stays put and does not get hot.
void rest(Axis& a) {
coils(a, 0);
}
// The LCD is slow (every character is an I2C message), so only what changed
// is written: the state on row 0, the count on row 1 when it moves.
void show() {
lcd.setCursor(0, 0);
lcd.print(NAMES[state]);
for (int i = strlen(NAMES[state]); i < 10; i++) lcd.print(' ');
}
void showCount() {
lcd.setCursor(7, 1);
lcd.print(cycles);
}
void enter(State next) {
state = next;
enteredAt = millis();
// The vacuum is decided here, in one place, for every state.
bool vacuum = (next == GRIP || next == PICK_UP || next == TO_PLACE || next == PLACE_DOWN);
digitalWrite(VACUUM_PIN, vacuum ? ON : OFF);
if (next == TO_PICK) { targetX = PICK_X; targetY = PICK_Y; }
if (next == TO_PLACE) { targetX = PLACE_X; targetY = PLACE_Y; }
if (next == RETURN) { targetX = 0; targetY = 0; }
Serial.print("state ");
Serial.println(NAMES[next]);
show();
}
// Whether the next half-step is due. Timed in microseconds: with millis()
// two steps "2 ms apart" can land barely 1 ms apart, faster than the rotor
// can follow, and the motor quietly loses steps.
bool stepDue() {
if (micros() - lastStep < STEP_US) return false;
lastStep = micros();
return true;
}
// Move X to its target, then Y. True once both are there.
bool moveTo() {
if (xAxis.pos == targetX && yAxis.pos == targetY) return true;
if (!stepDue()) return false;
if (xAxis.pos != targetX) {
step(xAxis, xAxis.pos < targetX ? 1 : -1);
if (xAxis.pos == targetX) rest(xAxis);
return false;
}
step(yAxis, yAxis.pos < targetY ? 1 : -1);
if (yAxis.pos == targetY) rest(yAxis);
return false;
}
// Creep an axis back toward its switch. True once the switch is pressed.
bool home(Axis& a, int switchPin) {
if (digitalRead(switchPin) == LOW) {
rest(a);
a.pos = 0;
return true;
}
if (stepDue()) step(a, -1);
return false;
}
// A clean press of START: low for DEBOUNCE_MS after being high.
bool startPressed(unsigned long now) {
bool down = digitalRead(START_PIN) == LOW;
if (down != startWas) {
startWas = down;
startChanged = now;
}
if (down != startStable && now - startChanged >= DEBOUNCE_MS) {
startStable = down;
return down;
}
return false;
}
void setup() {
pinMode(X_HOME_PIN, INPUT_PULLUP);
pinMode(Y_HOME_PIN, INPUT_PULLUP);
pinMode(START_PIN, INPUT_PULLUP);
pinMode(VACUUM_PIN, OUTPUT);
digitalWrite(VACUUM_PIN, OFF);
for (int i = 0; i < 4; i++) {
pinMode(X_PINS[i], OUTPUT);
pinMode(Y_PINS[i], OUTPUT);
}
Serial.begin(9600);
Serial.println("Pick and place");
z.begin();
z.write(Z_UP);
lcd.init();
lcd.backlight();
lcd.setCursor(0, 1);
lcd.print("Parts: 0");
enter(HOME_X);
}
void loop() {
unsigned long now = millis();
bool start = startPressed(now);
int wantZ = Z_UP;
switch (state) {
case HOME_X:
if (home(xAxis, X_HOME_PIN)) enter(HOME_Y);
break;
case HOME_Y:
if (home(yAxis, Y_HOME_PIN)) {
Serial.println("homed");
enter(IDLE);
}
break;
case IDLE:
if (start) enter(TO_PICK);
start = false;
break;
case TO_PICK:
if (moveTo()) enter(PICK_DOWN);
break;
case PICK_DOWN:
wantZ = Z_DOWN;
if (now - enteredAt >= Z_MS) enter(GRIP);
break;
case GRIP:
wantZ = Z_DOWN;
if (now - enteredAt >= GRIP_MS) enter(PICK_UP);
break;
case PICK_UP:
if (now - enteredAt >= Z_MS) enter(TO_PLACE);
break;
case TO_PLACE:
if (moveTo()) enter(PLACE_DOWN);
break;
case PLACE_DOWN:
wantZ = Z_DOWN;
if (now - enteredAt >= Z_MS) enter(RELEASE);
break;
case RELEASE:
wantZ = Z_DOWN;
if (now - enteredAt >= RELEASE_MS) enter(PLACE_UP);
break;
case PLACE_UP:
if (now - enteredAt >= Z_MS) enter(RETURN);
break;
case RETURN:
if (moveTo()) {
cycles++;
Serial.print("parts ");
Serial.println(cycles);
showCount();
enter(IDLE);
}
break;
}
// A press that no state used: the machine is busy.
if (start && state != HOME_X && state != HOME_Y) Serial.println("busy, start ignored");
if (wantZ != zAngle) {
zAngle = wantZ;
z.write(zAngle);
}
z.refresh();
}
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(X_HOME_PIN, INPUT_PULLUP);
pinMode(Y_HOME_PIN, INPUT_PULLUP);
pinMode(START_PIN, INPUT_PULLUP);
pinMode(VACUUM_PIN, OUTPUT);
digitalWrite(VACUUM_PIN, OFF);
for (int i = 0; i < 4; i++) {
pinMode(X_PINS[i], OUTPUT);
pinMode(Y_PINS[i], OUTPUT);
}
Serial.begin(9600);
Serial.println("Pick and place");
z.begin();
z.write(Z_UP);
lcd.init();
lcd.backlight();
lcd.setCursor(0, 1);
lcd.print("Parts: 0");
enter(HOME_X);
}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() {
unsigned long now = millis();
bool start = startPressed(now);
int wantZ = Z_UP;
switch (state) {
case HOME_X:
if (home(xAxis, X_HOME_PIN)) enter(HOME_Y);
break;
case HOME_Y:
if (home(yAxis, Y_HOME_PIN)) {
Serial.println("homed");
enter(IDLE);
}
break;
case IDLE:
if (start) enter(TO_PICK);
start = false;
break;
case TO_PICK:
if (moveTo()) enter(PICK_DOWN);
break;
case PICK_DOWN:
wantZ = Z_DOWN;
if (now - enteredAt >= Z_MS) enter(GRIP);
break;
case GRIP:
wantZ = Z_DOWN;
// ... 39 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- The relay does not click, or the board resets when it switches. Relay module
A relay coil needs more current than a pin can give. Use a module with a transistor driver, and power VCC from 5 V. Share ground between the module and the Arduino. - The relay works backwards: on when you expect off. Relay module
Many modules are active LOW: the input must go LOW to turn the relay on. Set the pin HIGH at startup so the load does not switch on during boot. - 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. - The servo jitters, twitches or resets the board when it moves. Hobby servo
Power it from its own 5 V supply, not the Arduino 5 V pin, and join the grounds. A hobby servo can pull several hundred milliamps. Add a 100 uF to 470 uF capacitor across its supply. - The servo hums at the end of its travel or will not reach full angle. Hobby servo
Stay inside the pulse range, about 1000 to 2000 us for most hobby servos. Pushing past the end stop makes it strain. Calibrate the min and max pulse for your servo. - The motor buzzes or shakes and does not turn. The coil pins IN1 to IN4 are in the wrong order for the library. Stepper motor
For Arduino's Stepper library on a 28BYJ-48, create it with pins 8, 10, 9, 11 (IN1, IN3, IN2, IN4). Or use AccelStepper with HALF4WIRE and the same pin order. - It stalls or skips when spun fast or powered from the Arduino 5 V pin. It also draws more than a USB port may give. Stepper motor
Power the ULN2003 board from a separate 5 V supply and join the grounds. Run it slowly, about 10 to 15 RPM, and ramp up speed gradually.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.