Kato Turntable Digital Control Version 3

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 :

  1. DCC Control.
  2. Local Keypad control.
  3. Display showing current track position.
  4. 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:
Demonstration of the completed controller.
The Schematic:
The Board :
Front of Board
Back of 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.

Loconet interface used for prototyping
OLED display to replace the 7 segment display and shift register.

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");
}
}

}  
And the completed Panel
The completed controller sits to the left of a touchscreen that brings the DCC control from the ECoS to the panel.
The inner ring of LEDs indicates the bridge position – blue-lit – here track 19. The Outer ring indicates Roundhouse/track occupancy and is powered by ECoS detectors
In ESU’s universe, the Kato Turntable simply does not exist – hence the best is a mockup made in the control panel mode – a la Woodsetts’ video.

Leave a Reply