Built-in project · Arduino Uno R3

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.

The circuit runs here once the page has loaded.

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.

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 pinConnects to
Keypad, 4x3 telephone pin C1Arduino Uno R3 pin 13
Keypad, 4x3 telephone pin C2Arduino Uno R3 pin 12
Keypad, 4x3 telephone pin C3Arduino Uno R3 pin 11
Hobby servo pin PWMArduino Uno R3 pin 9
Stepper motor, 28BYJ-48 with ULN2003 pin IN1Arduino Uno R3 pin 7
Stepper motor, 28BYJ-48 with ULN2003 pin IN2Arduino Uno R3 pin 6
Stepper motor, 28BYJ-48 with ULN2003 pin IN3Arduino Uno R3 pin 5
Stepper motor, 28BYJ-48 with ULN2003 pin IN4Arduino Uno R3 pin 4
Buzzer pin 1Arduino Uno R3 pin 3
Limit switch, lever microswitch pin NOArduino Uno R3 pin 2
Keypad, 4x3 telephone pin R4Arduino Uno R3 pin A0
Keypad, 4x3 telephone pin R3Arduino Uno R3 pin A1
Keypad, 4x3 telephone pin R2Arduino Uno R3 pin A2
Keypad, 4x3 telephone pin R1Arduino Uno R3 pin A3
Character LCD with I2C backpack pin SDAArduino Uno R3 pin A4
Character LCD with I2C backpack pin SCLArduino Uno R3 pin A5
Hobby servo pin GNDGround
Hobby servo pin VCC5 V
Stepper motor, 28BYJ-48 with ULN2003 pin VCC5 V
Stepper motor, 28BYJ-48 with ULN2003 pin GNDGround
Limit switch, lever microswitch pin COMGround
Buzzer pin 2Ground
Character LCD with I2C backpack pin GNDGround
Character LCD with I2C backpack pin VCC5 V

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// 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 editor

Troubleshooting

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.