Trains.com

Subscriber & Member Login

Login, or register today to interact in our online community, comment on articles, receive our newsletter, manage your account online and more!

Building an Arduino-Based DCC System

14201 views
6 replies
1 rating 2 rating 3 rating 4 rating 5 rating
  • Member since
    June 2009
  • From: QLD, Australia
  • 1,111 posts
Building an Arduino-Based DCC System
Posted by tbdanny on Saturday, January 23, 2016 12:50 AM

 

Hi all,

In early 2015, I realised that I wasn't happy with my current DCC system.  Originally, it was a Lenz set which I bought off Ebay in 2008. It worked well, and it served me under 3 previous layouts. However, when the time came to build the Bradford Valley Lumber Co, I decided that I wanted to have wireless walk-around control. Now, I hadn't done my research before purchasing the Lenz set off Ebay, and seven years later, that came back to bite me. At that point, the only wireless option Lenz offered was via a cordless phone, and I didn't like the sound of that.

 

 

I had an old laptop that I wasn't using, on which the graphics card had started to fail. I set this up under the layout, and connected it to the tracks via a SPROG 3 box. I then tried driving the trains with the Engine Driver app on my phone, but I didn't like it. I had to keep looking down at the phone all the time, which meant taking my eyes off the trains. I wanted a throttle that was tactile, yet wireless.

 

 

I did look at replacing the Lenz system with Digitrax, as that's what my club uses. However, that would have been quite expensive. Given that the BVLC models a medium-sized backwoods operation, most of the expense would have been going towards functionality that I wouldn't use, such as consisting, turnout control and decoder programming. (For the latter, I use a SPROG 2 hooked up to a PC running JMRI.)

 

 

There were already a couple of Arduino microcontrollers under the layout, running various animations and timers. While putting these together, I had read of Xbee modules for wireless communication. I decided to try and build my own simple wireless DCC system, using Arduino and Xbee, along with other bits. My goal was to build a system that would allow me to run the trains without taking my eyes off them.

 

 

Over this and my next few posts, I'll be outlining how I made & programmed the hand controller, the base station, and then used these to help automate part of my staging. Part 1 will cover the hardware, part 2 the software, and part 3 the staging automation.

 

 

Hardware – Controller

 

 

 

 

Starting with the hand controller, I wanted something that was tactile yet simple. To this end, I decided that there would be four main components; a 2-line LCD screen to display the currently selected loco and other data, a rotary encoder with a built-in push button switch, a keypad to select function and loco address and an Xbee module to transmit to the base station. As well as this, there are various switches, battery clips, etc. One thing I did try to do while building this was to try and keep the design as simple as possible, and this included moving some functions usually handled by the controller to the base station.

 

 

 

 

The case I used was a T-shaped case, with a built-in battery compartment, capable of holding 4 AA or 2 9v batteries. I cut two rectangular holes, one for the keypad in the bottom of the 'T', and one for the LCD at the top. I also drilled holes for the toggle switches, the rotary encoder and the red push button (which is for the emergency stop).

 

 

 

 

With the holes in the case in place, it was time to mount the hardware. The red board at the top left is the Arduino Micro, which is the brains of the whole thing. In this case, I was using a Sparkfun derivative of the Arduino design.

 

There are three main components on the hand-held controller that provide outputs from the Arduino. The first and most important is the Xbee wireless module. I purchased an Xbee 'breakout board' along with the Xbee module itself.  This is the red board in the centre of the controller, which provides a socket for the Xbee to plug into. This was wired to pins 0 and 1 of the Arduino, which are the hardware serial input and output. Pins 2 and 3 of the Arduino are the I2C bus, and here this is used for sending text to the LCD screen (the green and black boards at the top of the controller). The third output is the red LED at the top of the controller, which indicates when the battery is getting low.

 

 

There are several components which are used to provide inputs to the controller:

 

- A rotary encoder incorporating a push button on the rotary input (can be turned and pressed).

 

- A 12-digit keypad, which uses 7 wires for output.

 

- A red push button, for the emergency stop.

 

- Two toggle switches, one for power (inline with batteries) and the other to select if the controller is in 'switching' or 'mainline' mode. (In mainline mode, each step of the encoder will change the speed by 4 speed steps).

 

- A voltage divider, to measure the output voltage from the battery.

 

- A single black push button, used to toggle functions. This was a later addition, after initial testing of the controller.

 

 

With the exception of the voltage divider, all of the above components were wired into the digital I/O pins of the Arduino (or analogue pins configured to act as digital).

 

 

