NCE Compatible Arduino Cab

NCE Compatible Arduino Cab Firmware Description

The Arduino NCE cab code is divided into multiple files where several files focus on hardware support and the .ino file implements the desired behavior.

This is a copy of the code from March 2018. It still includes support to be run on an Arduino Mega and #ifdefs for debugging.

Organization

While global variables are used within each file, globals are not uses between files except possibly for debug. That means a function withing a file (e.g cabbusAddr()) is called to pass information from one file needed in another. Such a function is described as an external interface. This is an object oriented approach where each file contains state (i.e. variables) and functions with well defined external interfaces for manipulating a well defined object and nothing else.

Functionality within a file is broken down into sub-functions that have a single well defined purpose (e.g. cabbus out()). Sub-functions may call sub-functions. To avoid undefined symbol warnings and the need for forward references, a sub-function called by another sub-function must be defined before the sub-function calling it. Therefore, the main function is usually at the bottom of a file.

To avoid extern declaration of functions defined in one file and called in a different file, there are header (.h) files declaring all external interfaces and any common typedefs or #defines for each .cpp file. That same header is also included in the corresponding .cpp file. At the very least, there will be a compiler warning if the declaration in the header file is different than the definition in the .cpp file.

Style, the format that the coded is written is not unimportant. Most organizations have style guidelines so that code looks uniform and familiar to any developer who needs to look at it. Here are a few that help make the code readable:

  • The first letter of a symbol is capitalized for constants. This implies that all variables and functions names are not capitalized.

  • for the purpose of being able to print a copy of the code, no line is longer than 80 columns.

Cab12.ino

Cab12.ino implements the behavior of the cab. This includes recognizing button presses and responding to poll requests from the cabbus. It also requires a mechanism for setting the cab address, stoing and retrieving it from the EEPROM.

The top of the file includes the headers declaring the external interfaces of the other files and pins definitions. The cabAddr is used locally for transfer between the EEPROM.

// NCE const char version [] = "\"Cab12 0311b"; #include #include "cabbus.h" #include "keypad.h" #include "Led.h" #define ADC4 4 #define Hb 13 #define TxEn 12 #define CabLed 11 #define EE_EMPTY 0xFF #define EE_CAB_ADDR 1 #ifdef __AVR_MEGA__ # define DEBUG #else # undef DEBUG #endif int dbg = 0; byte cabAddr = 0;

A diagram illustrates the row and column pins used to scan the keypad. These pins are defined in kpCols[] and kpRows[].

// --------------------------------------------------------- // --------------------------------------------------------- // keypad // // pin 6 ------> + + + + // pin 7 ------> + + + + // pin 8 ------> + + + + // pin 9 ------> + + + + // // | | | -----------> pin 2 // | | -------------> pin 3 // | ---------------> pin 4 // -----------------> pin 5 // // define the pins in the row and column byte kpCols[] = { 5, 4, 3, 2 }; #define KP_COL_SIZE sizeof(kpCols) byte kpRows[] = { 6, 7, 8, 9 }; #define KP_ROW_SIZE sizeof(kpRows)

The keypad routines report the row and column of a button press. Cab12.ino needs to translate the row/col of a button into a cabbus button code during normal operation, or a decimal digit when programming the cab address. keyCodes[] and numCode[] are used to map the row/col of a button to the appropriate value as needed.

