GitHub - ZIMO-Elektronik/DCC: DCC protocol for controlling digital model railways (original) (raw)
DCC
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:
- ZIMO MN decoders
- ZIMO small- and large-scale MS decoders Table of Contents
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.
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 ❎
- Multi-function decoders
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
- C++23 compatible compiler
- CMake ( >= 3.25 )
- Optional
- for building ESP32 RMT encoder example
* ESP-IDF ( >= 5.0.3 ) - for building STM32 example
* arm-none-eabi-gcc ( >= 12.2.0 )
- for building ESP32 RMT encoder example
Installation
This library is meant to be consumed with CMake,
Either by including it with CPM
cpmaddpackage("gh:ZIMO-Elektronik/DCC@0.40.3")
or the FetchContent module
FetchContent_Declare( DCC GIT_REPOSITORY "https://github.com/ZIMO-Elektronik/DCC" GIT_TAG v0.40.3)
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.40.3"
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 position [0-7]> Read CV bit
- write_cv_bit <CV address [0-1023]> <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.40.3: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:
- Accelerate loco "3" to speed step 42 in forward direction
- Turn on green LED
- Wait for 2s
- Set function F3 on loco "3"
- Turn on yellow LED
- Wait for 2s
- Stop loco "3"
- Turn off green LED
- Wait for 2s
- Clear function F3 on loco "3"
- Turn off yellow LED
- 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 { friend dcc::rx::CrtpBase;
private: // Set direction (1 forward, 0 backward) void direction(uint16_t addr, bool dir);
// Set speed [-1, 255] (regardless of CV settings) void speed(uint16_t addr, int32_t speed);
// Set function inputs void function(uint16_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 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:
- 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(); - 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
} - 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);
// Write CV asynchronously void writeCv(uint32_t cv_addr, uint8_t byte, std::function<void(uint8_t)> cb);
// High-current BiDi void highCurrentBiDi(bool high_current);
// Set east-west direction void eastWestDirection(uint32_t addr, std::optional 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 { friend dcc::tx::CrtpBase;
private: // 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:
- After we have instantiated the class we can configure the track signal by calling the
init
method. The method takesConfig
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, ifinit
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}); - 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
}
Optional
In addition to the mandatory methods, there is also the convenience option of having the track outputs switched by the logic of the base class.
// Write track outputs void trackOutputs(bool N, bool P);
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{.level0 = 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.
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
- level0
Value corresponds to the level of the first half bit. - zimo0
Transmit 0-bit prior to preamble.