Built-in project · Arduino Uno R3

Vending machine on an Arduino Uno

Four spiral slots on continuous servos, a keypad, coins and exact change, an LCD price list, a drop sensor that refunds a failed vend, sold out counts and a service code. 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 Runs in your browser. Free, and no account needed.

Build it as the Vending Machine Kit: the parts in numbered bags and a booklet that checks every step. Retails for about $39, free on Mokxi.

Vending machine · Arduino Uno R3live0.000 s 0.00x
Click the bill switch, then type A then 1 on the keypad.
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.

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
// Vending machine: four spiral slots, a keypad, coins and change, the way a
// real machine does it.
//
//   pins 9, 8, 7, 6   keypad rows R1 to R4
//   pins 5, 4, 3, 2   keypad columns C1 to C4
//   pin 10            coin switch (one quarter a click)
//   pin 11            bill switch (one dollar a click)
//   pin 12            piezo
//   pin 13            drop sensor (an IR beam across the bin)
//   A0 to A3          spiral motors for A1, A2, B1, B2 (continuous servos)
//   A4, A5            LCD SDA and SCL (I2C, 0x27)
//
// Click the coin switch for quarters or the bill switch for dollars, then key
// a slot: a letter and a number, B then 1 for B1. The spiral turns once, and
// the drop sensor watches for the item to fall. In the simulator the cabinet
// does that part: the front item tips off the spiral and breaks the beam on
// its way into the bin. Your change comes back, the count for that slot goes
// down, and an empty slot says SOLD OUT. Press * at any time for a refund.
//
// # The drop sensor is the real trick
//
// A spiral can turn and the item can still hang on the end of it. So a real
// machine does not trust the motor: an infrared beam across the bottom of the
// cabinet has to see something fall. If it sees nothing, the machine turns the
// spiral once more, and if it still sees nothing it keeps your money as credit
// so you can choose again or take a refund. That is what this sketch does.
//
// # Change
//
// Prices and credit are kept in cents, as whole numbers, so a dollar and a
// quarter is 125 and the sums are exact. Floating point would turn 0.10 into
// 0.1000000015 and a till that is a cent out is a till nobody trusts. Change
// is credit minus price. A real machine pays it out of its coin tubes; this
// one shows it on the screen and in Serial.
//
// # Service mode
//
// Press # and type the code, 2580, then # again. The screen shows the sales
// total and the stock in each slot. A restocks every slot, and * goes back to
// selling. A real machine has a key switch inside the door for this; a code
// on the keypad is how many smaller machines do it.

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
#include <Servo.h>

const byte ROWS = 4;
const byte COLS = 4;
char keys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'},
};
byte rowPins[ROWS] = {9, 8, 7, 6};
byte colPins[COLS] = {5, 4, 3, 2};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

LiquidCrystal_I2C lcd(0x27, 16, 2);

const int COIN = 10;
const int BILL = 11;
const int PIEZO = 12;
const int DROP = 13;

const int SLOTS = 4;
const char *NAMES[SLOTS] = {"A1", "A2", "B1", "B2"};
const int MOTOR_PINS[SLOTS] = {A0, A1, A2, A3};
const int PRICES[SLOTS] = {125, 100, 75, 150};  // cents
const int FULL = 3;
int stock[SLOTS] = {FULL, FULL, FULL, FULL};
Servo spiral[SLOTS];

const int STOP = 90;      // a continuous servo stands still at its middle
const int TURN = 180;     // and turns at full speed at one end
const unsigned long TURN_MS = 560;     // one turn of the spiral
const unsigned long WATCH_MS = 1500;   // how long the beam is watched
const char SERVICE_CODE[] = "2580";

int credit = 0;      // cents
long sales = 0;      // cents taken since power-up
char letter = 0;     // the first half of a selection, or 0
bool coinWas = false;
bool billWas = false;

// ---------------------------------------------------------------- helpers

void printMoney(Print &out, long cents) {
  out.print('$');
  out.print(cents / 100);
  out.print('.');
  if (cents % 100 < 10) out.print('0');
  out.print(cents % 100);
}

void screen(const char *top, const char *bottom) {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(top);
  lcd.setCursor(0, 1);
  lcd.print(bottom);
}

