DCC Sniffer / Diagnostic Tool


If you have an interest in DCC and Arduinos, you may be familiar with RudyB’s DCC sniffer which runs on an Arduino Uno.  I was intrigued by this and downloaded it to play with it.  Initially I loaded it on an Arduino Mega, and found it did not work! 

Inspection of the code shows that it uses the Arduino’s Timer0 registers and direct port access for digital inputs.  The Arduino Mega port allocations are very different to those on the Uno, hence the failure. 

So I loaded it on a Uno.  It worked very well.  I didn’t have the recommended 6N137 Optocoupler, but I put together a circuit using one I had on the shelf (more about that later).


I then thought that it would be interesting to collect some diagnostic information regarding the DCC data stream, such as number of packets per second, number of checksum errors, bit lengths for 0 and 1 bits, unwanted interrupts etc.  Then I found that if there is no valid DCC signal then my bit statistics weren’t output, because of the blocking nature of the bit processing.  So I rewrote a large part of the acquisition logic to be non-blocking and to modify the approach to reading the bit train, but I retained the existing packet decoding more-or-less unchanged. 

The modified sketch decodes bits directly within the digital input interrupt code on each transition of the DCC signal (using the micros() function to determine the length of each half-bit) and queues the data bytes for processing by the loop() code.  Preamble, start and stop bits are validated within the interrupt code and then discarded.

When there is a complete message to process, the loop() code decodes and formats it for sending to the Serial (USB) stream.  It also periodically sends summary statistics to the Serial stream.

I have put the source code of this onto GitHub.  A link to it is at the bottom of the page.

Hardware Using PC817 Optocoupler

When it came to the hardware, I had some PC817 optoisolators in the cupboard so I started by using one of those.  According to the datasheet. it is slow (3-4 µs response time with 100 ohm load) but it is adequate for DCC speeds where we’re monitoring a signal that doesn’t change for 58 µs at a time.  I’ve had perfectly adequate results from the PC817, and it’s cheaper than the 6N137!  It works well with R2 = 220 ohms; reducing it further to 100 ohms does not appear to make any difference.  The value of R1 is chosen to give a forward current through the isolator of 15mA at a nominal DCC voltage of 15V.

DCC Isolator (PC817 based)

However, whilst this is adequate for decoding DCC signals, it is not sufficiently accurate to be able to measure the accuracy of the DCC signal, since the rising and falling edges contribute error on the measured pulse lengths.

6N137 Optocoupler Problems

I’ve tried replacing the PC817 with a 6N137 circuit attributed to Dave Falkenburg, which appears on various DCC web sites and is recommended by most DCC library authors.  However, I couldn’t get it to work in my test environment.  I’ve seen a few internet posts from people who have also struggled to get Rudy’s sniffer to work with this circuit.  Others say the design works well and their problems are caused by Chinese Arduinos / bad batch of couplers / Railcom cut-out  (delete as applicable).  Whatever the cause, it seemed that further investigation was warranted, particularly since I could easily reproduce the problem.  The circuit is shown here for reference:

Dave Falkenburg’s DCC Isolator

I carried out my tests using the circuit above, with a DCC signal of 15V (30V peak to peak) generated by DCC++ EX.   The sniffer was quiet and listed no packet information – instead, checksum errors were reported on all packets!  If I removed D1 for a short period of time, the sniffer reported and decoded DCC packets with no checksum errors.

Bit Count=24348 (Zeros=10059, Ones=14289)
Packets received=452, Checksum Error=0, Lost pkts=0, Long pkts=0
0 half-bit length (us): 116.0 (108-124), 1 half-bit length (us): 58.0 (50-66)
IRC Duration (us): 18.7 (16-23),  CPU load: 28%
Loc 6889 Rev128  Stop   11011010 11101001 00111111 00000000 
Loc 1234 Rev128  Stop   11000100 11010010 00111111 00000000 

If I reinstated D1, the sniffer stopped decoding and, again, reported checksum errors. Also, the bit lengths being reported were very strange compared to those with D1 removed.

Bit Count=36219 (Zeros=9982, Ones=26236)
Packets received=493, Checksum Error=493, Lost pkts=0, Long pkts=0
0 half-bit length (us): 105.7 (87-123), 1 half-bit length (us): 35.2 (19-65)
IRC Duration (us): 17.2 (15-22),  CPU load: 37%

Monitoring pin 6 with a logic analyser, I observed a spurious pulse (similar to contact bounce on a switch) on transitions from logic 1 to logic 0 of the output. Only one transition is shown here, but the spurious pulse was evident on every transition from 1 to 0.

