wavedcc - DCC Command Station running on a Raspberry Pi...

Glenn Butcher Jun 29, 2021

  1. Glenn Butcher

    Glenn Butcher TrainBoard Member

    167
    306
    9
    Contemplating the HOn3 shelf layout I want to build, I dug out my circa late '90s Digitrax DCS100, stared at it for a bit, then said, "Nah....."

    Been playing with Raspberry Pi SBCs for a few years now, so I researched what has been done with them in DCC applications. Found a few bit-bang concepts, but the scaling to practical application would always run into the context-switching multi-program OS, which plays havoc with generating the continuous pulse train DCC requires. But, in that research I also found a feature of libpigpio (yes, it was meant to be pi-gpio, but I can't get past pig-pio... :D ) called waves. A set of functions in the pigpio library that allow one to build waveforms and pass them to a mechanism in the library that does "DMA-gated PWM", direct-memory-access-gated pulse-width modulation. This mechanism will then play the waveform on the designated GPIO(s) with the DMA hardware, leaving your program to do other things like assembling the next waveform. Indeed, the libpigpio documentation has a code snippet that does precisely that, play a waveform, tag a following waveform for playing, rinse-and-repeat. So, I set out to mangle that into a DCC command station...

    The code I've developed to-date is here: https://github.com/butcherg/wavedcc

    A real work-in-progress, as of today it does the full baseline S9.2 packet set, S9.2.1 extended speed/dir, and ops mode programming, wrapped in a DCC++ EX command interface. There are two programs, wavedcc and wavedccd, the former does a shell command line interface using DCC++ EX commands (you don't have to type the <>"), and the latter is a TCP server that can be connected to in JMRI with the DCC++ specification. I've just started connecting it to JMRI today, so that interface may still be quite buggy.

    There are two ways to compile the programs, one using the pigpio library directly, the other using the pigpio daemon interface (Raspberry Pi OS runs the daemon by default). With the direct interface, wavedccd uses 7-12% CPU, with the pigpiod interface the load is 45%, roughly split between wavedccd and pigpiod.

    For those interested in the guts, the libpigpio waveform functions let me build DCC packets as pulsetrains. I encapsulated that in a C++ DCCPacket class that has helper methods to construct preambles, short/long addresses, checksums, so coding up a new packet has become simple and reliable. In the dccengine files you'll find three functions: dccInit(), dccCommand(), and dccFinish(), which are used by the wavedcc* applications to do DCC. In dccengine.cpp you'll find a function called runDCC(), designed to be started as a thread, which just implements the libpigpio example that sequences a pulstrain, a following pulsetrain, then does it again when the second pulsetrain kicks off transmission. I wrote CommandQueue and Roster classes to feed the beast, with appropriate mutex locking to keep the command processor from stepping on the runDCC() thread. Next software work is to get functions going; I have the packets coded, just need to write the command interface.

    Regarding JMRI, I put in some contrivances today to keep it from complaining so much. Those will eventually need to be addressed if wavedcc is to become a first-class JMRI citizen. But, I think it could function indefinitely as a DCC++ work-alike. I also tried to set up the code to accommodate other command interfaces, if that floats anyone's boat.

    Presently, I'm running this on a Raspberry Pi 3B with Raspberry Pi OS, connected to one of those commodity L298n motor driver boards that's in turn connected to a short piece of N scale track with two locomotives equipped with Digitrax DN140 (that should date me...) decoders. Works like a treat driving the two locos back and forth, and I can ops-mode-program CVs. Next hardware work is to incorporate current-sense for a full and separate service-mode programming track. Of note is that libpigpio only supports one waveform execution at a time, so for the forseeable future I'll not be able to do ops and programming simultaneously on a single Pi.

    Computing is really just a big layer cake of abstractions, each succeeding layer making it easy for someone to do something on their terms. I think I found the right layer in libpigpio to run DCC natively on the Raspberry Pi... comments and critiques are welcome. I'll also consider pull-requests from folks that want to contribute, and I'm presenting the code base with a GPL 3.0 license, so fork-and-go-off-on-your-own is completely legal. And I don't mind...

    FWIW...
     
    drbnc, BigJake, Sumner and 2 others like this.
  2. BigJake

    BigJake TrainBoard Member

    3,259
    6,173
    70
    Glenn,

    Nice work! I looked into this scheme a while back, but never went anywhere with it. The DMA approach seemed like a great solution, but Raspberry Pi's used to have a conflict/limitation that had to do with disabling audio when using this (or a similar scheme). And there were some changes WRT Pi 4b, IIRC.

    Does your approach interfere with audio playback on a Pi 4b?

    I plan to use JMRI Virtual Sound Decoder, so I'll need audio for that.

    I've since gone with a Pi SPROG 3 system, and no longer need the Pi to manage the DCC waveform, but I'm glad to see you got it working!
     
    Glenn Butcher likes this.
  3. Glenn Butcher

    Glenn Butcher TrainBoard Member

    167
    306
    9
    It looks like it does interfere: http://abyz.me.uk/rpi/pigpio/faq.html#Sound_isnt_working

    Not only that, the scheme as I implemented it doesn't support simultaneous ops and service mode. I think it's possible, but it would take a pulsetrain multiplex solution that presently hurts my head... :D

    I'm in HOn3 (although my early testing has been with my old N scale stuff, which started calling to me... ), with just one circa 2007 Blackstone K-27 to push cars around on a small shelf layout, so a single RPi with a hardware switched programming track will probably work ok for me. Gee, one loco, I could just do DC... nah, no fun in that.

    I've spent the past 5 years or so writing a raw processing program for photography; it has such an esoteric interface I'm likely to be the only user. I forsee a similar outcome for wavedcc. I at least want to get a decent level of interoperability with JMRI; the last major hurdle for that is reading CVs. I've ordered some 2W .5ohm resistors to drive a current threshold flag, so that work will likely happen next week.
     
  4. BigJake

    BigJake TrainBoard Member

    3,259
    6,173
    70
    Simultaneous ops and service mode, on the same track bus, would be impossible, IINM. The reason is that if trains are running on the same track on which service mode programming is underway, the response current pulses of the locomotive being service-mode programmed are masked by the current draw of the operating locomotives.

    Now, if you have separate program and operating track bus outputs, then some systems will let you use both simultaneously, automatically sending service- and ops-mode packets to the respective track bus.

    Railcom compatible systems (an extension of DCC) can provide feedback from the main track bus, and thus may be able to do this. But the CS and all on-track-bus decoders must be railcom-compatible.

    For the protocol to use between JMRI and your SW CS, you might look at the CBUS protocol used by the SPROG 3 Plus, Pi SPROG 3 v2 Pi SPROG 3 Plus, as well as MERG (who developed it). For some more information, see: https://www.sprog-dcc.co.uk/downloads/Sprog3PlusUserGuide.pdf

    You might also look at NCE's Auto_SW module that automatically routes ops-mode dcc messages to one output, and service mode DCC messages to another output, from a single DCC track bus input, to prevent inadvertently sending service mode broadcast programming commands to the layout.
     
  5. BigJake

    BigJake TrainBoard Member

    3,259
    6,173
    70
  6. Glenn Butcher

    Glenn Butcher TrainBoard Member

    167
    306
    9
    Oh, without a doubt. My code right now protects that two ways: 1) 'running' and 'programming' booleans, which I use to guard the commands from being executed in the wrong mode, and 2) separate MAIN and PROG variables for the GPIO pins used. When the programmer constructs a packet pulse train, they have to specify the GPIO pins to be modulated. Calls to the make*Packet routines for the service mode packets specify the PROG GPIOs, the calls to the other make*Packet routines use the MAIN GPIOs.

    The scheme I'd envision to have one RPi run simultaneous ops and service modes (always separate GPIOs) is to use the ability to drive multiple GPIOs in the same waveform. I already do that for single-packet waveforms to make a bipolar signal with two GPIOs; it looks to be straightforward to just add the pulse transitions for the service mode GPIOs to the same waveform. The bugaboo comes in different ops and service packet sizes, as the waveform would have to run for the length of the longest packet. The solution might be as simple as padding the shorter packet with '1's to make a long preamble for the next packet. The waveform interface in libpigpio was very well-designed, considering the scarcity of the PWM resource.

    Frankly though, I'm not really inclined to pursue such, as my needs are small and I have a number of RPis laying around, so having separate ops and service hardware is not onerous.
     
    BigJake likes this.
  7. BigJake

    BigJake TrainBoard Member

    3,259
    6,173
    70
    You might also think about being able to designate one GPIO (pair) as auto-reversible WRT to another, upon excessive current draw in running mode, such that it could be use for a reverse loop.

    How you want to do the two signals of a track bus is up to you. I would probably use a single signal and an enable, and have the driver hardware create the differential outputs, driven only when enabled. Small mismatches in timing between the two phases of the differential signal can create a LOT of noise.

    In the case of the enable signal to the driver hardware, it could enable/disable both the main and auto-reversed track outputs.
     
    Last edited: Jul 3, 2021
    Glenn Butcher likes this.
  8. Glenn Butcher

    Glenn Butcher TrainBoard Member

    167
    306
    9
    I wrote a cmake build script, works basically the same way as the Makefile except you don't have to edit the file to switch direct/pigpiod compilation. I also updated the README to describe the cmake build process, and also updated the rest of the file. Particularly, I added a limitations section.

    I'm awaiting delivery of a package from Adafruit with, among other things, two current sense boards. With those, I'll be able to write the DCC CV read command. That'll be it for new development for a while; my focus will shift to making it work well with JMRI and bug fixes. After a few months of that, I'll tag a 1.0 release and get started on the shelf layout I really want to build... :D

    https://github.com/butcherg/wavedcc
     
    BigJake likes this.
  9. Glenn Butcher

    Glenn Butcher TrainBoard Member

    167
    306
    9
    So, I've now built rudimentary versions of most DCC functionality into wavedcc: run trains, write CVs on the main, read and write CVs on the programming track. Still missing a few things, like headlight control (where did I miss that?) and overload shutoff (coming in a few commits). I'm also playing with a few things, like locomotive-adaptive baseline current for CV writing, and faster CV writing, but that will probably run afoul of the DCC spec. I'm going to play with it for a week or so, and then probably declare a version 0.1 (It would be too pretentious to declare a 1.0 on such untested software... :D )

    Here's a picture of the hardware setup:

    DSZ_8768.jpg

    Really pretty simple, a l298n motor driver driven and enable/disabled by RPi GPIOs 17,27, and 22. An I2C connection to an Adafruit INA219 current-sense module, which is inserted into power ahead of the L298n. The world's shortest DCC-layout connection. Off-image is a MRC Tech II 1500 power pack; I'm using the layout DC to feed about 15v into the L298n. Also off-image to the left is a Blackstone K-27, which happily chuffs, whistles, and bell-rings to my hand-crafted DCC signal.

    I've been pretty surprised regarding how easy it was to do this. A lot of poo-pooing on the internets about the RPi's inability to modulate signals, but the libpigpio library and its waveform routines really is almost tailor-made for this application. And the signal is really pretty tight to the spec, according to the piscope renderings. CPU% for CV writing is about 12% for direct, but pigpio goes to 62%, 36% for pigpiod and 26% for wavedccd. One could definitely use the direct version with JMRI on the Pi, but the pigpiod version, not so much IMHO...

    I hope you DCC++ folks don't mind that I absconded with your command protocol; made it easy see how my code would play with JMRI. I'm going to craft some PRs to integrate wavedcc with JMRI at some point, still trying to figure out how to engage them.

    Anyway, at this point it's more about "soaking" it, to use a term from my day job, beating on it to find the brittles. I don't have a real layout to run any sort of realistic workload, so if some decide to try it on their layout I'd appreciate any feedback. Multiple sets of eyes on the code is a good thing too, whether you clone my repo or fork it and go off on your own, I'm good either way.
     
    Sumner likes this.
  10. BigJake

    BigJake TrainBoard Member

    3,259
    6,173
    70
    It is interesting to follow your efforts (and success!) I'm curious to see how well it runs with JMRI also executing on the R-Pi, with GUI, running the Wi-Throttle server, etc.

    Or is your intention to dedicate the R-Pi to running WaveDCC, and use a separate computer (perhaps another R-Pi) to run JMRI?

    I run a Pi SPROG 3 hat that has its own microcontroller and motor drivers, current sense, etc., leaving the R-Pi free to run JMRI, etc.
     
  11. Glenn Butcher

    Glenn Butcher TrainBoard Member

    167
    306
    9
    I'm running JMRI on my desktop Linux computer right now, along with a controller and throttle program whipped up for testing wavedcc. I have an old Microsoft Surface 3, my thinking for the shelf layouts is to run JMRI on it with PanelPro for controlling the turnouts. I have two Pi 3Bs, I'll probably mount each under the benchwork of each layout, with a separate programming track and a DPDT switch. wavedcc won't be able to do both ops and service modes at the same time (can only run one waveform at a time on the RPi's Broadcom hardware), but I'm not worried about that for small layouts. If I had a bigger layout, I'd probably use separate PIs for DCC ops and service.

    I have a SD card with Steve Todd's JMRI image, I'll probably try running the direct-GPIO wavedcc on it using the Pi JMRI as a front-end, just to see if it works.

    I've had some fun writing wavedcc, but the really interesting thing to me is the libpigpio architecture at its foundation. You can write programs with it to directly control GPIOs, but it also has a daemon interface, a program called pigpiod, which mainly lets you run GPIO programs as a regular user, not root. But, the daemon's interface is a network socket, so you can run your GPIO program on another computer. Indeed, I tried to run an earlier version of wavedcc on my Linux desktop, feeding the pulsetrains to the RPi over my home network; the interface worked just fine, but there was enough latency to put gaps in the DCC packets. I've since changed a few things that might make that better, so I'll probably try again. But I think the really interesting thing would be to write a JMRI interface that used the pigpiod daemon directly over the network for turnouts, signals, and such. No model railroad-specific software on the Pi.

    But all this interesting computer stuff is keeping from building the layouts... :D
     
    Squasher1838 likes this.
  12. BigJake

    BigJake TrainBoard Member

    3,259
    6,173
    70
    LOL! Oh yeah, the layout...

    That's what's so cool about this hobby; there are so many different aspects to it. However, it is rather easy to get wrapped up in one or two aspects, and forget about the others!
     
  13. Glenn Butcher

    Glenn Butcher TrainBoard Member

    167
    306
    9
    Okay, so I tried this again just now, but this time I put an Ethernet cable between the PI and my home network. The Ubuntu box is already on Ethernet, and both are plugged into the same switch so the even the Ethernet hops are minimized. Much to my amazement, it works! I was able to start ops mode and run the locomotive back and forth, just as if I was doing it to wavedcc on the Pi. Now there are intermittent gaps in the pulsetrain, 2 - 10ms, but they don't seem long enough to interrupt the locomotive power. Now, the repeated speed/dir packets probably help, but I did some single-packet functions (whistle, bell, etc), and there was not a single miss of any function. Now, CV reading didn't work, only one attempt of 3 yielded the correct value. That relies on single-millisecond timing transmitted over the LAN, so I get why that might not work.

    I would not recommend running wavedcc this way, but geesh.....
     
  14. Glenn Butcher

    Glenn Butcher TrainBoard Member

    167
    306
    9
    Okay, so I tried this again just now, but this time I put an Ethernet cable between the PI and my home network. The Ubuntu box is already on Ethernet, and both are plugged into the same switch so the even the Ethernet hops are minimized. Much to my amazement, it works! I was able to start ops mode and run the locomotive back and forth, just as if I was doing it to wavedcc on the Pi. Now there are intermittent gaps in the pulsetrain, 2 - 10ms, but they don't seem long enough to interrupt the locomotive power. Now, the repeated speed/dir packets probably help, but I did some single-packet functions (whistle, bell, etc), and there was not a single miss of any function. Now, CV reading didn't work, only one attempt of 3 yielded the correct value. That relies on single-millisecond timing transmitted over the LAN, so I get why that might not work.

    I would not recommend running wavedcc this way, but geesh.....
     
  15. Glenn Butcher

    Glenn Butcher TrainBoard Member

    167
    306
    9
    Okay, there's another detriment: I didn't kill the Ubuntu wavedccd before posting the previous post, and the network delay prompted me to click the Submit button twice. Sorry 'bout that... :D
     
    BigJake likes this.
  16. Glenn Butcher

    Glenn Butcher TrainBoard Member

    167
    306
    9
    After testing with three locomotives, one HO steam, and two N diesels, I think I have workable logic to read CVs, subject to tweaking. I'm describing it here to put other eyes on it, as I'm working primarily to interpretation of the spec and what I'm seeing in loco behavior, and I may not have a complete understanding of whole thing.

    Instead of using the libpigpio wave sync functions that I use in the ops mode continuous pulsetrain construction, for reading and writing CVs I use the wave_chain function. It allows one to build an array of waves, then play them as a sequence. Tailor-made to service mode...

    You can follow the narrative in the code starting here: https://github.com/butcherg/wavedcc/blob/main/dccengine.cpp#L991

    I start a single CV read by first changing the current measurement interval from .5sec to 1millisec. That's needed to detect the current ack later, but it's also been helpful in defining all the logic. After that, the DCC spec calls for an optional power-on cycle, which I elect to use to find the quiescent current of the locomotive just reading packets off the rails. This cycle is made up of 20 reset packets; I build the array and send it with wave_chain(), then enter a busy loop to await the finish of transmission. In that loop, I collect current measurements to capture the transition from "quiescent-quiescent", which is just the load presented by the current-sense board, to "quiescent" which is the board+locomotive reading packets. When I fall out of this loop, I run another loop to collect the last ten current measurements and find the maximum one. This value is then multiplied by a factor (currently 1.1) to get a quiescent value out of the steady-state variation.

    With that information, I start a loop from 1 to 255, doing a verify of the loop value and looking for a sequence of current measurements > the quiescent * scalefactor. I count them, looking for at least 4. Not exactly what the DCC spec calls for in the ack pulse construction (60ma for 6ms +/- 1ms), but it clearly recognizes the margins presented by the three sample locomotives. So, one iteration of the loop builds a wave chain with the verify for the loop value, sends the chain and takes the measurements while the chain is transmitting, then looks for >4 measurements above the quiescent * scalefactor. If that criteria is met, the loop falls through and the acked value is returned to the client that issued the read command. Oh, per the DCC spec that packet chain consist of at least 3 reset packets (I do 4), followed by 5 verify packets, followed by at least 1 (I do 4) reset packets to add time to catch the ack pulse.

    What has surprised me in this endeavor is the current draws of the various locos ack pulse. The HO loco pulse is typically 400ma (measured - quiescent), but the N diesels, Kato and Atlas DC9s go from 200ma to as high as 3amps. and the Atlas loco varies within that range from op to op. The pulse durations seem to be a bit long, for all locos about 9ms, but that could be due to the coarse period of my current measurement.

    The whole endeavor prompted me to code a logging scheme that uses UDP broadcast and a third program added to the repo, dcclog.cpp. if logging=1 in the .conf file, wavedccd logs each voltage/current measurement, and I peppered the read command logic recording of various markers and value reporting. That has been invaluable in assessing the behaviors of the locomotives. I'll need to update the readme with instructions for using logging, but if you put logging=1 in the conf file, then run dcclog, it'll start printing log entries as they're produced. If you run logdcc with a filename, it'll record the log entries in the file instead. Ctrl-C exits it, and flushes the write buffer to the file in the process.

    My next task will be to run a JMRI page read or somesuch with wavedcc as the source. My reads, particularly of CVs containing large values, are rather time-consuming, wondering what other softwares do (if anything) to mitigate that. Also wondering if three sample locomotives give me the information needed to bound my logic. All in good fun... :D
     
    BigJake likes this.
  17. BigJake

    BigJake TrainBoard Member

    3,259
    6,173
    70
    I thought there was a way to query just a single bit of a CV...?

    At least that was the response when I posted somewhere that DCC should have a < (or >) query in addition to the = query, for performing a binary search instead of a linear search.

    As far as # of decoder samples upon which to test your code, I'll step out on a limb and say 3 is not nearly enough... but it depends on whether this exercise is for your own benefit/enjoyment (still a worthwhile endeavor), or if you want others to use it "out of the box".

    Hey, as long as you're having fun... And we enjoy reading about it (I do!), carry on!
     
  18. Glenn Butcher

    Glenn Butcher TrainBoard Member

    167
    306
    9
    In direct mode, S9.2.3 has these three packet types:
    The defined values for Instruction types (CC) are:
    CC=10 Bit Manipulation
    CC=01 Verify byte
    CC=11 Write byte

    I loop through CC=01 255 times, verify for val=1, verify val=2, etc, until I get an ack. Then, that last verify is the value contained in the CV.

    Regarding number of samples, you bet it's too small. They're all different brands, one is a different scale than the other two, but that's about the extent of diversity. I see a consistent ack time among all the three, but the load is quite different, and in one locomotive it differs with each attempt. All I have to go on for consistency is compliance with the standard, 60ma for 6ms +/- 1ms.

    Oh, this is a lot of fun!
     
  19. BigJake

    BigJake TrainBoard Member

    3,259
    6,173
    70
    The CC=10 Bit Manipulation instruction allows either writing or verifying a bit.

    From https://www.nmra.org/sites/default/files/standards/sandrp/pdf/S-9.2.3_2012_07.pdf

    See line 130 and on.

    long-preamble 0 011110AA 0 AAAAAAAA 0 111KDBBB 0 EEEEEEEE 1

    Where BBB represents the bit position within the CV (000 being defined as bit 0), and D contains the value of the bit to be verified or written, K=1 signifies a "Write Bit" operation and K=0 signifies a "Bit Verify" operation.

    Hope this helps...
     
  20. Glenn Butcher

    Glenn Butcher TrainBoard Member

    167
    306
    9
    Ah, I should read more closely before responding, I was in a hurry to get ready for bedtime.... :)

    So, using CC=10, I could just walk down the 8 bits of the CV value, rather than using CC=01 to linearly search the 256 possible values of the CV byte. Reading 9.2.3, I kept looking at CC=10, thinking it was more about a single value in a bit array rather than using it for a binary search. That'd be a LOT faster....
     

Share This Page