void showCredit() {
  lcd.clear();
  lcd.setCursor(0, 0);
  if (credit == 0 && !letter) {
    // The price list, two slots a line, with SOLD in place of an empty one.
    for (int i = 0; i < SLOTS; i++) {
      lcd.setCursor((i % 2) * 9, i / 2);
      lcd.print(NAMES[i]);
      lcd.print(' ');
      if (stock[i] == 0) {
        lcd.print("SOLD");
      } else {
        lcd.print(PRICES[i] / 100);
        lcd.print('.');
        if (PRICES[i] % 100 < 10) lcd.print('0');
        lcd.print(PRICES[i] % 100);
      }
    }
    return;
  }
  lcd.print("CREDIT ");
  printMoney(lcd, credit);
  lcd.setCursor(0, 1);
  lcd.print("SELECT: ");
  if (letter) lcd.print(letter);
}

int slotFor(char row, char column) {
  for (int i = 0; i < SLOTS; i++) {
    if (NAMES[i][0] == row && NAMES[i][1] == column) return i;
  }
  return -1;
}

void beep(unsigned int hz, unsigned long ms) { tone(PIEZO, hz, ms); }

void addMoney(int cents) {
  credit += cents;
  beep(1568, 60);
  Serial.print("Credit: ");
  printMoney(Serial, credit);
  Serial.println();
  showCredit();
}

// Read the coin and bill switches as edges, so one click is one coin.
void readMoney() {
  bool coin = digitalRead(COIN) == LOW;
  bool bill = digitalRead(BILL) == LOW;
  if (coin && !coinWas) addMoney(25);
  if (bill && !billWas) addMoney(100);
  coinWas = coin;
  billWas = bill;
}

// Turn one spiral one turn and watch the beam. True if something fell.
bool turnAndWatch(int slot) {
  bool clear = digitalRead(DROP) == LOW;
  bool fell = false;
  spiral[slot].write(TURN);
  unsigned long start = millis();
  while (millis() - start < WATCH_MS) {
    if (millis() - start >= TURN_MS) spiral[slot].write(STOP);
    bool blocked = digitalRead(DROP) == HIGH;
    if (blocked && clear) fell = true;  // light, then dark: it fell
    if (!blocked) clear = true;
    if (fell && millis() - start >= TURN_MS) break;
  }
  spiral[slot].write(STOP);
  return fell;
}

void vend(int slot) {
  if (stock[slot] == 0) {
    Serial.print(NAMES[slot]);
    Serial.println(": sold out");
    screen("   SOLD OUT", "pick another one");
    beep(220, 300);
    delay(1200);
    return;
  }
  if (credit < PRICES[slot]) {
    Serial.print(NAMES[slot]);
    Serial.print(" costs ");
    printMoney(Serial, PRICES[slot]);
    Serial.println(": add more money");
    lcd.clear();
    lcd.print(NAMES[slot]);
    lcd.print(" PRICE ");
    printMoney(lcd, PRICES[slot]);
    lcd.setCursor(0, 1);
    lcd.print("INSERT ");
    printMoney(lcd, PRICES[slot] - credit);
    delay(1500);
    return;
  }

  Serial.print("Vending ");
  Serial.println(NAMES[slot]);
  screen("   VENDING...", "");
  bool fell = turnAndWatch(slot);
  if (!fell) {
    Serial.println("Nothing fell: one more turn");
    fell = turnAndWatch(slot);
  }
  if (!fell) {
    Serial.println("Vend failed: credit kept");
    screen(" VEND FAILED", "choose again or*");
    beep(220, 400);
    delay(1500);
    return;
  }

  stock[slot]--;
  sales += PRICES[slot];
  int change = credit - PRICES[slot];
  credit = 0;
  Serial.print("Enjoy! ");
  Serial.print(NAMES[slot]);
  Serial.print(" left: ");
  Serial.print(stock[slot]);
  Serial.print("  change ");
  printMoney(Serial, change);
  Serial.println();
  lcd.clear();
  lcd.print("   ENJOY!");
  lcd.setCursor(0, 1);
  lcd.print("CHANGE ");
  printMoney(lcd, change);
  beep(1047, 90);
  delay(120);
  beep(1568, 120);
  delay(1800);
}

void refund() {
  if (credit > 0) {
    Serial.print("Refund: ");
    printMoney(Serial, credit);
    Serial.println();
    lcd.clear();
    lcd.print("REFUND ");
    printMoney(lcd, credit);
    credit = 0;
    beep(880, 150);
    delay(1200);
  }
  letter = 0;
}

// ---------------------------------------------------------------- service

void serviceScreen() {
  lcd.clear();
  lcd.print("SALES ");
  printMoney(lcd, sales);
  lcd.setCursor(0, 1);
  for (int i = 0; i < SLOTS; i++) {
    lcd.print(stock[i]);
    lcd.print(i < SLOTS - 1 ? ' ' : ' ');
  }
  lcd.print("A=fill");
}