The single analogue input is used to measure the remaining charge in the battery. To bring the voltage from the battery down to less than the Arduino's 5v working voltage, I built a voltage divider from two resistors to halve the input voltage from the 9v battery. This provides a voltage reading as an analogue value to the Arduino. When the voltage drops below 3v (6v left on the batteries), the LED is turned on to indicate the batteries will shortly need replacing.

 

 

You may notice that I'm referring to batteries in the plural. Initially, I used just a single 9v battery in the controller, but found that I wasn't getting the wireless range I needed and the batteries were draining rapidly. After checking the current draw of the Xbee modules, I realised more current was needed and so wired up two 9v batteries in parallel.

 

 

 

 

Once all the wiring was complete, I painted the mounting screws black. As well as holding the screen and keypad in place, these also act as tactile reference points to allow operation without having to look at the controller.

 

 

Hardware – Base Station

 

 

I decided to build my own base station, for three main reasons. Firstly, I would be able to define my own communication protocol between the controller and base station, which would be simpler to program. Secondly, it would allow me to move some lesser-used functions to the base station, rather than having them on the controller. Thirdly, being able to modify the hardware and software of the base station would allow me to expand it in future, to allow for automation and additional functionality.

 

 

Within the base station, there are five main modules, along with other buttons, switches, etc.

 

 

 

 

Firstly, there is the voltage regulator. This is a kit which I purchased from my local Jaycar Electronics store. The kit comes with a heatsink included, but by upgrading it to a larger one (as I did), it can provide up to 4A of current. The power input comes from an old laptop power supply, which connects into the DC socket on the right of the photo. From this voltage regulator, two outputs run to the DCC booster board and the Arduino.

 

 

 

 

Rather than reinventing the wheel, I decided to use a Tam Valley Depot booster board to put the DCC signal and power out to the track. As it uses the same power supply as the Arduino, the two devices have a 'common ground' connection between them. As such, only one wire is needed to carry the DCC signal between them. This is connected to the terminal on the booster which is marked with a digital waveform.

 

 

From this booster, the DCC output to the track is connected through two other components before reaching the screw terminals on the outside of the case. Firstly, it runs through the 'normally closed' terminals of the SPDT relay shown in this picture (via the orange and green wires). This relay is controlled by the Arduino through the yellow wires, and is used as the emergency stop mechanism. When an emergency stop occurs, the relay is activated, breaking the circuit and killing power to the track. After the relay, the output to the track runs through a 3A circuit breaker (not pictured), then to the screw terminals.

 

 

 

 

Running the base station is a Crowduino. This is a derivative of the Arduino Duemilanove, with a built-in Xbee socket. Usually, when I use an Arduino or derivative with this form factor, I'll usually use a prototyping shield (right) to connect the other components to the Arduino itself. This 'shield' is a circuit board with connections to each of the Arduino's pins, and an area of blank prototyping board in the middle, and it just plugs into the Arduino itself. This allows the Arduino to be removed for software updates, etc. When I took the above photo, the shield only has the circuitry to drive the relay and the power connections on it.

 

 

 

 

This LED display was built from four 7-segment LED displays, some spare prototyping board and a MAX7221 LED display driver IC. The IC takes instructions from the Arduino and changes the output on the LED display accordingly. This module is mounted at the front of the base station, and displays the 4-digit DCC address of the last locomotive which was allocated to a controller. This serves as positive confirmation that the address was correctly received, and also allows for error messages to be displayed if that locomotive is already allocated to another controller.

 

 

 

 

When designing the DCC system, I decided that the setting of functions between momentary and toggled would be done on the base station. All my locomotives have the same brand of sound decoder (Soundtraxx), all set up the same way. As such, it struck me as being more efficient to have this momentary/toggled setting done in the one place, rather than setting it up for each locomotive. Having one of these switches in the up (on) position means that function is momentary, if it's down (off) then it's toggled.

 

 

 

 

Behind the DIP switches is this 74HC4067 IC. This is a 16-channel multiplexer, which can be used to read or write up to sixteen different inputs/outputs using only 5 connections to the Arduino. When reading inputs via this IC, the Arduino tells the IC which input it wants to read. The IC then connects that input to the Arduino, which then reads it. The same occurs for outputs. Had I not used this method to read all 16 of the switches, then I would have run out of I/O pins on the Crowduino.

 

 

 

 

Once I had prepared all these components, I mounted them within the base station's case. Aside from what has already been mentioned, there's also a red button on the right of the case. This is connected across the ground and reset pins of the Arduino. Pressing it will cause a hard reboot of the software. This is the third Arduino I've used in a case like this, the first two being the animation controllers for each side of the layout. In each case, I've mounted an external reset button, just in case it's needed.

 

 

 

 

