Level crossing on an Arduino Uno
Two IR detectors start alternating red lamps and a bell, a gate servo comes down after a delay, and everything stops once the train has cleared the far detector. The whole build, an Arduino Uno and 8 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 Runs in your browser. Free, and no account needed.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Level crossing: two infrared beam detectors, alternating red lamps, a gate servo and a bell.
//
// pin 3 west detector (a slotted beam sensor, HIGH while a car breaks the beam)
// pin 2 east detector, the same
// pin 5 left red lamp pin 4 right red lamp
// pin 9 the gate servo pin 8 the bell, a piezo
//
// The sequence follows the prototype. A train reaches either detector: the lamps
// start alternating and the bell rings at once, and the gate waits a few seconds
// before it comes down, slowly, so a car already on the crossing can clear it.
// The train trips the far detector as it leaves; once that detector has seen
// nothing for CLEAR_MS (the gaps between cars are short), the gate rises and the
// lamps and bell stop. The detector a train leaves by is ignored for a moment, so
// the last car rolling off it does not start the crossing again.
//
// The lamps flash 50 times a minute each, inside the range U.S. rules set for
// real crossing lights. Many crossings stop the bell once the gates are down;
// RING_WHILE_DOWN chooses.
//
// Mokxi has no train: click a detector's card into its slot to put a car in the beam.
#include <Servo.h>
const int WEST_PIN = 3;
const int EAST_PIN = 2;
const int LAMP_LEFT = 5;
const int LAMP_RIGHT = 4;
const int BELL_PIN = 8;
const int GATE_PIN = 9;
const int GATE_UP = 90;
const int GATE_DOWN = 0;
const unsigned long GATE_DELAY_MS = 3000; // lamps and bell first, then the gate
const unsigned long GATE_STEP_MS = 40; // one degree per step: about 3.6 s to lower
const unsigned long FLASH_MS = 600; // each lamp on for 0.6 s: 50 flashes a minute
const unsigned long CLEAR_MS = 2000; // the exit detector quiet this long
const unsigned long IGNORE_MS = 4000; // then ignore detectors while things settle
const bool RING_WHILE_DOWN = false;
enum Phase { IDLE, WARNING, CLEARING };
Phase phase = IDLE;
int exitPin = -1; // the detector the train will leave by
bool exitSeen = false;
unsigned long phaseAt = 0, lastExit = 0, idleAt = 0, lastGateStep = 0, lastStrike = 0;
int gateAngle = GATE_UP;
Servo gate;
bool over(int pin) { return digitalRead(pin) == HIGH; }
void startWarning(int enteredBy) {
phase = WARNING;
phaseAt = millis();
exitPin = enteredBy == WEST_PIN ? EAST_PIN : WEST_PIN;
exitSeen = false;
Serial.println(enteredBy == WEST_PIN ? "train from the west: warning" : "train from the east: warning");
}
void setup() {
Serial.begin(9600);
pinMode(WEST_PIN, INPUT); // the sensor board has its own pull-up
pinMode(EAST_PIN, INPUT);
pinMode(LAMP_LEFT, OUTPUT);
pinMode(LAMP_RIGHT, OUTPUT);
gate.attach(GATE_PIN);
gate.write(GATE_UP);
Serial.println("Level crossing ready");
}
void loop() {
unsigned long now = millis();
if (phase == IDLE) {
if (now - idleAt > IGNORE_MS || idleAt == 0) {
if (over(WEST_PIN)) startWarning(WEST_PIN);
else if (over(EAST_PIN)) startWarning(EAST_PIN);
}
} else {
// Wait for the train at the far detector, then for that detector to stay clear.
if (over(exitPin)) {
exitSeen = true;
lastExit = now;
}
if (phase == WARNING && exitSeen && now - lastExit >= CLEAR_MS) {
phase = CLEARING;
Serial.println("train clear: gate rising");
}
}
// The gate: down after the delay while warning, back up when clearing.
int target = (phase == WARNING && now - phaseAt >= GATE_DELAY_MS) ? GATE_DOWN : GATE_UP;
if (gateAngle != target && now - lastGateStep >= GATE_STEP_MS) {
lastGateStep = now;
gateAngle += gateAngle < target ? 1 : -1;
gate.write(gateAngle);
if (gateAngle == GATE_DOWN) Serial.println("gate down");
if (gateAngle == GATE_UP && phase == CLEARING) {
phase = IDLE;
idleAt = now;
Serial.println("gate up: crossing idle");
}
}
// Lamps alternate while the crossing is active, and go dark when it is idle.
bool active = phase != IDLE;
bool leftOn = (now / FLASH_MS) % 2 == 0;
digitalWrite(LAMP_LEFT, active && leftOn ? HIGH : LOW);
digitalWrite(LAMP_RIGHT, active && !leftOn ? HIGH : LOW);
// The bell: a short strike every 300 ms, like an electronic crossing bell.
bool ringing = active && (RING_WHILE_DOWN || gateAngle != GATE_DOWN);
if (ringing && now - lastStrike >= 300) {
lastStrike = now;
tone(BELL_PIN, 1800, 60);
}
delay(5);
}
Parts list
11 parts, plus the jumper wires. Every one is in the editor's parts bin.
- 1 × Arduino Uno R3
- 1 × Full-size breadboard
- 1 × Power, 5 V
- 1 × Hobby servo
- 2 × Photo interrupter, slot
- 2 × LED, red
- 2 × Resistor, 220 Ω
- 1 × Piezo speaker
How it is wired
10 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; Hobby servo pin GND; Photo interrupter, slot (1) pin GND; Photo interrupter, slot (2) pin GND; Resistor, 220 Ω (1) pin 2; Resistor, 220 Ω (2) pin 2; Piezo speaker pin 2
- Arduino Uno R3 pin 9; Hobby servo pin PWM
- Arduino Uno R3 pin 8; Piezo speaker pin 1
- Arduino Uno R3 pin 5; LED, red (1) pin A
- Arduino Uno R3 pin 4; LED, red (2) pin A
- Arduino Uno R3 pin 3; Photo interrupter, slot (1) pin SIG
- Arduino Uno R3 pin 2; Photo interrupter, slot (2) pin SIG
- 5 V: Hobby servo pin VCC; Photo interrupter, slot (1) pin VCC; Photo interrupter, slot (2) pin VCC
- LED, red (1) pin C; Resistor, 220 Ω (1) pin 1
- LED, red (2) pin C; Resistor, 220 Ω (2) pin 1
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.