Sigrok Capture of Output of Optocoupler Showing Ringing Effect

DCC Signal

I looked at the DCC signal using my USB scope (Hantek 6022BL). It is evident that there is ringing at the point where the signal crosses zero. This most probably originates in the H-bridge drivers on the output of the DCC Command Station. The H-bridge alternates between two states: in one state, one output is connected to +Vss and the other is connected to 0; in the other state, the connections are reversed. During the transition between states, there is a short time where one or both outputs are moving between 0 and +Vss or, possibly, are connected to neither. In this region the DCC signal voltage is not well defined. The sample below shows a positive-to-negative transition and a negative-to-positive transition, superimposed. [Here I was using a 12V supply, the more eagle-eyed of you will notice.]

Oscilloscope Capture of DCC Signal Transitions Produced by Arduino Motor Shield (DCC++ EX)

The NMRA specification S-9.1 (2020) states the requirements for compliance in this area:

  • Transitions that cross the region between -4V and +4V shall occur at the rate of 2.4V/usec or faster.
  • This signal may contain non-monotonic distortion at the zero-crossing transitions, provided that this distortion shall have an amplitude of no greater than +/-2V.

This means that the signal produced by an NMRA-compliant Control Station is permitted to be in the region between -4V and +4V for up to 3.33 us, and may oscillate by up to 2 volts either way (up to 4V peak-to-peak) during this period. The signal shown on the sample above is within these criteria.

Effect on Optocoupler Signal

Using the scope, the following was seen across pins 2 & 3 (the input of the optocoupler), showing oscillation or ‘ringing’ effect when the DCC signal crosses zero:

Oscilloscope Trace of Optocoupler Input Showing Ringing at Zero Crossing

and the following between pins 6 (the output of the optocoupler), and 5 (ground):

Oscilloscope Trace of Optocoupler Output Showing Effect of Ringing

What To Do About It?

There are a number of possible solutions to this problem which could be implemented separately or together, for example:

  1. As with switch contacts, the ringing could be suppressed within the software; some DCC decoder libraries and algorithms ignore pulses shorter than a few milliseconds by design, and are not affected.  Examples include the NmraDCC library.   Some programs, like Rudy’s sniffer and the MynaBay DCC library fail to decode the DCC signal when ringing is present on the signal.
  2. We could add a small capacitor between pins 5 and 6 of the 6N137 to smooth out the ringing.  On Rudy’s page, Kirk commented (October 15th, 2020) that this approach worked for him with a 20pF capacitor.  Wolfgang Kuffer’s circuit also has a 10pF capacitor here.  However, this capacitor has more impact on the rising edge of the output (charging through R2) than on the falling edge (discharging via the 6N137’s output) so is not ideal when trying to measure timing accurately.
  3. We could add a small capacitor across the input to the optocoupler.  This would act as a low-pass filter and remove sharp edges from the DCC signal and hence reduce or remove the oscillation. Where people have reported that the circuit is working without modification, it is plausible that the capacitance of the layout and wiring is performing this role.  GeoffB’s circuit has 270pF across the input to the 6N137, and the commercial ARD-DCCSHIELD has 330pF.  Based on an RC network with a 1µs time constant, a value of 1nF would be acceptable.
  4. We could remove the 1N4148 diode, but that would expose the input of the 6N137 to a higher reverse voltage for alternate half-cycles (i.e. the full DCC voltage of 12-15V) unless the voltage is reduced, e.g. by a potential divider.

I tested option 4, replacing the diode with a 330 ohm resistor (R2) and reducing the series resistor to 820 ohms (R1);  at normal DCC voltages (below 17.5V), this limits the reverse voltage to below 5V and the forward current to within the 5-15mA operating range for the 6N137.  The pull-up resistor connected to pin 7 is unnecessary and has been removed, and the pull-up resistor on pin 6 has been reduced to 1K in line with the recommendations in the 6N137 datasheet. This looked good and the circuit is functional, i.e. the software is now successfully decoding all of the DCC packets and no glitches reported. However, when I later looked at the output signal (pin 6) using an oscilloscope, I noticed that the rising edge of the signal is slow (~3-4µs), much longer than the figure of tens of nanoseconds quoted for this component. Replacing the pull-up resistor with a 330 ohm resister (the minimum quoted in the 6N137 datasheet) makes the edge sharp (rise-time < 1µs). With this configuration, the diode D1 may be reinstated across pins 2 and 3 without affecting the performance.

