Skip to content

ZIMO-Elektronik/DCC

Repository files navigation

DCC

build tests

DCC is an acronym for Digital Command Control, a standardized protocol for controlling digital model railways. This C++ library of the same name contains platform-independent code to either decode (decoder) or generate (command station) a DCC signal on the track. For both cases, a typical microcontroller timer with microsecond precision is sufficient for implementing a receiver or transmitter class. Also included, but not platform-independent, is an encoder for the ESP32 RMT peripherals.

The implementation provided here is used in the following products:

Table of contents
  1. Protocol
  2. Features
  3. Getting started
  4. Usage

Protocol

The DCC protocol is defined by various standards published by the National Model Railroad Association (NMRA) and the RailCommunity. The standards are mostly consistent and we have attempted to match the English and German standards in the table below. However, if you can read German, we recommend that you stick to the RCN standards as they are updated more frequently.

NMRA (English) RailCommunity (German)
S-9.1 Electrical Standards for Digital Command Control RCN-210 DCC - Protokoll Bit - Übertragung
S-9.2 Communications Standards For Digital Command Control, All Scales RCN-211 DCC - Protokoll Paketstruktur, Adressbereiche und globale Befehle
S-9.2.1 DCC Extended Packet Formats RCN-212 DCC - Protokoll Betriebsbefehle für Fahrzeugdecoder
S-9.2.1 DCC Extended Packet Formats RCN-213 DCC - Protokoll Betriebsbefehle für Zubehördecoder
S-9.2.1 DCC Extended Packet Formats RCN-214 DCC - Protokoll Konfigurationsbefehle
S-9.2.3 Service Mode For Digital Command Control, All Scales RCN-216 DCC - Protokoll Programmierumgebung
S-9.3.2 Communications Standard for Digital Command Control Basic Decoder Transmission RCN-217 RailCom DCC-Rückmeldeprotokol
S-9.2.1.1 Advanced Extended Packet Formats RCN-218 DCC - Protokoll DCC-A - Automatische Anmeldung
S-9.2.2 Configuration Variables For Digital Command Control, All Scales RCN-225 DCC - Protokoll Konfigurationsvariablen

Features

  • Platform-independent (apart from the ESP32 RMT encoder)
  • Standard-compliant decoding within the bit duration tolerances
  • Support for BiDi (RailCom), a bidirectional extension to the DCC protocol

Receiver

  • Configures itself based on its CV values
  • Supports user-defined BiDi datagrams
  • Supported instructions
    • Multi-function decoders
      • Decoder control
        • Digital decoder reset ❎
        • Hard reset ❎
        • Factory test ❎
        • Set advanced addressing ❎
        • Decoder acknowledgement request ❎
      • Consist control
        • Set consist address ☑️
      • Advanced operations
        • Speed, direction and function ❎
        • Analog function group ❎
        • Special operating modes ☑️
        • 128 speed step control ☑️
      • Speed and direction
        • Basic speed and direction ☑️
      • Function groups
        • F0-F4 ☑️
        • F9-F12 ☑️
        • F5-F8 ☑️
      • Feature expansion
        • Binary state control long form ❎
        • Time and date ❎
        • System time ❎
        • Command station properties identifier ❎
        • F29-F36 ❎
        • F37-F44 ❎
        • F45-F52 ❎
        • F53-F60 ❎
        • F61-F68 ❎
        • Binary state control short form ❎
        • F13-F20 ☑️
        • F21-F28 ☑️
      • CV access
        • Long form ☑️
        • Short form ☑️
    • Accessory decoders
      • Currently not supported ❎

Transmitter

  • Configurable preamble, bit durations and BiDi cutout
  • Supports user-defined packets and transmission of raw bytes

ESP32 RMT encoder

  • Configurable preamble, bit durations and BiDi cutout
  • Only supports transmission of raw bytes

Getting started

Prerequisites

Installation

This library is meant to be consumed with CMake,

# Either by including it with CPM
cpmaddpackage("gh:ZIMO-Elektronik/[email protected]")

# or the FetchContent module
FetchContent_Declare(
  DCC
  GIT_REPOSITORY "https://github.com/ZIMO-Elektronik/DCC"
  GIT_TAG v0.34.0)

target_link_libraries(YourTarget PRIVATE DCC::DCC)

or, on ESP32 platforms, with the IDF Component Manager by adding it to a idf_component.yml file.

dependencies:
  zimo-elektronik/dcc:
    version: "0.34.0"

A number of options are provided to configure various sizes such as the receiver deque length or the maximum packet length. When RAM becomes scarce, deque lengths can be reduced. On the other hand, if the processing of the commands is too slow and cannot be done every few milliseconds, it can make sense to lengthen the deques and batch process several commands at once. Otherwise, we recommend sticking with the defaults.

set(DCC_RX_DEQUE_SIZE
    8u
    CACHE STRING "" FORCE)

Build

The library itself is header-only, so technically it can't be built. However, if run as top-level CMake project then, depending on the target platform, different examples can be built.

Host

