This page concerns developing a project that will control the points on a model railway. The area is known as the Coal Basin.
Abstract This project inherits a number of Cpp Libraries, Coal_Basin_Buttons, Coal_Basin_LEDs, Coal_Basin_Servos, Coal_Basin_Signals and LCD_1602 to control a model railwayyard.
Possible Audience: Model railway enthusiasts interested in controlling points and signals on a DCC layout. Cpp programmers interested in using CPP libraries. At the low level the point selection is performed using push switches.
Required Hardware: NANO microcontroller to implement the code, PCA9685 Servo Driver, a selection of Servos to control points, push bottons to select routes, LEDs to confirm routes and simulate signals. The interfacing will require serial to parallel and parallel chips.
Required Libraries: . <Coal_Basin_Buttons.h>, <Coal_Basin_LEDs.h>, <Coal_Basin_Servos.h>, <Coal_Basin_Signals.h> and <LCD_1602.h>. Coal_Basin_Servos in herits Wire.h and Adafruit_PWMServoDriver.h. LCD_1602 inheritsLiquidCrystal_I2C.h.
Key words: Cpp Libraries, Cpp Inheritance, Cpp objects, Cpp code development, Nano microcontroller, serial to parallel, parallel to serial, servos, points, LCD, LEDs
----------------------------
Photo shows the final prototype. Centre right is the control panel with the buttons to select the routes and the LEDs to indicate the active routes. (Currently silos and lower eastside). Seven cables then connect to the controller board on the left. The compute power is provided by the Arduino NANO. In the centre of the controller board is the PCA9685 Servo driver that can drive up to 16 servos. (Only two shown). The serial signals "pass through" the PCA board and are used to drive the LCD display. On the top of the controller board are serial to parallel components that can be used to drive signals at each point. (One proto set of signals shown) To the left is the power supply that draws its power from the track.
----------------------------------------------------------------Â
My model railway ground had a layout known as Leonland part of which consisted of a mainline track that ran to a depot at Lower Eastside. It was decided to take a branch line off the mainline and run to a new area known as the coal basin.
The electrical side of this project involves developing a control panel from where the points could be set to control the trains.
As shown the coal basin includes 9 points plus point two numbered 10 and 11 on the main line.**
Push button switches at locations "A" through M" are included on the control panel to select the various routes. To indicate the active routes LEDs are also included on the panel at locations "A" through "M".
A possible sequence for a loco coming from Lower Eastside to collect some trucks from the coal siding might be:
1. Depending upon track loco is on select either route "L" or "M" - this will control points 10 and 11.
2. Move the loco through point 10 and then select route "K" - this will control point 10.
3. Move the loco to the Coal Basin complex and select route "I" This will control points 1, 2, 4, 6, 8 and 9.
4. Move the loco to the station and select route "J". This will control point 9.
5. Move the loco onto the turntable - there could be other action in the coal complex so when ready select route "J" This will reset point 9 if required.
6. Move the loco to the station and select route "I". This will control points 1, 2, 4, 6, 8 and 9.
7. Move loco as far as necessary and select route "G" assuming this is where the trucks to pick up are. This will control points 1, 2, 4, 6 and 7.
8. Move the loco to the trucks - when ready select route "G". This will control points 1, 2, 4, 6, 8 and 9.
9. Move the loco to the entry to the Coal Basin complex and select route "K". This will control point 10.
10. The loco and train can move down the mainline from the area.
** Point 11 was present in the original layout but with the new coal basin it could not be reached to control manually so needed to be electrified.
-----------------------------------
Pictured below are three possible structures for the software.
In the first, or traditional method everything is contained in one file. While this structure might be suitable for small projects it has little to commend it as the projects get larger.
With the second structure there is only one Arduino project but includes multiple files. In the case of this project two such files would be Coal_Basin_Buttons.cpp and Coal_Basin_Buttons.h This structure might be the most suitable to develop all files from scratch. If necessary the individual files could be edited as the project progresses.
With the third structure a class is developed for each operation in turn and the main project includes all as separate objects. The advantage of such a structure is that libraries are created that can be simply included in other projects. The disadvantage is that if any of the libraries need to be changed the original library must be edited.
This project, Coal_Basin_Control has used the third approach with all the libraries being developed independently and linked into the main file.
---------------------------------------
The basic circuit is shown below.
1. On the left are the buttons "1" through "11" interfaced to the 74HC165 parallel to serial converters. These buttons are read by the object Coal_Basin_Buttons with the result rippling into the NANO.
2. The button is acknowledged by rippling the results into a 74HC164 via the Coal_Basin_LEDs object that controls the panel LEDs
3. The NANO will perform some logic to determine
(i) which points (servos) to drive via the object Coal_Basin_Servo,
(ii) what to display on the LCD using the object LCD_1602**, and
(iii) using the object Coal_Basin_Signals turn the signs on/off to signify the positions of the points**..
-----------------------------------------------
1. Developing a LCD class/library, LCD_1602 to be used for debugging as the project progressed and as a display in the final product. This library exposed the following public methods
void begin( );
void message(int row, char *mess);
void message(int row, int res);
void message(char *mess);
void message(int res);
void message(char mess1, char *mess2);
void clear_row(int row);
void cursor(int col, int row);
Inherited library: LiquidCrystal_I2C
2. The second component will be the firmware/library that reads the buttons. Coal_Basin_Buttons. This exposes the following public methods
void begin( );
unsigned int get_route( ); //this is the button pressed. 1==SILOS, 2==WHARF1, 4==WHARF2, 8==SIDING1 etc
The constructor will be Coal_Basin_Buttons (int load, int clkin, int serin );
3. The next layer up is Coal_Basin_LEDs that will control the LEDs located near each button. Public routines exposed are:
void begin( );
void control_signals(unsigned int leds);
4. The library Coal_Basin_Servos controls the servos and exposes the following public methods.
void begin( );
int do_servo(channel,value );
This class required the libraries <Wire.h> and <Adafruit_PWMServoDriver.h>
The servos are all initialised such that the points are in the RIGHT position.
: void begin( ); Will set all signals for RIGHT
5. Coal_Basin_Signals generates the signals to indicate the position of the points.
void begin( );
void control_signals(int sig_no, int val);
The signals are all initialised to GREEN for trains moving to the RIGHT.
The main program (this project) will combine all of these libraries
--------------------------------------
The starting point will be to basically collect all the previous libraries and create instances of all classes. Also invoke the begin( ) method for every object/instance.
#include <Coal_Basin_Signals.h>
#include <Coal_Basin_Servo.h>
#include <Coal_Basin_LEDs.h>
#include <LCD_1602.h>
#include <Coal_Basin_Buttons.h>
Coal_Basin_Buttons button(4,5,6);
LCD_1602 LCD;
Coal_Basin_LEDs led;
Coal_Basin_Servo servo;
Coal_Basin_Signals sig;
void setup() {
LCD.begin( );
button.begin( );
led.begin( );
servo.begin( );
sig.begin( );
}
void loop() {
}
With the code as given all servos and signals will be in the RIGHT position.
----------------------------
To the setup( ) method can be added a number of messages displayed using the LCD_1602 library. Since the keys pressed must be remembered a global variable routes is declared.
unsigned int routes;
The setup( ) function will initialise routes to SILOS+EASTSIDE1.
void setup() {
LCD.begin( );
LCD.message("---Coal Basin---","--Route Select--");
........
delay(1000);
routes = SILOS+EASTSIDE1;
........
delay(2000);
display_route(routes);
}
The active routes are displayed on rows 0 or 1 using a new function display_routes(routes).
void display_route(int routes){
if (routes & SILOS) LCD.message(0,"Silos");
if (routes & WHARF1) LCD.message(0,"Wharf 1");
if (routes & WHARF2) LCD.message(0,"Wharf 2");
if (routes & SIDING1) LCD.message(0,"Siding 1");
if (routes & SIDING2) LCD.message(0,"Siding 2");
if (routes & COAL1) LCD.message(0,"Coal 1");
if (routes & COAL2) LCD.message(0,"Coal 2");
if (routes & YARD) LCD.message(0,"Yard");
if (routes & STATION) LCD.message(0,"Station");
if (routes & TURNTABLE) {
if ((routes & (BASIN-1)) == TURNTABLE) LCD.message(0,"Turntable");
 else LCD.message(" + TurnT");
}
if (routes & BASIN) LCD.message(1,"Coal Basin");
if (routes & EASTSIDE1) LCD.message(1,"Lower Eastside 1");
if (routes & EASTSIDE2) LCD.message(1,"Lower Eastside 2");
}
The active routes need to be displayed on the LEDs. That is in the setup() include:
led.control_signals(routes);
where control_signals(routes) is part of the Coal_Basin_LEDs library.
---------------------------------------------------
By default the servos are initialized to RIGHT. This will not align with the routes = SILOS+EASTSIDE1; statement where servo9 and servo11, along with signals9 and signals11 must be LEFT. This is added to the setup( ) method.
servo.do_servo(servo9,LEFT); //not turntable
servo.do_servo(servo11,LEFT); //eastside 1
sig.control_signals(signal9,LEFT);
sig.control_signals(signal11,LEFT);
-------------------------------
The program loop will continuously monitor the buttons and if there is a new button pressed call the action( ) method.
void loop() {
static int last_request;
unsigned int request = button.get_route( );
if (request) {
if (request != last_request){
last_request = request;
// LCD.message(0,request); //early testing only
action(request);
}
}
}
action(request) will basically operate (i) the LEDs and (ii) the LCD display to verify the button pressed.**
action(request) will also operate (iii) the servos that control the points and (iv) operate the signals to indicate the point settings
There is some logic associated with the design. For example if the SILOS are selected routes WHARF1..STATION cannot be selected. This is the objective of the method routes = route_logic(request), that is to return the routes following a request. See next section
void action(unsigned int request){
routes = route_logic(request);
led.control_signals(routes); ++
display_route(routes);
fire_servos(routes);
change_signals(routes);
}
** display_route(routes) has been presented as part of the setup( ) function
++ led.control(routes) just uses the Coal_Basin_LEDs library. No additional code is required.
-------------------------------
This method takes the button request and returns the modified routes.
There are three possible options.
(i) Only one route SILOS through STATION may be selected. (requestion SILOS will cancel STATION etc)
(ii) Only one route BASIN through EASTSIDE2 may be selected.
(iii) If any SILOS through YARD are selected TURNTABLE may be selected allowing trains to run from STATION to TURNTABLE or vice versa. Selecting STATION will enable the route from the Basin input to the STATION.
The logic becomes:**
unsigned int route_logic(unsigned int request){
if (request >= BASIN) routes = (routes & (BASIN-1)) + request;
else if (request <= YARD) routes= (routes & (TURNTABLE + BASIN + EASTSIDE1 + EASTSIDE2)) + request;
else if (request == STATION) routes = (routes & (BASIN + EASTSIDE1 + EASTSIDE2)) + STATION;
else if (request == TURNTABLE) routes = (routes & (~STATION)) | TURNTABLE;
else routes = 0; //should not occur
return routes;
}
** A global variable routes has been declared earlier which stores the current routes. Note the set up program has initialised routes. That is routes = SILOS+EASTSIDE1;
-------------------------
This section involves setting the servos to allow the train to run on a selected route.
The Coal Basin layout is repeated below.
Basically, when the route includes the SILOS, point1 or servo1 must be set to RIGHT. All the other points/servos are left as is.
When STATION is selected points/servos 1, 2, 4, 6, 8 and 9 must all set LEFT.
The full code becomes where the_server adds a 20mS delay between each do_servo( ) method.
void the_servo(unsigned int ser, int dir){
servo.do_servo(ser,dir);
delay(20); //don't over write
}
void fire_servos(unsigned int routes){
if (routes & SILOS) { the_servo(servo1,RIGHT); }
if (routes & WHARF1) { the_servo(servo1,LEFT); the_servo(servo2,RIGHT); the_servo(servo3,RIGHT);}
if (routes & WHARF2) { the_servo(servo1,LEFT); the_servo(servo2,RIGHT); the_servo(servo3,LEFT);}
if (routes & SIDING1) { the_servo(servo1,LEFT); the_servo(servo2,LEFT); the_servo(servo4,RIGHT); the_servo(servo5,RIGHT);}
if (routes & SIDING2) { the_servo(servo1,LEFT); the_servo(servo2,LEFT); the_servo(servo4,RIGHT); the_servo(servo5,LEFT);}
if (routes & COAL1) { the_servo(servo1,LEFT); the_servo(servo2,LEFT); the_servo(servo4,LEFT); the_servo(servo6,RIGHT);
the_servo(servo7,RIGHT);}
if (routes & COAL2) { the_servo(servo1,LEFT); the_servo(servo2,LEFT); the_servo(servo4,LEFT); the_servo(servo6,RIGHT);
the_servo(servo7,LEFT);}
if (routes & YARD) { the_servo(servo1,LEFT); the_servo(servo2,LEFT); the_servo(servo4,LEFT); the_servo(servo6,LEFT); the_servo(servo8,RIGHT);}
if (routes & STATION) { the_servo(servo1,LEFT); the_servo(servo2,LEFT); the_servo(servo4,LEFT); the_servo(servo6,LEFT); the_servo(servo8,LEFT); the_servo(servo9,LEFT);}
if (routes & TURNTABLE) {the_servo(servo9,RIGHT);}
if (routes & BASIN) {the_servo(servo10,LEFT);}
if (routes & EASTSIDE1) {the_servo(servo10,RIGHT);the_servo(servo11,LEFT);}
if (routes & EASTSIDE2) {the_servo(servo10,RIGHT);the_servo(servo11,RIGHT);}
}
----------------------------------------
The change_signals( ) method controls the signals. The settings will be exactly the same as the fire_servos( ) logic
void change_signals(unsigned int routes){
if (routes & SILOS) {sig.control_signals(signal1,RIGHT);}
if (routes & WHARF1) {sig.control_signals(signal1,LEFT);sig.control_signals(signal2,RIGHT); sig.control_signals(signal3,RIGHT);}
if (routes & WHARF2) {sig.control_signals(signal1,LEFT); sig.control_signals(signal2,RIGHT); sig.control_signals(signal3,LEFT);}
if (routes & SIDING1) {sig.control_signals(signal1,LEFT);sig.control_signals(signal2,LEFT);
sig.control_signals(signal4,RIGHT);sig.control_signals(signal5,RIGHT);}
if (routes & SIDING2) {sig.control_signals(signal1,LEFT); sig.control_signals(signal2,LEFT); sig.control_signals(signal4,RIGHT); sig.control_signals(signal5,LEFT);}
if (routes & COAL1) { sig.control_signals(signal1,LEFT); sig.control_signals(signal2,LEFT); sig.control_signals(signal4,LEFT); sig.control_signals(signal6,RIGHT); sig.control_signals(signal7,RIGHT);}
if (routes & COAL2) { sig.control_signals(signal1,LEFT); sig.control_signals(signal2,LEFT); sig.control_signals(signal4,LEFT);sig.control_signals(signal6,RIGHT);sig.control_signals(signal7,LEFT);}
if (routes & YARD) { sig.control_signals(signal1,LEFT); sig.control_signals(signal2,LEFT); sig.control_signals(signal4,LEFT); sig.control_signals(signal6,LEFT); sig.control_signals(signal8,RIGHT);}
if (routes & STATION) { sig.control_signals(1,LEFT); sig.control_signals(2,LEFT); sig.control_signals(4,LEFT); sig.control_signals(6,LEFT); sig.control_signals(8,LEFT); sig.control_signals(signal9,LEFT);}
if (routes & TURNTABLE) {sig.control_signals(signal9,RIGHT);}if (routes & BASIN) {sig.control_signals(signal10,LEFT);}
if (routes & EASTSIDE1) {sig.control_signals(signal10,RIGHT);sig.control_signals(signal11,LEFT);}
if (routes & EASTSIDE2) {sig.control_signals(signal10,RIGHT);sig.control_signals(signal11,RIGHT);}
}
----------------------------------------
Since the design included working libraries testing basically became the sequence:
work through each button in turn and check
(i) The panel LEDs were correct,
(ii) The messages on the LCD matched the selected routes
(iii) The servos/points were all in the correct direction, and
(iv) The signals matched the selected routes.
One practical aspect was powering the project. The design can be powered from the DCC on the track. However any short circuits on the track will generate a spike that might reset the design. It is recommended that a separate AC supply be used to power the design.
--------------------------------------------------------