Photo shows the Arduino Nano that is used to monitor the DCC signal on the Model Railway Tracks. The red and blue wires go to the model railway tracks. The DCC signal goes to the Nano Interrupt pin (Pin D2) via an opto-coupler (The white chip)
The board on the top right (and the wires) is a PCA9685 Servo Controller that is used in a later project. The cable on the top left is the 5V power pack and the cable on lower right the USB interface to the PC for developing code and displaying the results.
Source code for project.
Header file: "DCC_Probe.h"
Constructor: DCC_Probe - no arguments
Public Methods: begin( ); - initialises D2 as interrupt
DCC_Dump( ); - displays DCC bytes
-----------------------------------
Summary: This project develops a class/library that extracts/captures the commands embedded in the signals on the tracks of a Model Railway DCC (Digital Command Control) system** and displays the DCC bytes. Later projects will expand the class.
Possible Audience: Model railway enthusiasts who are familiar with C++ programming in the Arduino environment and interested in extracting the DCC commands..
Required components: The prototype used an Arduino NANO (A UNO could be used). To extract the signals an Optocoupler is used. The power for the NANO comes from the USB during program development but the power was taken from the tracks for the final testing. Since the time budget is critical a Mixed Signal Oscilloscope/Logic Analyzer is used to evaluate the performance.
Keywords: C++ Class, C++ Library, DCC track protocol, NANO, Interrupts, Interrupt Service Routines, Interrupt modes, attachInterrupt( ), digitalPinToInterrupt( ). static and volatile functions/variables
Software: No additional libraries are required.
** Digital Command Control (DCC) is a standard for a system to operate model railways digitally
-------------------------------
The DCC signal is a PWM (Pulse Width Modulation) signal**:
The DCC signal commences with a pre-amble of at least ten 1s followed by a '0'. There are then several 8 bit packets. Between each packet there is a '0' indicating more data is to follow. After the last packet there is a '1' indicating the message or command is complete++.
A duration of each pulse^^ is captured below:
As captured the Logic one has a period of 118uS++ and the Logic zero 222uS
**This is the signal at the microcontroller. The signal on the tracks will be similar but >12 volts.
++ The captured signal shown is actually the Idle signal that consists of the pre-amble, 8 ones, 8 zeros plus 8 ones so the pre-amble actually starts after the first 8 ones shown.
^^ As captured the signal is changing state on the falling edge. On the other input the signal will be changing state on the rising edge. The rising edge will be used in the example that follows. What edge is used will depend upon how the circuit is wired to the tracks. (If the results do not make sense reverse the wiring).
++ 118us will be the time budget referred to in a later section. That is the software must perform all its actions in this time window.
-------------------------
The final circuit is shown below**. The critical element is the 6N138 opto-coupler that isolates the track signals from the micro-controller.++
** The NANO is powered by a bridge rectifier and 8Volt regulator that derives its power from the tracks.
++ The selection of the wiring to the tracks is important and after trial and error may need to be swapped.
-----------------------------------------------------------------
This project develops a class/library that extracts/captures the commands embedded in the signals on the tracks of a Model Railway DCC (Digital Command Control) system**.That is the project will act as a DCC Sniffer.
Basically the class will time each bit of the data string. If the bits are narrow they are ones, if they are wide they are zeros. These ones and zeros will be built up to form a packet which will contain an address and the actions to take with that address.
The pulse widths will be determined using interrupts**.
** Interrupts have some advantages however, operations such as Serial.print("DCC message") within the interrupt service routines will create sutble problems so are performed outside the interrupt service routine. For debugging purposes Serial.print( )'s will be included in the code but a close eye will be kept on the results to ensure there are no problems. Once debugging is complete the Serial.print( )'s will be commented out.
-----------------------
The proposed starting code is:
//DCC_Probe.ino --application or client code#include "DCC_Probe.h"DCC_Probe DCC; void setup() {DCC.begin();}void loop() {DCC.DCC_Dump();}The code creates an instance/object of the class DCC_Probe.++
As required DCC_Dump( ) will print the current DCC activity to the serial monitor.
The next step is to write the implementation and header code (DCC_Probe.cpp and DCC_Probe.h) to make this all happen in the background**.
++ This project will use interrupts. With interrupts there can only be one instance of DCC_Probe.
** The application code above declares an instance or object of the class DCC_Probe. A "gotcha" is that when there are no parameters parenthesis are not required. With parenthesis the compiler will assume a function and flag an error at a different point in the code.
--------------------------------------------------------------------
The following header file including all the public routines is created along with the implementation file**.
//DCC_Probe.h#ifndef DCC_Probe_H#define DCC_Probe_H#include "Arduino.h"class DCC_Probe {public : DCC_Probe( );void begin( );void DCC_Dump( );private : static void _DCC_ISR( );}; //don't forget this semi colon#endifThe next stage will be to populate the methods starting with the begin( ) method.
** There is the possibillity of a "gotcha" in defining the class. The class definition must terminate with a semi-colon. The complier does not say "missing ;" - it gets lost somewhere else.
-----------------------
The DCC_Probe::begin( ) method will need to
(i) initialise the serial port
(ii) initialise the interrupts
Possible code will be:
void DCC_Probe::begin(){ Serial.begin(115200);Serial.println("\nDCC Capture: "); //optionalattachInterrupt(digitalPinToInterrupt(IRQ_PIN), _DCC_ISR, MODE);pinMode(4,OUTPUT);pinMode(5,OUTPUT);};static void DCC_Probe::_DCC_ISR( ){ }; .....The begin( ) method initialises the serial port and uses attachInterrupt( ) to initialise the interrupt. Interrupts are discussed in the following section. Briefly
(i) The first parameter of attachInterrupt is the interrput number. For the NANO this can be interrupt 0 or interrupt 1 on pins 2 or 3. Rather than use the interrupt number it is reccomended that the function digitalPinToInterrupt(pin) so that the routines may be used with other processors. This project uses pin 2 which is defined in the header file (IRQ_PIN).
(ii) The second parameter is the address or label for the Interrupt Service Routine. Ther underscore is used to indicate the function will be private. A blank method for _DCC_ISR( ) is included. It is also defined in the header file
(iii) The final parameter is the MODE. This is defined in the header file where it is defined as RISING. That is there will be an interrupt for the rising edge. This is also defined in the header file.
To monitor the interrupts pins 4 and 5 will be used so are set as outputs.
The header file.
//DCC_Probe.h.......#define IRQ_PIN 2#define MODE RISINGclass DCC_Probe {public : DCC_Probe( );void begin( );void DCC_Dump( ); private : static void _DCC_ISR( ); }; //don't forget this semi colon#endif-----------------------------
Interrupts are used to achieve rapid response with minimal overhead. In this example the Arduino could be performing other actions in the background** and whenever the input signal transitions (toggles) an interrupt will be generated. There is no overhead waiting (polling) for the input signal.
To handle interrupts the program/sketch must include an Interrupt Service Routine, set up a vector to tell the processor where to go when an interrupt occurs and when the code is ready to handle the interrupts enable interrupts. Enabling interrupts is often a two step process: the interrupt for the relevant sub-system must be enabled and interrupts for the whole processor enabled.
The interrupt service routine must take the appropriate action to handle the interrupt and clear the source of the interrupt.
In operation the program will be executing some code in the background. When an interrupt is requested the micro-controller completes the current instruction and places the address of the next instruction on the stack along with the status register. The processor then jumps to the routine specified in the interrupt vector where it takes the necessary actions. At the conclusion of the interrupt service routine the status register and saved program counter will be returned and the micro-controller will continue executing the background routine from where it left off.
Two important points are that if the interrupt service routine does not clear the source of the interrupt the micro-controller will jump back into the interrupt service routine. To the user it appears as though the program has frozen. Secondly, while the interrupt service routine looks like a standard C/C++ it is different. It cannot include arguments nor can it return values - it is type void.
The attachInterrupt( ) method handles many of the issues mentioned above.
Possible reference (not Arduino) https://sites.google.com/site/johnkneenmicrocontrollers/interrupts
** DCC_Dump( ) is a background operation that will be interrupted
-----------------------------------
With interrupts there can only be one routine to handle the interrupt on pin 2. The interrupt service routine must be declared as static.
Reading from https://www.hellgeeks.com/static-variables-and-static-functions/
"These are the type of data members which are declared with the static keyword in the class. They are basically the part of a class instead of a part of the object. They are created in a memory when their class is created. The major difference between static and normal variables is that, the static data members are shared among all objects of a class, regardless of the number of objects because all the objects have an authority to access, modify and changed it, therefore only one copy of these variables are created in a memory, on the other hand every object of a class has its own copy of normal data members in a memory. Static variables are commonly used when there is no need to define different fields for different objects of a same class".
See also https://www.gammon.com.au/forum/?id=12983
------------------------------
The 1's and 0's are determined by the pulse width. Observing the waveforms there are several possibilities:
(i) Measure the time between each rising (or falling**) signal.
(ii) Measure the time between a rising and falling signal.
(iii) Start timing at a rising edge and if the signal is high after 80uS++ then its a '0'. If low its a '1'.
Method (ii) will require extra code to synchronise the readings to the data stream. Method (iii) will potentially require an additional interrupt after 80uS.
In this project method (i) will be used**.
** Depending upon how the design is wired to the tracks determines if the rising or falling edge should be used. If a logic analyzer is available it is easy to determine the relevant edge. However if a logic analyzer is not available the choice might be trial and error.
++ 80uS is half way between a short ('1') and a long ('0') signal.
-----------------------
The interrupt service routine will:
(i) Toggle pin 13 to verify the interrupts are operational and to allow measurements of the interrupt performance,
(ii) Measure the elapsed time between successive interrupts. This will require setting up several variables _start_time and _end_time**.
(iii) Compare the elapsed time with time DCC_TIME to determine if the data is a "1" or "0". DCC_TIME is defined in the header file as 160uS.
(iv) The "1" or "0" result is printed to the serial monitor++. Since many other actions will be required a separate method handle_bit( ) is used.
On the serial monitor the display will be a continuous stream of 1's and 0's.
Monitoring pin 4 with a logic analyzer gives the following result.:
Note the upper channel indicates the ISR occurring just after the rising edge of the input signal. In this example it is complete well before the next rising edge. (Good)
** Any variables that can be modified by the interrupt service routine must be defined as volatile.
++ The Arduino Reference states that any time routines such as delay( ) and including Serial.print( ) should not be included in the interrupt service routine. Serial,print( ) will be used for debugging but a close eye will be kept out for problems.
-----------------
Since a stream of 1's and 0's whizzing across the screen is impossible to debug the code is modified as illustrated:
(i) A counter is used to keep the display to less than SAMPLE_SIZE (defined in the header file)
(ii) The code, that is the print( ) statement will be conditionally compiled using the DEBUG definition. When the product is complete DEBUG will be defined as 0 so no printing will occur**.
** Other statements will be commented out.
------------------------
The captured results were**:
For clarity to aid explanation with commas added the code becomes.
1111111111111111111,0,11111111,0,00000000,0,11111111,1,
1111111111....etc
(i) The initial string of 1's (and the final 1's) are the pre-amble which is terminated with a "0".
(ii) There are then 8 bits set to 1 followed by a zero terminator.
(iii) The next byte is 8 zeros also with a zero terminator (making 10 zeros an total.
(iv) Finally there is a byte of 8 one's terminated by a "1" indicating the message is complete.
(v)The following ones are part of the preamble for the next message
The captured trace corresponds to an Idle command. The Idle Packet is a three byte packet which has the address (first) byte set to eight 1s, the second byte eight 0 bits, and the third byte is eight 1s. The idle packet is used to provide power to the track when there is no activity. All decoders should ignore the Idle packet.
The next step is to parse each byte in turn.
** The results were obtained using a NCE Twin controller with throttles A and B centered (That is there is no signal for locos A and B) so the data shown are the idle packets.
------------------------------------------------
The DCC message/command passes through a number of states. These are defined using an enum type**.
enum dcc_state { look4preamble, look4preambleEnd, look4zero, buildpkt1, getzero2,alldone} state;
The first step is to synchronise the code to the preamble. The preamble must be at least 10 sequential 1's so the code must first look for a "1", find at least 10 "1's" and then the terminating '0'. This will be done with a state machine. The begin( ) method should be enhanced to initialise the state to "look4preamble".
The handle_bit( ) code now becomes^^:
In the pre-amble state the program will be looking for at least 10 one's. The code must include the definition #define PREAMBLE_SIZE 10 in the header file. There must also be a counter. (preamble_count)
The begin( ) method will initialise the state and the counter:
state = look4preamble;preamble_count = 0;The results on the serial monitor will be the "1's" and "0's" as before but now separated first by an 'F' for Find, then an 'E' looking for 10"one's" and finally a 'Z' looking for the preamble end. ++.
Explanation:
(i) To synchronise the code is looking to find a one. The display will be 1F where "F" signifies Find start of preamble. Once the first '1' is found the code is looking to find the end of the pre-amble (at least ten one's). For each "1" the display will be "1E"
(ii) in the example after only five one's a zero is found ("0E") so it not the preamble so the sequence restarts looking for a one (data "1F")
(iii) This time 7 one's are found in sequence so the code is still not synchronised returns to look for 1F. After ten "0F's" a "1F" is found and the code moves to looking for at least ten "1E's"
(iv) After ten "1E's" the code is looking for a zero (ie "0Z"). This is found after many more "ones".
(v) The code following "0Z" is the next to be developed - as seen there are eight "1Z's" corresponding to the first data byte of the Idle command.
** These states may need to be refined as the design progresses.
^^ To aid debugging the code will display letters in the different states. ie 'F' - find start of pre-amble ( a '1'), 'E' find end of preable after at least 10 '1's' and 'Z' find the terminator (a '0') between the preamble and the first byte.
++ The results will reset back to 'F' if at least 10 "one's" are not found. Since at this point no code has been written for the "Z" state the display will freeze in the "Z" state
---------------------------
After the preamble there are a number of bytes each terminated with a '0' if there are more bits to follow or a '1' for the end of the message.
Following finding "0Z" the state machine will be placed in the state 'P'. (buildpkt1). Here the code will continuously sample 8 bits.
It will be necessary to add the following definitions to the code:
byte_pk[ ] stores the results of up to 8 bytes in the message.
Both byte_count and byte_value are initialised/cleared in the look4zero state.
The new code will become^^
void handle_bit(int the_bit){......................case look4zero : #if DEBUGSerial.print('Z');#endif if (the_bit == 0) {state = buildpkt1;byte_count = 0;byte_val = 0;}break; case buildpkt1 : #if DEBUGSerial.print('P');#endifbyte_count++; byte_val = 2*byte_val + the_bit;if (byte_count == 8){byte_pk[byte_ptr++] = byte_val;state = getzero2;#if DEBUG// Serial.print(byte_val); Serial.print(',');#endif} break;case getzero2 : #if DEBUGSerial.print('G');#endifif (the_bit) { state=alldone;}if (!the_bit) {byte_val = 0;byte_count = 0;state = buildpkt1;} break; case alldone :digitalWrite(5,HIGH); //Time will be critical#if DEBUGSerial.println('#'); #endif if (the_bit) { //message should terminate with a 1.save_bytes( );state = look4preamble;one_mess_done = true;} else Serial.print(" Error"); //should never happen digitalWrite(5,LOW); break; }} }A possible trace is shown below**
As noted interrupts should be kept as short as possible. The most critical interrupt will be at the end of the DCC message when there is additional overhead in transferring the captured data to the buffer "the_bytes[9]". To measure the performance pin 5 will be toggled and observed using a logic timing analyzer. While toggling an output pin does not involve significant time overhead it will use an output pin. Hence there is conditional compilation ("#if LTA" where LTA is defined in the header file.
A possible display triggering on the case alldone is shown below. Shown are two signals prior to the trigger point. These will be part of the data. There will then be the terminator where the code moves to the alldone state that part of the interrupt routine will be to save the data. Note the interrupt service routine is extended in this state. There are then two more (short) interrupts where the code is looking for the preamble to the next packet.
As shown the centre interrupt is 36uS and includes the save_bytes( ) method. The input is complete with a large margin well before the next opto-coupler rising edge**.
** In the example four bytes are transferred: 3,255,0,255 where 3 is the size of the message and 255,0,255 the Idle message. In the worst case the message could be 8 bytes long. Tests were performed with 9 bytes. This extended the interrupt service routine to 46us still well before the next rising edge.
------------------
The previous operations have used interrupts to read the DCC data on the railway tracks and stored it in an array the_bytes[]. When a packet is complete a flag one_mess_done is set.
The DCC_Dump() routine will poll the flag one_mess_done and when it is set write out the data stored in the array the_bytes[]. Idle commands will be ignored.
The resulting output where there are two locos 3 and 4 under control of the NCE Twin is shown below**.
** The actual DCC output is for two locos with addresses 3 and 4. The data is updated every several milli seconds so the it is rapidly flashing across the screen. By setting the speed of one loco to zero eliminates that output so the display for the other loco is easy to follow..
-----------------------------------------------
The DCC_Probe is a real-time system. As such it is important to ensure that the code to extract the measurements does not impact on the system performance**. The results for the DCC_Dump( ) timing are shown in the two traces below. The DCC_Dump( ) method will continuously poll/looping waiting for new data (one_mess_done). DCC_Dump( ) will be periodically interrupted by the interrupt service routine. Areas of no activity in the DCC_Dump trace. This is shown more clearly in the second trace which has expanded the first part of the display. On the third interrupt there is new data so on the next poll the DCC_Dump( ) will output the data to the serial port (the top channel)
(i) In most instances the DCC_Dump( ) routine finds there is no new data so it exits back to the program loop that invokes DCC_Dump( ) again.
(ii) When an interrupt occurs the timing for DCC_Dump( ) is extended/delayed while the interrupt is handled. In the first "delay" the code is in the program loop (trace is low) while in the second "delay" the program was actually executing the DCC_Dump( ) when the interrupt occurred.
(iii) The third delay is longer as here the interrupt routine includes the save_bytes( ) routine.
(iv) In the loop following save_bytes DCC_Dump( ) finds there is new data and writes the data to the serial port (top channel).
(v) As per (ii) when ever an interrupt occurs the DCC_Dump( ) is extended.
The traces verify that the DCC_Dump( ) routine has no impact on the capture of the DCC data. The traces for the lower three channels are identical to what they were before the DCC_Dump( ) was added.
** The appendix illustrates what can happen with the liberal use of Serial.print( ) in debugging the code.These results illustrate why the DCC_Dump( ) method must be performed as a background routine.
------------------------
In the text mention has been made of how the circuit is wired to the track. The track signals are as shown, one signal goes high while the other goes low. For the top signal a "1" is defined by the signal going high for a period and then low for the same time. The lower signal is the reverse.
To measure the times it is important that the wiring and choice of RISING/FALLING edge is consistent (Note the word is consistent) If the Opto-coupler probe is wired to the rising edge as per rthe top waveform then the code must be programmed for the RISING edge. If it is wired for the falling edge then the FALLING edge must be chosen. Unfortunately without being able to analyze the waveforms the choice will not be known. However if the wrong choice is made the results will be obvious** and either the wiring OR choice of edge in the code changed.
**The program as given will ignore the Idle message 225,0,225. If the wrong edge is chosen I have found it will not ignore the Idle message but capture the sequence 127,0,127. If this occurs just swap the wiring. In moving between different layouts I used crocodile clips to wire the circuit.
-------------------
This project has captured the DCC commands/messages/data that is on the model railway tracks and placed the raw data in an array. The data was captured using interrupts to measure the pulse width of each data bit. The routine DCC_Dump( ) is used to display the data, excluding the Idle command in raw format to a serial monitor.
The DCC_Dump( ) will be expanded in the DCC_Debug project to actually display decode the data. For example Loco 3 Forward Speed 20.
The DCC_Loco_Fn project will extract the functions for a specific loco and these will be used as an on/off signal**
** This project is useful in overcoming a limitation of the NCE DCC Twin controller that has provsion for controlling loco functions but not accessories.
---------------------------
Serial.print( ) has been used within the interrupt service routine to print a '0' or a '1' plus a character to aid the debugging**. Taking this one step further each loop calculates the decimal equivalent. With the Idle command this will be 255,0,255. As a check the result can be printed to the monitor after each loop.
As shown there are many "1Z's" the preamble terminated with a "0Z". The code is then investigating a packet ("P") and captures eight "1's". It prints the result "255," then the next terminator "0G" and now for the next packet finds eight "1's" and prints the result 255. This is all as expected except that it has now reached the end of the message and should find "1G" (see trace in previous section) Instead it finds "0G" so concludes it has not reached the end of the message and looks for more data. The code has become completely confused.
Observing the activity on a Logic Timing Analyzer high lights the problem.
The first part of the trace is the preamble and everything looks fine. The preamble terminator (a "0") is at the first cursor. There are then eight "1's" ending at the second cursor. At this point as part of the interrupt service routine "255," is printed. Here there is a problem that is expanded in the next trace.
Looking at the "print 255" (under the word "second") the interrupt is just completed when there is a second interrupt (the first of the eight '0's').
Moving along the interrupt service routine the response to the eight "0's" is fine but the response to the following eight "1's" takes too long to print the message and a complete input, the "1" terminator that says the message is complete is missed.
In summary using Serial.print( ) for debugging overloads the program (interrupt service routine) and critical information is skipped.
The code should be returned to #define DEBUG 0 that will eliminate all of the Serial.print( ) statements.
** All documents on interrupts strongly advise against including Serial.print( ) in the interrupt service routine.
--------------------------------