Basic Signaling for a Small Layout

Continuing with the theme of controlling a small layout with an UNO, I thought I’d accept my own challenge from the last post and talk about how one might implement signals on a small layout as I did on the Test Loop.

Signals on the Test Loop

While I was actively testing block occupancy detection on the test loop, I set up three sets of signals as part of that effort. I wanted to both test some off-the-shelf signals from Tomar and take a crack at building my own searchlight signals using BLMA unlit signal heads. The former turned out to work well, but because they are wired for common anode, they sent me on a quest to tame common anode wiring. The latter also came out well, once I learned how to reliably solder magnet wire to SMD micro LEDS!

Block and Signal layout on the Test Loop

Block and Signal layout on the Test Loop

I did this primarily to see the block detection system working (block detection is also shown on on my programmable control panel, but that is another story) as a train moves around the track.  You can see it in action in this video—note that only the locomotive is detectable by the block occupancy detection system; the rolling stock is not set up for detection.

Since the test loop is just an oval with a single turnout and siding, the system is fairly simple.  As you watch the train go around the oval you will see signals change state as the train moves in and out of blocks, and as the turnout changes state.  The logic is imperfect in a few cases but good enough to show the various parts of the system working as a whole under the control of an UNO, which was the point of the exercise.

A Framework for ABS

Automatic Block Signalling (ABS) is straight forward and prototypical for late nineteenth / early twentieth century railroads. ABS signals are autonomous and react to block occupancy and turnout status; the red (next block obstructed), yellow (a subsequent block is obstructed) and green (no obstruction) indicators are near universal. Typical implementations handle blocks in groups of three or four, depending on how far ahead the system sensors extend.

Because each signal is an autonomous object that responds to specific environmental conditions, ABS lends itself well to a data-oriented approach. As with turnouts in the previous post, the best starting point is to devise a data structure that will encapsulate and represent everything that needs to be known about each signal in your system. Here is what I came up with for the test loop (see this post for an explanation of my Duino nodes, and this post for the addressing system in use).

typedef struct SIGNAL_DEF {
  byte type; // bit mask indicating available signal aspects
             // bit 1 = Red; bit 2 = Green; bit 3 = Yellow)
             // 1=R; 2=G; 3=RG; 4=Y; 5=RY; 6=GY; 7=RGY
             // 3 and 7 are the two types on the test loop
  nodeAddress addr; // base address of the Duino node for this signal
  byte R_ID; // pin/bit id for red indication
  byte G_ID; // pin/bit id for green indication
  byte Y_ID; // pin/bit id for yellow indication
  
  // data elements for running the signal
  byte state; // current signal state
  T_ALIGN *turnouts; // these turnouts must be aligned as defined to get SIGNAL_GREEN
  byte numTurnouts;
  byte *following; // additional blocks ahead (depends on direction signal faces)
  // watched for occupancy resulting in SIGNAL_YELLOW caution   
  byte numFollowing; 
};

By now you should recognize that this is my preferred approach to dealing with complex, interactive objects in the system.  As always, I define a compound data structure (the structure contains other structures as elements, in this case the T_ALIGN and NODEADDRESS types)  to collect all relevant data for each signal. Feel free to reinvent any of this — the point is to collect all necessary data in one place for each signal.

“T_ALIGN *turnouts”  is an example of pointer notation which allows for an array of zero (empty array) or more of the respective types; the “numTurnouts” element indicates how many items the T_ALIGN array contains. The “byte *following” and “numFollowing” do the same thing for a list of subsequent block IDs that are watched for occupancy.

Here is the declaration of a signals array from the Test Loop encapsulating all the signals in use, using the data types discussed. Notice how structures and arrays within the SIGNAL_DEF structure are defined inside their own curly braces:

// Signals definitions and data
SIGNAL_DEF signals[NUM_SIGNALS] = {
  {3, {3, 0}, 0, 1, -1, SIGNAL_OFF,{0, ALIGN_DIVERGENT}, 1,{2}, 1 },
  {3, {3, 0}, 2, 3, -1, SIGNAL_OFF,{0, ALIGN_MAIN}, 1, {2}, 1 },
  {7, {1, 0}, 0, 1,  2, SIGNAL_OFF,{}, 0, {0}, 1},
  {7, {0, 0}, 0, 1,  2, SIGNAL_OFF,{0, ALIGN_DIVERGENT}, 1, {}, 0 },
  {3, {0, 0}, 3, 4, -1, SIGNAL_OFF,{0, ALIGN_MAIN}, 1,{2}, 1}
};

Signal logic is handled by one function that gets called at the end of each loop cycle on the UNO, after block occupancy has been established.