On host platforms a REPL example allows a handful of commands to be sent from a simulated command station running in one thread to a simulated decoder running in another.

cmake -Bbuild
cmake --build build --target DCCRepl

Available commands can be listed by using help.

./build/examples/repl/DCCRepl
dcc> help
Commands available:
 - help
        This help message
 - exit
        Quit the session
 - address <Address [0-16383] [default:3]>
        Set address all commands are sent to
 - direction_speed <Direction [1 forward, 0 backward]> <Speed [0-127]>
        Set direction and speed
 - f4-f0 <State [0b00000-0b11111]>
        Functions F4-F0
 - f8-f5 <State [0b00000-0b11111]>
        Functions F8-F5
 - read_cv_byte <CV address [0-1023]>
        Read CV byte
 - write_cv_byte <CV address [0-1023]> <CV value [0-255]>
        Write CV byte
 - read_cv_bit <CV address [0-1023]> <Bit> <Bit position [0-7]>
        Read CV bit
 - write_cv_bit <CV address [0-1023]> <Bit> <Bit position [0-7]>
        Write CV bit

Set speed level 10 in the reverse direction by sending an "advanced operations speed packet".

dcc> direction_speed 0 10
dcc> Read CV byte 28==2
dcc> Address 3: set direction backward
dcc> Address 3: set speed 18

ESP32

On ESP32 platforms examples from the examples subfolder can be built directly using the IDF Frontend.

idf.py create-project-from-example "zimo-elektronik/dcc^0.34.0:esp32"

STM32

An example that runs on STM32 platforms is a decoder and command station pair for a NUCLEO-H743ZI development board.

cmake -Bbuild -GNinja -DARCH="-mcpu=cortex-m7 -mfloat-abi=hard" -DCMAKE_TOOLCHAIN_FILE=CMakeModules/cmake/toolchain-arm-none-eabi-gcc.cmake
cmake --build build --target DCCStm32Decoder DCCStm32CommandStation

This example builds two firmwares, one for the decoder (DCCStm32Decoder.hex) and one for the command station (DCCStm32CommandStation.hex). Both files must be flashed onto a development board each (e.g. with the STM32CubeProgrammer).

Since this example simulates real transmission over a track, it is also necessary to connect the two PE5 pins (N track) and the two PE5 pins (P track) with each other. The command station uses the pins as outputs to send a DCC signal, the decoder uses the pins as inputs to receive the same signal again. The development board with command station firmware can be recognized by the permanently lit red LED.

During ongoing operation, the following steps are repeated in an endless loop:

  1. Accelerate loco "3" to speed step 42 in forward direction
  2. Turn on green LED
  3. Wait for 2s
  4. Set function F3 on loco "3"
  5. Turn on yellow LED
  6. Wait for 2s
  7. Stop loco "3"
  8. Turn off green LED
  9. Wait for 2s
  10. Clear function F3 on loco "3"
  11. Turn off yellow LED
  12. Wait for 2s

There is also a virtual com port (baud rate 115200) on the micro USB plug (CN1) through which the sent/received commands can be monitored.

Usage

Receiver

To create a receiver (decoder) class it is necessary to derive from dcc::rx::CrtpBase. As the name suggest this class relies on CRTP to implement static polymorphism. The template argument of the base is checked with a concept called Decoder. This concept verifies that the following methods can be called from the base. The friend declarations are only necessary if the methods the base needs to call are not public.

#include <dcc/dcc.hpp>

struct Decoder : dcc::rx::CrtpBase<Decoder> {
  friend dcc::rx::CrtpBase<Decoder>;

private:
  // Set direction (1 forward, 0 backward)
  void direction(uint32_t addr, bool dir);

  // Set speed [-1, 255] (regardless of CV settings)
  void speed(uint32_t addr, int32_t speed);

  // Set function inputs
  void function(uint32_t addr, uint32_t mask, uint32_t state);

  // Enter or exit service mode
  void serviceModeHook(bool service_mode);

  // Generate current pulse as service ACK
  void serviceAck();

  // Transmit BiDi
  void transmitBiDi(std::span<uint8_t const> bytes);

  // Read CV
  uint8_t readCv(uint32_t cv_addr, uint8_t byte = 0u);

  // Write CV
  uint8_t writeCv(uint32_t cv_addr, uint8_t byte);

  // Read CV bit
  bool readCv(uint32_t cv_addr, bool bit, uint32_t pos);

  // Write CV bit
  bool writeCv(uint32_t cv_addr, bool bit, uint32_t pos);
};

