The project controls 8 servos (2 shown) that control 8 points in a model railway layout. The DCC commands are decoded using an Arduino Nano driving a PCA9685 Servo Driver that controls the servos.
This project has been repeated using small individual libraries. Function_Control_Points
DCC_Points_Control Source Code.
include files #include <DCC_Probe.h>
#include "DCC_Points_Control.h"
constructor DCC_Points_Control(fn,ch,str,turn,sig);
//function to activate servo, channel of servo, count for servo straight, count for servo turn, channel for signal
public methods begin( ) includes setting servo period to 20mS
int the_fn( ); //returns function button pressed
void op(int con); //set servo and signal
---------------------------------
Abstract: This project extracts from the DCC tracks the signals for loco 3 functions 1-8 and uses these to control 8 points and up to 3 sets of signals for each point.
Possible Audience: The possible audience will be model railway enthusiasts who wish to use a NCE DCC Twin to control servos that drive points. The design will use software libraries. The description will basically just summarise the results so will not address those interested in the detailed design whether hardware or software.
Components Required: Arduino UNO or NANO, DB104 Bridge Rectifier, 4 Diodes, 6N138 Opto Coupler, 12V to 7V Buck Regulator, 7808 8V Voltage Regulator, PCA9685 Servo Driver, 2 100ufd Capacitors (25V) 220 nfd Capacitors, 1 1K ohm resistor, 2 10K resistors, 2 pin screw terminal for wiring to model railway tracks. Prototype used 8 servos driving 8 points/turnouts each with 3 green, 1 amber and 2 red LEDs. If a display is required LCM1602 I2C 16x2 LCD
Required Software: (i) DCC_Probe Library. See DCC_Function Library that enhanced DCC_Probe
(ii) Wire and Adafruit_PWMServo_Driver - both available on the Arduino IDE
(iii) If a display is used the library "LiquidCrystal_I2C" will need to be loaded from the internet.
-----------------------------
I have a small model train layout that consists of two loops with a single cross-over between the two and some shunting yards in the centre. There are 8 points/turnouts in total each of which will be controlled by a small servo. I have an NCE DCC Twin Controller that has provision for controlling two locos plus 8 separate functions for each loco. Each function switch will be used to control one servo**.
Thus to set point 1 function switch F1 on the DCC Twin will put the instruction onto the tracks. This will be read and decoded by an Arduino that in turn takes the appropriate actions. That is controls the specified Servo. Normally the Arduino would generate a PWM waveform where the high time controls the servo position. Unfortunately the Arduino cannot power multiple Servos so a separate servo driver board (PCA9685) is used. This board is controlled using I2C signals. To generate the I2C signals the "Wire" library is required. Layered on top of the wire library are the servo signals that will be generated using the "Adafruit-PWMServoDriver" library.
At each set of points there will be signals that will be used to indicate the status of the points.++
** Except for function 2 the switches toggle the function (one press to turn function on - the second press turns the function off). Function 2 is intended for loco whistles - the whistle blows while the switch is pressed. It is off otherwise. This is not really suitable for driving servos. However, with a cross-over both points should be either straight or in the turn position so two servos may be controlled with the one switch/push button. (1 and 2 in the above layout)
++ The Servo Driver can control 16 Servos. Since only 8 servos are used the remaining 8 may be used to control the signals at each set of points.
-------------------------------------
The circuit is shown above:
1. The lower portion is the circuit used in previous projects and uses an opto-coupler to probe the DCC signals on the model railway tracks**. The output of the Opto-coupler is wired to an interrupt pin (D2) on the Arduino. By timing the arrival for each data bit the Arduino is able to determine the "1's" and "0's" and so decode the DCC commands.
2. The DCC signal is also rectified via a bridge rectifier and the DC voltage used to power the Arduino, (VIN)
3. Since the Arduino is not capable of driving more than one servo a PCA9685 16 Channel 12 Bit PWN board is used to drive multiple servos. This board is powered via a Buck Regulator supplying 6 volts for the Servos.
**The opto-coupler will be essential to avoid multiple grounding problems when a mains-powered workstation (PC) is used to program the NANO
-------------------
The signals are implemented with LEDs**.
Three Leds are wired between the signal line and ground. These will be active when the points/turnout is in the normal/straight position. That is two GREENs on the straight track and a RED for the train approaching the points on the "side" track.++
Three Leds are wired between the signal line and 5V, These will be active when the points are in the turn position. A GREEN for trains approaching from the side track, a RED for trains approaching from the straight track and a BLUE for trains approaching the signal fork.^^
**Current limiting resistors (1K) in series with each LED are not shown.
++If the design cannot be implemented with both positive and negative signals an inverter (eg 74HC04) may be used.
^^ Railway enthusiasts can change the signals as they wish. For example approaching the fork could be dual GREEN-RED signals.
-----------------------------------------------
A previous project DCC_Function monitors the DCC track signals for function 4 loco 3. To verify the operation the LED on pin 13 is turned on and off. This code uses the library/class DCC_Probe. This example will be used as the starting point for this project.**
//DCC_Points_Control.ino --application or client code#include <DCC_Probe.h>#define LOCO 3#define FUNCTION 4DCC_Probe DCC;Pressing F4 should toggle the LED on pin 13.^^
** The code at this point uses the lower portion of the circuit given previously. The DCC signal is passed through the opto-coupler to the interrupt pin of the Nano. Power to the NANO is supplied from the USB port that is used for loading programs.
^^ There is a right and wrong way to connect to the DCC tracks. You can tell if you have an oscilloscope - otherwise if the results make no sense swap the wiring.
----------------------------------------------------------
As given the code will extract the status of the function (F4 in example) and control pin 13 that has the inbuilt LED. This operation has to be enhanced to control the servos. This will be encapsulated in the class/library DCC_Points_Control. In turn this class/library will use the Adafruit_PWMServoDriver to generate the servo signals. This library in turn uses the Wire library that generates the I2C signals used in the PCA9685 I/O device.
The header file that includes the class definition becomes
//DCC_Points_Control.h Header File#ifndef DCC_POINTS_CONTROL_H#define DCC_POINTS_CONTROL_H#include "Arduino.h"class DCC_Points_Control { public:DCC_Points_Control(int fn, int ch, int str, int turn, int sig);void begin( ); int the_fn( );void op(int con);private: int _fn; int _ch; //the function and servo to control int _str; int _turn; //servo settings int _sig; //signal};#endifNotes:
The constructor has five arguments (all integers):
(i) The function that the instance of the class will monitor (1-8)
(ii) The channel on the PCA9685 of the servo that will be driven. (0-15 possible although 0-7 used in example)
(iii) The time that the servo signal will be set to drive the points straight.**
(iv) The time that the servo signal will be set to drive the points to the turn position**,
(v) The channel where the signals are wired (8-15 if 0-7 used for servos)
The public methods will be reviewed in the following sections.
The private area defines variables that are private copies of the constructor arguments.
**Since what is straight and what is turn will depend upon the orientation of the Servo (See appendix) str and turn are probably not appropriate names for the two parameters.
------------------------------------
Notes.
1.The DCC_Points_Control will control the servos for the points using the Adafruit_PWMServoDriver class/library. This library in turn will use the Wire class/library to communicate with the PCA9685 servo board. It will be necessary to include the header files for both these libraries**.
2.The line Adafruit_PWMServoDriver pwm(0x40); creates one instance of the class Adafruit_PWMServoDriver. The argument is 0x40 to match the hardware address of the PCA9685 board.
3. The implementation of the DCC_Points_Control class assigns private variables to all the arguments.
4.The begin method for the DCC_Points_Control class requires setting up the pwm object. This uses the two methods begin( ) and setPWMFreq(50) where setPWMFreq(50) sets the frequency of the servo signal to 50 Hz or a 20mS period.
5. As given the two methods pwm.begin( ) and pwm.setPWMFreq(50) will be called for each DCC_Points_Control object created. If this is going to create problems the code could include a counter.++
6. DCC_Points_Control::the_fn ( ) returns the function the object is reading. In the example of the next section, DCC_Points_Control point1(1,0,250,180,8); the function is function 1. ^^
7. void DCC_Points_Control::op(int con) will physically control the servo and signal depending upon the parameter con. In the example of the next section the servo is _ch = 0 and the signal _sig = 8. For con = 1 the servo will be on for a time 0 to time _str and the signal for a time 4096 to 0 (always on). For con = 0 the servo time is 0 to _turn and the signal for a time 0 (always off)
** Both libraries are available as part of the Arduino IDE although it might be necessary to use the the Manage Libraries to load the Adafruit_PWMServoDriver. Ie Sketch->Include Library->Manage Libraries and search for Adafruit_PWM and when found select Install.
++ Any code in the implementation file that includes the scope operator, ie DCC_Points_Control:: a new copy will be generated for each instance. For the remaining code there is only one copy. This includes everything in the class Adafruit_PWMServoDriver Library.
^^ The 250 and 180 were my starting estimates for the required servo timings. These need to be adjusted in the final product.
------------------------------
Notes.
1. The code uses the library DCC_Probe to read the DCC tracks and return the reading.
2. An object DCC of the class DCC_Probe is created and the method DCC.DCC_function(LOCO,point1.the_fn( )) used to return the reading. In this example the LOCO is defined and fixed as LOCO 3.
3. The example only creates one object of the DCC_Points_Control class - point1(1,0,250,180,8) that is attached to function 1, the servo will output on channel 0 and the signal channel 8. The pulse width for the servo will be either 250 for straight or 180 for turn (These will be adjusted). See also comments in the Appendix.
4.point1.begin( ) will setup the object - see implementation code.
5. point1.the_fn( ) will return the function attached to this instance of the class. F1 in this example
6. point1.op(DCC.DCC_function(LOCO,point1.the_fn( ))) will perform the control operations.
See implementation file where con = DCC.DCC_function(LOCO,point1.the_fn( )).
---------------------------------
The trace below captures the I2C activity for the begin( ) method. The horizontal scale is 50uS/div meaning the begin occupies 300 uS. The solid portions on the right will be part of the program loop.
There are 6 areas of program activity covering 5 divisions which at 10mS/div becomes 50 mSec. _ Note the solid block at the right is when the program enters the loop( ) function.
The first burst in greater detail will be:
Decoding the 3 bytes become:
Byte 0: 0x80 - The command writing (bit 0 ==0) to address 0x40 (7 bit address so bit 7 has weight 0x40)
Byte 1: 0x00 - The PCA9685 expects to know what resister is being written. Here Mode Register 0.
Byte 2: 0x80 - For register 0 this is the Reset command.
------------------------------
The output on channel 0, that is the input to a servo is captured using a logic timing analyzer:
Notes:
1. The trace shows the waveform repeating every 4 divisions. At 5 mS/div this corresponds to 20mSec or 50Hz as set by the statement pwm.setPWMFreq(50).
2. The pulse is high for 1.2 mS. Since there are 4096 counts in the period the 1.2 corresponds to 245 counts which is approximately equal to count in the instance/object DCC_Points_Control point1(1,0,250,180,8).
-------------------------
Connecting the servo gave the following observations:
These results were obtained with the following modifications to the loop( ) function:
digitalWrite(12,HIGH); //Time performancepoint1.op(DCC.DCC_function(LOCO,point1.the_fn( )));digitalWrite(12,LOW);delay(1);The digitalWrite shows the time occupied by the actual code while the SDA/SCL show the actual execution. As illustrated the I2C output does not lag behind the code. This suggests further I2C activity can occur immediately - there is no danger that more activity will corrupt the first.++
++The delay(1) was added for clarity in the above trace - it was removed once the trace was obtained.
---------------------------
With 8 servos the code becomes: **
Observing the waveforms shows:
(i) an approximately 1mSec pulse to the servo. (Scale 2mS/div)++
(ii) For the 8 servos the time pulse shows the program loop to take 10mS. If the servos need only be updated once every 100mS the time budget would allow 90mS for other activities
**Note points 1 & 2 are both controlled by function 1. Since function 2 only gives a pulse it is not used.
++. This pulse is completely asynchronous to the I2C waveforms - several traces were taken to even capture it.
---------------------------------------------------
To confirm the operation of all 8 servos the following code was used:
The servos all moved as expected with the buck voltage converter just getting warm to touch.**
**In practice only one or two servos would move at a time. Some servos might never move in a session so since all servos 8 appeared to work together there was little likely hood that in practice the circuit would be overloaded.
--------------------------------------------------
One practical issue was zero'ing the servo-points combination. The program as given defined two positions for the servo - either setting the points straight or setting the points to turn. For ease of adjustment, it seemed worthwhile to be able to set the points in the middle. The code was enhanced to include a public routine "centre( )" that may be used to zero the points. The code tests the pin A6 and if it is high all servos will be programmed to a value Z0 = (_str + _turn)/2 that is the mid-point. The servo to point linkages can then be adjusted to place the points in the middle. Alternatively, the value Z0 can be adjusted to a value Z1 to place the points in the middle. Then when creating each object, the value of _str and _turn must be modified by an amount (Z1-Z0).
Header File:
class DCC_Points_Control {public:DCC_Points_Control(int fn, int ch, int str, int turn, int sig);..................void centre( );Implmentation File:
void DCC_Points_Control::centre( ){pwm.setPWM(_ch,0,(_str+_turn)/2);}Application File
if (digitalRead(A6)) {point1.centre( );point2.centre( );point3.centre( );point4.centre( );point5.centre( );point6.centre( );point7.centre( );point8.centre( );The values _str and _turn defined the servo movement. Ideally these should cause just enough pressure on the points to hold them in the desired position. Too much pressure then _str and _turn should be moved closer in value. Too little pressure then the servo needs to rotate further. That is _str and _turn should be separated more in value.**
** If all linkages are different all values of _str and _turn will be different. In this case the initial setting up will be "fiddly". See also Appendix
--------------------------------------------------------
A photo of my end product is shown below:
On the left of the PCB is the power supply that generates 6V for the PCA9685 servo board and 8V for the NANO Vin. The white package is the opto-coupler. The servo board is behind the NANO board, and it shows a range of connections to the servos two of which are shown in the photo**.++
My layout consisted of a number of points collected over the years so the design of my servo-point coupling was not standard. Further the most convenient place to locate the servo varied. In the photo shown above the lower servo is on the curve section of the point whereas the upper servo is on the straight side. In this example both servos are beside the point rather than in front. All of this implied that the coupling was not unique and had to be set up for each servo. The technique I used was to use the "centre" code to centre the servo and try and mount the servo and linkage so the points were centered.
When everything was mounted, and the linkages adjusted as best I could I modified the "centre" value until the point was in one position (It didn't matter which). In the sample code the starting point was (250 + 180)/2 = 215. This was adjusted to 230, 240 ... etc in turn. With a great deal of luck, the point will be straight position at 250. Now move in the other direction and also with luck it will be in the turn position with 180. Whatever these positions are modify the relevant constructor.
The first object/instance became DCC_Points_Control point1(1,0,250,180,8);
In my example I found values of 240 and 140 for point 3 possibly because everything was not aligned. Further due to the point/servo orientation the 140 corresponded to the points in the straight position and 240 in the turn. See appendix. Thus, the constructor became DCC_Points_Control point3(3,2,140,240,10). The procedure was repeated for all 8 points.
For the signals these are wired to the servo outputs 8 through 16. For prototyping I have mounted two LEDs on a blank PCB with 3 pin screw connectors. As shown the green signal will indicate the points are straight while amber indicates turning.
**Most enthusiasts will hide all the electronics under the baseboard. Since I do enough climbing under layouts at my local model railway club on my home layout I have placed everything where I can "get at it".
++In the photo the servo in the foreground operates point 1 while the servo at the back operates point 5. Thomas the Tank Engine is actually on point 6.
--------------------------------------
With the above design, while the USB cable for programming was connected the LEDs worked as expected giving either a solid GREEN or a solid AMBER. When the signal is high ( ie a duty cycle of 100%) current flows through the (lower) GREEN LED to ground. When the signal is low (duty cycle = 0) the current flows from the 5V to ground via the AMBER LED (shown as BLUE). However, when the cable, and hence the USB power was removed with the NANO being powered via Vin instead of just the GREEN being ON, both the GREEN and AMBER were active. This was due to the following factors:
For the servos the output of the buck regulator was set at 6V rather than 5V.
The output of the on board 7805 voltage regulator was only 4.2V not 5V as expected.**
The signal voltage output from the PCA9685 will never reach Vcc (4.2). I measured 3V - large enough to drive the servos. The path is now 6V via the AMBER (blue) LED and a 1K resistor (not shown) to 3V. Then via the GREEN LED and 1k resistor to ground so both LEDs are on. (With the USB cable the Vcc was closer to 5V and the signal closer to 4V so the AMBER LED is not visible.
The solution was to not use the 5V output from the NANO to power the Vcc of the PCA9685 board but rather to use a voltage dropping diode from the PCA Vin.
** I do not believe the PCA board current requirements exceed that available from the 7805. (See circuit on page NANO Power) I suspect I may have damaged the 7805 on my NANO board.
----------------------------------------
----------------------------------------
At power up the NCE DCC Twin controller came up in the state "0" for all functions. How the points all came up in practice was determined by both the physical positioning of the servo and the constructor. Initially in my layout some points were in their "Normal" or "Straight" position while others were in the "Turn" position. I decided that at power up I wanted all points in their "Normal" position. To do this I needed to reverse the timings in some of the constructors. (See appendix)
With all points resetting to straight the first press on a function button of the DCE DCC Twin Controller set the corresponding point for a turn.
------------------------------------------
With my layout everything is so close I can see the settings of the points. However, with a larger layout a status display might be appropriate. This can be achieved with a 16x2 LCD using I2C.
One library will be LiquidCrystal_I2C**
In the *.cpp file (not the *.ino file) include the following lines:
To initialize the new object lcd include the following in the begin method++
void DCC_Points_Control::begin ( ){.....Add the following method:
void DCC_Points_Control::point_dsp(int pt, int pos){lcd.setCursor(2*pt-1,1); //column & rowif (pt==1) lcd.setCursor(2,1);if (pos) lcd.print('T');else lcd.print('N'); }When called this will print either 'T' or 'N' under the corresponding point.
To call the above method include the following line in the op( ) method
void DCC_Points_Control::op(int con){...........//next line if using LCD point_dsp(_fn,con); //could be _ch+1}**Look for LiquidCrystal_I2C.h on internet and download *.zip file. In the Arduino IDE use sketch-->include library --> add.zip library
++ The print line will display the text corresponding to the 8 points on the top line of the LCD. In my example points 1 and 2 act together.
-------------------------------------
Once the servos were all adjusted everything worked electrically. While nominally each set of points could have 3 sets of signals making 24 in total because of the small size of the layout many would be sitting on top of one another. Since the layout was so small the status of the points could be determined by just looking at them - the signals and LCD display were really only for show. If it was a real layout and there was an accident the engine driver could say "there were so many lights I become confused!"
------------------
In this project it is assumed that each servo will be individually mounted and some/all of the points themselves might be different. The text has used as a starting point two values 250 and 180**. How these two values are included in the constructor will vary depending upon the orientation and location of the servo as illustrated in the diagram below. These are for left hand points - the story will be similar for right hand points.
A further factor is how accurate the servo centre line is able to be positioned and how accurate (consistent) the link can be constructed. Different lengths will change the values 250 and 180.
**These two values were decided after some simple tests of a servo.
--------------------------------