I also explored option 3, i.e. to add a ceramic capacitor parallel to the 1N4148.  I chose a capacitor of 1nF since 1nF x 1K = 1µs, which is a small enough delay to be negligible in DCC, but large enough to remove pulses of <0.5µs or so.  Under test, this eliminated all unwanted interrupts, while a value of 220pF had no effect and still produced over 5000 unwanted interrupts per second.

The scope now shows the following transition between pins 2 and 3 of the Optocoupler, a clean edge without ringing:

Oscilloscope Trace of Optocoupler Input Without Ringing

If it is desired that an LED be illuminated to indicate track voltage, it may be substituted in place of D1.  My own sniffer drives the Arduino Pin 13 LED through software, so is outside the scope of the optocoupler circuit.

For information, my measurements were carried out on a test system with about 1 metre of track, and no locos or other connections to the track, other than the standard optocoupler circuit.  When I added a loco to the track, the ringing reduced significantly.  This is largely consistent with observations that the reports of problems with the sniffer/optocoupler combination were from people who were experimenting;  those using the sniffer (or a decoder based on the same optocoupler) ‘in the field’ within larger layouts did not see any problems.  Also, reducing the DCC voltage from 15V to below 12V removed the glitches.  Further, and somewhat surprisingly, removing R3 and using the Arduino’s internal pull-up resistor eliminated the glitches, although the Arduino pull-up is in the region of 30-50 kOhms, well outside the range recommended in the 6N137’s datasheet.

UPDATE 9th Feb 2021: As part of the ongoing development of the diagnostic program, I’ve been further testing different configurations of the Optocoupler circuit exposed to different DCC voltages and also at 5V TTL levels, with the following conclusions:

  1. At 5V TTL levels there is no ringing, as the LED is never reverse-biased and the 1N4148 is always reverse biased. Consequently no capacitor is required across the input in this case, and in fact a capacitor may slow the response.
  2. At DCC power levels (+/- 12 to 18V), a capacitor is necessary to smooth the ringing or ‘glitches’ (unless the decoder is able to filter the glitches through software). A 100nF capacitor across the input DCC terminals (labelled DCC1 and DCC2 in the circuit diagram) is sufficient to remove the glitches without affecting the response time of the circuit significantly. However, if every decoder has such a capacitor, the effects is additive, so the signal becomes slower with each one added until decoders cease working. This is not desirable.
  3. The capacitor across the input of the Optocoupler only starts to charge or discharge once the DCC voltage is less than the forward voltage of the respective diode. Therefore, oscillations in the DCC voltage within this region will have a large effect on the Optocoupler current flow. A potential divider to reduce the DCC voltage would help to reduce this effect.

Consequently, my preference is to put a capacitor across the input pins of the optocoupler to reduce or remove the ringing effect on the DCC signal observed around the zero volt transition. I’ve also tried various resistors for pull-up on pin 6. With 470 ohms, the rise time and fall time observed on a scope are of the order of 150ns. Using an internal pull-up on the Arduino and ESP, the rise time is over 5us, and with 10kOhm resistor it is around 1-2 us. As previously mentioned, the pull-up resistor on pin 7 has been removed as it serves no purpose.

Optocoupler Output with only the Arduino Internal Pull-up Resistor
Output of Optocoupler with 10 kOhm Pull-up Resistor
Recommended Optocoupler Circuit with Capacitor

With this circuit, the following signal was observed across pins 5 and 6 of the optocoupler:

Output of Optocoupler with 470 Ohm Pull-up Resistor

Adapting for 3.3V Microcontrollers

For 3.3V controllers such as the ESP32 and ESP8266, the voltage at the input pin to the controller should not exceed the power supply voltage, i.e. 3.3V. This can be achieved by connecting the top end of the pull-up resistor (R3) to +3.3V (instead of +5V). Ideally the optocoupler should still be supplied with 5V to pin 8 but I have had good results using a supply of 3.3V. Because of the lower supply voltage, I reduce the value of R3 to 330 ohms for this configuration.

Output Format and Contents

Example output from the Diagnostic Sniffer is shown below. Like Rudy’s original sniffer, components of the output can be enabled/disabled through the terminal window, although the list of commands has been expanded. Press “?” followed by Enter to see a list of commands (or have a look at the source code below!). Initially, the 1nF capacitor C1 is not present, and many ‘glitches’ are reported and discarded. Between the first set of diagnostic counts and the second, I switched off the glitch filter using the “F” command; the bit count increased, and all packets are now rejected because of checksum errors on the signal. Between the second and third sets, I inserted the 1nF capacitor across pins 2 and 3 of the 6N137; the number of interrupt glitches reduce to zero.

