Parts list
| Part | Qty | Approx. | Buy |
|---|---|---|---|
| Arduino Uno R3 (CH340 clone) | 1 | ₹450 | Buy Arduino Uno R3 (CH340 clone) |
| IR obstacle sensor module (FC-51) | 2 | ₹80 | Buy IR obstacle sensor module (FC-51) |
| SG90 micro servo | 1 | ₹110 | Buy SG90 micro servo |
| Active buzzer module | 1 | ₹25 | Buy Active buzzer module |
| WS2812B LEDs (2 cut from a strip, or 2 single-LED boards) | 1 | ₹40 | Buy WS2812B LEDs (2 cut from a strip, or 2 single-LED boards) |
| 330 Ω resistor + 470 µF / 16 V capacitor | 1 | ₹10 | Buy 330 Ω resistor + 470 µF / 16 V capacitor |
| Toy train or a cardboard train on a string, sunboard base, ice-cream stick boom | 1 | ₹250 | Buy Toy train or a cardboard train on a string, sunboard base, ice-cream stick boom |
| Jumper wires | 1 | ₹30 | Buy Jumper wires |
| Total | ₹995 |
Affiliate disclosure: “Buy” links may earn FlashReady a small commission at no extra cost to you. Prices are indicative.
Wiring
| Arduino Uno R3 | Part · pin | Wire | Notes |
|---|---|---|---|
| D2 | IR obstacle avoidance sensor (FC-51) · OUT | green | Sensor A — LOW when a train is in front |
| 5V | IR obstacle avoidance sensor (FC-51) · VCC | red | |
| GND | IR obstacle avoidance sensor (FC-51) · GND | black | |
| D3 | IR obstacle avoidance sensor (FC-51) #2 · OUT | blue | Sensor B, on the other side of the road |
| 5V | IR obstacle avoidance sensor (FC-51) #2 · VCC | red | |
| GND | IR obstacle avoidance sensor (FC-51) #2 · GND | black | |
| D9 | SG90 micro servo (9 g) · SIG | orange | |
| 5V | SG90 micro servo (9 g) · VCC | red | 470 µF capacitor across the servo’s 5V/GND |
| GND | SG90 micro servo (9 g) · GND | black | |
| D8 | Active buzzer module (high-level trigger) · I/O | purple | |
| 5V | Active buzzer module (high-level trigger) · VCC | red | |
| GND | Active buzzer module (high-level trigger) · GND | black | |
| D6 | WS2812B addressable RGB LED strip (NeoPixel) · DIN | green | Via 330 Ω — Two LEDs cut from a strip — the road lamps |
| 5V | WS2812B addressable RGB LED strip (NeoPixel) · 5V | red | |
| GND | WS2812B addressable RGB LED strip (NeoPixel) · GND | black |
Board quirks: The FC-51 sees reflected infrared, so it detects the side of the train passing 2–10 cm in front of it. Turn its blue potentiometer until the green LED lights only when the train is there. Black plastic reflects little IR — stick white paper on the side of a dark toy train. The gate opens on the far sensor going clear for 2 seconds, not on a timer. A long train with gaps between coaches would otherwise reopen the boom between two coaches. If the far sensor never sees the train (it stopped, or reversed), the gate stays closed and the Serial Monitor prints a warning. Opening on a timeout would be the unsafe choice. Sunlight contains a lot of infrared and can make FC-51 sensors trigger by themselves. Indoors under tube lights they are reliable; near a window, shade them with a small cardboard hood.
Code
// Automatic railway level-crossing gate model — Arduino Uno + 2 IR sensors + SG90 + buzzer + WS2812B.
// Sensor A and sensor B sit on the track on either side of the road. Whichever one sees the train
// first closes the gate (road lights flash red, buzzer beeps). The gate opens only after the train
// has passed the sensor on the OTHER side and that sensor is clear again — so it works for trains
// from either direction.
// IR sensor A OUT → D2, sensor B OUT → D3 (LOW = train), SG90 signal → D9,
// buzzer module I/O → D8, WS2812B DIN → D6 through 330 Ω.
#include <Servo.h>
#include <Adafruit_NeoPixel.h>
constexpr uint8_t SENSOR_A = 2, SENSOR_B = 3, LED_PIN = 6, BUZZER_PIN = 8, SERVO_PIN = 9;
constexpr uint8_t NUM_LEDS = 2; // the two red road lamps
constexpr uint8_t GATE_OPEN = 90, GATE_CLOSED = 0; // servo angles — adjust to your boom
constexpr unsigned long CLEAR_MS = 2000; // exit sensor must stay clear this long
constexpr unsigned long STUCK_MS = 60000; // no train at the exit after this: warn, stay closed
Servo gate;
Adafruit_NeoPixel lamps(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
enum State : uint8_t { OPEN, CLOSING_WAIT_EXIT, EXIT_SEEN };
State state = OPEN;
uint8_t exitPin = 0;
unsigned long closedAt = 0, exitClearSince = 0;
int angle = GATE_OPEN, target = GATE_OPEN;
unsigned long lastStep = 0, lastBlink = 0;
bool blinkPhase = false, warned = false;
bool train(uint8_t pin) { return digitalRead(pin) == LOW; }
void lampsIdle() {
lamps.setPixelColor(0, 0x002000); // dim green: road open
lamps.setPixelColor(1, 0x002000);
lamps.show();
}
void setup() {
pinMode(SENSOR_A, INPUT_PULLUP); // FC-51 has its own pull-up; this one just keeps an unplugged input HIGH
pinMode(SENSOR_B, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
Serial.begin(115200);
Serial.println(F("Level crossing ready — gate open. Block sensor A (D2) or B (D3) to send a train."));
lamps.begin();
lamps.setBrightness(120);
lampsIdle();
gate.attach(SERVO_PIN);
gate.write(GATE_OPEN);
}
void loop() {
unsigned long now = millis();
switch (state) {
case OPEN:
if (train(SENSOR_A) || train(SENSOR_B)) {
exitPin = train(SENSOR_A) ? SENSOR_B : SENSOR_A;
state = CLOSING_WAIT_EXIT;
closedAt = now;
warned = false;
target = GATE_CLOSED;
Serial.print(F("Train at sensor "));
Serial.print(exitPin == SENSOR_B ? 'A' : 'B');
Serial.println(F(" — closing gate"));
}
break;
case CLOSING_WAIT_EXIT:
if (train(exitPin)) {
state = EXIT_SEEN;
exitClearSince = 0;
Serial.println(F("Train reached the far sensor"));
} else if (!warned && now - closedAt > STUCK_MS) {
// Fail-safe: never open just because time passed. A stopped train is still on the road.
warned = true;
Serial.println(F("WARNING: train not seen at the far sensor for 60 s — gate stays closed"));
}
break;
case EXIT_SEEN:
if (train(exitPin)) exitClearSince = 0;
else if (!exitClearSince) exitClearSince = now;
else if (now - exitClearSince >= CLEAR_MS) {
state = OPEN;
target = GATE_OPEN;
Serial.println(F("Last coach gone — opening gate"));
}
break;
}
// Move the boom 1° every 20 ms (about 2 s end to end) instead of snapping it.
if (angle != target && now - lastStep >= 20) {
lastStep = now;
angle += angle < target ? 1 : -1;
gate.write(angle);
}
// Warning: lamps flash alternately and the buzzer beeps while the gate is closed or moving.
bool warning = state != OPEN || angle != GATE_OPEN;
if (warning && now - lastBlink >= 400) {
lastBlink = now;
blinkPhase = !blinkPhase;
lamps.setPixelColor(0, blinkPhase ? 0xFF0000 : 0);
lamps.setPixelColor(1, blinkPhase ? 0 : 0xFF0000);
lamps.show();
// Beep only while the boom is moving and for the first seconds after closing, like a real crossing bell.
digitalWrite(BUZZER_PIN, blinkPhase && (angle != target || now - closedAt < 5000));
} else if (!warning && lastBlink) {
lastBlink = 0;
digitalWrite(BUZZER_PIN, LOW);
lampsIdle();
}
}// Automatic railway level-crossing gate model — Arduino Uno + 2 IR sensors + SG90 + buzzer + WS2812B.
// Sensor A and sensor B sit on the track on either side of the road. Whichever one sees the train
// first closes the gate (road lights flash red, buzzer beeps). The gate opens only after the train
// has passed the sensor on the OTHER side and that sensor is clear again — so it works for trains
// from either direction.
// IR sensor A OUT → D2, sensor B OUT → D3 (LOW = train), SG90 signal → D9,
// buzzer module I/O → D8, WS2812B DIN → D6 through 330 Ω.
#include <Servo.h>
#include <Adafruit_NeoPixel.h>
constexpr uint8_t SENSOR_A = 2, SENSOR_B = 3, LED_PIN = 6, BUZZER_PIN = 8, SERVO_PIN = 9;
constexpr uint8_t NUM_LEDS = 2; // the two red road lamps
constexpr uint8_t GATE_OPEN = 90, GATE_CLOSED = 0; // servo angles — adjust to your boom
constexpr unsigned long CLEAR_MS = 2000; // exit sensor must stay clear this long
constexpr unsigned long STUCK_MS = 60000; // no train at the exit after this: warn, stay closed
Servo gate;
Adafruit_NeoPixel lamps(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
enum State : uint8_t { OPEN, CLOSING_WAIT_EXIT, EXIT_SEEN };
State state = OPEN;
uint8_t exitPin = 0;
unsigned long closedAt = 0, exitClearSince = 0;
int angle = GATE_OPEN, target = GATE_OPEN;
unsigned long lastStep = 0, lastBlink = 0;
bool blinkPhase = false, warned = false;
bool train(uint8_t pin) { return digitalRead(pin) == LOW; }
void lampsIdle() {
lamps.setPixelColor(0, 0x002000); // dim green: road open
lamps.setPixelColor(1, 0x002000);
lamps.show();
}
void setup() {
pinMode(SENSOR_A, INPUT_PULLUP); // FC-51 has its own pull-up; this one just keeps an unplugged input HIGH
pinMode(SENSOR_B, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
Serial.begin(115200);
Serial.println(F("Level crossing ready — gate open. Block sensor A (D2) or B (D3) to send a train."));
lamps.begin();
lamps.setBrightness(120);
lampsIdle();
gate.attach(SERVO_PIN);
gate.write(GATE_OPEN);
}
void loop() {
unsigned long now = millis();
switch (state) {
case OPEN:
if (train(SENSOR_A) || train(SENSOR_B)) {
exitPin = train(SENSOR_A) ? SENSOR_B : SENSOR_A;
state = CLOSING_WAIT_EXIT;
closedAt = now;
warned = false;
target = GATE_CLOSED;
Serial.print(F("Train at sensor "));
Serial.print(exitPin == SENSOR_B ? 'A' : 'B');
Serial.println(F(" — closing gate"));
}
break;
case CLOSING_WAIT_EXIT:
if (train(exitPin)) {
state = EXIT_SEEN;
exitClearSince = 0;
Serial.println(F("Train reached the far sensor"));
} else if (!warned && now - closedAt > STUCK_MS) {
// Fail-safe: never open just because time passed. A stopped train is still on the road.
warned = true;
Serial.println(F("WARNING: train not seen at the far sensor for 60 s — gate stays closed"));
}
break;
case EXIT_SEEN:
if (train(exitPin)) exitClearSince = 0;
else if (!exitClearSince) exitClearSince = now;
else if (now - exitClearSince >= CLEAR_MS) {
state = OPEN;
target = GATE_OPEN;
Serial.println(F("Last coach gone — opening gate"));
}
break;
}
// Move the boom 1° every 20 ms (about 2 s end to end) instead of snapping it.
if (angle != target && now - lastStep >= 20) {
lastStep = now;
angle += angle < target ? 1 : -1;
gate.write(angle);
}
// Warning: lamps flash alternately and the buzzer beeps while the gate is closed or moving.
bool warning = state != OPEN || angle != GATE_OPEN;
if (warning && now - lastBlink >= 400) {
lastBlink = now;
blinkPhase = !blinkPhase;
lamps.setPixelColor(0, blinkPhase ? 0xFF0000 : 0);
lamps.setPixelColor(1, blinkPhase ? 0 : 0xFF0000);
lamps.show();
// Beep only while the boom is moving and for the first seconds after closing, like a real crossing bell.
digitalWrite(BUZZER_PIN, blinkPhase && (angle != target || now - closedAt < 5000));
} else if (!warning && lastBlink) {
lastBlink = 0;
digitalWrite(BUZZER_PIN, LOW);
lampsIdle();
}
}Compiled for uno on avr 1.8.8 with Servo 1.3.0 · 3 Nov 2026
How it works
Unmanned level crossings were a major cause of accidents on Indian Railways until they were closed or manned. An automatic railway gate is one of the most built school and diploma projects for that reason — and a model that handles trains from both directions properly is the kind examiners remember.
How it works
Two IR sensors watch the track, one on each side of the road (A on D2, B on D3). Each one pulls its output LOW while something is in front of it.
The sketch is a small state machine with three states:
- Open — road lamps glow green, boom up. When either sensor sees the train, that one is the entry side and the other is the exit side. The boom starts to close.
- Waiting for the exit sensor — lamps flash red alternately and the buzzer beeps while the boom comes down (1° every 20 ms, about 2 seconds, like a real boom rather than a snap).
- Exit seen — the train has reached the far side. The gate opens only after that sensor has been clear for 2 seconds in a row, so gaps between coaches don’t reopen it.
The servo is an SG90 on D9; the lamps are two WS2812B LEDs on D6; the buzzer module is on D8.
What to show at the exhibition
Run the train from left to right, then from right to left — the gate works both ways. Then stop the train between the sensors and let the audience see that the gate does not open after a minute: the Serial Monitor prints a warning instead. That is the key safety idea: when in doubt, keep the road closed.
Questions the examiner may ask
- Why two sensors and not one? One sensor can’t tell whether the train is arriving or leaving. With two, the first one triggered is the entry side, and the train must reach the other one before the gate can open.
- How does the servo know the angle? The Uno sends a pulse every 20 ms; its width (about 1–2 ms) sets the angle. The Servo library generates it with Timer1.
- How does the IR sensor work? An IR LED shines forward and a photodiode measures the reflection. An LM393 comparator turns it into a digital LOW/HIGH; the pot sets the threshold.
- What would a real system add? Track circuits or axle counters instead of IR sensors, interlocking with the railway signal so it stays red until the gate is closed, and a battery backup.
Cost in India
About ₹1,000 including the Uno and a simple cardboard model. The sensors and servo together are under ₹200; a battery-powered toy train adds ₹300–600 but makes the demo much more fun.
Troubleshooting
The gate closes by itself
One sensor sees the track, the base or a hand. Turn its potentiometer anticlockwise to shorten the range, and angle it so nothing is in front of it when no train is there.
The gate never opens again
The far sensor never saw the train, or still sees something. Watch the Serial Monitor — it says which step it is waiting for — and check that sensor’s green LED goes off when the track is clear.
The Uno resets when the boom moves
The servo’s current spike drags the 5 V rail down. Fit the 470 µF capacitor at the servo, or power the servo from a separate 5 V supply with its GND joined to the Uno’s GND.
The servo hums or the boom overshoots
Change GATE_OPEN and GATE_CLOSED so the horn doesn’t press against an end stop. 0° and 90° are a starting point; most booms need something like 5° and 95°.
Upgrades
- Add a second servo for a boom on each side of the road, as on real Indian Railways crossings.
- Count trains and show the time of the last train on a 16×2 LCD.
- Drive a toy train with an L298N and stop it at a red signal when the gate is stuck open.
- Use HC-SR04 sensors to also check that no vehicle is under the boom before it closes.



