Servo turnouts with a panel on an Arduino Uno
Two servo turnouts that glide across one degree at a time, a panel button and a pair of route lamps for each, and positions kept in EEPROM so nothing jumps at power-up. The whole build, an Arduino Uno and 12 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.
// Servo turnouts with a control panel: two turnouts, two buttons, four lamps.
//
// pin 5, pin 2 panel buttons for turnouts 1 and 2 (to GND, internal pull-ups)
// pin 9, pin 10 the two turnout servos
// pins 7 and 6 turnout 1's lamps: green for through (closed), yellow for diverging (thrown)
// pins 4 and 3 turnout 2's lamps, the same way round
//
// A hobby servo under the layout, with a stiff wire up through the throwbar,
// is the cheapest slow-motion turnout motor there is. Three things make it look
// and last like a real one:
//
// 1. Slow motion. The horn moves one degree every 30 ms instead of snapping,
// so the points glide across, the way the prototype's do.
// 2. A throw set per turnout. CLOSED_ANGLE and THROWN_ANGLE are where the
// points just touch each stock rail. Past that the servo pushes against
// the rail forever, hums and gets hot, so set them on the layout.
// 3. Memory. The last position of each turnout is kept in EEPROM, so at power
// up the servo is sent where it already is and does not jump.
//
// The panel lamp for the new route blinks while the points travel and goes
// steady when they arrive. Type "1" or "2" in the Serial Monitor to throw from
// the keyboard, the way a computer panel such as JMRI might send a command.
//
// On a DCC layout the same sketch would take its commands from accessory
// decoder packets instead of buttons; the servo side does not change.
#include <EEPROM.h>
#include <Servo.h>
const int TURNOUTS = 2;
const int BUTTON_PIN[TURNOUTS] = {5, 2};
const int SERVO_PIN[TURNOUTS] = {9, 10};
const int CLOSED_LAMP[TURNOUTS] = {7, 4};
const int THROWN_LAMP[TURNOUTS] = {6, 3};
const int CLOSED_ANGLE[TURNOUTS] = {75, 80};
const int THROWN_ANGLE[TURNOUTS] = {105, 100};
const unsigned long STEP_MS = 30; // one degree per step: slower is more realistic
const unsigned long BLINK_MS = 250;
Servo servo[TURNOUTS];
bool thrown[TURNOUTS]; // where each turnout is going (or is)
int angle[TURNOUTS]; // where its servo is now
bool buttonWas[TURNOUTS];
unsigned long buttonAt[TURNOUTS];
unsigned long lastStep = 0;
int targetOf(int t) { return thrown[t] ? THROWN_ANGLE[t] : CLOSED_ANGLE[t]; }
void showLamps(int t, bool blinkOn) {
bool moving = angle[t] != targetOf(t);
bool lit = !moving || blinkOn;
digitalWrite(CLOSED_LAMP[t], (!thrown[t] && lit) ? HIGH : LOW);
digitalWrite(THROWN_LAMP[t], (thrown[t] && lit) ? HIGH : LOW);
}
void flip(int t) {
thrown[t] = !thrown[t];
EEPROM.update(t, thrown[t] ? 1 : 0);
Serial.print("turnout ");
Serial.print(t + 1);
Serial.println(thrown[t] ? " throwing" : " closing");
}
void setup() {
Serial.begin(9600);
for (int t = 0; t < TURNOUTS; t++) {
pinMode(BUTTON_PIN[t], INPUT_PULLUP);
pinMode(CLOSED_LAMP[t], OUTPUT);
pinMode(THROWN_LAMP[t], OUTPUT);
thrown[t] = EEPROM.read(t) == 1; // a blank EEPROM reads 255: closed
angle[t] = targetOf(t);
servo[t].attach(SERVO_PIN[t]);
servo[t].write(angle[t]);
showLamps(t, true);
}
Serial.println("Turnout panel ready: press a button, or type 1 or 2");
}
void loop() {
unsigned long now = millis();
// Buttons: act on the press, then ignore the pin for 50 ms of contact bounce.
for (int t = 0; t < TURNOUTS; t++) {
bool down = digitalRead(BUTTON_PIN[t]) == LOW;
if (down != buttonWas[t] && now - buttonAt[t] >= 50) {
buttonWas[t] = down;
buttonAt[t] = now;
if (down) flip(t);
}
}
while (Serial.available() > 0) {
char c = Serial.read();
if (c == '1' || c == '2') flip(c - '1');
}
// Slow motion: every STEP_MS, each moving servo takes one degree toward its target.
if (now - lastStep >= STEP_MS) {
lastStep = now;
bool blinkOn = (now / BLINK_MS) % 2 == 0;
for (int t = 0; t < TURNOUTS; t++) {
int target = targetOf(t);
if (angle[t] != target) {
angle[t] += angle[t] < target ? 1 : -1;
servo[t].write(angle[t]);
if (angle[t] == target) {
Serial.print("turnout ");
Serial.print(t + 1);
Serial.println(thrown[t] ? " thrown" : " closed");
}
}
showLamps(t, blinkOn);
}
}
}
Parts list
15 parts, plus the jumper wires. Every one is in the editor's parts bin.
- 1 × Arduino Uno R3
- 1 × Full-size breadboard
- 2 × Hobby servo
- 1 × Power, 5 V
- 2 × LED, green
- 4 × Resistor, 220 Ω
- 2 × LED, yellow
- 2 × Pushbutton
How it is wired
14 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 (1) pin GND; Hobby servo (2) pin GND; Resistor, 220 Ω (1) pin 2; Resistor, 220 Ω (2) pin 2; Pushbutton (1) pin 2a; Pushbutton (1) pin 2b; Resistor, 220 Ω (3) pin 2; Resistor, 220 Ω (4) pin 2; Pushbutton (2) pin 2a; Pushbutton (2) pin 2b
- Arduino Uno R3 pin 10; Hobby servo (2) pin PWM
- Arduino Uno R3 pin 9; Hobby servo (1) pin PWM
- Arduino Uno R3 pin 7; LED, green (1) pin A
- Arduino Uno R3 pin 6; LED, yellow (1) pin A
- Arduino Uno R3 pin 5; Pushbutton (1) pin 1a; Pushbutton (1) pin 1b
- Arduino Uno R3 pin 4; LED, green (2) pin A
- Arduino Uno R3 pin 3; LED, yellow (2) pin A
- Arduino Uno R3 pin 2; Pushbutton (2) pin 1a; Pushbutton (2) pin 1b
- 5 V: Hobby servo (1) pin VCC; Hobby servo (2) pin VCC
- LED, green (1) pin C; Resistor, 220 Ω (1) pin 1
- LED, yellow (1) pin C; Resistor, 220 Ω (2) pin 1
- LED, green (2) pin C; Resistor, 220 Ω (3) pin 1
- LED, yellow (2) pin C; Resistor, 220 Ω (4) 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.