Since the writing of the last post Kato Turntable V2, I have been collaborating with Fred Downing from Calgary, Canada and we have together made several significant improvements to the design that originated with Clement Chan, was modified by me and now has been further improved by Fred. The new design is stable and reliable and features :
- DCC Control.
- Local Keypad control.
- Display showing current track position.
- Loconet based feed back to the DCC system to update track position.
A. The primary DCC decoding and turntable control as well as display is all handled by a single micro controller (AT Mega 2560) on the Arduino Mega.
B. The loconet feedback is best handled on a separate Loconet board driven by a Arduino Nano.
C. The Mega communicates with the Nano using Serial communication.
Finished Product:
The Schematic:
The Board :
Prototyping
The Loconet component of the board was prototyped using Timo Sariwating’s Loconet adpater board and the excellent libraries of Alex Shepherd. Here is the assembled board. (Note the required elements are now incorporated into my Control board above.
More details of the project, the updated eagle files and the code files to follow soon.
The Code
Decoder and Turntable Controller (Runs On Arduino Mega)
# This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. (C) 2021 This code is the collaborative effort of Dheerendra Prasad and Fred Downing and is based on and inspired by work done by Clement Chan. #include <DCC_Decoder.h> #include <Wire.h> #include <Adafruit_FRAM_I2C.h> #include <SPI.h> #include <Keypad.h>; #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> /*Display Declaration*/ #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels // Declaration for an SSD1306 display connected to I2C (SDA, SCL pins) // The pins for I2C are defined by the Wire-library. // On an arduino UNO: A4(SDA), A5(SCL) // On an arduino MEGA 2560: 20(SDA), 21(SCL) // On an arduino LEONARDO: 2(SDA), 3(SCL), ... #define OLED_RESET 4 // Reset pin # (or -1 if sharing Arduino reset pin) Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); int v = 18; int v2= 45; int h1 = 51; int h2= 57; int h0 = 10; int h4 =80; /*Keypad declaration*/ // Keypad Setup const byte ROWS = 4; // Four Rows const byte COLS = 4; // Four Columns char keys[ROWS][COLS] = { {'1', '2', '3', 'Z'}, {'4', '5', '6', '<'}, {'7', '8', '9', '>'}, {'0', 'G', 'E', 'H'} }; byte rowPins[ROWS] = {22, 24, 26, 28}; // Arduino pins connected to the row pins of the keypad byte colPins[COLS] = {23, 25, 27, 29}; // Arduino pins connected to the column pins of the keypad Keypad mykeypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); // Keypad Library definition /*Constants and variables*/ char padcommand; const int LOCK_PINS[2] = {5, 6}; //AIN1 - AIN2 const int ROTATE_PINS[2] = {10, 11}; //BIN1 - BIN2 const int CONTROL_PIN = 2; //D2 const int LOCK_SPEED = 128; //lock motor speed - default 128 const int LOCK_CYCLE = 550; //lock cycle time in ms - default 500 const int ROTATE_SPEED = 192; //rotation motor speed - default 192 const int TICK_INTERVAL = 300; //interval to prevent control switch bounce - default 300 const int HOME_POSITION = 1; const int POSITIONS = 36; const int CCW = -1; const int CW = 1; const bool UNLOCK = 0; const bool LOCK = 1; const int DCC_INTERRUPT = 1; //D3 const int DCC_DECODER_BASE = 900; //base decoder # for DCC input const int DCC_DECODER_MAX = 949; const int FRAM_LOCK_BYTE = 0x1; const int FRAM_INDEX_BYTE = 0x2; const int FRAM_DIR_BYTE = 0x3; const int CMD_NULL = -1; const int CMD_STOP = 0; const int CMD_STEP_CW = 40; const int CMD_STEP_CCW = 41; const int CMD_FLIP = 46; const int CMD_HOME = 47; const int CMD_SET_HOME = 48; int command; static Adafruit_FRAM_I2C fram; static bool isLocked = true; // bridge locked? static int seekPostion = 0; // position to move bridge volatile int currentPosition = 0; // current bridge position volatile int rotation = 0; // -1 CCW, 0 OFF, 1 CW volatile unsigned long lastTick = 0; // time last index counted static bool isReset = false; // count first tick volatile int commandCode = -1; // next command to run /* DCC Accessory Packet Handler */ void onAccesoryDecoderPacket(int packet, boolean activate, byte data) { static int prevDecoder = 0; static bool prevEnable = false; int address = (((packet - 1) * 4) + 1) + ((data & 0x06) >> 1); boolean enable = (data & 0x01) ? 1 : 0; //ignore multiple button press if ( !((address == prevDecoder) && (enable == prevEnable )) ) { if (enable) { if ((address >= DCC_DECODER_BASE) && (address <= DCC_DECODER_MAX)) { //pass command code to main loop commandCode = address % DCC_DECODER_BASE; } } prevDecoder = address; prevEnable = enable; } } /* TURNTABLE INTERRUPT */ void onControlTick() { if (rotation) { unsigned long currentTick = millis(); //ignore bounce if ((currentTick - lastTick) > TICK_INTERVAL) { currentPosition += rotation; if (currentPosition > POSITIONS) { currentPosition = HOME_POSITION; } else if (currentPosition < HOME_POSITION) { currentPosition = POSITIONS; } } lastTick = currentTick; } } /* SKETCH */ void setup() { Serial.begin(9600); Serial1.begin(9600); Serial2.begin(38400); Serial.setTimeout(50); //Turntable Pin Setup pinMode(CONTROL_PIN, INPUT); attachInterrupt(0, onControlTick, FALLING) ; pinMode(ROTATE_PINS[0], OUTPUT); pinMode(ROTATE_PINS[1], OUTPUT); pinMode(LOCK_PINS[0], OUTPUT); pinMode(LOCK_PINS[1], OUTPUT); pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, LOW); pinMode(12, OUTPUT); digitalWrite(12, LOW); //DCC Setup DCC.SetBasicAccessoryDecoderPacketHandler(onAccesoryDecoderPacket, true); DCC.SetupDecoder( 0x00, 0x00, DCC_INTERRUPT ); commandCode = CMD_NULL; //Initialize Turntable //Serial.println("Initializing ..."); //set defaults isLocked = true; isReset = false; rotation = false; currentPosition = HOME_POSITION; seekPostion = 0; //set current state from memory if (fram.begin()) { int lastLockState = (fram.read8(FRAM_LOCK_BYTE)); int lastIndex = (fram.read8(FRAM_INDEX_BYTE)); int lastDir = (fram.read8(FRAM_DIR_BYTE)); //Serial.print("Current state loaded: "); //Serial.print((lastLockState == 0)?"Unlocked Index:":"Locked Index:"); //Serial.println(lastIndex); if (lastLockState == 0 && lastIndex != 0) { //turntable needs to reset because it is unlocked //checking lastIndex is not NULL which means FRAM hasn't been initialized isLocked = false; isReset = true; if (lastDir == 1) { commandCode = CMD_STEP_CW; } else { commandCode = CMD_STEP_CCW; } } if (lastIndex !=0) { currentPosition = lastIndex; } } //Initialize OLED Display // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32 Serial.println(F("SSD1306 allocation failed")); for(;;); // Don't proceed, loop forever } show(); Serial1.println(currentPosition); } void loop() { DCC.loop(); if (rotation) { if (currentPosition == seekPostion) { StopRotate(); } else { fram.write8(FRAM_INDEX_BYTE, currentPosition); } } if (commandCode != CMD_NULL) { if (!rotation) { DoCommand(commandCode); commandCode = CMD_NULL; } else { DoCommand(CMD_STOP); } } else if(Serial.available()) { commandCode = SerialCommand(); } else { padcommand = GetKeyFromKeyPad(); } ParseCommand(padcommand); padcommand = ' '; } int SerialCommand() { String input = Serial.readString(); input.toLowerCase(); switch (input.charAt(0)) { case 's': return CMD_STOP; break; case '<': return CMD_STEP_CCW; break; case '>': return CMD_STEP_CW; break; case 'r': return CMD_FLIP; break; case 'h': return CMD_HOME; break; case 'z': return CMD_SET_HOME; break; default: return (input.toInt()); } } void DoCommand(int command) { if (command >= HOME_POSITION && command <= POSITIONS) { StartRotate(command); } else { switch (command) { case CMD_STOP: //Serial.println("Stop Turntable"); if (rotation) {seekPostion = currentPosition + rotation;} break; case CMD_STEP_CW: //Serial.println("Rotate Turntable CW"); StartRotate(currentPosition + CW); break; case CMD_STEP_CCW: //Serial.println("Rotate Turntable CCW"); StartRotate(currentPosition + CCW); break; case CMD_HOME: //Serial.println("Rotate Home"); StartRotate(HOME_POSITION); break; case CMD_FLIP: //Serial.println("Rotate 180"); //seekPostion = currentPosition + (POSITIONS / 2); //if (seekPostion > POSITIONS) {seekPostion -= POSITIONS;} StartRotate(currentPosition + (POSITIONS / 2)); break; case CMD_SET_HOME: //Serial.println("Reset"); currentPosition = HOME_POSITION; break; default: //Serial.println("Command Error"); break; } } } /* TURNTABLE COMMANDS */ void StartRotate(int nPosition) { //keep nPosition is in bounds if (nPosition > POSITIONS) {nPosition -= POSITIONS;} if (nPosition < HOME_POSITION) {nPosition += POSITIONS;} if ((nPosition != currentPosition) && (!rotation)) { int cwPositions = 0; int ccwPositions = 0; //calculate rotation distance for CW & CCW if (nPosition > currentPosition) { cwPositions = nPosition - currentPosition; ccwPositions = (POSITIONS - nPosition) + currentPosition; } else { cwPositions = (POSITIONS - currentPosition) + nPosition; ccwPositions = currentPosition - nPosition; } /* Serial.print("Go to position "); Serial.print(nPosition); Serial.print(" from "); Serial.println(currentPosition); Serial.print("CW:"); Serial.print(cwPositions); Serial.print(" CCW:"); Serial.println(ccwPositions); */ //start to rotate SetLock(UNLOCK); digitalWrite(12, HIGH); if (cwPositions <= ccwPositions) { analogWrite(ROTATE_PINS[0], ROTATE_SPEED); digitalWrite(ROTATE_PINS[1], 0); rotation = CW; show2(); } else { digitalWrite(ROTATE_PINS[0], 0); analogWrite(ROTATE_PINS[1], ROTATE_SPEED); rotation = CCW; show3(); } fram.write8(FRAM_DIR_BYTE, rotation); seekPostion = nPosition; if (!isReset) { lastTick = millis(); } else { lastTick = 0; isReset = false; } } } void StopRotate() { if (rotation) { digitalWrite(ROTATE_PINS[0], 0); digitalWrite(ROTATE_PINS[1], 0); rotation = 0; SetLock(LOCK); digitalWrite(12, LOW); fram.write8(FRAM_DIR_BYTE, rotation); fram.write8(FRAM_INDEX_BYTE, currentPosition); show(); // cpt=900+currentPosition; // setLNTurnout(cpt, TURNOUT_NORMAL); // delay(10); // setLNTurnout(cptPrev, TURNOUT_DIVERGING); // cptPrev = cpt+0; Serial1.println(currentPosition); Serial2.write(currentPosition); } } void SetLock(bool state) { if (isLocked != state) { //start lock motor if (state == LOCK) { digitalWrite(LOCK_PINS[0], 0); analogWrite(LOCK_PINS[1], LOCK_SPEED); } else { digitalWrite(LOCK_PINS[1], 0); analogWrite(LOCK_PINS[0], LOCK_SPEED); } //allow lock motor to run full cycle unsigned long cycle = millis(); while (millis() - cycle < LOCK_CYCLE); //stop lock motors digitalWrite(LOCK_PINS[0], 0); digitalWrite(LOCK_PINS[1], 0); fram.write8(FRAM_LOCK_BYTE, state); isLocked = state; } } /* Display routines*/ void show(){ display.clearDisplay(); display.setTextSize(1); // Normal 1:1 pixel scale display.setTextColor(SSD1306_WHITE); // Draw white text display.setCursor(10,0); // Start at top-left corner display.println(F("Turntable position")); display.setTextSize(3); // Draw 2X-scale text display.setTextColor(SSD1306_WHITE); if (currentPosition < 10){ display.setCursor(58,v); // Start at top-left corner } else{ display.setCursor(51,v); } display.println(currentPosition); display.display(); } void show2(){ display.clearDisplay(); display.setTextSize(1); // Normal 1:1 pixel scale display.setTextColor(SSD1306_WHITE); // Draw white text display.setCursor(10,0); // Start at top-left corner display.println(F("Turntable position")); display.setTextSize(3); // Draw 2X-scale text display.setTextColor(SSD1306_WHITE); if (currentPosition < 10){ display.setCursor(58,v); // Start at top-left corner } else{ display.setCursor(51,v); } display.println(currentPosition); display.setCursor(85,v); display.println(">"); display.display(); } void show3(){ display.clearDisplay(); display.setTextSize(1); // Normal 1:1 pixel scale display.setTextColor(SSD1306_WHITE); // Draw white text display.setCursor(10,0); // Start at top-left corner display.println(F("Turntable position")); display.setTextSize(3); // Draw 2X-scale text display.setTextColor(SSD1306_WHITE); if (currentPosition < 10){ display.setCursor(58,v); // Start at top-left corner } else{ display.setCursor(51,v); } display.println(currentPosition); display.setCursor(20,v); display.println("<"); display.display(); } void showcommand(String showkp){ //display.clearDisplay(); display.setTextSize(1); // Normal 1:1 pixel scale display.setTextColor(SSD1306_WHITE); // Draw white text display.setCursor(10,v2); // Start at top-left corner display.println(F("Turntable Action")); display.setTextSize(3); // Draw 2X-scale text display.setTextColor(SSD1306_WHITE); display.setCursor(10,v); display.println(showkp); display.display(); } /* Keypad code*/ char GetKeyFromKeyPad() { char key = ' ', tmp; int keyState; tmp = mykeypad.getKey(); if (tmp) { keyState = mykeypad.getState(); //value = Keypad.readPin(); if (keyState == PRESSED) { key = tmp; //Serial.print(value); // Serial.print(" "); // Serial.println(key); } } return key; } int GetNumberFromKeyPad(int number) { char buffer[2]; int i; char key; i = 1; buffer[0] = '0'; buffer[1] = '0'; number = 0; while (true) { key = GetKeyFromKeyPad(); if (key >= '0' && key <= '9') { buffer[i] = key; number = (((buffer[1] - 0x30) * 10) + (buffer[0] - 0x30)); i--; if (i < 0) i = 1; } if (key == 'E' || key == '>') { break; } else if (key == 'R' || key == '<') { break; } else if (key == 'G') { showcommand (String(number)); Serial.println(number); return number; break; } } } void ParseCommand(int padcommand) { int i = 0; int searchPos; byte inputBuffer[10]; int number; if (padcommand == 'g' || padcommand == 'G') { Serial.println("Go to mode"); showcommand("GoTo"); command = GetNumberFromKeyPad(number); Serial.println(number); } else if (padcommand == 'z' || padcommand == 'Z') { showcommand("STOP"); Serial.println("Rotate Turntable Stop"); command = CMD_STOP; } else if (padcommand == '>') { showcommand(" ->"); Serial.println("Rotate Turntable Turn Clockwise "); command = CMD_STEP_CW; } else if (padcommand == '<') { showcommand("<-"); Serial.println("Rotate Turntable Turn Counter Clockwise "); command = CMD_STEP_CCW; } else if (padcommand == 'h' || padcommand == 'H') { showcommand("HOME"); show(); Serial.println("Set Home "); command = CMD_SET_HOME; } else if (padcommand == 'e' || padcommand == 'E') { showcommand("180"); show(); Serial.println("Flip "); command = CMD_FLIP; } DoCommand(command); command = CMD_NULL; }
Loconet feedback Module (Runs on the Nano)
/*# This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. (C) 2021 Dheerendra Prasad * Loconet Control Panel Feedback for the Kato 20-283 Turntable controller. * Based on work done by John Plocher 2014 - Public Domain * * Circuit: * Loconet on pins 8 (RX on UNO) and 7 (TX) (via a LocoShield or similar) * */ #include <LocoNet.h> #include <EEPROM.h> // #define LN_TX_PIN 47 // MEGA // #define LN_TX_PIN 6 // UNO #define LN_TX_PIN 7 // LocoShield // LN_RX_PIN Hardcoded in library for UNO and MEGA. Will not work with Leonardo (yet) // Loconet turnout position definitions to make code easier to read #define TURNOUT_NORMAL 1 // aka closed #define TURNOUT_DIVERGING 0 // thrown lnMsg *LnPacket; // pointer to a received LNet packet int cpt; // Turnout number to switch (900+currentposition) int cptPrev = 900; // Last turntable position int currentPosition; // incoming Turntable position message from Serial2 on Mega int i; // Construct a Loconet packet that requests a turnout to set/change its state // In the context of this application the message will have no effect on the state of the //turntable - since the turnout was set to normal by DCC/turntable was already moved to //said track by a keypad command. The mirrored request simply updates the DCC display //acting as feedback void sendOPC_SW_REQ(int address, byte dir, byte on) { lnMsg SendPacket ; int sw2 = 0x00; if (dir == TURNOUT_NORMAL) { sw2 |= B00100000; } if (on) { sw2 |= B00010000; } sw2 |= (address >> 7) & 0x0F; SendPacket.data[ 0 ] = OPC_SW_REQ ; SendPacket.data[ 1 ] = address & 0x7F ; SendPacket.data[ 2 ] = sw2 ; LocoNet.send( &SendPacket ); } // Some turnout decoders (DS54?) can use solenoids, this code emulates the digitrax // throttles in toggling the "power" bit to cause a pulse void setLNTurnout(int address, byte dir) { sendOPC_SW_REQ(address - 1, dir, 1); sendOPC_SW_REQ(address - 1, dir, 0); } void setup() { Serial.begin(9600); LocoNet.init(LN_TX_PIN); delay(2000);// allow DCC to catch up EEPROM.get(10, cptPrev); setLNTurnout(cptPrev, TURNOUT_NORMAL); } void loop() { if (Serial.available()) { String input = Serial.readString(); input.toLowerCase(); currentPosition = (input.toInt()); cpt = 900+currentPosition; } if (cpt==cptPrev) { //turntable still on same track... // ignore it. } else { // turntable has moved .. update DCC if (cpt!=0){ setLNTurnout(cpt, TURNOUT_NORMAL); delay(10); setLNTurnout(cptPrev, TURNOUT_DIVERGING); setLNTurnout(900, TURNOUT_DIVERGING); setLNTurnout(940, TURNOUT_DIVERGING); setLNTurnout(941, TURNOUT_DIVERGING); setLNTurnout(942, TURNOUT_DIVERGING); setLNTurnout(946, TURNOUT_DIVERGING); setLNTurnout(947, TURNOUT_DIVERGING); setLNTurnout(948, TURNOUT_DIVERGING); cptPrev = cpt+0; storedata(); } } LnPacket = LocoNet.receive() ; if( LnPacket ) { LocoNet.processSwitchSensorMessage(LnPacket); // this function will call the specially named functions below... } } void storedata(){ if (cptPrev != 0){ EEPROM.put (10, cptPrev); Serial.println("Written"); } } }