I had to adjust the design a number of times during testing. Most of these were software changes. The only notable hardware change was the addition of the black push button to toggle functions. Originally, I had planned to use the keypad to control functions, with momentary functions having their button held down. However, this functionality didn't work with this particular keypad. The Arduino could tell when a key had been pressed, but not when it had been released. As such, I had to use the keypad to select the function, then a separate button to trigger it. Given that I usually use function 2 anyway, this wasn't a big issue.

 

 

So, that's the hardware for my Arduino-based DCC system. In my next post, I'll be covering the software that runs the whole system.

 

The Location: Forests of the Pacific Northwest, Oregon
The Year: 1948
The Scale: On30
The Blog: http://bvlcorr.tumblr.com

  • Member since
    July 2006
  • From: Bradford, Ontario
  • 15,593 posts
Posted by hon30critter on Saturday, January 23, 2016 1:53 AM

Hi Danny!

WOW, once again I am impressed with your electronic abilities. I read your post thoroughly and I understand the concepts, but I'm a bit lost when it comes to the details. I do like the idea of a tactile controller. No worry, if I manage to get to the point where I want to create such a device I will be back to ask you tons of questions.

Thanks

Dave

I'm just a dude with a bad back having a lot of fun with model trains, and finally building a layout!

  • Member since
    September 2003
  • 10,582 posts
Posted by mlehman on Saturday, January 23, 2016 3:50 AM

Danny,

I agree with Dave. Fascinating and insprational stuff.

Mike Lehman

Urbana, IL

  • Member since
    February 2002
  • From: Reading, PA
  • 30,002 posts
Posted by rrinker on Saturday, January 23, 2016 11:08 AM

 Intersting design choices. Since your throttle case can hold it, why not 4x AA instead of the 9V? Are you using something in there that needs more than 6V? 4x AA would have a much larger mAH capacity - one of my onlt beefs with Digitrax is they contonue to use a 9V battery for the radio throttles and they don't last very long.

 Good to know on the keypad - if I go this route I'll have to make my own, or find one that can signal press and release. An array of tactile buttons with a multiplexor should work, if the software loop isn't fast enough there's enough room to maybe use a second Nano (you are using a Nano - the Micro doesn't have a USB port built in - just so people aren't confused) to decoder the keypad and signal the main processor.

 Even if you didn't use all the same decoders - I've never had reason to use anythign other than F2 as momentary for the horn/whistle. Switching on the base makes a lot of sense in that case. I might just put the settings DIP switches inside though, since they'd pretty much never be touched. I might even be tempted to simply hard code that in my software and if in the future some reason would come along to make different functions momentary, it would just take a simple program change.

 I like the use of the Tam Valley booster - why reinvent the wheel? Though somewhere I have a pair of power amps that could be used to make a booster, I ordered them years ago based on a DIY booster design and then never built it.

 The software part is what I'm waiting for. I have a pretty good handle on hardware design, but things like the NMRA DCC library and Loconet library seem crazy complex - although I'm sure once I see a full example it's rather quite simple.

                       --Randy

 


Modeling the Reading Railroad in the 1950's

 

Visit my web site at www.readingeastpenn.com for construction updates, DCC Info, and more.

  • Member since
    June 2009
  • From: QLD, Australia
  • 1,111 posts
Posted by tbdanny on Saturday, January 23, 2016 11:49 AM

Randy,

To be honest, the question of which batteries to use is the aspect into which I put the least thought.  My only real experience with wireless throttles was with Digitrax down at the club, so I just assumed 9v Tongue Tied.

The keypad library I used does support the detection of buttons being pressed and released on the keypad, but it just didn't work on this particular keypad.  The pinout of it was also a bit irregular, and the one you get may be a bit different.  I do like your idea of the array of tactile switches with a multiplexer, and I can see how that would give you the flexibility to add extra buttons as needed.

We've got a public holiday here in Australia on Tuesday - it's Australia Day, which is our Fourth of July.  I'll try to get the software writeup done then, with as much detail as possible.  I used quite a few libraries in it, and I'll be providing links to where I downloaded them.  I should also probably mention that my particular implementation doesn't have any sort of loconet support - it's very basic.

The Location: Forests of the Pacific Northwest, Oregon
The Year: 1948
The Scale: On30
The Blog: http://bvlcorr.tumblr.com

  • Member since
    February 2002
  • From: Reading, PA
  • 30,002 posts