DCC Packet Analyze initialising...  done.
Updates every 4 seconds
Loc 7130 Forw128  Stop   11011011 11011010 00111111 10000000 
Loc 6889 Rev128  Stop   11011010 11101001 00111111 00000000 
Loc 1234 Rev128  Stop   11000100 11010010 00111111 00000000 
Loc 3 Forw128  Stop   00000011 00111111 10000000 
Loc 3 L F4-F1 0  00000011 10000000 
Loc 8683 Forw128  Stop   11100001 11101011 00111111 10000000 
Loc 6039 Rev128  Stop   11010111 10010111 00111111 00000000 
Bit Count=24348 (Zeros=10256, Ones=14092), Glitches=22663

Packets received=452, Checksum Error=0, Lost pkts=0, Long pkts=0
0 half-bit length (us): 116.0 (108-124), 1 half-bit length (us): 58.0 (49-67)
IRC Duration (us): 18.4 (16-22),  CPU load: 25%
Loc 3 Forw128  Stop   00000011 00111111 10000000 
Loc 3 L F4-F1 0  00000011 10000000 
Loc 8683 Forw128  Stop   11100001 11101011 00111111 10000000 
Loc 6039 Rev128  Stop   11010111 10010111 00111111 00000000 
Loc 7130 Forw128  Stop   11011011 11011010 00111111 10000000 
Loc 6889 Rev128  Stop   11011010 11101001 00111111 00000000 
Loc 1234 Rev128  Stop   11000100 11010010 00111111 00000000 
filter input = 0
Bit Count=35609 (Zeros=10257, Ones=25352), Glitches=0
Packets received=480, Checksum Error=480, Lost pkts=0, Long pkts=0
0 half-bit length (us): 106.7 (87-124), 1 half-bit length (us): 36.0 (18-68)
IRC Duration (us): 17.0 (16-22),  CPU load: 31%
Bit Count=24350 (Zeros=10254, Ones=14096), Glitches=0

Packets received=451, Checksum Error=0, Lost pkts=0, Long pkts=0
0 half-bit length (us): 116.0 (107-125), 1 half-bit length (us): 58.0 (48-68)
IRC Duration (us): 18.4 (16-22),  CPU load: 27%
Loc 6889 Rev128  Stop   11011010 11101001 00111111 00000000 
Loc 1234 Rev128  Stop   11000100 11010010 00111111 00000000 
Loc 3 Forw128  Stop   00000011 00111111 10000000 
Loc 3 L F4-F1 0  00000011 10000000 
Loc 8683 Forw128  Stop   11100001 11101011 00111111 10000000 
Loc 6039 Rev128  Stop   11010111 10010111 00111111 00000000 
Loc 7130 Forw128  Stop   11011011 11011010 00111111 10000000

The diagnostics shown include:

  • Number of bits detected (total, ‘1’s and ‘0’s). Includes framing bits and data bits.
  • Number of unwanted fleeting interrupts (glitches). This is a ‘change’ interrupt that is measured to be 3µs or less after the preceding valid interrupt, or a ‘change’ interrupt where the digital input state has not apparently changed (i.e. the time between consecutive input transitions is too short for the interrupt response code to execute).
  • Number of DCC packets received, before checksum validation.
  • Number of packets rejected for checksum errors.
  • Number of lost packets (i.e. packets that were discarded because of running out of buffers within the sniffer).
  • Number of packets discarded because they were longer than the buffer size within the sniffer.
  • Average, minimum and maximum half-bit length, for ‘0’ bits and for ‘1’ bits.
  • Estimated duration of the interrupt response code.
  • CPU loading caused by DCC processing as a percentage. Zero percent corresponds to no interrupts (i.e. the sniffer is just doing its base work) and 100 percent corresponds to full loading, i.e. the loop() function always processes some data on every execution with no spare loop cycles. It is evident from above that the load is not excessive (25% running on an Arduino Uno with continuous DCC signal).
  • Optional break-down by pulse length of the number of positive and negative half-bits detected.

In addition, the Diagnostic Sniffer may be configured to control LEDs to aid testing of the DCC. This opens up possibilities for a stand-alone device powered by battery, or from rectified DCC power, which shows the DCC’s state of health through the LEDs.

  • An LED may be configured to brighten or dim according to the speed setting associated with Loco address 3.  This allows the response to a throttle to be visually confirmed, without requiring a loco or track.
  • An LED may be configured to light if, within a sample period, an error has been encountered, i.e. an interrupt ‘glitch’, a lost or long packet, or a checksum error.  The LED is extinguished at the end of the sample period.
  • An LED may be configured to light when a DCC packet has been successfully received without errors.  It is extinguished once the packet decoding has been completed.
    An LED may be configured to light during the interrupt processing, if the interrupt is accepted (i.e. not a glitch).

