Multiple Turnouts and Other Multitasking

Arduino Uno R3

Arduino Uno R3

The microcontroller at the heart of your Arduino contains a single processor that can execute one instruction at a time.  But it can do that task over and over very quickly — around 16 million times per second on an UNO. At that speed, the microcontroller can handle a lot of different tasks, more or less simultaneously.

That said, the processor can only do what you tell it to do, one instruction at a time; it is up to the programmer to structure the code in a way that effectively divvies up processor time between different tasks.

Avoid Delay()

The enemy of multitasking is the delay() function, which is frequently used to time actions. The problem with delay() is that it stops the program flow cold; nothing can happen while thousands of cycles are frittered away. In Arduino parlance, delay() is a blocking function.

The classic Arduino example for timing actions without using delay() is the Blink Without Delay sketch, where the main loop checks the elapsed time and, if a wait period has passed, turns the LED on or off creating a blinking LED. In between changing the state of the LED, the loop runs and could do other things.

Looping for Multiple Tasks

Checking the elapsed time at the start of every iteration of your main loop(), then taking actions based on time, is at the heart of simple multitasking. To make it work effectively, you have to split up tasks that take time to complete, handle complex actions that consume multiple cycles as efficiently as possible and prioritize the tasks so they interact in the ways you want.

For example, the test loop UNO has four task groups it has to handle simultaneously:

  1. Block Occupancy Detection — all five sensors have to be checked on each cycle and the block state updated as necessary.
  2. Turnout Management — move the turnout when commanded over several seconds for slow turnout motion.
  3. Signal Management — maintain and change signal states based on turnout position and block occupancy, or in response to a command override.
  4. Communications — listening for messages, processing and executing commands and transmitting data to other devices.

That is quite a bit to do, but well within the capabilities of the UNO. Each of these task groups gets its turn during each iteration of the main loop.

To get a sense of how that works, lets focus in on one core task group: turnout management. Running multiple slow motion turnouts encompasses all the techniques needed for simple Arduino multitasking.

Slow Motion Turnouts

Complete servo installation.

Demo servo installation.

If you’ve experimented with using servo motors as turnout motors, or read Turnout Control with Arduino and Servos, then you know that slowing the motion down requires moving one degree at a time, with a time delay between moves. Using delay() to time the motion, as in the basic example, renders the microcontroller unable to do anything else while the turnout is moving. Further, with this method only one turnout can be moved at any one time, which would be a problem with multiple trains, turnouts and operators.

Using our multitasking model to handle turnout motion requires keeping track of the turnout’s current position, whether of not it is currently moving, and its destination if in motion.  With that data we can effectively divide its motion into smaller increments, executing the next move whenever a delay period has passed. Scaling to multiple turnouts is primarily a matter of organizing your turnout data appropriately.

Organizing Turnout Data

I start with a data structure to represent a turnout:

typedef struct TURNOUT_DEF {
  int pin;
  int pos_main;
  int pos_div;
};
typedef struct TURNOUT_DATA {
  TURNOUT_DEF data;
  bool is_moving;
  byte alignment;
  int pos_now;
  int target_pos;
  unsigned long last_move;
};

The test loop has a single turnout, but I declare the data for the turnout in the form of an array of TURNOUT_DATA structures just as I would with a multi-turnout layout. This way, a single variable turnouts represents all turnouts the sketch has to manage:

TURNOUT_DATA turnouts[NUM_TURNOUTS] = {
  {{8, 93, 117}, false, ALIGN_MAIN, 93, 93, 0}
};

Methods

The methods for using the array would be the same no matter how many turnouts there are. For example, in setup, a for loop is used to step through the turnout array and initialize the servos ( variable servos is an array of pointers to servo objects, declared in this way: servo *servos[NUM_TURNOUTS]. The servo pointer could be included within the TURNOUT_DEF structure, but here I chose to keep the servo pointers in a separate array, index synchronized with turnouts.):