Posted by rrinker on Saturday, January 23, 2016 1:39 PM

 I'll have to check out some keypads. My kit didn't come with one, about the only missing thing that I could possibly want.

 I keep mixing up the Mini and Micro - Mini is the one that needs an external USB to program. Micro and Nano look almost identical, Nano has a dedicated USB chip, Micro uses the chip itself. Micro is a miniature Leonardo, Nano is a miniatures Uno. One of these days I will be able to keep it straight. Or I'm going to skip Arduinos and just work directly with the micros themselves.

 Not so sure I would even need Loconet anymore - I'm already working on my design for detection and signalling and I considered RS485 like C/MRI and now I'm thinking Ethernet to connect each node back to the main computer. Doesn't even need to be any fast stuff like gig, a 10Mb hub would be plenty - still a polled system, I'm not sure I can figure out the software for a true peer system, and polled would be plenty fast. For turnout controls I am planning to use servos and just build controllers with Arduinos like Dr Geoff Bunza's designs. Straight DCC input, no Loconet or other bus.

                              --Randy


Modeling the Reading Railroad in the 1950's

 

Visit my web site at www.readingeastpenn.com for construction updates, DCC Info, and more.

  • Member since
    June 2009
  • From: QLD, Australia
  • 1,111 posts
Posted by tbdanny on Friday, January 29, 2016 11:53 PM

 

Part 2 – Software

 

 

In part 1 of this write-up, I outlined how I had constructed the hardware for a basic DCC wireless controller and a base station for it. Here, I will outline how I wrote the software to drive both of these devices. It was written in the Arduino IDE, which uses a language very similar to C++.

 

 

Arduino Libraries

 

 

Arduino is very widely used, and as such, there are several libraries available to interface with various hardware and perform other functions. I used several of these libraries for both parts of the DCC system, all of which are available for free download.

 

