Model Train Forum banner
1 - 4 of 4 Posts

207 Posts
Discussion Starter · #1 · (Edited)
This program works for 3-rail applications where you can isolate one of the rails.


The addition of 1k resistors may be necessary between the track connections and the Arduino board. I tried without them and the circuit was unstable, triggering at random.

I don't know if I reinvented the wheel, but I wrote an Arduino program to give more realistic control of the signal lights for a level crossing.

The spark for this idea was from seeing how many implement crossing signals:
  1. If you want a long lead-in to the crossing, you have 1 long track that triggers the crossing signal. The problem with this implementation is that the crossing signal stays on for a long time after the train has left the crossing to allow for an equally long lead-in from the other direction.
  2. The crossing signal triggers just before the train enters the crossing. This eliminates the signal being on during the long lead-out, but it also means that the signal turns on at the last second.

This program solves both problems.

What You Need:
  • Arduino Uno and the knowlege to use it. (Amazon, a whopping $15)
  • 9V DC power supply for the Arduino (Amazon, about $10)
  • 2x 4 channel relay board (Amazon, about $8 each)
    I spec'd this board because it has screw terminals for easy, hook-up.
    These relay boards require a separate 5VDC power supply. DO NOT supply it from the 5V pin on the Arduino. You will blow out the voltage regulation on the Arduino and kill it.
  • 5VDC power supply for the above relay boards (Amazon, about $8)
    Cut off the plug, strip the wires, determine the polarity, and hook it up.
  • Now if you want to get really fancy, you can get the Arduino terminal strip connections to make all of your hookups screwdriver friendly. (Amazon, about $10)
So, for about $60 (+ the cost of your preferred crossing signal), you can control up to 5 level crossings.

What you DON'T need:
  • Component level external hardware (resistors, transistors, OP-Amps, capacitors, etc.)
  • Advanced electronics knowledge needed to implement.

Design Specs:
  1. Handles 5 track crossings total.
  2. Each crossing can be either stand-alone, or grouped. There are 2 groups. This allows for a single output to control the signals for a single crossing, even if there are multiple tracks that need to control that one signal.

Electrical Connections:
See the attached drawing.

How the Program Works:
The train approaches the crossing. When it hits the leader track, it triggers the leader input. This turns the crossing signal on. The crossing signal then stays on until the train leaves the crossing. The fact that the train is still triggering the lead-out is ignored. Once both the crossing track and the leader track are no longer occupied by the train, the program resets itself and will trigger the crossing signal with the next train approach. This allows for a long lead into the crossing, but turns the crossing signal off shortly after the train leaves the crossing.
Both the leads and the crossing lengths are determined by the length of isolated track.

Crossing Groups:
Crossing groups allow multiple tracks to trigger the same crossing signal. Each track operates independently of the others in the group, but all tracks in the group will turn on the crossing signal.

Scenario: A train on track #1 is just about to leave the crossing while a train on track #2 has just triggered its leader section. The crossing signal needs to stay on, and will turn off when train #2 leaves the crossing.

There is one deficiency in the program:
Scenario: 2 trains on the same track, traveling in the same direction, #2 following #1. Train #1 has passed the crossing and the signal will turn off. While train #1 is still on the lead-out rails, train #2 hits the lead-in on the other side of the crossing. Since the leaders are tied together, this scenario fails to trigger the crossing signal with train #2 on the lead-in. However, as train #2 enters the crossing, normal operation will occur (signal on, then off when the train leaves the crossing).

Crossing #1
  • Lead - A0
  • Crossing - D9
  • Output - D0

Crossing #2
  • Lead - A1
  • Crossing - D10
  • Output - D1

Crossing #3
  • Lead - A2
  • Crossing - D11
  • Output - D2

Crossing #4
  • Lead - A3
  • Crossing - D12
  • Output - D3

Crossing #5
  • Lead - A4
  • Crossing - D13
  • Output - D4

Group Outputs
  • Group 1 - D5
  • Group 2 - D6

int Crossing[6][7]; //[crossing number][attribute ID]
int GroupOutput[3]; 
int nDirtyTrackBuffer;