void refreshSignals() {
  // First pass, set Stop (RED) state; default is GREEN
  // from block occupancy or turnout states
  for(int i = 0; i < NUM_SIGNALS; i++){ // for each signal
    // default state
    int state = SIGNAL_GREEN;
    SIGNAL_DEF sig = signals[i];
    for(int j = 0; j < max(sig.numTurnouts, sig.numBlocks); j++){ 
      if(j < sig.numTurnouts){
        // if the turnout is in motion OR 
        // if turnout alignment does not equal the required alignment
        if(turnout[sig.turnouts[j].id].is_moving || 
            turnout[sig.turnouts[j].id].alignment != sig.turnouts[j].align){
         state = SIGNAL_RED;
        } 
      }
      if(j < sig.numBlocks){ // for each linked block in the SIGNAL_DEF
        if(blocks[sig.blocks[j]].occ){ // if occupied
          state = SIGNAL_RED;
        }
      }
    }
    setSignalBits(i, state);
  }
  
  // Second pass to set caution states on
  // signals that support it and are currently set to GREEN
  
  for(int i = 0; i < NUM_SIGNALS; i++){ // for each signal
    SIGNAL_DEF sig = signals[i];
    if(bitRead(sig.type, 2)){ // if the signal supports the caution state
      if(sig.numFollowing > 0 && sig.state == SIGNAL_GREEN){
        // check occupancy of following block(s) if any
        for(int j = 0; j < sig.numFollowing; j++){
          if(blocks[sig.following[j]].occ){
            setSignalBits(i, SIGNAL_YELLOW);
          }
        }
      }
    }
  }
  // Refresh the nodes to show signals in their updated state
  nodeRefresh();
}

void setSignalBits(int signalID, byte signalState) {
  SIGNAL_DEF sig = signals[signalID];
  if (sig.state != signalState) {
    signals[signalID].state = signalState;
    byte nodeBits = nodeGet(sig.addr);
    switch (signalState) {
      case SIGNAL_OFF:
        if(bitRead(sig.type, 0)) bitWrite(nodeBits, sig.R_ID, LOW);
        if(bitRead(sig.type, 1)) bitWrite(nodeBits, sig.G_ID, LOW);
        if(bitRead(sig.type, 2)) bitWrite(nodeBits, sig.Y_ID, LOW);
        break;
      case SIGNAL_RED:
        if(bitRead(sig.type, 0)) bitWrite(nodeBits, sig.R_ID, HIGH);
        if(bitRead(sig.type, 1)) bitWrite(nodeBits, sig.G_ID, LOW);
        if(bitRead(sig.type, 2)) bitWrite(nodeBits, sig.Y_ID, LOW);
        break;
      case SIGNAL_GREEN:
        if(bitRead(sig.type, 0)) bitWrite(nodeBits, sig.R_ID, LOW);
        if(bitRead(sig.type, 1)) bitWrite(nodeBits, sig.G_ID, HIGH);
        if(bitRead(sig.type, 2)) bitWrite(nodeBits, sig.Y_ID, LOW);
        break;
      case SIGNAL_YELLOW:
        if(bitRead(sig.type, 0)) bitWrite(nodeBits, sig.R_ID, LOW);
        if(bitRead(sig.type, 1)) bitWrite(nodeBits, sig.G_ID, LOW);
        if(bitRead(sig.type, 2)) bitWrite(nodeBits, sig.Y_ID, HIGH);
        break;
    }
    nodeSet(sig.addr, nodeBits);
  }
  return;
}

void setSignal(int signalID, byte signalState) {
  setSignalBits(signalID, signalState);
  nodeRefresh();
}

For more in-depth discussion of my Duino Node devices, node functions and how they are used,  see Adding Signals to the Test Loop and Adding Signals to the Test Loop Part 2.

The idea here is that the logic of the signal system is executed in the refreshSignals() function. That function, in turn, calls setSignalBits() to interface with the hardware, using the hardware specific Duino Node functions to  drive the hardware.

How to Integrate Signals into Your Small Layout

Adding signals to your small layout consists of two basic steps: 1) setup your signal hardware so that it can be turned on and off in some way; either by direct connection to your UNO or using a shift register chain along similar lines to what I do with Duino Nodes. The choice of common anode vs. common cathode wiring is yours to make, but will depend on how your signal gear is wired. 2) Integrate signal handling in the sketch using turnout state and (if you have it) block occupancy data. Call your signal logic function at the end of each iteration of the main loop, and let that function interact with the signal hardware. Your signal logic should be in one place, and should be written as an “abstraction” that doesn’t know how to change signal display, but relies on other hardware specific functions to do that job.

Tomar Signals

Tomar N Scale Signals

I consider signals to be the most basic form of animation you can add to your layout to bring it to life. Its a little bit of trouble, but I hope you can see its really not hard.  The advantage of the Arduino approach over a hardware/hardwired approach (eg, connecting signals to the outputs of a stand alone block occupancy device) is the flexibility you gain in implementing signals while keeping wiring to an absolute minimum. Adding Absolute Permissive Block signalling is just as matter of additional logic to the sketch for the stretch of track you are trying to protect. Even full CTC functionality can be readily supported by responding to messages from a CTC control panel or system.

You’ll be amazed that how much work a single UNO can actually do for you.

Leave a Reply

Your email address will not be published.