For the hand-held controller, the following libraries were used:

 

 

  • <EasyTransfer.h> - This is the EasyTransfer library by Bill Porter (http://www.billporter.info/2011/05/30/easytransfer-arduino-library/). This is designed to easily transfer data between Arduinos, via a connection over their serial ports. Given that this is how the Xbee modules connect, it seemed like the simplest way of transferring data.

  • <Keypad.h> - This is the library to handle matrix-style keypads, by Mark Stanley & Alexander Brevig (http://playground.arduino.cc/Code/Keypad). It contains several functions for reading these keypads.

  • <LiquidCrystal_I2C.h> - Written by F. Malpartida, this library allows for easy control of two and four line LCD displays using the I2C serial communication protocol. It can be downloaded from https://bitbucket.org/fmalpartida/new-liquidcrystal/downloads.

  • <Wire.h> & <LCD.h> - These libraries are required by the LiquidCrystal_I2C.h library. The wire.h library comes with Arduino and the LCD.h library is included in the download for the LiquidCrystal_I2C.h library. Wire.h is also required by the EasyTransfer library.

  • <OneButton.h> - This was written by Matthias Hertel (http://www.mathertel.de/Arduino/OneButtonLibrary.aspx), and provides functionality to use a single button for multiple inputs, by reading clicks, double-clicks and being held down.

  • <Encoder.h> - Used to read rotary encoders, and provides an integer as an output. Can be downloaded from here: http://www.pjrc.com/teensy/td_libs_Encoder.html.

  • <Bounce2.h> - This is used to smooth out the input from a push-button, so that only a single press is registered each time it's pushed. This is known as 'debouncing'. This library is written by Thomas Fredericks and can be downloaded from https://github.com/thomasfredericks/Bounce2

 

 

In the base station, the following libraries were used:

 

 

  • <DCCPacketScheduler.h> - This is the CmdrArduino library from RailStars, which can be downloaded from https://github.com/Railstars/CmdrArduino. It's used to generate, queue and output NMRA DCC packets.

  • <DCCHardware.h>, <DCCPacket.h>, <DCCPacketQueue.h> - these are support libraries for the CmdrArduino library, and are included in the download for it.

  • <MUX74HC4067.h> - Written by Nick Lamprianidis

  • (https://github.com/pAIgn10/MUX74HC4067), this library provides easy reading of multiple inputs/outputs via a 74HC4067 integrated circuit. There's also a rather good tutorial on using it here: http://blog.codebender.cc/2014/01/30/mux74hc4067/

  • <LedControl.h> - This library, by Eberhard Fahle (https://github.com/wayoda/LedControl), provides easy control of 7-segment LED displays via a MAX7219 or MAX7221 IC.

  • <EasyTransfer.h> & <Wire.h> - same as used in the hand-held controller.

 

 

Software – Hand-held Controller

 

 

There are several pieces of information that need to be read from the hand-held controller's hardware and sent to the base station. These include the address of the current locomotive being controlled, the current DCC speed step and direction of travel, which function has been selected, if it has been toggled, if there's an emergency stop and if the system is in 'dispatch mode' (for selecting a locomotive). On top of this, each transmission between controller and base station contains an ID number for the hand-held, to ensure that locomotives are not allocated to more than one controller at once. Although I did initially only build one controller, I wrote the system to be able to handle up to 8 at once, in case it was needed.

 

 

It was actually the documentation for the EasyTransfer library that provided the key inspiration for how the software should be structured. In C++, there is a type of data called a 'struct', which is a collection of several variables in one object. EasyTransfer works by taking a struct and sending it out the serial port. After reading this, I realised that all the data needed could be stored in one such struct, and the rest of the program would simply be a matter of reading data in and sending it. With this approach worked out, I was ready to start writing the program.

 

 

The struct used in the controller is set up as follows (default values are in brackets):

 

struct controllerData{
//This struct is used for sending controller data (for the train) to the base station
boolean emergStop; (false)
int speedStep; (0)
int locoAddress; (0)
int dirOfTravel; //Direction of travel - 1 for forward, -1 for reverse (1)
byte functToggled; //This provides the number of the function toggled (2 – I use the whistle the most)
boolean functStatus; //This conveys whether it should be true or false (false)
boolean isDispatch; //Conveys whether this is in dispatch mode or not (false)
int controllerID; (1, for the first controller – this is not changed in the code).
};

 

 

When it comes to Arduino, there is a basic program outline which all sketches (Arduino programs) follow. Firstly, the setup() function is run when the Arduino is first started. Any code needed to set up the program, such as initialising variables, creating program objects, etc. goes in here. Once the setup function has completed, the Arduino moves to the loop() function. As the name implies, this is a function that keeps looping until the Arduino is turned off. This is where the main program code is placed.

 

 

For the hand-held controller, the setup() function opens the serial port for communication with the Xbee, initialises the hardware (I/O pins, LCD screen, keypad, etc.) and sets up the struct with its default values. Lastly, it displays 'All Aboard!' on the screen for two seconds, then sets the controller into dispatch mode, to acquire a locomotive. I had it do this two second text display as a way of showing that the setup had completed, and that the controller was ready to go.

 

 

In addition to these two functions, I have written several other functions, which are called by the main body of the program. These cover things like updating the display, handling dispatch mode, looking after emergency stops, etc. Breaking the code up like this makes it easier to understand the code, and simpler to reuse different sections of code where needed.

 

 

The loop() function of the hand-held controller loops through the below sequence:

 

 

1) Check for low battery level. If the input on analogue pin A3 is less than 580, then the battery is low, and the LED at the top of the controller needs to be turned on.

 

 

2) Check if the emergency stop button has been pressed. If it has, then the emergStop() function is called. This function sets the emergStop variable in the struct to true, and immediately transmits it to the base station. Once this has been done, it clears the screen and prints the text 'EMERGENCY STOP' in the middle of it. Next, the function enters a while loop, which will keep going until the emergency stop button is pressed again. This effectively pauses the program until the operator has dealt with the emergency situation, and is ready to resume operation.

 

 

Once the emergency stop button has been pressed again, the emergStop() function saves the locomotive address in a temporary variable, then initialises the struct (via the initHandData() function). Now that the struct has been initialised, the address of the locomotive is put back into it. This results in the struct being in the same state as it is immediately after a locomotive has been acquired – the loco is active on the controller, on speed step 0 and direction set to forwards. This struct is then sent to the base station, and the LCD is returned to normal (via the updateDisplay() function). The function then ends, and the main loop of the program continues.

 

 

3) Check if the isDispatch variable is true. If so, then the controller is in dispatch mode. While in dispatch mode, the screen displays the following prompt:

 

 

 

 

The program then checks the keypad to see if any of the buttons on it have been pressed. If the '*' key has been pressed, then the exitDispatch() function is called, to verify and send the loco address to the base station, and return to running mode. If the '#' key has been pressed, then the address is cleared and the display returned to the clear state above. If neither of these keys have been pressed, then it is a number that has been entered and this digit is appended to the addrInput string. This is a global string variable, which is initially blank. The updateDispScreen() function is then called, to update the dispatch screen with this new information.

 

 

This continues until 4 digits have been entered, or the '*' key has been pressed. When addrInput is 4 digits long, the display changes to the following:

 

 

 

 

At this point, the operator can press the '*' key to confirm the locomotive, or the '#' key to clear the address and re-enter it (as outlined above).

 

 

The exitDispatch() function first checks that the length of the addrInput is greater than 0. If not, then it simply returns to the main body of the program loop, which continues in dispatch mode. If addrInput does have at least one digit, then the struct is initialised and the locoAddress is set to the value of addrInput. The isDispatch variable in the struct is then set to 'true', and the struct is transmitted to the base station. Once this has been done, the isDispatch is set to false, and the updateDisplay() function is called to set up the display for running mode.

 

 

On the next iteration of the main program loop, the program will pick up that it is no longer in dispatch, and will continue on to running mode.

 

 

4) If the isDispatch variable is false, then the controller is in running mode. While in running mode, the display looks like this:

 

 

 

 

For running mode, the main program loop follows the following sequence:

 

 

- Read the input from the toggle switch used for the throttle mode. If it is high, then we're in mainline mode, and the global variable 'shuntingMode' is set to false. If it is low, then we're in shunting mode, and shuntingMode is set to true. If the mode has changed, then the updateDisplay() function is called to update the display, to show either 'ML' (for mainline) or 'SW' (for switching) as the case may be.

 

 

- Check the button built into the rotary encoder. If there's been a single-click, then the speed step will be set to 0. If there's been a double-click, the speed step will be set to 0 and the direction will be reversed, which is done by multiplying the dirOfTravel variable in the struct by -1. Again, the display is updated, using the updateDisplay() function. On the top line, the letter next to the locomotive address (top left) will indicate either 'F' or 'R'.

 

 

- Get the current speed step. This is done by calling the getSpeed() function, which returns an integer value between 0 and 128. This uses the encoder library to read the current position of the rotary encoder, and compares it to the previous value (which is stored in a global variable). If that value has changed, it needs to calculate the new speed step. Given that the rotary encoder uses quadrature encoding, each 'click' of the encoder changes the value of it by 4. As such, if the controller is in shunting mode, then the number returned from the encoder is divided by 4, to give a difference of one speed step per 'click'. In mainline mode, however, no division is done, and this results in a difference of four speed steps per 'click'.

 

 

Once these calculations have been done, the function checks that the speed value has not gone above 128 or below 0. If it has, then it sets the speedStep variable in the struct to either 128 or 0 respectively. If the resulting speed step is within the range 0-128, speedStep is set to whatever value was calculated.

 

 

- Check which function has been selected. As I mentioned in part 1, the matrix keypad I was using didn't work with the functions in the keypad library that detect when a key on the pad has been pressed, released or held. As such, I was unable to use the keypad to both select and trigger a function, and added a separate trigger button to trigger the function once it had been selected.

 

 

To read which function has been selected, the main loop calls the getFunctions() function. The keypad library works by having a 1-character buffer, which is filled when a key is pressed. The getFunctions() function starts by checking if this buffer is full. If so, then it reads the character in the buffer.

 

 

If it is the '*' key, then it sets the isDispatch variable in the struct to true, and changes to the dispatch mode screen using the updateDispScreen() function. On the next iteration of the main program loop, it will operate in dispatch mode as outlined above. If the '#' key has been pressed, then it will set the funcShift global variable to true. This is what allows the upper 10 functions (11-19) to be selected. An indicator ('^') will be shown on the display, and the next number pushed will have 10 added to the value of it. Once this next number has been selected, the funcShift variable will be set back to false.

 

 

If neither of these keys are pressed, then it is a number that has been selected. This value (with 10 added, if shift had previously been pushed), is put into the functToggled variable in the struct, and the display is updated with the updateDisplay() function.

 

 

5) Check if the function trigger button has been pressed. This is wired to one of the input pins in a 'pull-up' configuration, which means that pressing the button will make the pin go low. This button (as well as the emergency stop button) are created as debounce objects by the Bounce2 library, and calling the read() function of that library indicates if they have been pushed or not.

 

 