Keycodes.h is included in keypad.h and provides symbols for the buttons (e.g. F1, FAST) avoiding the hardcode hex values in the commented out (#if 0) table.

// button translation into cab bus codes char keyCodes [KP_ROW_SIZE] [KP_COL_SIZE] = { #if 0 { 0x51, 0x52, 0x53, 0x54 }, // 1, 2, 3, 4 { 0x55, 0x56, 0x57, 0x58 }, // 5, 6, 7, 8 { 0x59, 0x50, 0x5A, 0x5B }, // 9, 0, +, - { SEL, FOR, REV, ENTER }, // S, F, D, E #else { F1, F2, F3, F4, }, // 1, 2, 3, 4 { F5, F6, F7, F8, }, // 5, 6, 7, 8 { F9, F0, FAST, SLOW }, // 9, 0, +, - { SEL, FOR, REV, ENTER }, // S, F, D, E #endif }; // button translation for values when setting cab addr char numCodes [KP_ROW_SIZE] [KP_COL_SIZE] = { { '1', '2', '3', '4' }, { '5', '6', '7', '8' }, { '9', '0', NO_KEY, NO_KEY }, { NO_KEY, NO_KEY, NO_KEY, ENTER }, };

GetKey() is called with a pointer to one of the above tables to determine if a button is pressed and its value depending on the table passed.

// ------------------------------------- // scans for a keypress and translates using spcified tbl static char getKey ( char tbl [KP_ROW_SIZE] [KP_COL_SIZE] ) { byte row = 0; byte col = 0; if (NO_KEY != keypad (& row, & col)) { if (dbg) { Serial.print ("getKey: "); Serial.println (tbl [row][col], HEX); } return tbl [row][col]; } return NO_KEY; }

ReadButton() is used during normal operation to check for button presses and post a two byte cab response to be sent during the next polling cycle using cabbusUpdt(). It can optionally (i.e. #if read a speed control knob (potentiometer) connected to analog input 4 and send its value as the speed byte in the response

// ----------------------------------------------- // button routine for normal operation static void readButton () { #if 0 int spd = int(analogRead (ADC4) / 8); #else int spd = SPD_BUTTON; #endif char key = getKey (keyCodes); key = NO_KEY == key ? NO_KEY_DN : key; // only report button presses;:w if (NO_KEY_DN != key) { cabbusUpdt (key, spd); } }

The Arduino loo() simply invokes a function pointer, loopFunc and heartbeat(). It is defined here because setCabAddr() references it. And because its default value is normal(), there is a forward declartion of normal above it. Normal() is defined below. The forward declartion describes it as a function returning no value and taking no arguments which matches the definition of loopFunc.

// --------------------------------------------------------- // --------------------------------------------------------- // *loopFunc supports different modes of operation static void normal (void); void (*loopFunc) (void) = normal;

LoopFunc is set to setCabAddr() in butHold() if the SELECT button is held down as the cab starts up when plugged into a panel. This puts the cab into a mode for setting the cab address.

SetCabAddr() monitors the keypad. If a digit button is pressed, it updates the decimal value of the cab in val. It writes the cab address to EEPROM when the ENTER button is pressed, turns off the LED and sets loopFunc to normal. Subsequent calls to loop() will invoke normal(). Notice that the numCodes table has NO_KEY entries for all buttons except the digits and ENTER. SetCabAddr() will ignore those button presses.

// ----------------------------------------------- // read keypad to set cab addr in EEPROM static void setCabAddr (void) { static int val = 0; char c = getKey (numCodes); LedOn (CabLed); if (NO_KEY != c) { if (ENTER == c) { loopFunc = normal; EEPROM.write (EE_CAB_ADDR, val); cabbusAddr (val); LedOff (CabLed); return; } val = (10 *val) + (c - '0'); } }

Normal() calls readButton() which checks for button presses and queues them for transmission. It call cabbusRead() to check for polls and send queued button responses from readButton() and service any subsequent commands from the command station.

// ----------------------------------------------- // normal mode, monitor keypad and cabbus for input static void normal (void) { readButton (); cabbusRead (); }

The Arduoino loop() is invoked periodically from the Arduino main() (Arduino/hardware/arduino/avr/cores/arduino/main.cpp). It invokes the loopFunc() and calls heartbeat() that winks the LED 13 twice every second, indicating the the program is running.

// ----------------------------------------------- // loop repeatedly perform desired mode and heartbeat void loop (void) { (*loopFunc) (); heartbeat (Hb); }

EeDump() is a debug routine used with the Mega to display the contents of the EEPROM.

// --------------------------------------------------------- // --------------------------------------------------------- // display specified # eeprom locations static void eeDump ( int N) { Serial.println ("eeDump:"); for (int addr = 0; addr < N; addr++) { Serial.print (addr); Serial.print ("\t"); Serial.println (EEPROM.read (addr)); } }

ButHold() calls keyHold() to determine if a button has been held down continuosly for some period. If one has, three buttons are valid. The SELECT button sets loopFunc to setCabAddr. The FORWARD button simply prints a debug message used to test the function. The REVERSE button invokes eeDump(). The FORWARD and REVERSE buttons subsequently enter normal operation.

// ----------------------------------------------- // check for button held down at startup #define MIN_BUT_CNT 80 static void butHold (void) { byte col = 0; byte row = 0; byte key = keyhold (&row, &col, MIN_BUT_CNT); if (NO_KEY == key) { return; } #ifdef DEBUG Serial.print ("butHold: "); Serial.println (key); #endif // perform action dictated by button switch (keyCodes [row][col]) { case SEL: loopFunc = setCabAddr; break; case FOR: dbg = 1; Serial.println ("butHold: dbg"); break; case REV: eeDump (10); break; default: break; } }

The Arduino setup() is called once from the Arduino main().

It initializes pins using arrays defining the purposes of the pins. It invokes keypadSetup() with pointers to the keypad row and col pin arrays which will configure those pins appropriately.

It passes cab paramters by invoking cabbusSetup(). These include the I/O pins used to enable the transmitter and faceplate LED and Serial port definintions when using an Arduino Mega. (There is an attempt to use the __AVR_MEGA__ macro to determine if the code is being loaded onto a Mega which I'm not sure works).

It invokes butHold() to support programming the cab address and flashes the LED for half a second to indicate that it is done;

// --------------------------------------------------------- // one time actions at startup byte ioPins [] = {Hb, CabLed }; #define NumIoPins sizeof(ioPins) void setup (void) { Serial.begin(9600); for (uint8_t i= 0; i < NumIoPins; i++) pinMode (ioPins[i], OUTPUT); keypadSetup (kpCols, KP_COL_SIZE, kpRows, KP_ROW_SIZE); #undef __AVR_MEGA__ #ifdef __AVR_MEGA__ Serial.println ("setup: MEGA using Serial 13"); # if 1 cabbusSetup (TxEn, CabLed, &Serial3, &Serial); # else cabbusSetup (TxEn, CabLed, &Serial3, NULL); # endif #else cabbusSetup (TxEn, CabLed, &Serial, NULL); #endif cabbusAddr (EEPROM.read (EE_CAB_ADDR)); #ifdef DEBUG Serial.println (version); Serial.print ("cabbus addr: "); Serial.println (EEPROM.read (EE_CAB_ADDR)); #endif butHold (); LedOn (CabLed); delay (500); LedOff (CabLed); }

Keypad.cpp

Keypad.cpp includes function to recognize a button being held down, returning the row and column of the button. It simply detects the button press without identifying its purpose.

pin 6 ---> + + + + pin 7 ---> + + + + pin 8 ---> + + + + pin 9 ---> + + + + | | | |--> pin 2 | | |----> pin 3 | |------> pin 4 |--------> pin 5

Button-pad matrix

The diagram illustrates how the buttons are wired in row and columns. The I/O pins for each column (2-5) are configured as inputs with pull-ups. The pins for the rows are configured as outputs, HIGH by default. KeypadSetup() is given two arrays of pins and configures the column and row pins as just described.

// keypad #include #include "keypad.h" #undef DEBUG static byte* _kpCols = NULL; static byte* _kpRows = NULL; static unsigned int _kpColSize = 0; static unsigned int _kpRowSize = 0; // --------------------------------------------------------- void keypadSetup ( byte* kpCols, unsigned int kpColSize, byte* kpRows, unsigned int kpRowSize ) { _kpCols = kpCols; _kpColSize = kpColSize; _kpRows = kpRows; _kpRowSize = kpRowSize; for (unsigned int c = 0; c < _kpColSize; c++) pinMode (_kpCols [c], INPUT_PULLUP); for (unsigned int r = 0; r < _kpRowSize; r++) { pinMode (_kpRows [r], OUTPUT); digitalWrite (_kpRows [r], HIGH); } }

KeyScan() detects a button press by setting each row output LOW and checking if any of the columns inputs are LOW. The pull-ups on the inputs make the input HIGH. When the row connected to a button being presses is set LOW, the column associated with the button will be LOW. Knowing the row and column identifies the button. It returns a 4-bit column and row as an 8-bit byte, as well as returning their values in the pointer arguments.

If no columns inputs are LOW, the row is set HIGH and the next row checked. It returns NO_KEY if not buttons pressed.

#define DEB_PER 10 int keyscan ( byte* row, byte* col ) { for (unsigned int r = 0; r < _kpRowSize; r++) { digitalWrite (_kpRows [r], LOW); for (unsigned int c = 0; c < _kpColSize; c++) { if (! digitalRead (_kpCols [c])) { digitalWrite (_kpRows [r], HIGH); *row = r; *col = c; #if DEBUG Serial.print ("keyscan: "); Serial.print (r); Serial.print (", "); Serial.println (c); #endif int res = (r << 4) | c; return res; } } digitalWrite (_kpRows [r], HIGH); } return NO_KEY; }

Keypad() returns a debounced button press. It resets a timer, ms, if the button has changed. If the timeout has expired, it checks if it's a new button (i.e. state != pend). It returns the row/column value if a new button press is detected.

// --------------------------------------------------------- // return debounced key int keypad ( byte* row, byte* col ) { static char state = NO_KEY; static char pend = NO_KEY; static uint32_t ms = 0; char key = keyscan (row, col); if (pend != key) { pend = key; ms = millis (); #ifdef DEBUG Serial.print ("keypad: new key "); Serial.println (key, HEX); #endif } if (DEB_PER < (millis () - ms)) { if (state != pend) { #ifdef DEBUG Serial.print ("keypad: debounced key "); Serial.println (pend, HEX); #endif return state = pend; } } return NO_KEY; }

Keyhold() is used at startup to determine if a button was held down during startup. It repeatedly calls keyscan() for up to 100 times, counting the number of times it has seen the same button. If the count exceeds a minimum, it returns the button press as a row/col byte.

// ----------------------------------------------- // check for button held down at startup int keyhold ( byte* row, byte* col, int minCnt ) { int cnt = 0; byte key = NO_KEY; byte c; for (int n = 100; n > 0; n--) { c = keyscan (row, col); if (NO_KEY != c) { cnt += key == c ? 1 : 0; key = c; } } if (minCnt > cnt) { return NO_KEY; } return c; }

Cabbus.cpp

Cabbus.cpp contains routines to read and transmit bytes from an RS-485 bus. The external interfaces are

  • cabbusSetup()

  • cabbusAddr()

  • cabbusUpdt()

  • cabbusRead()

CabbusRead() is the main function expected to be called from the Arduino loop(). It uses various sub-functions to read, transmit data and decode commands.

The top of the file contains a pinout for the cable and diagram illustrating how the MAX485 chip can be used to interface the RD and TX signals from the Arduino to the balanced-pair bi-directional RS-485 bus. Bi-directional means that the same pair of wires is used to both transmit and receive data. This is termed half-duplex and implies that the transmitter must be disabled when no transmitting. Balanced-pair means a pair of wires transmit the data where each wire changes polarity depending on bit value.

// cabbus inteface // check for, read byte from CabBus // // pin 1 wh power to track // pin 2 bk Ground // pin 3 rd -RS-485 // pin 4 gn +RS-485 // pin 5 ye +12 volts // pin 6 bu power to track // // _________ // RXD1(2) / RD <---- |o | ---- V+ // D12 --+-- RE_N ----> | | ---- B(-) Red // |-- TE <---> | MAX-485 | ---- A(+) Green // TXD1(1) / TD ----> |_________| ---- Gnd // - cmd-station expects 2/5 byte response // - cab should initially send refresh LCD, 0x7e, and // no speed, 0x7f, commands // - response to cab type, 0xd2, cab 'a' cab w/ LCD, 'b' w/o LCD // - send without delay (e.g. prints)

Since this is a .cpp, not a .ino file, Arduino.h must be explicitly included. This code can supports used of two Serial interfaces allowing it to be used with an Arduino Mega and serial monitor debugging. The serial monitor must use Serial1, which means the cabbus must use one of the other three serial interfaces available on the Mega.

#include #include "cabbus.h" #define TYPE_MASK 0xC0 #define TYPE_POLL 0x80 #define TYPE_CMD 0xC0 #define CAB_TYPE_LCD 'a' #define CAB_TYPE_NO_LCD 'b' #define DEBUG byte cabId = 0; byte cabKeyCode = NO_KEY_DN; byte cabSpeed = SPD_BUTTON; byte txEnPin = 0; byte ledPin = 0; HardwareSerial* cabSerial = NULL; HardwareSerial* monSerial = NULL;

CabbusSetup() provide hardware parameters: the pins used to enable the transmitter and the LED controlled throught cabbus commands, the Serial interface for the cab and for debugging using the serial monitor. It should be called from the Arduino setup().

// ------------------------------------- void cabbusSetup ( byte txEn, byte led, HardwareSerial* cab, HardwareSerial* mon ) { txEnPin = txEn; ledPin = led; cabSerial = cab; monSerial = mon; pinMode (txEnPin, OUTPUT); pinMode (ledPin, OUTPUT); digitalWrite (txEnPin, LOW); cabSerial->begin(9600); }

CabbusAddr() specifies the cab address that this code should respond to when polled. It should be called from the Arduino setup().

// ----------------------------------------------------------------------------- void cabbusAddr ( byte addr ) { cabId = TYPE_POLL + (addr & (~TYPE_MASK)); }

CabbusUpdt() specifies a two byte response that should be transmitted during the next poll cycle.

// ------------------------------------- void cabbusUpdt ( byte keyCode, byte speed ) { if (NO_KEY_DN != keyCode) { cabKeyCode = keyCode; } cabSpeed = speed; }

Out() is passed an array of bytes and and the number of bytes to transmit out the RS-485 interface. It must first enable the transmitter and waits one millesecond. It calls Serial write() to load the byte into the Arduino UART. If the serial monitor is used, it prints debugging information.

Once all bytes have been queued, flush() is called to wait for all bytes to be transmitted before disabling the transmitter.

// ------------------------------------- void out ( byte vec [], int nByte ) { int n; digitalWrite (txEnPin, HIGH); delay (1); for (n = 0; n < nByte; n++) { cabSerial->write (vec [n]); if (NULL != monSerial) { if (NO_KEY_DN != vec [0]) { // ignore monSerial->print ("out: "); monSerial->println (vec [n], HEX); } } } cabSerial->flush (); digitalWrite (txEnPin, LOW); }

BusOutput() handles a common two byte cab response which is a optional button press or NO_KEY_DN and a speed value from a knob or SPD_BUTTON, meaning speed is controlled through button commands.

// ------------------------------------- void busOutput ( byte c, byte speed ) { byte vec [2]; vec [0] = c; vec [1] = speed; out (vec, 2); }

BusInput() checks if there is input using available(), and if there is calls read() to get the received byte value from the UART.

// ------------------------------------- byte busInput () { byte c = (byte) NULL; if (cabSerial->available()) { c = cabSerial->read (); } return c; }

Decode() processes a non-poll command. It really only supports turning on/off the LED and responding to the cab type. This is where supporting other commands could be added.

// ------------------------------------- void decode ( byte c) { static int cnt = 0; byte vec [5]; if (cnt) { cnt--; return; } switch (c) { case 0xc0: case 0xc1: case 0xc2: case 0xc3: case 0xc4: case 0xc5: case 0xc6: case 0xc7: cnt = 8; break; case CURSOR_OFF: // turn LED off digitalWrite (ledPin, HIGH); break; case CURSOR_ON: // turn LED on digitalWrite (ledPin, LOW); break; case 0xd8: cnt = 2; break; case 0xdb: cnt = 5; break; case CAB_TYPE: vec [0] = CAB_TYPE_NO_LCD; out (vec, 1); break; default: break; } }

CabbusRead() handles all input from the bus. It first checks for and reads any received byte, returning immediately if there are none. If prints the value of the received byte if serial monitoring is available.

It checks if the received byte is a poll command and if it matches the address of the cab set using cabbusAddr(). If it does, and there is a pending response posted using cabbusUpdt(), it immediatley sends it using busOutput().

The poll flag to determine if any subsequent commands should be processed by the cab using decode(). Those commands are printed is serial monitoring is available.

// ------------------------------------- void cabbusRead (void) { static byte poll = 0; byte c; if ((byte) NULL == (c = busInput())) return; #ifdef DEBUG if (NULL != monSerial) { if (TYPE_POLL != (TYPE_MASK & c)) { // ignore polls monSerial->print ("cabbusRead: "); monSerial->println (c, HEX); } } #endif if (TYPE_POLL == (c & TYPE_MASK)) { if (cabId == c) { if (cabKeyCode) { busOutput (cabKeyCode, cabSpeed); cabKeyCode = NO_KEY_DN; } poll = 1; } else { poll = 0; } } // only accept commands after being polled else if (poll && TYPE_CMD == (c & TYPE_MASK)) { decode (c); if (NULL != monSerial) { monSerial->print ("cabbusRead: "); monSerial->println (c, HEX); } } }

Led.cpp

Led.cpp comes from other applications used to flash/wink an LED an arbitrary number of times for debugging. The cab firmware uses heartbeat() to periodically flash the Arduino LED twice to indicate the firmware is running.

Heartbeat() reads the current time in milleseconds, comparing it to various values to turn on and off the LED twice each second.

#define HB_PERIOD 1000 long msec_last = 0; void heartbeat ( int Hb) { long msec = millis (); long delta = msec - msec_last; if ( (delta) > HB_PERIOD) { msec_last = msec; digitalWrite (Hb, HIGH); } else if ( (delta) > 300) digitalWrite (Hb, LOW); else if ( (delta) > 200) digitalWrite (Hb, HIGH); else if ( (delta) > 100) digitalWrite (Hb, LOW); }

flashLed() turns on the specified LED for a specified numb of milleseconds, pause.

void flashLed ( int led, int pause ) { digitalWrite (led, LOW); delay (pause); digitalWrite (led, HIGH); }

FlashLedHex() flashes a specified LED using a pattern specified by the bits in val.

#define LED_LONG 400 #define LED_SHORT 50 #define LED_PERIOD 700 void flashLedHex ( int led, byte val ) { for (int i = 8; i > 0; i--, val <<= 1) { if (val & 0x80) { flashLed (led, LED_LONG); delay (LED_PERIOD - LED_LONG); } else { flashLed (led, LED_SHORT); delay (LED_PERIOD - LED_SHORT); } } }