/*Once the crossing lights are triggered, ignoreLeaders is set to TRUE.  This will cause the lights to turn off as soon as
* the last car (with metal wheels) leaves the crossing, even if that car is still on the lead-out track, which 
* is the lead-in track for the other direction.
* Once there are no active triggers, ignoreLeaders is set to FALSE and the lead-in track will again be evaluated.
boolean ignoreLeader[6] = {false};
void setup() {

 *            DO NOT EDIT ANYTHING ABOVE THIS POINT               *
 *                     User settings below                        *
 /*define multi-track crossings
 * 0 = single, stand alone crossing (default)
 * 1 = Assign this crossing to group 1.
 * 2 = Assign this crossing to group 2.
Crossing[1][6] = 0;
Crossing[2][6] = 0;
Crossing[3][6] = 0;
Crossing[4][6] = 0;
Crossing[5][6] = 0;

/* Dirty Track Buffer 
 * The program will sample each track (leaders and crossing) up to this number of times.
 * If ANY of the readings are TRUE, the track will be considered triggered.
 * Allowable range: 50 - 5000
 * Default value = 1000
 * For super clean track, 50 may be used.  For absolutely filthy track that was 
 * last cleaned when Reagan was president, a value as large as 5000 may be needed.
 * NOTE: A higher value will slow down the response time of the crossing triggers.
nDirtyTrackBuffer = 1000;

 *                     END User settings                          *
 *            DO NOT EDIT ANYTHING BELOW THIS POINT               *

/* crossing states
 *  00 = no input, everything is off
 *  01 = lead activated, lights on (unless leading out from the crossing)
 *  10 = crossing activated, lights on
 *  11 = crossing and lead activated, lights on

//current crossing state  
Crossing[1][1] = 0;
Crossing[2][1] = 0;
Crossing[3][1] = 0;
Crossing[4][1] = 0;
Crossing[5][1] = 0;

//last crossing state
Crossing[1][2] = 0;
Crossing[2][2] = 0;
Crossing[3][2] = 0;
Crossing[4][2] = 0;
Crossing[5][2] = 0;

//crossing input pin (DIGITAL input)
Crossing[1][4] = 9;  
Crossing[2][4] = 10; 
Crossing[3][4] = 11;
Crossing[4][4] = 12; 
Crossing[5][4] = 13;

//lead input pin (ANALOG pins remapped to their digital counterparts)
Crossing[1][3] = 14;  //A0
Crossing[2][3] = 15;  //A1
Crossing[3][3] = 16;  //A2
Crossing[4][3] = 17;  //A3
Crossing[5][3] = 18;  //A4

//output pins (all digital)
Crossing[1][5] = 0;
Crossing[2][5] = 1;
Crossing[3][5] = 2;
Crossing[4][5] = 3; 
Crossing[5][5] = 4;

GroupOutput[1] = 5; 
GroupOutput[2] = 6; 

//D7 and D8 are unused

  for (int ii=9; ii<=18; ii++) {
    //set mode for digital input pins
      pinMode(ii, INPUT_PULLUP);

  for (int ii=0; ii<=6; ii++) {
      //set mode for digital output pins
      pinMode(ii, OUTPUT); 
      digitalOutput(ii, false);  //set all output pins low by default


void loop() {

  for (int ii=1; ii<=5; ii++) {
    //loop through all 5 crossings
    GetCurrentState(ii);    //read the tracks
    SetCrossingOutput(ii);  //set the individual crossing output
    for (int jj=1; jj<=2; jj++) {
      SetGroupOutput(jj);  //set the group crossing outputs

void GetCurrentState(int CrossingID) {

  boolean bLeadBuffer = false;  //nDirtyTrackBuffer number of lead track readings that will be ORed to determine if the track is triggered
  boolean bCrossingBuffer = false;  //same as bLeadBuffer, only for the track in the crossing

  Crossing[CrossingID][2] =  Crossing[CrossingID][1]; // set global last state 
  for (int ii=0; ii <= max(nDirtyTrackBuffer, 50); ii++) {
    //Look for input.  As soon as it's triggered, exit the loop
    bLeadBuffer = bLeadBuffer || !digitalInput(Crossing[CrossingID][3]);
    if (bLeadBuffer == true) {break;}
  for (int ii=0; ii <= max(nDirtyTrackBuffer, 50); ii++) {
    //Look for input.  As soon as it's triggered, exit the loop
    bCrossingBuffer = bCrossingBuffer || !digitalInput(Crossing[CrossingID][4]);
    if (bCrossingBuffer == true) {break;}    
  int CrossingInput = (bCrossingBuffer == true ? 10 : 0);
  int LeadInput = (bLeadBuffer == true ? 1 : 0);
  Crossing[CrossingID][1] = CrossingInput + LeadInput; // set current state 

void SetCrossingOutput (int CrossingID) {

  // if the current state is 00 (no triggers) 
  if (Crossing[CrossingID][1]  == 0) {
    digitalOutput(Crossing[CrossingID][5], false);
    ignoreLeader[CrossingID] = false;
  // lead and/or crossing are triggered
  if (  (Crossing[CrossingID][1] >= 10 || Crossing[CrossingID][1] == 1) && ignoreLeader[CrossingID] == false) {
    digitalOutput(Crossing[CrossingID][5], true);    
    ignoreLeader[CrossingID] = true;

  // the transition from crossing to lead-out
  if (Crossing[CrossingID][1]== 1 && Crossing[CrossingID][2] == 11) {
    digitalOutput(Crossing[CrossingID][5], false);    
  //if every other state is skipped AND the crossing is triggered (regardless of the leader)
  if (Crossing[CrossingID][1] >= 10) {
    digitalOutput(Crossing[CrossingID][5], true);    
    ignoreLeader[CrossingID] = true;


void SetGroupOutput (int GroupID) {

  boolean bGroup = false;
  for (int ii=1; ii<=5; ii++) {
    //look through each of the 5 crossings, but stop as soon as TRUE is found
    bGroup = bGroup || ( Crossing[ii][6] == GroupID && digitalInput(Crossing[ii][5]) ? true : false) ;
    if (bGroup){break;}  //break out of loop as soon as TRUE
  digitalOutput(GroupOutput[GroupID], bGroup); 

*             boolean IO functions                    *
boolean digitalInput(int nPin) {
  return (digitalRead(nPin) == HIGH ? true : false);

void digitalOutput(int nPin, boolean bState) {
  switch (bState) {
    case true :
      digitalWrite(nPin, HIGH);
    case false :
      digitalWrite(nPin, LOW);


2,887 Posts
I guess you need some indication of direction, so that a train exiting the cross crossing can be forgotten about and the train right behind it coming in can be used to trigger. ... maybe. so also then, a train coming in from either direction should trigger.
1 - 4 of 4 Posts
This is an older thread, you may not receive a response, and could be reviving an old thread. Please consider creating a new thread.