void service() {
  Serial.print("Service mode: sales ");
  printMoney(Serial, sales);
  Serial.println();
  serviceScreen();
  while (true) {
    char key = keypad.getKey();
    if (key == 'A') {
      for (int i = 0; i < SLOTS; i++) stock[i] = FULL;
      Serial.println("Service: every slot restocked");
      beep(1319, 80);
      serviceScreen();
    } else if (key == '*') {
      Serial.println("Service: back to selling");
      return;
    }
  }
}

// After #: read four digits and a closing #, and open service mode on a match.
void askForCode() {
  char typed[5] = {0, 0, 0, 0, 0};
  int n = 0;
  screen("SERVICE CODE", "");
  unsigned long start = millis();
  while (millis() - start < 10000) {
    char key = keypad.getKey();
    if (!key) continue;
    if (key == '#') break;
    if (key == '*') return;
    if (n < 4) {
      typed[n++] = key;
      lcd.setCursor(n - 1, 1);
      lcd.print('*');
    }
  }
  if (n == 4 && strcmp(typed, SERVICE_CODE) == 0) {
    service();
  } else {
    Serial.println("Service: wrong code");
    screen("WRONG CODE", "");
    beep(220, 300);
    delay(800);
  }
}

// ---------------------------------------------------------------- setup

void setup() {
  Serial.begin(115200);
  pinMode(COIN, INPUT_PULLUP);
  pinMode(BILL, INPUT_PULLUP);
  pinMode(DROP, INPUT);
  for (int i = 0; i < SLOTS; i++) {
    spiral[i].attach(MOTOR_PINS[i]);
    spiral[i].write(STOP);
  }
  lcd.init();
  lcd.backlight();
  showCredit();
  Serial.println("Vending machine ready: add money, then key a slot");
}

void loop() {
  readMoney();
  char key = keypad.getKey();
  if (!key) return;

  if (key == '*') {
    refund();
  } else if (key == '#') {
    askForCode();
    letter = 0;
  } else if (key >= 'A' && key <= 'D') {
    letter = key;
  } else if (letter && key >= '1' && key <= '9') {
    int slot = slotFor(letter, key);
    letter = 0;
    if (slot < 0) {
      Serial.println("No such slot");
      screen("NO SUCH SLOT", "try A1 to B2");
      delay(1000);
    } else {
      vend(slot);
    }
  }
  showCredit();
}

Parts list

11 parts, plus the jumper wires. They retail for about $39; on Mokxi they're free.

How it is wired

24 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.

  • Ground: Arduino Uno R3 pin GND
  • Arduino Uno R3 pin 13; Vending machine cabinet pin DROP
  • Arduino Uno R3 pin 12; Piezo speaker pin 1
  • Arduino Uno R3 pin 11; Limit switch, lever microswitch (2) pin NO
  • Arduino Uno R3 pin 10; Limit switch, lever microswitch (1) pin NO
  • Arduino Uno R3 pin 9; Keypad, 4x4 pin R1
  • Arduino Uno R3 pin 8; Keypad, 4x4 pin R2
  • Arduino Uno R3 pin 7; Keypad, 4x4 pin R3
  • Arduino Uno R3 pin 6; Keypad, 4x4 pin R4
  • Arduino Uno R3 pin 5; Keypad, 4x4 pin C1
  • Arduino Uno R3 pin 4; Keypad, 4x4 pin C2
  • Arduino Uno R3 pin 3; Keypad, 4x4 pin C3
  • Arduino Uno R3 pin 2; Keypad, 4x4 pin C4
  • Arduino Uno R3 pin A0; Vending machine cabinet pin M1
  • Arduino Uno R3 pin A1; Vending machine cabinet pin M2
  • Arduino Uno R3 pin A2; Vending machine cabinet pin M3
  • Arduino Uno R3 pin A3; Vending machine cabinet pin M4
  • Arduino Uno R3 pin A4; Character LCD with I2C backpack pin SDA
  • Arduino Uno R3 pin A5; Character LCD with I2C backpack pin SCL
  • Ground: Limit switch, lever microswitch (1) pin COM; Limit switch, lever microswitch (2) pin COM; Piezo speaker pin 2
  • Ground: Character LCD with I2C backpack pin GND
  • Supply: Character LCD with I2C backpack pin VCC
  • 5 V: Vending machine cabinet pin VCC
  • Ground: Vending machine cabinet pin GND

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.