Using the same principles, further signal conditions may be monitored and indicated by LED.

Output Bit Length Analysis

I should say something about the half-bit length values shown in the log above, i.e.:

0 half-bit length (us): 116.0 (108-124), 1 half-bit length (us): 58.0 (49-67)

I have recently started migrating from DCC++ to DCC++ EX, which is a plug-in replacement for DCC++ (same hardware and interfaces) and has been re-written from scratch, with some interesting new functionality.  In the NMRA standard, nominal DCC half-bit lengths are 58µs for a ‘1’ bit and 100µs for a ‘0’ bit, although ‘0’ bits may be transmitted with a half-bit length up to 9900µs.  The DCC++ EX implementation simplifies the clock requirements by sending ‘0’ bits as 116µs, i.e. exactly twice the length of a ‘1’ bit, thereby using only one 58µs regular clock interrupt to drive both the Main and Programming tracks (DCC++ needed two independent irregular clocks).  The values shown for the average half-bit length reflect this (116.0 µs and 58.0 µs).   [Note that some decoder libraries require ‘Stretched bits’ to be enabled in order to allow the 116µs ‘0’ bits to be accepted (e.g. in the NmraDcc library it can be disabled, but is enabled by default).]

The figures shown in brackets are the minimum and maximum observed values.  These are wider apart than one might expect, but can, at least partly, be explained by the Arduino’s clock interrupts which occur every 1ms and take at least 6.5µs to complete (measured using a logic analyser to look for irregularities on an output being toggled by the Arduino loop at around 1.33MHz).  Additional time may be taken by application-specific timer interrupt code.  Whenever a clock interrupt in the DCC++ EX Controller Arduino coincides with a bit transition, the transition may be delayed by up to 7µs plus any application-specific interrupt execution, and whenever a clock interrupt in the Sniffer Arduino coincides with a bit transition, its recognition may be delayed by the same amount.  Any Arduino-based DCC decoder must take this into consideration to avoid spurious errors.  The Diagnostic Sniffer typically records a variation in half-bit length of +/- 8µs.  Further, when I look at the DCC++ EX signal using the Sigrok logic analyser and littleyoda’s excellent DCC decoder, I can confirm that some bit transitions are delayed.  In the example shown below, a ‘1’ bit is composed of half-bits of 66µs and 50µs, i.e. 58+8µs and 58-8µs (rounded to nearest microsecond), and is rejected by the DCC decoder, causing it to fail to decode the remainder of the DCC packet.

Sample of delayed pulse transition from DCC-ex

The DCC++ EX pulses are generally recorded by the signal analyser as 118µs and 114µs, or 60µs and 56µs, perhaps due to asymmetric slew rates in the motor driver module or perhaps just because the opto-coupler is only looking at the positive half of the incoming DCC wave.

Note that the MynaBay DCC decoder library attempts to follow the NMRA specification regarding the length of the ‘1’ bit, and will reject bits whose measured half-period is less than 52µs or greater than 64µs.  However, with a measurement error of up to 4µs from the micros() function, the decoder is only guaranteed to accept pulses between 56µs and 60µs.  In addition, it will discard any DCC packet where a bit transition coincides with an Arduino clock interrupt in the receiver, because of an additional 6.5µs error induced by the Timer0 clock interrupt.  For reliable detection, the valid range of pulse measurements for a ‘1’ bit should be increased to 42µs to 74µs.  Similarly, for the ‘0’ pulse the minimum pulse measurement should be reduced to 80µs.  Although the DCC protocol has enough retransmissions that you are unlikely to notice this in use, it is a simple edit to change the definitions of the valid pulse lengths in your copy of the decoder library.

Sketch Code

The full sketch code is now on GitHub on the DCC++EX web site as DCCInspector-EX.  It is designed to work best on Arduino Uno, Nano and Mega and on the ESP32 platform.  All of these support an Input Capture mode where the current value of a timer counter is captured when a digital input changes state.   This allows much higher accuracy timing since the timer capture is not dependent on scheduling an interrupt – it typically happens within a single clock cycle of the digital input change.

Also, the ESP32 supports statistics viewing on an OLED or via WiFi using a web browser. 

Standalone DCC Monitor Showing DCC Statistics
Standalone DCC Monitor and DCC Controller
Standalone DCC Monitor Showing DCC Packet Trace
Standalone DCC Monitor Web Interface

1 Comment

Leave a Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s