If the function trigger button has been pushed, then it will set the functStatus variable in the struct to true, and will also set the global variable prevTrigStat to true. If the function trigger button has not been pushed, then the program checks prevTrigStat, to see if the button was pushed on the last iteration of the loop. If this was the case, then functStatus in the struct is changed to false, to tell the base station to stop triggering the function.

 

 

6) Finally, the program checks the global variable structChanged, to see if any of the data in the struct has been changed in this iteration of the loop. In all functions, and at the appropriate points in the main program loop, structChanged is set to true if any of the variables in the struct data are changed. It will only be false if nothing has happened during an iteration of the loop.

 

 

If structChanged is true, then the main loop will call the updateDisplay() function to show the latest data on screen, then it will send the struct to the base station and reset structChanged to false for the next iteration of the loop.

 

 

Software – Base Station

 

 

In contrast to the hand-held controller, the software for the base station is a bit more straightforward. All of the data input and processing has already been done on the hand-held controller, so the base station just needs to take what's been transmitted from the hand-held controller and act on it.

 

 

As with the hand-held controller, the setup() function of the Arduino is used to initialise the hardware, as well as the data struct (which is the same as the one for the controller). Once this has been done, the main loop of the program listens for a transmission from a hand-held controller. Once a transmission has been received, the main loop does the following:

 

 

