Merge jam on an Arduino Uno
Two feeder conveyors take turns putting items onto one shared belt, counted by three E18-D80NK sensors, and watchdog timers stop both feeders on a stuck or lost item until it is cleared and RESET is pressed. 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 $54. On Mokxi they're free.
What you'll learn
- Sensors
- Motors
- Controllers
- Reading a button
- Difficulty: Advanced
- Time: About 35 minutes
- Board: Arduino Uno R3
- Parts: 17
How it works
Two feeder conveyors take turns putting items onto one shared belt, counted by three E18-D80NK sensors, and watchdog timers stop both feeders on a stuck or lost item until it is cleared and RESET is pressed.
Arduino Uno R3 runs the sketch below. The circuit around it: Traffic light module, IR proximity switch, E18-D80NK, Relay board, two channels, DC motor, Pushbutton, Character LCD with I2C backpack. pinMode sets MOTOR_A_PIN as an output. digitalRead reads whether pin is HIGH or LOW. delay waits 10 milliseconds. 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 $54; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × Traffic light module
- 3 × IR proximity switch, E18-D80NK
- 6 × Power, 5 V
- 1 × Relay board, two channels
- 2 × DC motor
- 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 |
|---|---|
| Traffic light module pin R | Arduino Uno R3 pin 13 |
| Traffic light module pin Y | Arduino Uno R3 pin 12 |
| Traffic light module pin G | Arduino Uno R3 pin 11 |
| IR proximity switch, E18-D80NK (1) pin OUT | Arduino Uno R3 pin 8 |
| Pushbutton pin 1a | Arduino Uno R3 pin 6 |
| Relay board, two channels pin IN2 | Arduino Uno R3 pin 5 |
| Relay board, two channels pin IN1 | Arduino Uno R3 pin 4 |
| IR proximity switch, E18-D80NK (2) pin OUT | Arduino Uno R3 pin 3 |
| IR proximity switch, E18-D80NK (3) pin OUT | Arduino Uno R3 pin 2 |
| Character LCD with I2C backpack pin VCC | Arduino Uno R3 pin 5V |
| Character LCD with I2C backpack pin GND | Arduino Uno R3 pin GND |
| Character LCD with I2C backpack pin SDA | Arduino Uno R3 pin A4 |
| Character LCD with I2C backpack pin SCL | Arduino Uno R3 pin A5 |
| Traffic light module pin GND | Ground |
| IR proximity switch, E18-D80NK (1) pin VCC | 5 V |
| IR proximity switch, E18-D80NK (1) pin GND | Ground |
| Relay board, two channels pin GND | Ground |
| Relay board, two channels pin VCC | 5 V |
| DC motor (1) pin 1 | Relay board, two channels pin NO1 |
| Relay board, two channels pin COM1 | 5 V |
| DC motor (2) pin 1 | Relay board, two channels pin NO2 |
| Relay board, two channels pin COM2 | 5 V |
| DC motor (1) pin 2 | Ground |
| DC motor (2) pin 2 | Ground |
| IR proximity switch, E18-D80NK (2) pin VCC | 5 V |
| IR proximity switch, E18-D80NK (2) pin GND | Ground |
| IR proximity switch, E18-D80NK (3) pin VCC | 5 V |
| IR proximity switch, E18-D80NK (3) pin GND | Ground |
| Pushbutton pin 2a | Ground |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Merge jam: two feeder conveyors take turns putting items onto one shared
// belt, and watchdog timers catch a jam.
//
// 2 E18-D80NK at feeder A's merge point, OUT low while it sees an item
// 3 E18-D80NK at feeder B's merge point
// 8 E18-D80NK on the shared belt, past the merge
// 4 relay 1, feeder A's motor (its 5 V is on the relay's contacts)
// 5 relay 2, feeder B's motor
// 6 RESET, a pushbutton to GND
// 13/12/11 the stack light: red, yellow, green
// LCD SDA -> A4 SCL -> A5 VCC -> 5 V GND -> GND
// (each sensor: brown on 5 V, blue on GND)
//
// Two belts that feed one belt must never push at the same time, or items
// collide at the merge. So only one feeder runs at a time, and they take
// turns, one item each. That is arbitration: a rule that decides who goes.
//
// FEED the feeder whose turn it is runs until an item has passed its
// merge beam, then it stops.
// TRANSIT both feeders stopped. The item rides the shared belt to its
// sensor; when it gets there it is counted, and the turn goes to
// the other feeder.
// JAM both feeders stopped, red light. Only RESET gets you out.
//
// A feeder with nothing to send loses its turn after IDLE_MS, so an empty
// lane never holds up a full one.
//
// The jam detection is two watchdog timers. A machine cannot see a jam, but
// it knows how long things should take:
//
// JAM A / JAM B an item sits in a running feeder's merge beam for more
// than STUCK_MS. Something is wedged at the merge.
// LOST ITEM an item passed the merge but never reached the belt
// sensor within LOST_MS. It fell off, or it is stuck out of
// sight.
//
// After a jam someone has to clear it by hand, then press RESET. RESET is
// refused while any beam is still blocked, so the line cannot restart into
// the same jam.
//
// Press Run, then move the sensors' targets close and away to send items.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
const int SENSOR_A_PIN = 2;
const int SENSOR_B_PIN = 3;
const int BELT_PIN = 8;
const int MOTOR_A_PIN = 4;
const int MOTOR_B_PIN = 5;
const int RESET_PIN = 6;
const int RED_PIN = 13;
const int YELLOW_PIN = 12;
const int GREEN_PIN = 11;
const unsigned long STUCK_MS = 5000; // longest an item may block a merge beam
const unsigned long LOST_MS = 6000; // longest the trip to the belt sensor may take
const unsigned long IDLE_MS = 4000; // a feeder with nothing to send gives up its turn
// The relay board switches on a low input.
const int ON = LOW;
const int OFF = HIGH;
enum State { FEED, TRANSIT, JAM };
enum Fault { NONE, JAM_A, JAM_B, LOST_ITEM };
const char* const FAULTS[] = {"", "JAM A", "JAM B", "LOST ITEM"};
LiquidCrystal_I2C lcd(0x27, 16, 2);
State state = FEED;
Fault fault = NONE;
int turn = 0; // 0 = feeder A, 1 = feeder B
unsigned long enteredAt = 0; // when this state began
unsigned long blockedSince = 0;
bool wasBlocked = false;
bool beltWasBlocked = false;
bool resetWasDown = false;
unsigned long resetAt = 0;
unsigned long counts[2] = {0, 0};
char feederName(int f) {
return f == 0 ? 'A' : 'B';
}
bool seesItem(int pin) {
return digitalRead(pin) == LOW;
}
bool mergeBlocked(int f) {
return seesItem(f == 0 ? SENSOR_A_PIN : SENSOR_B_PIN);
}
void printCount(unsigned long n) {
if (n < 100) lcd.print(' ');
if (n < 10) lcd.print(' ');
lcd.print(n);
}
void show() {
lcd.setCursor(0, 0);
lcd.print('A');
printCount(counts[0]);
lcd.print(" B");
printCount(counts[1]);
lcd.print(" Tot");
printCount(counts[0] + counts[1]);
lcd.setCursor(0, 1);
if (state == JAM) {
lcd.print(FAULTS[fault]);
lcd.print(" press RESET");
} else if (state == FEED) {
lcd.print("Feeding ");
lcd.print(feederName(turn));
} else {
lcd.print("To the belt");
}
lcd.print(" ");
}
void enter(State next) {
state = next;
enteredAt = millis();
// One place sets every output, so no state can leave both feeders on.
bool feeding = next == FEED;
digitalWrite(MOTOR_A_PIN, feeding && turn == 0 ? ON : OFF);
digitalWrite(MOTOR_B_PIN, feeding && turn == 1 ? ON : OFF);
digitalWrite(GREEN_PIN, next == FEED);
digitalWrite(YELLOW_PIN, next == TRANSIT);
digitalWrite(RED_PIN, next == JAM);
// A beam that is already blocked starts its watchdog now.
wasBlocked = feeding && mergeBlocked(turn);
blockedSince = enteredAt;
if (next == FEED) {
Serial.print(F("feeding "));
Serial.println(feederName(turn));
} else if (next == TRANSIT) {
Serial.print(F("item from "));
Serial.print(feederName(turn));
Serial.println(F(" on its way"));
}
show();
}
void jam(Fault why) {
fault = why;
Serial.print(FAULTS[why]);
if (why == LOST_ITEM) {
Serial.print(F(": the item from "));
Serial.print(feederName(turn));
Serial.println(F(" never reached the belt"));
} else {
Serial.println(F(": an item is stuck in the merge beam"));
}
enter(JAM);
}
void setup() {
// Relay pins high before they become outputs, so no motor twitches.
digitalWrite(MOTOR_A_PIN, OFF);
digitalWrite(MOTOR_B_PIN, OFF);
pinMode(MOTOR_A_PIN, OUTPUT);
pinMode(MOTOR_B_PIN, OUTPUT);
pinMode(RED_PIN, OUTPUT);
pinMode(YELLOW_PIN, OUTPUT);
pinMode(GREEN_PIN, OUTPUT);
pinMode(SENSOR_A_PIN, INPUT_PULLUP);
pinMode(SENSOR_B_PIN, INPUT_PULLUP);
pinMode(BELT_PIN, INPUT_PULLUP);
pinMode(RESET_PIN, INPUT_PULLUP);
Serial.begin(9600);
Serial.println(F("Merge jam"));
lcd.init();
lcd.backlight();
enter(FEED);
}
void loop() {
unsigned long now = millis();
bool belt = seesItem(BELT_PIN);
bool beltArrived = belt && !beltWasBlocked;
beltWasBlocked = belt;
bool resetDown = digitalRead(RESET_PIN) == LOW;
// A press counts once; the contact's bounce in the next 100 ms does not.
bool resetPressed = resetDown && !resetWasDown && now - resetAt >= 100;
if (resetPressed) resetAt = now;
resetWasDown = resetDown;
switch (state) {
case FEED: {
bool blocked = mergeBlocked(turn);
if (blocked && !wasBlocked) blockedSince = now;
if (blocked && now - blockedSince >= STUCK_MS) {
jam(turn == 0 ? JAM_A : JAM_B);
} else if (!blocked && wasBlocked) {
// The item has gone through the beam onto the shared belt.
enter(TRANSIT);
} else if (!blocked && now - enteredAt >= IDLE_MS) {
Serial.print(feederName(turn));
Serial.println(F(" has nothing to send"));
turn = 1 - turn;
enter(FEED);
} else {
wasBlocked = blocked;
}
break;
}
case TRANSIT:
if (beltArrived) {
counts[turn]++;
Serial.print(F("item "));
Serial.print(counts[0] + counts[1]);
Serial.print(F(" from "));
Serial.print(feederName(turn));
Serial.print(F(" A "));
Serial.print(counts[0]);
Serial.print(F(" B "));
Serial.println(counts[1]);
turn = 1 - turn; // fairness: the other feeder goes next
enter(FEED);
} else if (now - enteredAt >= LOST_MS) {
jam(LOST_ITEM);
}
break;
case JAM:
if (resetPressed) {
if (mergeBlocked(0) || mergeBlocked(1) || belt) {
Serial.println(F("reset refused: a beam is still blocked"));
} else {
Serial.println(F("reset"));
// A lost item was sent, so the other feeder is next. A stuck
// item was not, so the same feeder tries again.
if (fault == LOST_ITEM) turn = 1 - turn;
fault = NONE;
enter(FEED);
}
}
break;
}
delay(10);
}
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() {
// Relay pins high before they become outputs, so no motor twitches.
digitalWrite(MOTOR_A_PIN, OFF);
digitalWrite(MOTOR_B_PIN, OFF);
pinMode(MOTOR_A_PIN, OUTPUT);
pinMode(MOTOR_B_PIN, OUTPUT);
pinMode(RED_PIN, OUTPUT);
pinMode(YELLOW_PIN, OUTPUT);
pinMode(GREEN_PIN, OUTPUT);
pinMode(SENSOR_A_PIN, INPUT_PULLUP);
pinMode(SENSOR_B_PIN, INPUT_PULLUP);
pinMode(BELT_PIN, INPUT_PULLUP);
pinMode(RESET_PIN, INPUT_PULLUP);
Serial.begin(9600);
Serial.println(F("Merge jam"));
lcd.init();
lcd.backlight();
enter(FEED);
}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 belt = seesItem(BELT_PIN);
bool beltArrived = belt && !beltWasBlocked;
beltWasBlocked = belt;
bool resetDown = digitalRead(RESET_PIN) == LOW;
// A press counts once; the contact's bounce in the next 100 ms does not.
bool resetPressed = resetDown && !resetWasDown && now - resetAt >= 100;
if (resetPressed) resetAt = now;
resetWasDown = resetDown;
switch (state) {
case FEED: {
bool blocked = mergeBlocked(turn);
if (blocked && !wasBlocked) blockedSince = now;
if (blocked && now - blockedSince >= STUCK_MS) {
jam(turn == 0 ? JAM_A : JAM_B);
} else if (!blocked && wasBlocked) {
// The item has gone through the beam onto the shared belt.
enter(TRANSIT);
} else if (!blocked && now - enteredAt >= IDLE_MS) {
Serial.print(feederName(turn));
Serial.println(F(" has nothing to send"));
turn = 1 - turn;
enter(FEED);
} else {
wasBlocked = blocked;
}
// ... 37 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- One color never lights, or all three light together. Traffic light module
Check each wire goes to its own pin labeled R, Y, G, and that GND is connected. A pin driven high lights its LED. Resistors are already on the board, so do not add more. - Lights seem dim or the wrong color comes on in a sequence. Traffic light module
Confirm the pins in your code match the wires, and that the module gets the right supply voltage. Keep each state's delay long enough to see, and turn the old color off before the next. - OUT floats or reads high even with a target in range. IR proximity switch
The output is NPN open collector. It needs a pull-up resistor (a few kohm) to the logic supply, or INPUT_PULLUP. Detection pulls OUT low. - It does not detect, or the range is wrong. IR proximity switch
Power it from 5 V (supply is 5 V on the E18-D80NK). Adjust the range screw. Shiny or dark objects reflect differently than light matte ones. - A relay is on at boot or the logic is inverted. Relay board
These boards are active low, so LOW on IN turns the relay on. Set the pin HIGH before setting it as an output so it does not click at start. - The relay clicks but the load does nothing, or the board resets. Relay board
Use the correct contact: COM with NO for normally off. Power the relay coil with enough current, ideally from a separate 5 V supply, with grounds joined. Mains wiring is dangerous, so be careful. - 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.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.