ASRS picker on an Arduino Uno
A stepper carriage homes on a limit switch, then counts its steps to the shelf you type on a keypad, picks with a servo fork and comes back to the dock. Shelves that do not exist are refused, and a limit hit out on the rail is a fault. The whole build, an Arduino Uno and 6 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 30 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $42. On Mokxi they're free.
What you'll learn
- Controllers
- Motors
- Displays
- Moving a servo to an angle
- Making sound with a buzzer
- Difficulty: Advanced
- Time: About 30 minutes
- Board: Arduino Uno R3
- Parts: 11
How it works
A stepper carriage homes on a limit switch, then counts its steps to the shelf you type on a keypad, picks with a servo fork and comes back to the dock. Shelves that do not exist are refused, and a limit hit out on the rail is a fault.
Arduino Uno R3 runs the sketch below. The circuit around it: Keypad, 4x3 telephone, Hobby servo, Stepper motor, 28BYJ-48 with ULN2003, Limit switch, lever microswitch, Buzzer, Character LCD with I2C backpack. pinMode sets COIL_PINS[i] as an output. digitalWrite drives COIL_PINS[i] LOW. digitalRead reads whether HOME_PIN 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
11 parts, plus the jumper wires. They retail for about $42; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Keypad, 4x3 telephone
- 1 × Hobby servoIn Halloween Prop Kit, Christmas Lights Kit, Starter Projects Kit.
- 3 × Power, 5 V
- 1 × Stepper motor, 28BYJ-48 with ULN2003
- 1 × Limit switch, lever microswitchIn Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × BuzzerIn Starter Kit, Rocket Flight Computer 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 C1 | Arduino Uno R3 pin 13 |
| Keypad, 4x3 telephone pin C2 | Arduino Uno R3 pin 12 |
| Keypad, 4x3 telephone pin C3 | Arduino Uno R3 pin 11 |
| Hobby servo pin PWM | Arduino Uno R3 pin 9 |
| Stepper motor, 28BYJ-48 with ULN2003 pin IN1 | Arduino Uno R3 pin 7 |
| Stepper motor, 28BYJ-48 with ULN2003 pin IN2 | Arduino Uno R3 pin 6 |
| Stepper motor, 28BYJ-48 with ULN2003 pin IN3 | Arduino Uno R3 pin 5 |
| Stepper motor, 28BYJ-48 with ULN2003 pin IN4 | Arduino Uno R3 pin 4 |
| Buzzer pin 1 | Arduino Uno R3 pin 3 |
| Limit switch, lever microswitch pin NO | Arduino Uno R3 pin 2 |
| Keypad, 4x3 telephone pin R4 | Arduino Uno R3 pin A0 |
| Keypad, 4x3 telephone pin R3 | Arduino Uno R3 pin A1 |
| Keypad, 4x3 telephone pin R2 | Arduino Uno R3 pin A2 |
| Keypad, 4x3 telephone pin R1 | Arduino Uno R3 pin A3 |
| Character LCD with I2C backpack pin SDA | Arduino Uno R3 pin A4 |
| Character LCD with I2C backpack pin SCL | Arduino Uno R3 pin A5 |
| Hobby servo pin GND | Ground |
| Hobby servo pin VCC | 5 V |
| Stepper motor, 28BYJ-48 with ULN2003 pin VCC | 5 V |
| Stepper motor, 28BYJ-48 with ULN2003 pin GND | Ground |
| Limit switch, lever microswitch pin COM | Ground |
| Buzzer pin 2 | 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.
// ASRS picker: a storage carriage that homes itself, counts its steps to a
// shelf, picks with a fork and comes back to the dock.
//
// 7 6 5 4 28BYJ-48 board IN1 to IN4 (+ to 5 V, - to GND)
// 2 home limit microswitch, COM to GND and NO to the pin
// 9 the fork, a servo (red on 5 V, brown on GND)
// A3 A2 A1 A0 keypad rows R1 to R4
// 13 12 11 keypad columns C1 to C3
// 3 buzzer, + to the pin and - to GND
// LCD SDA -> A4 SCL -> A5 VCC -> 5 V GND -> GND
//
// A stepper has no idea where it is. It only knows how many steps it has been
// told to take. So an automated store starts every day the same way:
//
// HOMING creep toward the home switch until it closes.
// BACKOFF step off the switch, then call that spot step zero.
// READY type a shelf 1 to 6 and press #. Anything else is refused with
// a beep: a soft limit in code, before the motor ever moves.
// TRAVEL count steps out to shelf * SHELF_STEPS.
// FORK the fork reaches in, then pulls back with the box.
// RETURN count steps back to zero, the dock.
// FAULT the home switch closed while the carriage was out on the rail.
// Something moved it or the count is wrong, so it stops dead and
// beeps. * homes again.
//
// From then on position is open loop: the count is the position, as long as
// no step is missed. The limit switch is the check on that, a hard safety
// behind the soft limit.
//
// Press Run, and press the switch to tell the carriage it has reached home.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
#include "mokxi_servo.h"
const int COIL_PINS[4] = {7, 6, 5, 4}; // IN1 to IN4
const int HOME_PIN = 2;
const int FORK_PIN = 9;
const int BUZZER_PIN = 3;
const int SHELVES = 6;
const long SHELF_STEPS = 512; // half-steps between shelves, 45 degrees
const long BACKOFF_STEPS = 200; // off the switch before zeroing
const long HOMING_MAX = SHELF_STEPS * 8; // no switch after this: fault
const unsigned long TRAVEL_US = 2000; // one half-step every 2 ms
const unsigned long CREEP_US = 4000; // slower while looking for home
const unsigned long REACH_MS = 800; // fork out
const unsigned long PULL_MS = 600; // fork back in
const int FORK_IN = 0;
const int FORK_OUT = 90;
// The half-step sequence, IN1 in the lowest bit.
const byte HALF_STEPS[8] = {0b0001, 0b0011, 0b0010, 0b0110, 0b0100, 0b1100, 0b1000, 0b1001};
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] = {A3, A2, A1, A0};
byte colPins[COLS] = {13, 12, 11};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
enum State { HOMING, BACKOFF, READY, TRAVEL, FORK, RETURN, FAULT };
const char* const NAMES[] = {"HOMING", "BACKOFF", "READY", "TRAVEL", "FORK", "RETURN", "FAULT"};
LiquidCrystal_I2C lcd(0x27, 16, 2);
Servo fork(FORK_PIN);
State state = HOMING;
unsigned long enteredAt = 0;
unsigned long steppedAt = 0;
long position = 0; // half-steps from the dock
long moved = 0; // half-steps taken in this state
byte phase = 0; // where the coils are in HALF_STEPS
int typed = 0; // the shelf typed so far, 0 for none
int target = 0; // the shelf being fetched
int forkAngle = FORK_IN;
unsigned long beepUntil = 0;
bool atHome() {
return digitalRead(HOME_PIN) == LOW;
}
// One half-step forward (+1) or back (-1), and the coils follow the count.
void step(int dir) {
position += dir;
moved++;
// The phase follows the coils, not the count, so zeroing the count at home
// does not make the rotor jump.
phase = (phase + dir) & 7;
byte pattern = HALF_STEPS[phase];
for (int i = 0; i < 4; i++) digitalWrite(COIL_PINS[i], (pattern >> i) & 1);
}
void release() {
for (int i = 0; i < 4; i++) digitalWrite(COIL_PINS[i], LOW);
}
void beep(unsigned long ms) {
beepUntil = millis() + ms;
digitalWrite(BUZZER_PIN, HIGH);
}
void show() {
lcd.setCursor(0, 0);
lcd.print(NAMES[state]);
lcd.print(" ");
lcd.setCursor(0, 1);
if (state == READY) {
lcd.print("Shelf? ");
if (typed) lcd.print(typed);
} else {
lcd.print("Target ");
lcd.print(target);
}
lcd.print(" ");
}
void enter(State next) {
state = next;
enteredAt = millis();
moved = 0;
if (next == READY || next == FORK || next == FAULT) release();
Serial.print("state ");
Serial.println(NAMES[next]);
show();
}
void setup() {
for (int i = 0; i < 4; i++) pinMode(COIL_PINS[i], OUTPUT);
pinMode(HOME_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
Serial.begin(9600);
Serial.println("ASRS picker");
fork.begin();
fork.write(FORK_IN);
lcd.init();
lcd.backlight();
enter(HOMING);
}
void handleKey(char key) {
if (key == '*' && state == FAULT) {
enter(HOMING);
return;
}
if (state != READY) return;
if (key >= '0' && key <= '9') {
typed = key - '0';
show();
} else if (key == '#') {
// The soft limit: refuse a shelf that is not there before moving.
if (typed < 1 || typed > SHELVES) {
Serial.print("refused shelf ");
Serial.println(typed);
beep(300);
typed = 0;
show();
return;
}
target = typed;
typed = 0;
Serial.print("fetch shelf ");
Serial.println(target);
enter(TRAVEL);
}
}
// Steps toward goal at one half-step per period; true once it is there.
bool moveTo(long goal, unsigned long period) {
if (position == goal) return true;
unsigned long nowUs = micros();
if (nowUs - steppedAt >= period) {
steppedAt = nowUs;
step(goal > position ? 1 : -1);
}
return false;
}
void loop() {
char key = keypad.getKey();
if (key) handleKey(key);
unsigned long now = millis();
switch (state) {
case HOMING:
if (atHome()) {
Serial.println("home switch");
enter(BACKOFF);
} else if (moved >= HOMING_MAX) {
Serial.println("fault: no home switch");
beep(1000);
enter(FAULT);
} else {
moveTo(position - 1, CREEP_US);
}
break;
case BACKOFF:
if (moved >= BACKOFF_STEPS) {
if (atHome()) {
Serial.println("fault: switch stuck");
beep(1000);
enter(FAULT);
} else {
// This spot is the dock: the count starts here.
position = 0;
Serial.println("homed, step 0");
enter(READY);
}
} else {
moveTo(position + 1, CREEP_US);
}
break;
case TRAVEL:
case RETURN:
// The hard limit: out on the rail the switch must stay open.
if (atHome()) {
Serial.println("fault: limit hit");
beep(1000);
enter(FAULT);
} else if (moveTo(state == TRAVEL ? target * SHELF_STEPS : 0, TRAVEL_US)) {
if (state == TRAVEL) {
Serial.print("at shelf ");
Serial.println(target);
enter(FORK);
} else {
Serial.println("docked");
target = 0;
enter(READY);
}
}
break;
case FORK: {
int want = now - enteredAt < REACH_MS ? FORK_OUT : FORK_IN;
if (want != forkAngle) {
forkAngle = want;
fork.write(forkAngle);
Serial.println(forkAngle == FORK_OUT ? "fork out" : "fork in");
}
if (now - enteredAt >= REACH_MS + PULL_MS) enter(RETURN);
break;
}
default:
break;
}
if (beepUntil && now >= beepUntil) {
beepUntil = 0;
digitalWrite(BUZZER_PIN, LOW);
}
}
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() {
for (int i = 0; i < 4; i++) pinMode(COIL_PINS[i], OUTPUT);
pinMode(HOME_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
Serial.begin(9600);
Serial.println("ASRS picker");
fork.begin();
fork.write(FORK_IN);
lcd.init();
lcd.backlight();
enter(HOMING);
}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();
switch (state) {
case HOMING:
if (atHome()) {
Serial.println("home switch");
enter(BACKOFF);
} else if (moved >= HOMING_MAX) {
Serial.println("fault: no home switch");
beep(1000);
enter(FAULT);
} else {
moveTo(position - 1, CREEP_US);
}
break;
case BACKOFF:
if (moved >= BACKOFF_STEPS) {
if (atHome()) {
Serial.println("fault: switch stuck");
beep(1000);
enter(FAULT);
} else {
// This spot is the dock: the count starts here.
position = 0;
Serial.println("homed, step 0");
// ... 44 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 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. - The input reads the wrong level, or floats when released. Limit switch
Use COM and one contact. NC is closed at rest, NO closes when pressed. Add a pull-up or INPUT_PULLUP so the pin never floats. - Readings chatter when the lever moves. Limit switch
Contacts bounce for a few milliseconds. Debounce in software with a 10 to 20 ms delay, or add a small capacitor across the switch.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.