// initialize turnouts
  for(i = 0; i < NUM_TURNOUTS; i++){
    // create a SERVO instance and store it in the servos array
    servos[i] = new Servo; 
    servos[i]->attach(turnouts[i].data.pin);
    setTurnout(i, ALIGN_MAIN);  // set initial turnout position
  }

The setTurnout() function does exactly what you expect — sets a turnout position — but it does not move the turnout. Instead, it initiates motion by setting turnout data elements: assigning a destination to the target_pos element, setting the is_moving element to true and setting the last_move element to zero. ALIGN_MAIN and ALIGN_DIVERGENT are arbitrary integer macro/constants used to represent those alignment states.

void setTurnout(int id, int align){
   switch(align){
        case ALIGN_MAIN:
          turnouts[id].is_moving = true;
          turnouts[id].last_move = 0;
          turnouts[id].target_pos = turnout[id].data.pos_main;
          turnouts[id].alignment = ALIGN_MAIN;
          break;
        case ALIGN_DIVERGENT:
          turnouts[id].is_moving = true;
          turnouts[id].last_move = 0;
          turnouts[id].target_pos = turnout[id].data.pos_div;
          turnouts[id].alignment = ALIGN_DIVERGENT;
          break;
      }
}

The command and communications process elsewhere in the test loop sketch uses setTurnout() to trigger turnout moves when it receives appropriate commands.

Moving Along

Actual turnout motion is handled within the main loop, beginning with a call to millis() at the beginning of the loop to get current elapsed time in milliseconds. Then the code steps through the turnouts array, checking the is_moving element to determine if a turnout is in motion. If is_moving is true, the code then checks to see if enough time has elapsed since the last move — if so, another move is made and the time is recorded.

Movement timing is controlled by by the macro/constant STEP_DELAY, which is the number of milliseconds (200 on the test loop) to delay between moves. If you wanted to customize the delay for different turnouts, add a member to the data structure to hold the delay period for each turnout, and use that data instead of STEP_DELAY. Every turnout or other animated object can have its own unique delay period.

  // get elapsed milliseconds at loop start
  unsigned long currentMillis = millis();

  // Turnout Control
  for(int i = 0; i < NUM_TURNOUTS; i++){
    if (turnouts[i].is_moving) {
      if ( (currentMillis - turnouts[i].last_move) >= STEP_DELAY ) {
        turnouts[i].last_move = currentMillis;
        // if the new angle is higher
        // and not already at destination
        if (turnouts[i].pos_now < turnouts[i].target_pos) { 
          // increment and write the new position
          servos[i]->write(++turnouts[i].pos_now);
        } else {  
           // otherwise the new angle is equal or lower
           // if not already at destination
          if (turnouts[i].pos_now != turnouts[i].target_pos) { 
            // decrement and write the new position
            servos[i]->write(--turnouts[i].pos_now);
          }
        }
      }
      // if target position is reached, turn motion off
      if (turnouts[i].pos_now == turnouts[i].target_pos) {
        turnouts[i].is_moving = false;
      }
    }
  }

Using this technique, turnouts start and stop moving asynchronously and can be independently controlled.

Station and Signal 5

Extending the Technique

Any number of animated objects could be handled in this fashion. The key is to represent your animated object with data, then use the data to trigger and control motion, moving incrementally with a delay. Start each iteration of the main loop by getting the current elapsed time with millis(), then use the time to determine when incremental actions should happen.

Since the Arduino IDE is a C++ platform, you could write the same code as a C++ object encapsulating the data for each turnout,  and the methods used, in turnout objects. This kind of problem lends itself well to an Object Oriented approach.

OOP would be semantically elegant, but not necessarily more efficient. This is a matter of personal preference.  I love OOP programming, but find the efficiency of straight C coding compelling for many Arduino tasks.  Plus, I think, straight C is easier for novices to read and understand, especially if they’ve been exposed to any other structured procedural language (javascript, for example). So the code here is all C; but OOP would work fine.