Fun with Arduino 17 Railway Crossing State Transition Diagram, switch() case

Crossing_RealIt’s time for something new. The plan is to build an automatic model railway crossing. We’ll have some new challenges with this project: train detection, a gate that closes and opens with a servo and a fun way to tie it all together: state transitions. And o yes, there are blinking LEDs too … we already know how to do that!

STD_Crossing_smallThe diagram on the left is a so called State Transition Diagram. It shows what states a system can be in and what events make it transition from one state to another. For our railway crossing this is:

  • When a train triggers sensor1, start blinking
  • After a certain time, close the gate
  • When the train triggers sensor 2, open the gate
  • When the gate is open, stop blinking

Read on below the video …

 

 

To translate a State Transition Diagram into code, every transition becomes an if() {…} statement. With our railroad crossing we have 4 of these transitions. However … we don’t want to test all the if() statements all the time … we only want to test the one that belongs to the current state we are in. There is a very handy structure that we can use for this: switch() + cases.

This is an example of a switch() with two cases. Only one of the cases is executed, depending on the value of variable. If variable is not 1 or 2, then nothing happens.

switch(variable) {
  case 1:
    // code to run if variable = 1 goes here
  break;
  case 2:
    // code to run if variable = 2 goes here
  break;
}

STD-Blink_smallTo see this work in practice, let’s make code for a simple State Transition Diagram based on the well known LED blink, with two events added: we want to start blinking when button1 is pressed, and to stop blinking when button2 is pressed. This is specified with the State Transition Diagram on the left.

We’ll first write the blink code as we did before, with a millis() timer. To already prepare for the railway crossing, we use two LEDs that are each other’s inverse.

#define BLINK_SPEED 400 // [ms] smaller number is faster blinking
#define LED1_PIN 6
#define LED2_PIN 7

byte led1, led2;
unsigned long time_to_blink;

void setup() {
  pinMode(LED1_PIN, OUTPUT);
  pinMode(LED2_PIN, OUTPUT);
}

void loop() {
  if(millis() > time_to_blink) {
    time_to_blink = millis() + (unsigned long)BLINK_SPEED;
    led1 = !led1;
    led2 = !led1;
  }
  digitalWrite(LED1_PIN, led1);
  digitalWrite(LED2_PIN, led2);
}

Let’s test this … yes … both LEDs blink turn by turn, great.

To create the code according to the State Transition Diagram, we create two switch() structures: one contains the two states, the other contains the two transitions. The numbers correspond to the diagram. I already included the if() statements in the states.

switch (state) {
  case 1: // idle
    // code for state 1, runs every loop() 
    if(digitalRead(BUTTON1_PIN) == LOW) transition = 12;
  break;

  case 2: // blinking
    // code for state 2, runs every loop() 
    if(digitalRead(BUTTON2_PIN) == LOW) transition = 21;
  break;
}

switch (transition) {
  case 12:
    // code for transition 1 to 2, runs once
    transition = 0; // we reset the variable 'transition'
    state = 2;      // such that this transition code only runs once
  break;

  case 21:
    // code for transition 2 to 1, runs once
    transition = 0;
    state = 1;
  break;
}

Right now it’s just the framework … there’s no code in the transitions yet. Let’s analyze what happens here. We start in state 1 … all we do is test for button 1. If it is detected we jump to transition 12. The (still to be added) code there is executed just once and we jump to state 2. In state 2, all we do is test for button 2. If it is detected we jump to transition 21, execute the code that is in there just once and jump back to state 1.

You might wonder: “Why use the switch(transition), while we could as well write the code that’s in them inside the if() statements in the states?”. The reason is: better oversight. With more complex State Transition Diagrams states can have multiple transitions coming out of them. We’d have multiple if() statements in such state … it can quickly become less easy to follow which if() belongs to which transition. With the switch(transition) all transitions are neatly numbered and easy to find in the code. The little overhead quickly pays off.

OK, our framework is ready. But there are no actions in it yet, so let’s add the code that performs the actions we want to take place. Code that we place in the states runs every loop(). Code that we write in the transitions runs only once.  We also add the #defines and the setup() from the previous blink code … and … done. I changed the names of button1 and 2 into sensor1 and 2, already preparing for when we are going to use the train detection sensors later.

#define BLINK_SPEED 400 // [ms] smaller number is faster blinking
#define LED1_PIN      6
#define LED2_PIN      7
#define SENSOR1_PIN   8
#define SENSOR2_PIN   9

byte led1, led2, blink_enabled;
byte state = 1, transition;
unsigned long time_to_blink;

void setup() {
  pinMode(SENSOR1_PIN, INPUT_PULLUP);
  pinMode(SENSOR2_PIN, INPUT_PULLUP);
  pinMode(LED1_PIN, OUTPUT);
  pinMode(LED2_PIN, OUTPUT);
  Serial.begin(9600);
  Serial.println("Blink ready, waiting for button 1");
}

void loop() {
  switch (state) {
    case 1: // idle
      if(digitalRead(SENSOR1_PIN) == LOW) transition = 12;
    break;

    case 2: // blinking
      if(digitalRead(SENSOR2_PIN) == LOW) transition = 21;
    break;
  }
  switch (transition) {
    case 12:
      Serial.print("Button 1 - blinking started");
      blink_enabled = 1;    
      transition = 0;
      state = 2;
    break;

    case 21:
      Serial.print("Button 2 - blinking stopped");
      blink_enabled = 0;
      time_to_blink = 0;
      led1 = 0;
      led2 = 0;
      transition = 0;
      state = 1;
    break;
  }

  if(blink_enabled == 1) {
    if(millis() > time_to_blink) {
      time_to_blink = millis() + (unsigned long)BLINK_SPEED;
      led1 = !led1;
      led2 = !led1;
    }
  }
  digitalWrite(LED1_PIN, led1);
  digitalWrite(LED2_PIN, led2);
}

Testing 123 … yes that works fine. Some print statements are added in the transitions to easily see if it all works as expected.

No matter how large a State Transition Diagram is, to write code for it always stays straightforward … just methodically follow these 5 steps:

  1. create a switch(state) with as many cases as you have states
  2. create a switch(transition) with as many cases as you have transitions
  3. add an if() test for every event in a state and point to a transition
  4. add code to be executed in the states, this runs every loop()
  5. add code to be executed in the transitions, this runs only once

With this way of working a project is split up in manageable parts. With every state or transition we can focus on just that one part, write the code and test it, step by step.

Servo_SG90We will build the code for the State Transition Diagram of the railway crossing a couple of videos down the line. First we have something else to play with … in the next video we are going to operate the gate with a servo motor. If you like to follow along, you may want to get yourself one of these.

— 0 —

8 thoughts on “Fun with Arduino 17 Railway Crossing State Transition Diagram, switch() case

  1. CAN’T WAIT for your next tutorial.
    I’m actually working on a similar project which involves 3 IR Sensors (installed at the ends of a turnout) and a MP3 audio player (for Clanging Bells).
    Parts included are: Arduino Nano, LED’s (4), IR Detectors(3), Servo Motors(2), DFPlayer Mini, PAM8403 Mini Amplifier and a (4 Ohm) 3W Speaker.

    Like

    • I don’t understand the wuestion … there is no external switch involved in this code, only two push buttons. But always, if there are switches or push buttons involved, the are connected to an input pin as stated in the #define, and they switch to GND.

      Like

Leave a comment