See Signals2_Class for RED-GREEN only signals.
Signals3_Class
Header File
<Signals3_Class.h>
Constructor
Signals3_Class(int red_led, int green_led)
Assumes amber _led generated with external gate
Signals3_Class(int red_led, int amber_led, int green_led)
Pins wired to red,amber and green leds.
Public Methods
begin(int pol)
Positive signals pol =1
Negative signals pol =0
signal_control(int dsp)
Argument dsp controls RED-AMBER-GREEN sequence.
Default time 5 seconds
signal_control(int dsp, int red_wait)
red_wait - changes wait time for RED
signal_control(int dsp, int red_wait,int amber_wait)
amber_wait - changes wait time for AMBER
Abstract/Summary: This HTML page/project develops a class/library Signals3_Class that controls a three aspect railway signal. The signals may be either active high or active low. The class constructor is overloaded to permit two or three arguments. The class generates a public method signal_control that controls signals in a RED-AMBER-GREEN sequence.
Possible Audience: Model railway hobbyists interested in electronically controlling signals and programmers interested in simple examples of classes and overloading in the C++ environment. Also software design of state machines.
Keywords: Three aspect signals, C++ Class, C++ overloading, state machine, Arduino UNO NANO
Required Components: Micro-controller (Project tested with Arduino NANO) plus LEDs and LDR for on bench testing.
Required Libraries: either LCD_Sensor_Class or IR_Sensor_Class
With model railways two common places where signals are used are:
1. At points/switches/turnouts to indicate if the train can proceed. See photo to left of full size train signals. Trains can proceed up the main track. Trains cannot turn left.
2. Along blocks of track to indicate if the block is occupied. The signal will go RED immediately a train enters a block telling following trains not to enter. It will return first to AMBER and then to GREEN sometime after the train has passed. In a full size railway there will be a sensor up the track that is used to set the signal back to AMBER and then GREEN
This HTML page/project develops a C++ class for a RED-AMBER-GREEN ( 3 aspect) signal. The constructor has two options:
1. Two arguments that specifies where the RED and GREEN signals (LEDs) are wired. This design assumes that the AMBER signal is generated externally from the RED and GREEN signals using NOR/NAND gates.
2. Three arguments that specify where the RED, AMBER and GREEN signals are wired. Effectively the external logic of option 1 is now generated in software.
Since model railway signals maybe activated by pulling them high (pins 3,4 and 5) or pulling them low (pins 10, 11 and 12) this option will be determined by the parameter in the begin( ) method.
There will also be an option for changing the delays in the RED-AMBER-GREEN sequence.
The program will be developed using the Arduino IDE.
In the IDE create 3 files:
1. file-->new--> Signals3_Class. The Arduino will give this file the extension "*.ino"
2. In the editor use inverted triangle near the top right and select new_tab and give it the label Signals3_Class.cpp. Note the extension.
3. Repeat using inverted triangle select new_tab and give it the label Signals3_Class.h. Note the extension.
The *.cpp and *.h files must have the same label.
I have chosen to give the *.ino file along with this page the same name to assist with the documentation.
All three files will be compiled/verified together.
The next step is to populate the three files.
As a starter the skeleton header file is generated as shown below.
The header file has the following features:
1. The first and last lines #ifndef Signals3_Class_h ......#endif ensures that the header file is only included once in a project.
2. #define Signals3_Class_h If Signals3_Class_h was not previously defined it is now defined and the included code is implemented.
3. #include "Arduino.h" This includes the Arduino header file. It is implicitly included in *.ino files but not library routines. It will be needed for pin definitions.
4. class Signals3_Class { defines a class Signals3_Class. This class will have two regions
public : any method listed in the public area can be invoked by extermal code. eg Signals3_Class.ino can call the begin( ); method
private: all variables in the private area can only be accessed by the Signals3_Class.cpp code only : All other files including Signals3_Class.ino cannot
The class has two constructors both with the same name/label but a different number of parameters/arguments. At compile/verify time the compiler will pick the correct one to use. This is known as overloading and illustrates C++ polymorphism. That is Signals3_Class can have more than one form.
In this example the constructor with two arguments is for a design that uses an external gate to generate the amber signal. With three arguments the logic is entirely generated by the micro-controller.
For more information on C++ classes/libraries see either the LDR_Sensor_Class or IR_Sensor_Class pages. For more information on overloading see LDR_Overload_Class.
The first pass at the implementation or *.cpp code is shown below:
The features of the *.cpp file at this point are:
1. #include "Signals3_Class.h" that includes the header file of the previous section.
2. Two constructors of the form: Signals3_Class:: Signals3_Class ( ) { };
3. The begin ( ) method. void Signals3_Class::begin( ){} The :: is the scope resolution operator that says begin( ) is associated with the class Signals3_Class.
In the constructors three private variables, _red_led and _green_led declared in the header file are assigned to the constructor arguments. By convention these start with an underscore. Being private they may only be accessed from inside the library.
The other private variable _external_gate is a flag used later in the program to flag if there is an external gate. Assuming there is _external_gate is set in the header file. Since there is no external gate with the three argument version _external_gate is cleared as part of the constructor implementation.
The first pass at the application code is shown below. This is the code that will test the class/library Signals3_Class
#include "Signals3_Class.h"Signals3_Class signal(5,4,3);void setup() { // put your setup code here, to run once:The program creates an instance/object of the Signals3_Class. The object signal. has three arguments where the RED signal is attached to pin 5, the AMBER to pin 4 and the GREEN to pin 3. (For testing two arguments the '4' can be omitted from the argument list).
At this point the program should verify/compile
To drive the signals the micro-controller pins will need to be set as outputs. This will be part of the begin( ) method. The begin will be invoked once as part of the setup() routine.
void setup() { signal.begin( );The signal.begin( ) says there is a begin( ) method associated with the object signal.
While there are two constructors there will only ever be one "begin( )" method. The "begin( )" method must handle both forms of constructor.
In the above code the first four lines of the method will initialise the _red_led and _green_led pins as an OUTPUT and set them it HIGH.
The last two lines are conditional and only apply for the instance where the constructor has three arguments and will set the _amber_led as a HIGH OUTPUT.
The begin( ) as given assumed positive polarity signals. If you are testing with negative polarity set the output LOW. See next section.
The next section will handle both polarities as part of the code.
To allow for both positive and negative signals rather than add an additional argument to the constructor the begin( ) method will be overloaded. An argument/parameter will be added to define the signal polarity. The new header code will become:
...Note a new private variable _pol is declared and initialised to 1 for positive logic. The new begin(int pol) method will over-writte _pol. See additional implementation code below:
The design should be tested using various combinations of the begin method. For negative signals the lights will be turn on with a LOW signal.
The output signal sequence is defined by the following state sequence:
1. Initially the signals will be in the state "wait4train". The signals will be GREEN
2. When a train is detected the design will move to the state "wait4end". The signals will go RED
3. After the train passes the design will move to the "wait4red" The signals will remain RED
4. After a time delay the design moves to the "wait4amber" state. The signals are now AMBER
5. After the time delay the design returns to the initial "wait4train" state where the signals are GREEN
Note in a practical design the design should include paths back to the "wait4end" state if a second train is detected while the first train is in either the "wait4red" or "wait4amber" states.
This state machine will be implemented with the function/method signal_control( ).
Without any tests the method signal_control( ) should move through 4 states: GREEN, RED, RED, AMBER
This can be implemented with the following code:
void Signals3_Class::signal_control(int train_status){ switch(signal_state) { case wait4train: digitalWrite(_red_led,0); digitalWrite(_amber_led,0); digitalWrite(_green_led,1); signal_state = wait4end; break; case wait4end: digitalWrite(_red_led,1); digitalWrite(_amber_led,0); digitalWrite(_green_led,0); signal_state = wait4red; break; case wait4red: digitalWrite(_red_led,1); digitalWrite(_amber_led,0); digitalWrite(_green_led,0); signal_state = wait4amber;The header file will need to be updated to include the public method signal_control ( ) and the private enum type state. ie
public: ....... void signal_control(int dsp); //control lights private: ....... enum state {wait4train, wait4end, wait4red, wait4amber} signal_state =wait4train;A variable signal_state is created and initialised to the state "wait4train"
To verify the operation signal_control( ) needs to be called from the main code.
void loop() { signal.signal_control(1); delay(1000); }Since at this point the signal_control( ) contains no tests its argument doesn't matter.
The delay (1000) makes the lights visible. They should go through the sequence GREEN, RED, RED, AMBER and then repeat.
The next test is to add some tests to the signal_control( ) method.
The above design for signal_control(int _dsp) will become quite messy when expanded to handle the 8 combinations of signals. That is:
1. Negative and positive signals. These are defined by the private variable "_pol" that was set by the begin( ) method.
2. The argument "_dsp" that determines if the light signal is "on" or "off"
3. The variable "external_gate" that defines if the amber light is generated by an external gate or by internal software (as in this example).
A better solution is to create a (private) method that handles the signal combinations:
//header file class Signals3_Class { public: ............ private: ........ int _external_gate = 1; int _pol = 1; //set for positive enum state {wait4train, wait4end, wait4red, wait4amber} signal_state =wait4train; void drive_signals(int red, int amber, int green); }; //implementation filevoid Signals3_Class::drive_signals(int red,int amber,int green){ if (!_pol) { red ^= 1; amber ^= 1; green ^=1;} digitalWrite(_red_led,red); digitalWrite(_green_led,green); if (!_external_gate) digitalWrite(_amber_led,amber); }For each state the 3 "digitalWrite( )'s" are replaced with a call to the drive_signals( ) method that handles all the combinations.
As given the code does not include any tests. It just moves through the 4 states: GREEN-RED-RED-AMBER each time signal_control( ) is invoked. The tests will be developed in the following sections.
The argument train_status is monitored in the two states wait4train and wait4end. These tests are now added to the code:
void Signals3_Class::signal_control(int train_status){ switch(signal_state) { case wait4train: drive_signals(0,0,1); if (train_status ==1) signal_state = wait4end; break; case wait4end: drive_signals(1,0,0); if (train_status == 0) signal_state = wait4red; break; case wait4red: drive_signals(1,0,0); signal_state = wait4amber; if (train_status ==1) {signal_state = wait4end;} break; case wait4amber: drive_signals(0,1,0); signal_state = wait4train; if (train_status ==1) {signal_state = wait4end;} break; default: signal_state = wait4train; } };Also added to the states wait4red and wait4amber is code that resets the RED-AMBER sequence if ever a second train is detected before the first has left.
To test this "new" code the loop in the main code was modified.
void loop() { for (int i = 0; i<3 ; i++) { signal.signal_control(1); delay(1000); } for (int i = 0; i<7 ; i++) { signal.signal_control(0); delay(1000); }}With the numbers used in the loops the RED should be active for 3 seconds and the GREEN 7-1 seconds - the other one second is due to the transient AMBER state.
Once the train has passed the signals will remain RED for a short interval, move to AMBER also for a short time before moving to GREEN. These states are defined as wait4red and wait4amber. In both of these states the elapsed time must be checked.
Possible additional code will be:
Generating the delay uses the standard library function millis( ) that reads the current micro-controller time. Prior to each loop the time is stored in the variable start_time and the elapsed time is compared with two preset variables delay4red and delay4amber. These variables are all defined in the header file.
private: ....long start_time;long delay4red = 5000; //RED remains on for 5 seconds long delay4amber = 5000; //AMBER on for 5 secondsThe time of the OFF loop in the main code was changed from 7 to 20 and the program operation verified - RED was active for 3+5 = 8 seconds, AMBER for 5 seconds and GREEN for 20 - 5 -5 = 10 seconds.
In a practical situation the Signals3_Class will receive its input from an external sensor. In the example below the LDR is used. An instance, sensor of the LDR_Sensor_Class is declared. sensor is attached to pin A0.
If LDR_Sensor_Class is not in your contributed libraries folder this can be created as per project LDR_Sensor_Class.
*To add LDR_Sensor_Class to the project use Sketch-->Include Library and select LDR_Sensor_Class from contributed libraries
The main code becomes:
Notes:
1. The IR_Sensor_Class may also be used.
2. The LDR_Sensor_Class ( and the IR_Sensor_Class) provide an output train_over_sensor( )
3. The sensor class contains a one second filter causing train_over_sensor( ) to go inactive after a one second delay
4.The argument for signal_control( )is now sensor.train_over_sensor( )
5. This code does not require any changes to the Signals3_Class.
6. The example assigns sensor to pin A0. In the Arduino A0 is a digital pin but because it has analog capabilities Arduino have give it the label A0. The example is using the digital features.
Testing using the LDR the design went RED immediately the LDR was covered (train present), it went to AMBER 6 seconds after the LDR was uncovered and then returned to GREEN 5 seconds later.
As designed there is a 5 second delay before the signals go to AMBER and then a further 5 seconds delay before they return GREEN. These times may be changed by overloading the signal_control( ) method with a second argument and third argument- the wait time in seconds.
To implement the overloaded signal_control method the additional code in the Signals3_Class.cpp implementation file to modify the times delay4red and delay4amber from their initial values of 5 seconds is as shown:
void Signals3_Class::signal_control(int dsp, int red_wait){ delay4red = red_wait*1000; signal_control(dsp); }void Signals3_Class::signal_control(int dsp, int red_wait, int amber_wait){ delay4amber = amber_wait * 1000; signal_control(dsp,red_wait);} ;To test the modified class the following code was added to the main program loop.
void loop() { // signal1.signal_control(sensor.train_over_sensor( )); //follows LDR with 1 second delay signal1.signal_control(sensor.train_over_sensor( ),1,8);}When the LDR was covered by a train the signal went RED. When the train passed the signal turned amber after 2 seconds (1 second due to filter in sensor +1 second set in the signal_control( ) method.) and then GREEN a further 8 seconds later.
This project has used the LDR_Sensor_Class::train_over_sensor( ) method (IR_Sensor_Class could also be used) as input to the Train_Signals3_Class. Possible combinations are:
Train_Signals3_Class( )
Two arguments: RED and GREEN pins. AMBER implemented using an external gate:
Three arguments: RED, AMBER and GREEN pins
begin( )
No argument assumes positive signals
Argument = 0 negative signals
Argument = 1 positive signals
signal_control( )
One argument: 1 RED active, 0 goes through RED-AMBER_GREEN sequence
Two arguments: Second argument: Change delay before RED turns AMBER
Three arguments: Third argument: Change delay before AMBER turns GREEN
This project was for a three aspect signal (RED-AMBER-GREEN). A previous project Signals2_Class looked at two aspect signals. (RED-GREEN)