Coin pusher on an Arduino Uno
Drop coins in front of a sweeping servo pusher, count the ones that fall off the front, win a jackpot light show, and watch it back out of a jam. The whole build, an Arduino Uno and 7 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 $42. On Mokxi they're free.
What you'll learn
- Games
- Motors
- Lights
- Sensors
- Moving a servo to an angle
- Difficulty: Intermediate
- Time: About 35 minutes
- Board: Arduino Uno R3
- Parts: 13
How it works
Drop coins in front of a sweeping servo pusher, count the ones that fall off the front, win a jackpot light show, and watch it back out of a jam.
Arduino Uno R3 runs the sketch below. The circuit around it: Arcade button, Piezo speaker, Obstacle sensor, FC-51, Four-digit display, TM1637, Addressable RGB LEDs, Limit switch, lever microswitch. pinMode sets DROP_PIN as an input with its pull-up on. digitalRead reads whether pin is HIGH or LOW. Serial.begin opens the serial port at 9600 baud. tone plays a note on PIEZO_PIN. millis reads the time since power-up, so the sketch can time things without stopping.
Parts list
13 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 × Full-size breadboard
- 1 × Arcade buttonIn Claw Machine Kit, Fight Stick Kit, Pinball Table Kit.
- 1 × Piezo speakerIn Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Obstacle sensor, FC-51
- 4 × Power, 5 V
- 1 × Four-digit display, TM1637In Pinball Table Kit, Spaceship Panel Kit.
- 1 × Addressable RGB LEDsIn Claw Machine Kit, Halloween Prop Kit, Christmas Lights Kit.
- 1 × Limit switch, lever microswitchIn Claw Machine Kit, Vending Machine Kit.
- 1 × Hobby servoIn Halloween Prop Kit, Christmas Lights Kit, Starter Projects 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 |
|---|---|
| Arcade button pin 2 | Arduino Uno R3 pin GND |
| Piezo speaker pin 2 | Arduino Uno R3 pin GND |
| Hobby servo pin PWM | Arduino Uno R3 pin 9 |
| Piezo speaker pin 1 | Arduino Uno R3 pin 8 |
| Limit switch, lever microswitch pin COM | Arduino Uno R3 pin 7 |
| Addressable RGB LEDs pin DIN | Arduino Uno R3 pin 6 |
| Four-digit display, TM1637 pin CLK | Arduino Uno R3 pin 5 |
| Four-digit display, TM1637 pin DIO | Arduino Uno R3 pin 4 |
| Obstacle sensor, FC-51 pin OUT | Arduino Uno R3 pin 3 |
| Arcade button pin 1 | Arduino Uno R3 pin 2 |
| Arduino Uno R3 pin GND | Ground |
| Obstacle sensor, FC-51 pin GND | Ground |
| Obstacle sensor, FC-51 pin VCC | 5 V |
| Four-digit display, TM1637 pin VCC | 5 V |
| Four-digit display, TM1637 pin GND | Ground |
| Addressable RGB LEDs pin VDD | 5 V |
| Addressable RGB LEDs pin VSS | Ground |
| Limit switch, lever microswitch pin NO | Ground |
| Hobby servo pin GND | Ground |
| Hobby servo 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.
// Coin pusher: the arcade game where a shelf slides back and forth and every
// coin you drop might shove a pile over the edge.
//
// DROP button pin 2 -> arcade button -> GND (INPUT_PULLUP, pressed = LOW)
// coin-out eye FC-51 OUT -> pin 3 (LOW while a coin falls past)
// payout display TM1637 CLK -> pin 5, DIO -> pin 4
// light strip 8 WS2812B LEDs, DIN <- pin 6
// stall switch microswitch COM -> pin 7, NO -> GND (pressed = LOW)
// piezo pin 8 -> piezo -> GND
// pusher servo PWM -> pin 9
//
// How to play. You start with 10 coins. Press DROP to drop one onto the
// playfield. The servo sweeps the pusher shelf forward and back, and coins
// pushed off the front fall past the IR eye: each one is paid back to you and
// counted on the display. Three coins in two seconds is a JACKPOT: five bonus
// coins and a light show on the strip.
//
// Stall detection. The pusher's face is on a spring with a microswitch
// behind it. If a coin wedges the shelf while it pushes forward, the face
// gives and the switch closes. The sketch reverses the pusher back out of
// the jam, waits a moment and carries on. Three stalls in ten seconds means
// something is really stuck, so the machine locks itself (a safety
// interlock: no more pushing, no more coins in) until an attendant clears it
// and holds DROP for two seconds.
//
// Skills: a state machine (PUSHING, REVERSING, PAUSED, LOCKED), sweeping a
// servo with millis() instead of delay(), edge detection with debouncing,
// and counting events inside a time window.
//
// The strip's data line usually goes through a 330 ohm resistor on a real
// build; in the simulator a straight wire is fine.
#include <Servo.h>
#include <TM1637Display.h>
#include <Adafruit_NeoPixel.h>
const int DROP_PIN = 2;
const int COIN_PIN = 3;
const int DIO_PIN = 4;
const int CLK_PIN = 5;
const int STRIP_PIN = 6;
const int STALL_PIN = 7;
const int PIEZO_PIN = 8;
const int SERVO_PIN = 9;
const int LEDS = 8;
const int BACK = 40; // pusher fully back (degrees)
const int FRONT = 140; // pusher fully forward
const int STEP_DEG = 2; // degrees per servo step
const unsigned long STEP_MS = 20; // one step every 20 ms: 1 s a stroke
const unsigned long DROP_GAP_MS = 400; // one coin at a time
const unsigned long DEBOUNCE_MS = 30;
const unsigned long STALL_CONFIRM_MS = 60; // the switch must stay closed this long
const unsigned long STALL_WINDOW_MS = 10000;
const int STALLS_TO_LOCK = 3;
const unsigned long PAUSE_MS = 500;
const unsigned long JACKPOT_WINDOW_MS = 2000;
const unsigned long SHOW_MS = 4000;
const unsigned long UNLOCK_HOLD_MS = 2000;
Servo pusher;
TM1637Display display(CLK_PIN, DIO_PIN);
Adafruit_NeoPixel strip(LEDS, STRIP_PIN, NEO_GRB + NEO_KHZ800);
enum State { PUSHING, REVERSING, PAUSED, LOCKED };
State state = PUSHING;
int angle = BACK;
int direction = 1; // +1 forward, -1 back
unsigned long lastStep = 0;
unsigned long pausedAt = 0;
int credits = 10;
int paidOut = 0;
unsigned long lastDrop = 0;
// The last three coins out, for the jackpot window.
unsigned long recentCoins[3] = {0, 0, 0};
unsigned long showUntil = 0;
int noteIndex = -1;
unsigned long noteAt = 0;
const int FANFARE[] = {784, 988, 1175, 1568};
// Stalls: when each recent one happened.
unsigned long stallTimes[STALLS_TO_LOCK] = {0, 0, 0};
unsigned long stallClosedAt = 0;
// Inputs, debounced.
int coinStable = HIGH, coinRaw = HIGH;
unsigned long coinChanged = 0;
int dropStable = HIGH, dropRaw = HIGH;
unsigned long dropChanged = 0;
unsigned long dropDownAt = 0;
unsigned long lastStripAt = 0;
// Debounce one input. Returns true once, when it settles LOW.
bool settledLow(int pin, int &stable, int &raw, unsigned long &changed, unsigned long now) {
int r = digitalRead(pin);
if (r != raw) {
raw = r;
changed = now;
}
if (r != stable && now - changed >= DEBOUNCE_MS) {
stable = r;
return r == LOW;
}
return false;
}
void dropCoin(unsigned long now) {
if (state == LOCKED) {
Serial.println("Machine locked, no coins in");
tone(PIEZO_PIN, 196, 200);
return;
}
if (now - lastDrop < DROP_GAP_MS) return; // too fast: the chute holds one coin
if (credits == 0) {
Serial.println("Out of coins");
tone(PIEZO_PIN, 196, 200);
return;
}
lastDrop = now;
credits--;
Serial.print("Coin dropped, ");
Serial.print(credits);
Serial.println(" left");
tone(PIEZO_PIN, 1047, 40);
}
void coinOut(unsigned long now) {
paidOut++;
credits++;
display.showNumberDec(paidOut);
Serial.print("Coin out! Paid ");
Serial.println(paidOut);
tone(PIEZO_PIN, 1568, 30);
// Slide the window along and see if three fell within two seconds.
recentCoins[0] = recentCoins[1];
recentCoins[1] = recentCoins[2];
recentCoins[2] = now;
if (recentCoins[0] != 0 && now - recentCoins[0] <= JACKPOT_WINDOW_MS && showUntil == 0) {
credits += 5;
showUntil = now + SHOW_MS;
noteIndex = 0;
noteAt = now;
recentCoins[0] = recentCoins[1] = recentCoins[2] = 0;
Serial.print("JACKPOT! +5 coins, ");
Serial.print(credits);
Serial.println(" coins");
}
}
void stall(unsigned long now) {
// Keep the last few stall times and count those inside the window.
for (int i = 0; i < STALLS_TO_LOCK - 1; i++) stallTimes[i] = stallTimes[i + 1];
stallTimes[STALLS_TO_LOCK - 1] = now;
if (stallTimes[0] != 0 && now - stallTimes[0] <= STALL_WINDOW_MS) {
state = LOCKED;
angle = BACK;
pusher.write(angle);
Serial.println("Pusher jammed: locked. Clear it, then hold DROP 2 s");
tone(PIEZO_PIN, 147, 800);
return;
}
state = REVERSING;
direction = -1;
Serial.println("Stall! Reversing pusher");
tone(PIEZO_PIN, 330, 120);
}
void movePusher(unsigned long now) {
if (now - lastStep < STEP_MS) return;
lastStep = now;
angle += direction * STEP_DEG;
if (angle >= FRONT) {
angle = FRONT;
direction = -1;
}
if (angle <= BACK) {
angle = BACK;
direction = 1;
if (state == REVERSING) {
state = PAUSED; // back out of the jam: rest a moment before pushing again
pausedAt = now;
}
}
pusher.write(angle);
}
void drawStrip(unsigned long now) {
if (now - lastStripAt < 40) return; // 25 frames a second is plenty
lastStripAt = now;
if (showUntil != 0) {
strip.rainbow((uint16_t)(now * 60)); // the jackpot: every LED, spinning colors
} else if (state == LOCKED) {
bool on = (now / 300) % 2 == 0; // blinking red: call an attendant
for (int i = 0; i < LEDS; i++) strip.setPixelColor(i, on ? strip.Color(150, 0, 0) : 0);
} else {
// One amber light follows the pusher shelf.
strip.clear();
int at = (long)(angle - BACK) * (LEDS - 1) / (FRONT - BACK);
strip.setPixelColor(at, strip.Color(120, 70, 0));
}
strip.show();
}
void setup() {
Serial.begin(9600);
pinMode(DROP_PIN, INPUT_PULLUP);
pinMode(COIN_PIN, INPUT_PULLUP);
pinMode(STALL_PIN, INPUT_PULLUP);
pusher.attach(SERVO_PIN);
pusher.write(angle);
display.setBrightness(0x0f);
display.showNumberDec(0);
strip.begin();
strip.setBrightness(120);
Serial.println("Coin pusher ready: 10 coins, press DROP");
}
void loop() {
unsigned long now = millis();
bool dropPressed = settledLow(DROP_PIN, dropStable, dropRaw, dropChanged, now);
if (dropPressed) {
dropDownAt = now;
dropCoin(now);
}
if (settledLow(COIN_PIN, coinStable, coinRaw, coinChanged, now)) coinOut(now);
switch (state) {
case PUSHING:
movePusher(now);
// Stall check: only while pushing forward, and only once the switch
// has stayed closed long enough to be sure.
if (direction == 1 && digitalRead(STALL_PIN) == LOW) {
if (stallClosedAt == 0) stallClosedAt = now;
if (now - stallClosedAt >= STALL_CONFIRM_MS) {
stallClosedAt = 0;
stall(now);
}
} else {
stallClosedAt = 0;
}
break;
case REVERSING:
movePusher(now);
break;
case PAUSED:
if (now - pausedAt >= PAUSE_MS) {
state = PUSHING;
Serial.println("Pushing again");
}
break;
case LOCKED:
// Unlock: DROP held for two seconds with the jam cleared.
if (dropStable == LOW && dropDownAt != 0 && now - dropDownAt >= UNLOCK_HOLD_MS) {
dropDownAt = 0;
if (digitalRead(STALL_PIN) == LOW) {
Serial.println("Still jammed");
} else {
for (int i = 0; i < STALLS_TO_LOCK; i++) stallTimes[i] = 0;
state = PUSHING;
direction = 1;
Serial.println("Pusher restarted");
tone(PIEZO_PIN, 880, 100);
}
}
break;
}
// The jackpot fanfare, one note at a time without blocking.
if (noteIndex >= 0 && now - noteAt >= 150) {
tone(PIEZO_PIN, FANFARE[noteIndex], 120);
noteAt = now;
noteIndex = noteIndex < 3 ? noteIndex + 1 : -1;
}
if (showUntil != 0 && (long)(now - showUntil) >= 0) {
showUntil = 0;
Serial.println("Show over");
}
drawStrip(now);
}
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() {
Serial.begin(9600);
pinMode(DROP_PIN, INPUT_PULLUP);
pinMode(COIN_PIN, INPUT_PULLUP);
pinMode(STALL_PIN, INPUT_PULLUP);
pusher.attach(SERVO_PIN);
pusher.write(angle);
display.setBrightness(0x0f);
display.showNumberDec(0);
strip.begin();
strip.setBrightness(120);
Serial.println("Coin pusher ready: 10 coins, press DROP");
}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 dropPressed = settledLow(DROP_PIN, dropStable, dropRaw, dropChanged, now);
if (dropPressed) {
dropDownAt = now;
dropCoin(now);
}
if (settledLow(COIN_PIN, coinStable, coinRaw, coinChanged, now)) coinOut(now);
switch (state) {
case PUSHING:
movePusher(now);
// Stall check: only while pushing forward, and only once the switch
// has stayed closed long enough to be sure.
if (direction == 1 && digitalRead(STALL_PIN) == LOW) {
if (stallClosedAt == 0) stallClosedAt = now;
if (now - stallClosedAt >= STALL_CONFIRM_MS) {
stallClosedAt = 0;
stall(now);
}
} else {
stallClosedAt = 0;
}
break;
case REVERSING:
movePusher(now);
// ... 39 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- The button reads pressed all the time, or never changes, when wired to a pin. Arcade button
Use the COM and NO terminals of the microswitch. Wire COM to ground, NO to the pin, and use INPUT_PULLUP. NC is the opposite and reads pressed when released. - One press counts as several presses in your code. Arcade button
Microswitch contacts bounce for a few milliseconds. Ignore changes for 10 to 20 ms after the first one, or react only to the edge and wait before checking again. - A passive piezo stays silent when you just set the pin HIGH. Piezo speaker
A passive piezo needs a changing signal. Use tone(pin, frequency) or a PWM wave. Around 2 to 4 kHz is loudest. A buzzer with a built in oscillator makes sound from plain DC instead. - Sound is very quiet or the pin seems overloaded. Piezo speaker
Drive the piezo from a pin directly, or through a 100 ohm resistor. For a bigger buzzer, switch it with a transistor. Check that you wired the right two legs and joined grounds. - It detects nothing or always detects. Range depends on the pot and the surface, and black or shiny things reflect poorly. Obstacle sensor
Turn the pot until the OUT LED switches at your distance. Test with a pale, matte surface. OUT is LOW when something is seen, so read it as active low. - Bright sunlight or an incandescent lamp makes it fire at random, because they give off infrared light. Obstacle sensor
Shade the sensor or use it indoors. Power it from 3.3 V to 5 V, join the grounds, and keep it from facing other IR parts. - The display does nothing. Four-digit display
Connect CLK and DIO to two digital pins, plus VCC and GND. It is not I2C, so no address and no Wire library. Use a TM1637 library and give it the same two pins. - Digits look wrong or in a strange order. Four-digit display
Check the library's segment mapping and digit order. The colon is bit 7 of the second digit. Brightness is set separately and can be 0 to 7.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.