1) Checks if the emergStop variable in the received struct is set to true. If so, then it calls the emergencyStop() function. The first thing this function does is set the pin controlling the base station's relay to high. This activates the relay, (which has the normally closed contacts connected between one wire from the DCC booster and the track outputs) and kills the power to the whole layout. Next, the function clears the LED display on the front of the base station, then set it to read '-ES-', using the setChar() function of the LedControl library. It then enters a while loop, which listens for an incoming transmission (the EasyTransfer receiveData() function) while the emergStop variable is true.

 

 

Once a transmission is received with a false emergStop variable, the function turns off the relay, restoring track power. It then clears the LED display and sets it to show the address of the currently selected loco, using the disNumLed() function. This is a helper function I wrote, which takes a number up to 4 digits long and puts it on the LED display, using the LedControl library.

 

 

2) Checks if the received struct has the isDispatch set to true. If so, then it calls the handleDispatch() function.

 

 

Before I outline how handleDispatch() works, I should explain how I have the data set up. When defining a struct in C++, you give the struct a name, and set it as a datatype. You are then able to create variables of this datatype, with each one being a separate copy of the struct you defined. The struct used for receiving data from the controller has been defined as the datatype 'ControllerData', and is set up identically to the struct in the controller.

 

 

To store control information about each locomotive, however, I had to define a second struct, which I named 'LocoData', and which is set up as follows:

 

 

 

struct locoData{
//Used for storing info about which loco is assigned to which controller
int locoAddress; //Loco address
byte addressType; //Loco address type (DCC_SHORT_ADDRESS or DCC_LONG_ADDRESS)
byte func0to4; //Byte to toggle/trigger functions 0-4
byte func5to8; //Same as above for functions 5-8
byte func9to12; //Same as above for functions 9-12
int curSpeed; //Current speed of loco
int curDirection; //Current direction of loco
long lastFunctTrig; //Time the last function from this controller was triggered
};

 

 

Most of these variables are used by the DCCPacketScheduler.h library. In order to be able to keep track of up to 8 controllers at once, I created a global array of LocoData structs, called locoAllocations. The struct for each controller is defined by the controller ID – controller 1 will be allocated the first struct in the array, controller 2 the second, etc.

 

 

Getting back to dispatch mode, when the handleDispatch() function is called, the first thing it does is to check if the locoAddress of the received ControllerData struct is already allocated. To do this, it calls the verifyController() function. This is a simple function, which uses a for loop to go through the locoAllocations array. For each LocoData struct in the array, it pulls out the locoAddress and compares it to the received locoAddress. If they match, then it returns the ID of the controller which has that locomotive address allocated to it. If none of them match, then it returns a value of -1.

 

 

Back in the handleDispatch() function, if verifyController() returns a value other than -1, the function clears the LED display and sets it to show 'A-C<num>', where <num> is the ID of the controller which currently has that loco allocated, for 5 seconds. It then goes back to the main program loop.

 

 

If the locomotive address is not already allocated, then the LocoData struct for that controller in the locoAllocations array will be cleared, then the locoAddress value of that struct will be set to the received locomotive DCC address. The disNumLed() function is then used to display the address of the newly-allocated loco on the LED display on the front. With this done, it then returns to the main program loop.

 

 

3) If the received ControllerData struct has the isDispatch set to false, then the main program loop calls the runMode() function. This processes the received data from the controller and takes the appropriate actions, as follows:

 

 

- Call the verifyController() function to confirm that the controller issuing the command has that locomotive allocated to it. If the function returns a number other than -1, then the runMode() function just ignores the rest of the command and returns to the main program loop. If this is not the case, then runMode() continues processing the rest of the received struct as follows.

 

 

