This project develops a library to interface with the NCE Bus of a DCC Model Train System. The project will be in several parts:
1. Initially the project will be to understand and probe the activity on the NCE Bus
2. To add an LCD display that duplicates the display of the NCE hand held controller.
3. Implement a set accessory method that could control an accessory (point).
Future enhancement would include:
4. To develop a project that will control locos via the NCE Bus. Currently I have a NCE Twin that will control two locos only at address 3 and 4. It would be nice to handle an additional loco.
5. To develop a project that will control several points. At my club there is a congested area that contains several points. It would be nice to have a small mimic panel where the settings may be quickly selected without the need to program macros in the NCE controllers.
Libraries to include: <DCC_NCE_Bus.h> this project
<LiquidCrystal.h> for LCD
Constructors bus(5); //project at address 5
LiquidCrystal lcd(8,9,4,5,6,7);
The project uses a TTL to RS485 breakout board to perform the interface between the Arduino and the NCE Bus. On one side of the board are the two RS485 signals "A" and "B" or "+" and "-" (**) plus a ground. On the other side there are 4 or 5 pins that include the power (Vcc) and Ground plus 2/3 pins for the TTL signals that are wired directly to the Arduino I/O pins. These include the Receive and Transmit pins. If present the third pin will select the board function to either receive or transmit. For boards without this third pin the direction is selected using the transmit signal as illustrated in the circuit below.
In the circuit below the heart of the breakout board is the SN65HVD3082 TTL to RS485 converter (See also MAX485) where the direction is controlled by the Data Enable (DE) and the NOT Receive Enable Pins. As shown, these are typically wired together, and it the 3-wire interface exposed to the Arduino to control the direction. With the 2-wire interface the enable signals are derived from the transmit signal. Normally TX0 will idle high but when the Arduino wishes to transmit it pulls the TX0 line low which forces the DE active and the RE inactive the device going into transmit mode. To hold the device in transmit mode a 1000pF capacitor is used. (!!)
** Examples on the internet indicate that the manufacturers definitions are not consistent, and it may be necessary to swap the wiring to A/B.
!! The project prototype used both 2 and 3 wire interfaces.
Photo of the TTL to RS485 interface mounted on an Arduino based prototyping board. On the left the cable to the NCE Bus while on the right are pins that underneath are wire wrapped to the prototyping board socket that in turn is wired to the Arduino. The centre chip is the RS485 to TTL converter while the bigger chip on the right is a hex buffer (**).
** This surface mount chip had a dry joint and the link between its output pin and the RO pin had to be resoldered.
-----------------------------------------------------------------------------------------------
Once the TTL to RS485 interface board is powered the received signal at RO may be observed with a mixed signal oscilloscope (MSO). The left-hand trace below shows a typical result (**). By using the timing features of the MSO the signal can be determined to be 9600 baud and the time between each byte approximately 3.88 mS (See cursors). Further the data may be deduced/read as 0x80, 0x82, 0x83 through 0x8A (^^). That is the NCE Bus first pings address 80, the broadcast address, it misses device 1, but then pings/addresses devices "2" through "10" before repeating.
Every so often the broadcast address (0x80) is followed by a string of data. With much patience this can be captured. (!!) For later information this appears to be inserted at what would normally be the 0x82 ping and consists of 9 bytes followed by the 0x82 ping.
** To synchronise the trace the trigger has been set for a negative pulse of greater than 800uS which synchronises on the 0x80 or broadcast ping address.
^^ To extract this data it will be necessary to set the cursors to one-bit and scroll through each byte to conclude from the timing that the most significant bit must be an "8". Scrolling through the waveforms will show the sequence repeating after the 0x8A.
!! In this example the resultant trace was captured after 84 attempts.
The above trace has captured the signal on the NCE bus. Note it goes in the sequence 0x80, 0x82, 0x83 through 0x8A and then repeats. Each byte is 9600 baud and the time between each byte is 3.88mS,
Occasionally following the 0x80 Broadcast ping/command a burst of activity is inserted before the 0x82 ping. It will be the job of the project firmware to capture this string and take the appropriate action,
Note. In this example the scan was repeated 84 times before this stretched sequence was captured.
Normally the recommended procedure for developing a library to interface with the NCE Bus is to collect all the bus specifications and design the project using these specifications. In this example, except for observing the waveforms given above I have assumed nothing about the bus protocol and developed a program to probe the bus to determine the protocol. This is a learning project. This technique has the disadvantage that there might be a number of back tracks required and the end result might not be as good as knowing exactly what is required before you start.
The first step is to generate 3 files DCC_NCE_Bus with extensions ino, cpp and h
1. Create a new project with the label DCC_NCE_Bus. This will present a new file DCC_NCE_Bus with the extension .ino.
2. Using the inverted triangle in the Arduino IDE to create two new tabs. Give these the labels DCC_NCE_Bus.cpp and DCC_NCE_Bus.h
-------------------------------------------------------------
It is possible to start generating the files at either end. For example, in the *ino file there needs to be a function/method to read the bytes on the NCE bus. That is a method of the form handleByte(). Alternatively, starting with the *.h file there needs to be a class DCC_NCE_Bus that ultimately performs all the underlining detailed work and provides functions available to the client (*.ino) code.
Possible client or *ino code will be: (The code at this point the code will initialise the serial port and write a message to the monitor. The serial port will not be part of the actual NCE bus interface but will be used for debugging)
Notes
The header file has created a class that has two methods DCC_NCE_Bus (the constructor) and begin( ).
At a later date it was decided to specify the target address (ie the address used by this project) as part of the constructor. The code will become DCC_NCE_Bus(uint8_t address);
The interface to the NCE bus will require the SoftwareSerial library that is included in the header file. The standard serial port cannot be used as it is reserved for loading code and debugging messages.
------------------------------
The implementation file will be:
Notes: The implementation file includes the code for the constructor and the begin( ) method.
In later upgrade the constructor specified the address of the object. The new code became DCC_NCE_Bus :: DCC_NCE_Bus ( 5 ) { };
The header file has included the SoftwareSerial library, and the implementation code creates one instance/object RS485 of that class. The object is attached to Arduino pins 12 and 11.
The begin method initialises the RS485 object and sets its baud rate to 9600.
The given code is for a RS485 interface with 3 pins Transmit (Tx), Receive (Rx) and Enable (defined as RS485_Tx_En). The Tx and Rx pins were defined when the object was created while RS485_Tx_EN is defined as pin 13 in the header file and set to a LOW output. (Receiver enabled) If a RS485 board with internally generated enables is used then any lines referring to RS485_Tx_En are not required and may be omitted.
====================================================To handle the NCE data (i) an instance of the DCC_NCE_Bus class must be created in the client code and (ii) a method handleByte() included in the class:
That is in the client or *.ino code add:
DCC_NCE_Bus bus;
void loop() { bus.handleByte( ); }In the implementation code add:
void DCC_NCE_Bus::handleByte( ) { }
And in the class definition (header file)
class DCC_NCE_Bus {
public:
DCC_NCE_Bus ( );
void begin( );
void handleByte( );
}
NOTE: The code was later upgraded to include the address (5) as part of the constructor. (Ping address == 0x85)
-----------------------------------------
The next step is to start to extract the data on the NCE bus. In the begin( ) method the program can print dots to illustrate something is happening until the RS485 becomes available. Since there will be a significant delay the number of dots can be limited to less than 50 (for example)
void DCC_NCE_Bus::begin ( ) { RS485.begin(9600); pinMode(RS485_Tx_En, OUTPUT); digitalWrite(RS485_Tx_En, LOW); //receiver int count = 0; while (!RS485.available( )) {if (count<50) {Serial.print("."); count++;}Once the RS485 bus is available the code can move to the handle_NCE_Byte( ) routine.
uint8_t rxByte;Notes:
The code as given in bold will send a string of data across the screen at an "invisible" rate. See captured results below.
The left hand trace below shows the start up code with a number of dots.
In general the captured data will be the pings to addresses 0x80 through 0x8A ( less address 0x81.) as per the two traces below.
Occasionally in the Broadcast state there will be a message. This message could be 2 bytes starting with D4 (telling all controllers to turn on a green light) or 9 bytes starting with 0xC1 - C1 tells LCD to display message on top row to right. The following 8 bytes are the message. In the example " 12:28PM"
Looking ahead a following step in the project will be to extract the data from these longer lines and take the appropriate actions. That is if an LCD is available actually display 12:28PM!
To display the received data a conditional compile is included. In the header file "#define TRACE 1" will direct the compiler to include all the conditional print out lines that will be used to understand/verify the program operation. Once the code is operational the define may be set to 0 or the comment lines removed.
The monitor display following reset. After the messages the code has captured the data 0x80, 0x82, 0x83 .... 0x8A. For clarity the program adds a new line prior to printing each 0x80, the broadcast ping. Note the trace has also by chance captured a short broadcast message 0xD4 0x0A.
By trial and error a trace was obtained that captured the broadcast message 0xC1, 0xE0, 0xF1, 0xF2, 0xFa, 0xF2, 0xF8, 0xD0,0xCD. This will be the NCE Fast Clock. The line is completed with the pings of address 0x82,0x83 etc.
Notes:
What is expected is the NCE controller "pinging" every address in turn. This will start with the broadcast address 0x80 so to format the display a new line can be sent on receipt of the 0x80. To further tidy the display a space may be sent after each data byte.
The display will become:
80 82 83 84 85 86 87 88 89 8A
Occasionally there will be additional items that code will be added to capture.
As the program development progresses the display routines above will not be required. They can either be commented out or include a conditional compile. That is #if TRACE .....#endif where TRACE will be defined in the header file.
--------------------------------------------
The data on the NCE bus has been captured as 80 82 83 84 85 86 87 88 89 8A.
There are three different situations or states:
(i) The Broadcast ping (0x80) that will be received by all stations. BROADCAST
(ii) One of the ping addresses will correspond to the "receiving" device. That is this device or ITS_ME. This project will assign the ITS_ME address to 0x85.
(iii) The ping address of other devices that will be ignored. That is OTHER
In addition, when the system starts up there is an unknown state START
These four states are defined in the header file
enum NCE_BUS {START,BROADCAST,ITS_ME,OTHER};
and a private variable of the type NCE_BUS declared.
NCE_BUS BUS_STATE;
and the variable STATE initialised to START in the DCC_NCE_Bus::begin( ) method.
void DCC_NCE_Bus::begin ( ){ BUS_STATE = START; ....Initially the design will be in START state and will skip all bytes (probably pings) until it gets the BROADCAST PING 0x80. It will move to the BROADCAST state.
In the BROADCAST state the next byte will either be the start of a broadcast message or a further ping. If the data is a ping the code will move to the OTHER state.
void DCC_NCE_Bus::handle_NCE_Byte( )
{
while (RS485.available( ))
{
rxByte = RS485.read();
................
switch (BUS_STATE) {
case START : if (rxByte==0x80)
{
BUS_STATE = BROADCAST; _BusPtr = 0;
}
break;
case BROADCAST :
if ((rxByte & 0xf0)!= 0x80) {
build_message( ); //should loop through until next ping
}
else BUS_STATE = OTHER;
break;
case OTHER : if (rxByte==0x80)
{
BUS_STATE = BROADCAST; _BusPtr = 0;
}
break;
}}}
---------------------------------
On receipt of a non-ping character the code will invoke the build_message routine. Initially all this does is verifies that the code really reaches this method.
void DCC_NCE_Bus:: build_message( )
{
if (_BusPtr == 0) Serial.print(" Build_mess ");
_busPtr++;
}
Initially as shown the code inserts the message "Build_mess" to verify that the method is actually reached.
The function build_message will need to be enhanced to determine the size of message and then build/collect bytes until the buffer is full. A variable _bufflen is included in the class DCC_NCE_Bus to save the length of the buffer. The pointer to the next entry will be _BuffPtr. That is:
if ((rxByte >= 0xC0)&& (rxByte <= 0xC7)) _bufflen=9; **
else _bufflen = 2;
The ultimate aim is to do something with the message. In example the 8 message bytes will be " 04:49AM". This will be displayed on an LCD screen.
Some correction to ASCII code required. This conversion is undertaken in the function NCE_ASCII( ) for the ASCII messages.
if (_BusPtr && (_BusBuffer[0]>= 0xC0)&& (_BusBuffer[0]<= 0xC7)) //an ASCII string
_BusBuffer[_BusPtr] = NCE_ASCII(rxByte); //modified ASCII -but don't modify first char
else _BusBuffer[_BusPtr] = rxByte; //save new byte
In the example the received data is E0, F0, F4,FA,F4,F9,C1,CD.. The true ASCII will become 20 30 34 3A 34 41 4D or space, "0", "4", ":", "4", "9", "A", "M"
This is the data that has to be handled. This will use the handle_message( ) function which will be called when the receive buffer is full. The code for handle_message( ) will need to be general as it will probably be required as part of messages when in the "ItsMe" state.
void DCC_NCE_Bus:: build_message( )
{
.........................
_BusPtr++;
if ((_BusPtr == _bufflen)) { //message complete
//handle_message( );
_BusPtr = 0;
}
}
** At a later phase of the project it was found that some messages were only one byte so the following line needed to be added
else if ((rxByte ==0xCE)||(rxByte==0xCF)) _bufflen= 1;
From the document NCE Cab bus protocol-2006.pdf possible messages are:
0xc7 print next 8 chars to 4th line right of LCD (addr = D8h)
0xc6 print next 8 chars to 4th line left of LCD (addr = D0h)
0xc5 print next 8 chars to 3rd line right of LCD (addr= 98h)
0xc4 print next 8 chars to 3rd line left of LCD (addr = 90h)
0xc3 print next 8 chars to 2nd line right of LCD (addr = C8h)
0xc2 print next 8 chars to 2nd line left of LCD (addr = C0h)
0xc1 print next 8 chars to 1st line right of LCD (addr = 88h) Note: this is where the clock prints
0xc0 print next 8 chars to 1st line left LCD (addr = 80h)
Depending upon the final objectives of the project the handle_message function may or may not be required. For this project it is assumed a 16x2 LCD is available so the handle_message( ) will generate messages for the LCD.
Further the question then becomes where are the LCD routines located? One option is to make them part of the DCC_NCE class that each time a new message is generated the LCD is updated.
Another option is to have the LCD routines as part of the client code (ie the *.ino). Here there are two options: (i) the client code can poll testing when a new message is available, or (ii) the DCC_NCE object can inform the client code when new data is available. This will effectively be an interrupt service or callback routine. This is the chosen option. As well as the code in both the client and implementation programs the links between the two will need to be programmed. This will require code as part of the class, there will be some initialisation code and then code when the LCD is to be updated.
Possible code in the DCC_NCE class will be
void DCC_NCE_Bus::handle_message ( ) {
if (MessHandlerFn) //if client has not set up link this code is skipped
MessHandlerFn(_BusBuffer[0],_BusBuffer[1],_BusBuffer[2],_BusBuffer[3],_BusBuffer[4],_BusBuffer[5],_BusBuffer[6], _BusBuffer[7], _BusBuffer[8]);
}
where MessHandlerFn includes the 9 parameters for each of the elements of the data buffer (_BusBuffer[ ]) and will match the code NCE_Message that is written is code written as part of the client code.
void NCE_Message(uint8_t loc, uint8_t a, uint8_t b, uint8_t c, uint8_t d, uint8_t e, uint8_t f, uint8_t g, uint8_t h)
{
// Serial.print("\nNCE Message ");
................
//display operations
}
}
In summary when a new message is received the library routine handle_message( ) will vector the program to the client method NCE_Message that will take the required actions (display message on LCD).
In this example there is a message C1 E0 F1 F2 FA F3 F4 C1 CD (9 bytes) that gives the "Fast Clock Time" that the client may wish to use.
The client has a method/function
void MessHandlerFn(_BusBuffer[0],_BusBuffer[1],_BusBuffer[2],_BusBuffer[3],_BusBuffer[4],_BusBuffer[5],_BusBuffer[6], _BusBuffer[7], _BusBuffer[8]);
where the library/object must transfer the results. This will require the library to be set up as follows:
In the header file define a type
typedef void (*MessHandler)(uint8_t loc, uint8_t a, uint8_t b,uint8_t c,uint8_t d,uint8_t e, uint8_t f, uint8_t g, uint8_t h);
where the type definition includes nine arguments that match that of the target or client method.
To transfer the clock result from the library to the client needs a public routine that the client can call to setup the link and a method to perform the actual transfer operation. That is
class DCC_NCE_Bus {
public:
DCC_NCE_Bus ( );
void setMessHandler(MessHandler funcPtr);
private:
MessHandler MessHandlerFn;
To set up the link the setMessHandler( ) method is called from the client set up function where the address of the MessHandler( ) method is passed. That is in the *ino file
void MessHandlerFn(_BusBuffer[0],_BusBuffer[1],_BusBuffer[2],_BusBuffer[3],_BusBuffer[4],_BusBuffer[5],_BusBuffer[6], _BusBuffer[7], _BusBuffer[8]);
void setup( ){
....
bus.setMessHandler(&MessHandler);
}
The setMessHandler implementation needs to be part of the implementation file. That is
void DCC_NCE_Bus::setMessHandler(MessHandler clientPtr)
{
MessHandlerFn = clientPtr;
}
To transfer the results the following code is added to the handle_message( ) method.
void DCC_NCE_Bus::handle_message( ){
if ((_BusBuffer[0] >= 0xC0) && (_BusBuffer[0] <= 0xC7)) //this is a message to the controller screen where C0 - C7 give position on screen
{ if (MessHandlerFn) //if client has not set up link this code is skipped
MessHandlerFn(_BusBuffer[0],_BusBuffer[1],_BusBuffer[2],_BusBuffer[3],_BusBuffer[4],_BusBuffer[5],_BusBuffer[6], _BusBuffer[7], _BusBuffer[8]);
Note if the pointer MessHandlerFn does not exist the function exits.
There are some complications.
(i) The given code only applies to the "C1" string that is dealing in hours and minutes so an if (_BusBuffer[0] == 0xC1){... is required.
(ii) The ASCII data is in a different format so an adjustment is required.
for (int i = 1; i < _BusSize; i++) {
_BusBuffer[i] = NCE_ASCII(_BusBuffer[i]);
}
where the NCE_ASCII( )method will be:
uint8_t NCE_ASCII(uint8_t rr){
if (rr & 0x20) return (rr & 0x3F);
else return (rr & 0x7F);
}
It has been assumed that the messages will be displayed on a 16x2 LCD. The client file must include the LiquidCrystal library and create an object lcd as illustrated:
#include <LiquidCrystal.h>
LiquidCrystal lcd(8,9,4,5,6,7);
The lcd is initialised in the set up method and the message NCE Bus displayed in the top left location.
void setup()To actually write to the LCD the NCE_Message( ) method including debugging messages will be:
void NCE_Message(uint8_t loc, uint8_t a, uint8_t b, uint8_t c, uint8_t d, uint8_t e, uint8_t f, uint8_t g, uint8_t h)
{
// Serial.print("\nNCE Message ");
if (loc ==0xc0) { //Serial.print("Top row left "); //debugging
lcd.setCursor(0,0);}
else if (loc ==0xc1) { //Serial.print("Top row right ");
lcd.setCursor(8,0);}
else if (loc ==0xc2) { //Serial.print("2nd row left ");
lcd.setCursor(0,1);}
else if (loc ==0xc3){ //Serial.print("2nd row right ");
lcd.setCursor(8,1);}
lcd.print(char(a));cd.print(char(b));lcd.print(char(c));lcd.print(char(d));lcd.print(char(e));lcd.print(char(f));lcd.print(char(g));lcd.print(char(h));
Serial.print(char(a)); Serial.print(char(b)); Serial.print(char(c)); Serial.print(char(d)); Serial.print(char(e)); Serial.print(char(f)); Serial.print(char(g)); Serial.println(char(h));
}
At this stage of the code development all messages start with 0xC1 so the cursor is placed at bit 8 on row 0. This is where the lcd.print(char(a)) prints. The cursor will move to the right for each of the 8 characters. See screen snapshot below:
The code to date has only handled the receipt of broadcast data.
The controller sends each ping address in turn waiting a short time between each for a response. To date no device on the bus has been set up to talk. The following sections will look at setting up code to make requests from the controller (ie the NCE Twin) and the controller response.
This is still an investigative project. Possible sequences to send have been gleamed from articles on the Internet and the controller response determined by the liberal use of print( ) statement.
One issue will be the state of the NCE Twin Controller. Initially it will be in a Start-up state. Once we send a command the NCE Twin will know something about this project and will then be expecting or asking for something else. When testing in order to obtain consistent results it will be necessary every time new code is reloaded or rerun to reset the NCE Twin. For example, once the NCE Twin knows the project is a "Dumb Terminal" it will never ask that question again.
This project will assume the controller/monitor (this project) is at address 0x85.
For starters
1. if the project is in the OTHER state and receives a ping for ITS_ME (0x85) while still in the OTHER state the code should send out an "I'm here" message and set the state to the ITS_ME state.
2. In the ITS_ME state the program should listen for commands/requests from the NCE Twin and respond to them. When an OTHER ping (in this example 0x86) is received, the program should return to the OTHER state. On the OTHER state the program should test for the BROADCAST ping and then for the next ITS_ME ping.
The firmware will be looping in the handle_NCE_byte method. Code needs to be added to handle the Its_Me state.
void DCC_NCE_Bus::handle_NCE_Byte( )
{
while (RS485.available( ))
{
rxByte = RS485.read();
.......
switch (BUS_STATE) {
case START : .....
break;
case BROADCAST :
..................
break;
case OTHER : if (rxByte==0x80)
{
BUS_STATE = BROADCAST; _BusPtr = 0;
}
else if ( rxByte == 0x85)
{
its_me_start( ); //need to tell NCE I'm on the bus.
BUS_STATE = ITS_ME;
}
break;
case ITS_ME :
if ((rxByte & 0xf0)!= 0x80) {
//handle_me( )
}
else {
BUS_STATE= OTHER;
}
break;
default : Serial.print("State-Unknown");
}
}
While in the 84 state the 85 ping comes along. The its_me_start( ) method/function needs to tell controller device 85 is present.
On the first occurrence the message 0x7E and 0x7F should be sent then if nothing is required from the NCE Twin the subsequent response should be 0x7D and 0x7F. Possible code will be:
void DCC_NCE_Bus::its_me_start( ){
if (!_mess_count) { //send 7E 7F once
Send2Bytes485(0x7E,0x7F);
Serial.print("7E-7F ");
}
else
{
Send2Bytes485(0x7D,0x7F);
Serial.print('.');
};
_mess_count++;
}
the variable _mess_count will need to be defined private in the DCC_NCE_Class. On the first loop the two bytes 7E and 7F are sent to the NCE_Twin while on subsequent passes 7D and 7F are sent.
This is illustrated below where there is a message for the 7E/7F but as coded above only a "." for the 7D/7F.
Note in the above trace following the 7D/7F message to the NCE_Twin a "D2" message is received. This will be discussed in a following section. But first the Send2Bytes485( ) method needs to be written.
The Send2Bytes( ) method writes two bytes to the RS485 port. The only complication is that the NCE Twin requires the message within a specified time window. Using a delay of 200 microseconds the code appeared to work**
void SendByteRS485(uint8_t mess){
//digitalWrite(RS485_Tx_En,HIGH);
delayMicroseconds(200);
RS485.write(mess);
// digitalWrite(RS485_Tx_En,LOW);
}
void Send2Bytes485(uint8_t m1,uint8_t m2) {
//digitalWrite(RS485_Tx_En,HIGH);
delayMicroseconds(200);
RS485.write(m1);
RS485.write(m2);
// digitalWrite(RS485_Tx_En,LOW);
}
** If a RS485 board with a separate enable pin is used the digitalWrite lines will be required.
The resultant trace is shown below. The upper channel is the received data while the second channel is the transmitted data and shows the two bytes 0x7E and 0x7F being sent.
In detail the top channel shows a 85 ping is received from the NCE Twin, the project responds/transmits with the two bytes and then the NCE Twin sends the message/command/request which by observation may be seen to be 0xD2.
** The NCE TWIN expects two byte commands. In this example 7E requests that the data be displayed and 7F that the loco speed is zero. Further examples of these will be given in later sections.
In the above section the 0x7E command is requesting that the NCE Twin return the current data. However, the data format will depend upon the type of terminal and at this point the NCE Twin does not know the terminal type so sends the command 0xD2 to request the type of terminal. The response will be "a" for a Dumb Terminal. Possible code will be:
void DCC_NCE_Bus::handle_me( ){
if (rxByte == 0xD2){
SendByteRS485('a'); //a dumb terminal
_BusPtr = 0; //clear buffer
_BusBuffer[0] = ' ';
Serial.print("'a' sent.");
}
else {
build_message( );
}
}
For 0xD2 the NCE Twin expects an immediate response so the code must respond immediately and not build up a long message.
A typical response is shown below. A while(1) statement is used in the code to freeze the serial output to permit easy capture of the trace of interest.
At this point the NCE_Twin know there is a device present at address 85. It thinks it is an NCE Dumb Terminal. The NCE_Twin will start sending messages. The code has been written as part of the broadcast messages.
For interest this project was interfaced to a more complex NCE Controller that was communicating with controllers (CABs) at address 11 (0x8B) and 15 (0x8F). A trace at start up is shown below,
Some snippets gathered from the trace.
The controller does not always ping every address or every address in sequence.
The device at address 0x8B is continually sending the message 7D/7F - no key pressed and speed zero.
Device 0x8F is also sending 7D/7F although there was an operator on that controller and other messages were obtained.
With the NCE Twin there was a two line welcome message. With the larger controller the welcome message received by device 0x85 is four lines.
LOCO: 5025
FWD: 000 ------
PRESS <EXPN> FOR
A NORMAL DISPLAY
With a 2 line display only the first two lines can be displayed.
Sometime later the time is broadcast. This will go to the top row right of all the devices on the bus. For device 0x85 beside the LOCO: 5025**
** Why Loco 5025 is sent to device 0x85 is unknown. It is the loco being controlled by device 0x8F.
At this stage the project simply monitors the NCE Bus. To add the additional capability to request that the NCE_Twin take certain actions the NCE Twin expects a series of external key presses that echos as the command is built up. For example, if an accessory 345 is selected the NCE Twin will display the accessory message and will show the accessory numbers build up as the keys are typed in ("3","4","5" terminated with the enter code). This and following sections will add the facility to control an accessory via the NCE_Twin.
The implementation will be undertaken in two phases:
1. A request will be simulated, ** and
2. A keypress will be used to initiate the request.
** As part of programming the simulation it was necessary to implement some of the cursor comands. Eg 0xCE 0xCC placed the cursor on the second row at position 12 to display the first keypad entry. 0xCA 0xF4 will print character "4" at the cursor location.
Initially the sequence to request an accessory will be done in software.
In real life the key-pressed, key_displayed, next_key etc will happen sequentially at human speeds, there will be no timing issues. During code development there needs be sufficient delays to match the real-life key enter and key display timings.
For the code development in this project the timings will be a little crude. A counter will be set up that increments each time an ITS_ME ping is received. Initially the response to the NCE_Twin will be "7E", "7F" as previous. When the count reaches 500 an accessory will be issued. The NCE_Twin will give adequate time to respond before at a count of 550 the first number "3" is sent. When the count reaches 600 the next number "4" is sent etc. For other counts the null response "7D","7F" is sent. (**)
A possible trace is captured across.
In this example the TRACE option has been removed so most of the detailed activity is not shown.
Following start up the code will advise that there is a device at address 85 and one of the NCE_Twin's response will be the two messages LOC: 000 and speed FWD: 000. There is then the dash message followed by the three messages giving the clock time. On a NCE Handheld Controller the LCD display would read:
LOC: 000 02:39AM
FWD: 000 -------
Some 19 seconds (34:13 - 33:45) the code sends an Accessory request to the NCE_Twin.
The NCE_Twin will respond with the message ACC NUMB for the location second row on the left (over writing the FWD: 000.
The NCE_Twin then sends the command to move the cursor to the second-row location 0xC (in amongst the dashes) and enables the cursor.
Two seconds later the code sends the number/character "3".
Almost instantaneously the NCE_Twin resends the position cursor message followed by an acknowledgement of the letter "3". (The Print Character Command) On an NCE LCD the display would be:
LOC: 000 02:39AM
ACC NUMB ---3---
The code then sends the number "4" which the NCE_Twin acknowledges.
The number 5 follows followed by a space which is the NCE enter key (listed as CR).
The NCE_Twin disables the cursor and sends several messages so the display becomes:
ACC: 345 02:40AM
1=N(ON) 2=ROFF) (^^)
The code then sends the command "2" which tells the NCE_Twin to set Accessory 2 to reverse or turn. The NCE bus will not give any feedback as to whether this happened or not. This will need to be tested with a real layout with an accessory set to address 345. (--)
(**) For testing the timing was changed to use two constants BASE and DELTA. With DELTA =10 the code appeared to operate without any obvious glitches.
(^^) This message was captured once other problems in the code were rectified. (See trace below)
(--) The project using a hardware request worked so it is assumed this was all ok.
This section will develop the code to control a fixed accessory when an external key is pressed.
The client code will be of the form:
bus.SetAccessory(acc_no, position);
where the command/request has two parameters the accessory number and the position.
This will require adding a public method to the class. That is
It will be the job of this section to develop the implementation code. From the previous results the sequence will be: (using the 345-address example as before).
Request accessory. Wait for the command to enable cursor.
send the numbers "3", "4" and "5" waiting for a Print Character after each.
send the command 0x40 to terminate the number entry. Wait for a message to second row with "1" in byte 2.
Send 1 or 2 to select the desired accessory setting.
For testing the analog pin A0 of the Arduino is used. Possible client or test code will be included in the program loop( ). That is**:
void loop() {
bus.handle_NCE_Byte( );
//If there is an input then if accessory free set accessory request
if (analogRead(A0)<800)
{
Serial.println("\nAcc Req");
if (bus.accessFree( ))
bus.setAccessory(1234,2); //accessory 1234 turn/reverse
while (analogRead(A0)<800) { }; //seems to eliminate contact bounce
}
}
void DCC_NCE_Bus:: setAccessory(int acc,uint8_t dir)
{
_acc =acc; _dir = dir;
_afree = 0; //no more requests until done
//for this example need to send 4E,53,54,55,40, then direction
_accArray[0] = 0x4E;
_accArray[1] = 0x50+_acc/1000; _acc %= 1000;
_accArray[2] = 0x50+_acc/100; _acc %= 100;
_accArray[3] = 0x50+_acc/10; _acc %= 10;
_accArray[4] = 0x50+_acc;
_accArray[5] = 0x40;
_accArray[6] = 0x50+_dir;
_accPt = 0;
_waiting = 0; //handshake
Serial.println("Set Accessory");
for (int i = 0; i<=6 ; i++) {
Serial.print(_accArray[i],HEX);
Serial.print(' ');
}
Serial.println();
_avalid = 1; //valid data to print
}
** To synchronise the operation a variable _afree is used. _afree may be read using the public method access.free( ). _afree is cleared at the start of the setAccessory( ) method and set once the completed command is sent to the NCE_Twin.
The its_me_start( ) code can be modified to handle an external request. Possible code to transfer the 7 bytes in the accessory array will be **:
if (!_mess_count) { //send 7E 7F once
Send2Bytes485(0x7E,0x7F);
Serial.println("7E-7F");
_mess_count++;
}
else if (_avalid) { //code for accessory
if (_mess_count >20)
{
if (_accPt <7) {
Send2Bytes485(_accArray[_accPt],0x7F);
Serial.print("Sending ");
Serial.println(_accArray[_accPt],HEX);
_accPt++;
_mess_count = 1; //restart count but not at zero
}
if (_accPt == 7)
{
_avalid = 0; //accessory handled
_afree = 1; //can load another request
_accPt =0;
}
}
else {
Send2Bytes485(0x7D,0x7F);
_mess_count++;
}
}
else
{
Send2Bytes485(0x7D,0x7F);
// Serial.print('.');
}
** To avoid timing problems each bit of data is only transferred to the NCE Twin every 20 cycles. On sending the ENTER key for example the NCE_Twin will send the messages to display the accessory number (eg ACC: 1234) on the top line and "1=N(ON)" and "2=R(OFF)" on the lower line which will require several cycles so waiting 20 cycles is quite conservative.
The project was tested on a system with points at addresses 59, 60 and 61.
Modifying the client code to bus.setAccessory(60,dir); it was found that the points responded as expected. Note the points were slow moving so the delay completely swamped the delay of 20 cycles used in the previous section.
In the test model railway macros were used. That is selecting macro 172 would operate points 59, 60 and 61. The client code was then modified to handle macros. This was not quite a case of sending the 3 point settings one after the other but including handshaking so only after the first point was switched would the request for the second be sent.
This page went through the development of an Arduino based system that monitored the RS485 bus output of the NCE Twin. The final product:
Displayed the NCE fast clock as transmitted by the NCE in Broadcast mode.
2. "Logged" on to the system simulating an additional controller or CAB.
3. Implement a set accessory method that could control an accessory (point).
The project development was assisted by the liberal use of print statements to both verify what was received from the NCE_Twin and the program activity. While the liberal use of print statements did clutter the display and were removed once their job was done it was not found that the print statements impacted on the overall system. (ie no data was missed).
Possible additional enhancements include implementing a loco control. This would mean the NCE Twin would not be restricted to locos 3 and 4.
At Club: Reset and tells I'm on bus but controller already knows type so no request for type
Controller sends out time - different from to that from the DCC DCE Twin
Throttle 91 gets on bus and says it is here (7E,7F)
Type of terminal requested (D2) and response 0x61 - a dumb terminal
After start up display is saturated with I am here messag
Another start up - device 95 detected (only time??)
Screen is saturated with I'm here message. Modify code to miss &D messages
Device 91 sends a MACTO request (5C)
Controller responds with message MACRO NUMBER: in two passes
There's a C8 CF which will be something to do with display
Another macro request following a speed change
5C is the macro request
C0 cursor
Then message in two chunks
51 first digit in macro
4E accessary request .063
Completing accessary request
Othe project with Macros .
Send request 5C
Response MACRO NUMBER in two chunks
CAB sends 1, 7, 3 plus terminates with 0x40
Controllerthen goes back and displays loco information. Currently its loco 0 with a forwrd speed of 0.