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
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;
// --------------------------------------------------------- // --------------------------------------------------------- // 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)
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 }, };
// ------------------------------------- // 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; }
// ----------------------------------------------- // 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); } }
// --------------------------------------------------------- // --------------------------------------------------------- // *loopFunc supports different modes of operation static void normal (void); void (*loopFunc) (void) = normal;
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 mode, monitor keypad and cabbus for input static void normal (void) { readButton (); cabbusRead (); }
// ----------------------------------------------- // loop repeatedly perform desired mode and heartbeat void loop (void) { (*loopFunc) (); heartbeat (Hb); }
// --------------------------------------------------------- // --------------------------------------------------------- // 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)); } }
// ----------------------------------------------- // 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; } }
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
pin 6 ---> + + + + pin 7 ---> + + + + pin 8 ---> + + + + pin 9 ---> + + + + | | | |--> pin 2 | | |----> pin 3 | |------> pin 4 |--------> pin 5
Button-pad matrix
// 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); } }
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; }
// --------------------------------------------------------- // 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; }
// ----------------------------------------------- // 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
cabbusSetup()
cabbusAddr()
cabbusUpdt()
cabbusRead()
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)
#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;
// ------------------------------------- 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); }
// ----------------------------------------------------------------------------- void cabbusAddr ( byte addr ) { cabId = TYPE_POLL + (addr & (~TYPE_MASK)); }
// ------------------------------------- void cabbusUpdt ( byte keyCode, byte speed ) { if (NO_KEY_DN != keyCode) { cabKeyCode = keyCode; } cabSpeed = speed; }
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); }
// ------------------------------------- void busOutput ( byte c, byte speed ) { byte vec [2]; vec [0] = c; vec [1] = speed; out (vec, 2); }
// ------------------------------------- byte busInput () { byte c = (byte) NULL; if (cabSerial->available()) { c = cabSerial->read (); } return c; }
// ------------------------------------- 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; } }
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); } } }