Implementing the Decoder concept alone is not enough to get a working receiver though. The following points are still necessary:

  1. After instantiating the class, the init method must be called. This triggers the actual configuration and results in a series of CV read calls. Things like the primary address, the number of speed steps or whether BiDi is enabled is determined. These things are intentionally not done in the constructor in case the class is instantiated globally and the CVs aren't available at that point.

    // Initializing the decoder is mandatory
    decoder.init();
  2. The DCC signal on the track must be used as input. At the receiving end, decoding is done by measuring the time between two consecutive zero crossings of the signal. Typically this is done using the capture/compare unit of a hardware timer. The timer triggers a hardware interrupt in which the captured value must be read and passed to the receive method. receive expects a time in microseconds.

    // Timer interrupt handler
    void isr() {
      auto const ccr{TIM->CCR};  // Read capture/compare register
      decoder.receive(ccr);      // Pass captured value in µs
    }
  3. In order to keep the time in handler mode (interrupt context) as short as possible, received packets (with the exception of RCN-218 ones) are not executed immediately. For received packets to be executed, the execute method must be called periodically. This could either be done either inside a super-loop or, as in the snippet below, in an RTOS task.

    // RTOS task
    void task(void*) {
      for (;;) {
        decoder.execute();
        vTaskDelay(pdMS_TO_TICKS(5u));
      }
    }

Warning

During the BiDi cutout, execution is temporarily blocked and may return immediately.

Optional

There are various optional methods that can be implemented if required. One of them are asynchronous CV methods that contain a callback as the last parameter. These methods allow to return immediately and execute the callback at a later point in time. Another addition can enable or disable high-current BiDi if the corresponding bit is set in CV29. And last but not least, the east-west direction according to RCN-212 is supported.

  // Read CV asynchronously
  void readCv(uint32_t cv_addr, uint8_t byte, std::function<void(uint8_t)> cb);

  // Read CV bit asynchronously
  void readCv(uint32_t cv_addr,
              bool bit,
              uint32_t pos,
              std::function<void(bool)> cb);

  // Write CV asynchronously
  void
  writeCv(uint32_t cv_addr, uint8_t byte, std::function<void(uint8_t)> cb);

  // Write CV bit asynchronously
  void writeCv(uint32_t cv_addr,
               bool bit,
               uint32_t pos,
               std::function<void(bool)> cb);

  // High-current BiDi
  void highCurrentBiDi(bool high_current);

  // Set east-west direction
  void eastWestDirection(uint32_t cv_addr, std::optional<bool> dir);

Transmitter

As before for the receiver, for the transmitter (command station) we need to derive from a class, this time from dcc::tx::CrtpBase. The template argument of the base is checked with a concept called CommandStation.

#include <dcc/dcc.hpp>

struct CommandStation : dcc::tx::CrtpBase<CommandStation> {
  friend dcc::tx::CrtpBase<CommandStation>;

private:
  // Write track outputs
  void trackOutputs(bool N, bool P);

  // BiDi start
  void biDiStart();

  // BiDi channel 1
  void biDiChannel1();

  // BiDi channel 2
  void biDiChannel2();

  // BiDi end
  void biDiEnd();
};

Again implementing the CommandStation concept isn't sufficient:

  1. After we have instantiated the class we can configure the track signal by calling the init method. The method takes Config as a parameter and lets us set the number of preamble bits, the bit durations and whether a BiDi cutout should be generated. This step is optional, if init it is not called, then default settings are used.

    // Initializing the command station is optional
    command_station.init({.num_preamble = 17u,
                          .bit1_duration = 58u,
                          .bit0_duration = 100u,
                          .bidi = true});
  2. The DCC signal must be generated as output. A transmitter usually uses an H-bridge for this, in which the left and right sides are switched at dedicated times. The switching times are best maintained with a hardware timer interrupt. The times between the interrupts, i.e. the periods, correspond to the return value of the transmit method. Each time a new period is returned, the hardware timer must be reloaded with it. In order to comply with the standard timings, it is advisable to assign this interrupt a very high priority.

    // Timer interrupt handler
    void isr() {
      auto const arr{command_station.transmit()};  // Get next timer period
      TIM->ARR = arr;                              // Set timer period register
    }

ESP32 RMT encoder

Similar to the other encoders of the ESP-IDF framework, the RMT encoder has only one function to create a new instance. For more information on how to use the encoder please refer to the ESP-IDF Programming Guide or the RMT example.

#include <rmt_dcc_encoder.h>

dcc_encoder_config_t encoder_config{.num_preamble = 17u,
                                    .bidibit_duration = 60u,
                                    .bit1_duration = 58u,
                                    .bit0_duration = 100u,
                                    .endbit_duration = 58u - 24u,
                                    .flags{.invert = false, .zimo0 = true}};
rmt_encoder_handle_t* encoder;
ESP_ERROR_CHECK(rmt_new_dcc_encoder(&encoder_config, &encoder));

The following members of dcc_encoder_config_t may require some explanation.

BiDi bit duration

This duration may be set to values between 57-61 to enable the generation of BiDi cutout bits prior to the next preamble. These four cutout bits would be sent in the background if the cutout was not active. The following graphic from RCN-217 visualizes these bits with a dashed line. BiDi cutout

End bit duration

Mainly due to a workaround of esp-idf #13003 the end bit duration can be adjusted independently of the bit1 duration. This allows the RMT transmission complete callback to be executed at the right time.

Flags

  • invert
    Boolean value which corresponds to the level of the first half bit.

  • zimo0
    Transmit 0-bit prior to preamble.