NCE USB Interface and Arduino |
2016-01
This article explains what is the NCE USB interface, how it works, and more important how can we simply and efficiently use it with an Arduino directly with a simple serial port instead of USB.
The NCE USB interface
The USB interface connects a computer to an NCE command station.
It's available from the NCE online store as well as many eBay or Amazon-based stores.
This is a fairly simple and modest looking board:
On one side is a USB type B connector, and on the other side is the RJ12 which connects to an NCE cab bus using RS485.
There are 4 jumpers to configure depending on the kind of command station is used with it.
NCE provides a bunch of technical info on their cab bus page:
- Jumper configuration (PDF)
- NCE ops/command protocol (PDF)
On the cab bus, the interface shows up as cab number 3.
Depending on how you configure it using the jumpers that cab number might be fixed or changeable via software.
Mark Gurries has a very good description of how to configure it, so check his NCE USB page for details, which explains the differences between V6 and V7 and the various limitations..
So, what we really have here is basically a cab throttle, but without any buttons nor screen. The whole point is to have a computer operate and interact with the layout. The board doesn't do anything by itself, it needs to be controlled by something. For most people that means running JMRI or RocRail, which both have excellent support for this board:
- JMRI page for the NCE USB connection.
- RocRail page for the NCE USB connection.
I personally use both. If all you're going to use is either JMRI or RocRail, you can't stop reading at that point.
Sometimes I also want to go beyond what JMRI or RocRail offer and have direct control on my layout with my own software, either running on Linux or Arduino. For that, let's talk internals.
Now all that follows is based on my own limited understanding of the board so take it with a grain of salt. Also note that I'm using the NCE USB V7.
First, how does the board work? At a high level, it's extremely simple:
On the USB side this is nothing more than a serial port using the ubiquitous CP2102. Which means drivers are readily available for Mac, Windows or Linux and can be found on this NCE web page. For Linux you probably already have the cp210x driver installed. For Windows, I think you can now let Windows find it automatically for you.
The board itself is really composed of 2 obvious parts: the USB port and the CP2102 on one side, powered by the USB bus. On the other side is the PIC16 and the RJ12 jack, powered by the cab bus. In the middle is a simple dual opto coupler that carries the serial TX and RX yet isolates both parts. The USB part runs at 3.4V and the PIC16 part runs at 5V.
Note: some NCE documentation refers to their cab bus connector as an RJ11, others as an RJ12. The RJ12 is a 6P6C and the RJ11 is a 6P4C means 6 wire "slots" but only 4 actually being used. NCE cab bus wiring schema actually shows the latter.
Protocol is all serial and fairly well documented by NCE in the accompanying documentation. Commands are defined using one byte with high-bit set (0x80 or above) followed by a few bytes for parameters, and replies are generally one byte for status.
The 4 jumpers on the board control the baud rate (9600 or 19200) and the capabilities depending on which command station is being used. That part is important because the jumpers are actually used to limit what can be done, with the idea of disabling commands that a command station could not understand.
The serial protocol is similar to what a PowerPro gives via its serial port but with more limitations, for example the clock commands are indicated as not supported.
Python Access
So let's use it directly. First connect the board to a computer via USB, install drivers if necessary and plug the cab bus.
I generally like to do a quick check with JMRI that the board is properly recognized. That also conveniently gives me the port to use, e.g. right now I have COM8 on the Windows box and /dev/ttyS2 on the Linux one. Port numbers will change of course with your own setup.
I've used that setup at GGMRC with a Raspberry Pi and it worked like a charm. No drivers to install (the cp210x module was already in Raspbian if I remember correctly).
However on Linux I have another arduino that shows up as a ttyS and I use an udev rule to fix the port:
$ cat /etc/udev/rules.d/052_nce.rules SUBSYSTEMS=="usb", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", MODE:="0666", OWNER:="root", GROUP:="root", SYMLINK+="usb/nce" |
Once we've verified that the board works with JMRI, close the app to release the comm port and try our own.
For a quick test we'll use a Python script and the pyserial library:
# Install PIP from https://pip.pypa.io/en/stable/installing/ if needed $ pip install pyserial |
The program itself:
#!/usr/bin/python # Program to test communicating directly with NCE USB over COM port # Requires: PySerial: pip install pyserial (http://pyserial.readthedocs.org/)
import serial import time
# COM port to use. That can take any of the following forms: # COM8 for Python Windows, /dev/com8 or /dev/ttyS7 for Cygwin, /dev/ttyS2 for Linux COM = "/dev/ttyS2"
LOCO = 4014 LOCO_H = 0xC0 + (LOCO >> 8) LOCO_L = LOCO & 0xFF
def test_check_version(ser): # NCE USB 0xAA return C/S software version n = ser.write(bytearray([0xAA])) print "0xAA Write reply", n time.sleep(0.01) v = ser.read(size=3) # 3 bytes print "0xAA Read reply", repr(v) time.sleep(0.01)
def test_send_dummies(ser): # NCE USB 0x80 should always reply with a single !, 0x8C with 3 bytes ! \n\r
n = ser.write(bytearray([0x80])) print "0x80 Write reply", n time.sleep(0.01) v = ser.read() # 1 byte print "0x80 Read reply", repr(v) time.sleep(0.01)
n = ser.write(bytearray([0x8C])) print "0x8C Write reply", n time.sleep(0.01) v = ser.read(size=3) print "0x8C Read reply", repr(v) time.sleep(0.01)
def test_headlight_F4_on_off(ser): # NCE USB 0xA2 <adr H/L> 07 10|00 (4=F0/light on/off), reply 1 byte status for state in [0x10, 0x00]: n = ser.write(bytearray([0xA2, LOCO_H, LOCO_L, 0x07, state])) print "0xA2 light Write reply", n time.sleep(0.01) v = ser.read() # 1 byte print "0xA2 light Read reply", repr(v) time.sleep(2)
def test_move_forward_stop(ser): # NCE USB 0xA2 <adr H/L> 04 0..7F (forward 128) and 06 00 (estop forward), reply 1 byte status for speed in [0x02, 0x00]: n = ser.write(bytearray([0xA2, LOCO_H, LOCO_L, 0x04, speed])) print "0xA2 fwd Write reply", n time.sleep(0.01) v = ser.read() # 1 byte print "0xA2 fwd Read reply", repr(v) time.sleep(2)
n = ser.write(bytearray([0xA2, LOCO_H, LOCO_L, 0x06, 0x00])) print "0xA2 estop Write reply", n time.sleep(0.01) v = ser.read() # 1 byte print "0xA2 estop Read reply", repr(v) time.sleep(2)
def main(): print COM, "open port" ser = serial.Serial(port=COM, baudrate=9600, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, timeout=None) try: test_check_version(ser) test_send_dummies(ser) test_headlight_F4_on_off(ser) test_move_forward_stop(ser) finally: ser.close() print COM, "closed port"
if __name__ == "__main__": main() |
I think the program is self explanatory: open the comm port, send a few commands, close the port.
All error checking code is omitted for brevity. One obvious omission is that the serial port access will block when the command station is not connected or not powered up. In a real program you'd use the timeout option of pyserial and deal with the error (retry later, notify the user, etc.).
The commands are described in detail in this NCE documentation. In my case the only commands I need are:
- 0xAA to read the software version in the PIC16. I'll use that as a comm test, and also to make sure I'm talking to the right kind of board.
- 0xA2 to control a locomotive: address, operation and parameters.
- I only need a handful of operations: 03 to send a reverse 128 speed, 04 for a forward 128 speed, 05 for estop reverse, 06 for estop forward and 07-09 for controlling functions F0..F12.
Note that setting the speed to zero and using estop is not the same thing. Obviously both stop an engine but on a few decoders I've tried such as the Tsunami the estop also turns off the engine sound by running the shutdown sound sequence, whereas speed zero leaves it with the idling sound. It's not clear why there are two estop operations though.
One thing I did not see was a way to read the current functions states for a given engine. Setting them is done in groups of 5 bits for F0..F3 and groups of 4 bits above. That means we'll always be setting F1..F3 at the same as F0.
A closer look
The previous part was good when using the board as intended via USB. A typical example is to connect the board to a Raspberry Pi and then program it directly. But what if we have something more basic like, say, an Arduino?
In practice, that means I'd have to use an Arduino with a micro-USB and somehow manage to make it talk to the USB type B on this board. That's what USB-OTG is designed for, except I don't think this board supports OTG.
Bottom line, it seems a bit overkill to use USB. Remember the USB is just a glorified serial port, and Arduinos are generally loaded with a couple serial ports or even more.
That's why I started looking a bit closer at the board and I noticed that at the core, the design looks like this:
In case that's not obvious, I'll spell it out:
- The USB connector is directly connected to a CP2102, a typical USB to serial TTL converter.
- The CP2102 takes D+/D- from the USB on one side and transforms that in an UART RX/TX on the other side.
- The RX/TX connects to the RX/TX UART port of a PIC16F via an optocoupler (D213).
So can we have an Arduino bypass the USB part and talk directly to the PIC16?
A few data sheet references would be useful here:
- SILABS CP2102, a typical USB to serial TTL converter:
http://www.mouser.com/ds/2/368/silicon%20laboratories_102-short-552101.pdf
https://www.silabs.com/Support%20Documents/TechnicalDocs/CP2102-9.pdf
Only UART pins used are TXD (left) and RXD (right) on top. - Fairchild D213, dual channel phototransistor:
http://www.mouser.com/ds/2/149/MOCD213M-116327.pdf - PIC16F913, a nice "old school" microprocessor in 28-pin SOIC format:
http://ww1.microchip.com/downloads/en/DeviceDoc/41250F.pdf
Pin 18: RC7 / RX / DT / SDI / SDA / SEG8
Pin 17: RC6 / TX / CK / SCK / SCL / SEG9
I love this design. It's super simple and efficient. Not to mention that the PCB is a simple dual layer and extremely trivial to follow the routes. Anyway, enough drooling, moving on.
One thing to remember is that we have two independent modules here. The power supplies are separated:
- The USB side is powered by the USB bus and the CP2102 includes its own voltage regulator. Testing it shows it seems to be around 3.4 V, confirmed by the datasheet.
- The PIC16 side is at 5 V, powered by the cab bus via a 78L05 in SOIC form.
That's important because in my case I use a DigiX with 3.3V logic levels. This has the same 3.3 V limitation as the Raspberry Pi GPIO which means we can't randomly connect 5V I/O without a level shifter. But in this case we don't have to, because all we need is to connect directly to the D213 on the CP2102 "side." There's an optocoupler on that board, we might as well use it just for that.
It's interesting to note that the board has 7-pin through-hole "header" which is the PIC16F's ICSP (In-Circuit Serial Programming, see datasheet chapter 28, page 361).
The pins connect to the PIC16 as follows:
- 1 = Pin 1 (Vpp / !MCLR)
- 2 = Header 1 with a 10k resistor to Pin 20 (Vdd)
- 3 = GND / Pin 8 (Vss)
- 4 = Pin 28 (ICSPDATA)
- 5 = Pin 27 (ICSPCLK)
- 6 = Not Connected
- 7 = Not Connected
Note: Vss is the ground ref, Vdd is the positive supply, Vpp is the programming voltage.
Vdd max is +5.5 V and Vpp max is +9 V.
Even if I had an Arduino that supports 5 V levels, I'd still use the optocoupler to make sure not to mix ground and power levels from the cab bus with the arduino power source. The NCE cab bus wiring schema indicates the cab bus carries 12 V, but I don't remember reading how much power the accessories can draw from that. Mine takes 1A @ 5 V so that's probably too much anyway.
Bypassing the USB port
Let's put the findings from the previous paragraph in application. First we need access to the RX and TX serial signals on the CP2102 side. What I ended up doing is drilling 4 little holes in the board just above the USB port in an empty location and then added a 4-pin header. The header carries 4 signals:
- A +3.3 V power for the USB side, which will be delivered by the Arduino.
- The TX pin from the CP2102.
- The RX pin from the CP2102.
- The ground for the USB side, which will be connected to the Arduino ground.
I can't solder on the CP2102 as it's using a surface mount pitch which is way too small for me. However the D213 optocoupler is a standard size SOIC and is something I could solder onto with some care.
I noticed that pin 1 of the D213 was connected to Vcc on the large capacitor nearby via a pull-up and I wanted to keep it that way. The other pin I need is pin 5 on the D213 and that's connected to ground.
Turns out this large capacitor is perfect for bridging the Vcc and the ground I need, so that was a good place to pick up both ground and the 3.3 Vcc.
Here's the result of this little kitbash:
The little green wires are a bit hard to see and that's exactly why I choose this color.
Note that I must not power via both the USB and the new header at the same time, but that's the whole point. This way the board is still usable via regular USB if I later change my mind. I didn't have to cut any traces or desolder anything. Note that this applies the Arduino's 3.3 V to the Vdd pin #6 of the CP2102 and should match its self-powered mode used to bypass its internal voltage regulator (see page 19 on the datasheet here).
Now to use this:
- The TX pin from the CP2102 goes to the TX1 pin 17 from the DigiX.
- The RX pin from the CP2102 goes to the RX1 pin 18 from the DigiX.
Of course I originally got this in the wrong order -- usually we want to cross RX/TX with UARTs but here we are in fact replacing the output of the CP2102 but the DigiX. It's when it gets to the PIC16 that RX/TX are crossed at the optocoupler level.
Note that Serial1 on the DigiX is connected to the wifi module. I can use it as in this case the wifi module is deactivated by removing the wifi enable jumper. Otherwise the DigiX has other options since it has 4 serial ports (Serial0 goes to USB, Serial1 to wifi and there's Serial2 and 3 which are free to use).
Here's a trivial sketch that will toggle a locomotive headlight:
void setup() { Serial1.begin(9600 /*SERIAL_8N1*/); }
#define LOCO 1538 byte cmd_buf[10];
void headlight(int light) { int LOCO_H = 0xC0 + (LOCO >> 8); int LOCO_L = LOCO & 0xFF;
cmd_buf[0] = 0xA2; cmd_buf[1] = LOCO_H; cmd_buf[2] = LOCO_L; cmd_buf[3] = 0x07; cmd_buf[4] = light ? 0x10 : 0; Serial1.write(cmd_buf, 5);
if (Serial1.available()) { // '!' on success char c = Serial1.read(); lcd.setCursor(pos, 1); lcd.print(c); } } } |
You can clearly recognize the 0xA2 byte sequence from the previous Python script.
Note that you must match the baud rate with the one of the board, controlled by JP1.
To finish here's the module being used on the prototype board for my current project:
The DigiX (top left) drives the 20x4 blue LCD via I2C and the NCE card via Serial1.
At the bottom there's the gray cable from my cab bus on the RJ12 connected.
In the middle on the breadboard is a Xminilab that displays the NCE protocol that goes over the serial port. It clearly shows the 0xA2 commands being sent and the 0x21 ('!') replies.
The breadboard also has a light-dependent resistor connected on analog pin 0.
To close this, I will add that there are a few electronics articles online that explain how to create your own DCC controller using an Arduino -- such as the OpenDCC project and MRRWA. But most of them take the holistic approach of creating a command station from scratch. Instead what I showed you here is a simple way to relegate the Arduino as a mere cab throttle for an existing NCE-powered layout. No change needed on my layout, I can still use the same command station, the same ProCab or engineer throttles, and I can still use my computer with JMRI, but whilst I can still do all that I can also have a small dedicated automation controlled by a fully separate Arduino and its own logic.
~~
~~