- Check that the speed and direction for the locomotive haven't changed. This is done by comparing the speedStep and dirOfTravel variables in the ControllerData struct with the curSpeed and curDirection values in the relevant LocoData struct. If they have changed, then the curSpeed and curDirection values are multiplied. As curDirection is either 1 or -1, then this produces a positive value for going forwards, or a negative number for going in reverse. Following this, the setSpeed128() function of the DCCPacketScheduler.h library is called and provided with the locomotive address, locmotive type and new address value. The next step is to update the speedUpdateDelay global variable to the current time, via the millis() function. This is a global timer, used to keep track of when the last speed packet was put onto the track. Finally, the curSpeed and curDirection values of the LocoData struct are updated to the new values.

 

 

- Check if the functToggled value of the ControllerData struct is true. This is where it gets a bit involved. The DCCPacketScheduler.h library handles functions by using three functions – setFunctions0to4(), setFunctions5to8() and setFunctions9to12(). Each of these functions takes a locomotive address and a byte as arguments, and uses these to generate and queue a DCC packet which turns the relevant function on or off. Selecting which function is to be toggled is done by flipping the bit in the byte which corresponds to that function. E.g. to turn function 0 (lighting) on, the first bit of the byte is set to 1, then this is fed into the setFunctions0to4() function. Feeding the same byte into setFunctions5to8() would toggle function 5.

 

 

When the runMode() function reaches this point, it first checks to see if the function specified in the ControllerData struct is momentary or not. This is done by calling the functMom() function, which reads the DIP switch for the specified function via the MUX74HC4067 chip. It returns true if the specified switch is on, and false if it's off.

 

 

If the function is momentary, then the next check done is if the functStatus value of the ControllerData struct is true. If so, then the relevant byte from the LocoData struct (func0to4, func5to8, or func9to12) is pulled out of the struct, and the bit for the relevant function is turned on by using the built-in bitSet() function. For functions 5-8, the number of the bit to set is determined by subtracting five from the functToggled value, and by subtracting 9 for functions 9-12. This is because computers start counting from 0, so the first bit in the byte is bit 0, not bit 1. Once the bit has been set, the releveant setFunctions function is called, which queues the DCC packet. Following this, the byte generated is stored in the relevant variable in the LocoData struct.

 

 

If the functStatus is false, then the relevant byte from the LocoData struct is checked to see if it was previously true. If so, then the bit for the relevant function is set to 0 using the bitClear() function, and the relevant setFunction function is used to queue a DCC packet which will turn that function off.

 

 

If the function is a toggled function, it is handled in a manner similar to the above. The difference is when the function is checked to see if it should be turned off. When the functStatus value of the ControllerData struct is true, the bit for the relevant function is checked. If it is 1, then it is set to 0 and if it is 0, it is set to 1. There is then a 250 millisecond delay in order to avoid the function being toggled on/off multiple times during the duration of a single button press. After all, the hand-held controller is transmitting constantly while the function trigger is being held down, in order to toggle the momentary functions.

 

 

- With the speed and functions processed, the runMode() function ends, and the program returns to the main loop. One last check is performed here. Earlier on, I mentioned that after queueing a speed update packet, a global timer called speedUpdateDelay is updated. If it's been at least 20 milliseconds since the last update of the speedUpdateDelay timer, then the program goes over the locoAllocations array via a for loop, and queues a speed packet for each locomotive currently allocated to a controller, using the values stored for that locomotive. This was put in following testing. Originally, the speed update packets were only sent when a locomotive's speed was changed, which meant that if the locomotive stalled or lost contact with the rails, the speed would have to be adjusted to get it going again. Adding this timer and constant update removes this problem.

 

 

Up to this point, all the DCC packets have been queued. The last line in the main program loop calls the update() function of the DCCPacketScheduler.h library. This function takes all the packets in the queue and outputs them onto pin 9 of the Arduino, one after the other. This pin is connected to the DCC input of the Tam Valley booster.

 

 

So that's how the software side of this homebrew DCC system works. I apologise for the wall of text, but I tried to provide as much information about how the program works as possible. Feel free to ask me any questions, and I'll be happy to provide copies of the code for both the hand-held controller and base station on request.

 

 

Part 3 of this write-up will cover how I was able to use another Arduino, working in conjunction with this system, to automate my staging.

 

The Location: Forests of the Pacific Northwest, Oregon
The Year: 1948
The Scale: On30
The Blog: http://bvlcorr.tumblr.com

Subscriber & Member Login

Login, or register today to interact in our online community, comment on articles, receive our newsletter, manage your account online and more!

Users Online

Search the Community

ADVERTISEMENT
ADVERTISEMENT
ADVERTISEMENT
Model Railroader Newsletter See all
Sign up for our FREE e-newsletter and get model railroad news in your inbox!