// File : RailwayControllerV2.ino
//
// Version 1, 5 August 2021, by Koepel
// Initial version.
// Version 2, 5 August 2021, by Koepel
// Layout of the wiring made better.
// Version 3, 13 August 2021, by Koepel
// Changed 'SCK' to 'clockPin'.
//
// Cascade of five+ 74HC165 shift-in registers.
// Only three pins are used on the Arduino board, to read 40+ switches.
//
// Using the 74HC165 is safe, because a pulse to the Latch pin
// ('PL' on the 74HC165) will make a new start every time.
// In case of an error or a wrong clock pulse by noise,
// it synchronizes the data when inputs are read the next time.
//
// Based on:
// (1)
// Demo sketch to read from a 74HC165 input shift register
// by Nick Gammon, https://www.gammon.com.au/forum/?id=11979
// (2)
// 74HC165 Shift register input example
// by Uri Shaked, https://wokwi.com/arduino/projects/306031380875182657
//
//
// Only needed if driving Servos locally
#include <Servo.h>
// For I2C communications
#include <Wire.h>
// For I2C Servo breakout board
#include <Adafruit_PWMServoDriver.h>
// Arduino Stepper Library
//#include <Stepper.h>
// Include the AccelStepper Library
#include <AccelStepper.h>
// Include NewLiquidCrystal Library for I2C
#include <LiquidCrystal_I2C.h>
// Memory considerations:
// Use PROGMEM for large arrays
// Only use globals if they are repeatedly referred to
// Remove support for on-board servo. Saves loading the Servo library
// Don't use Serial unless debugging
// Use the smallest datatype possible (not sure that saves much?)
// Pins
// 0,1: TX/RX - do not use
// 2-4: Shift In
// 5-8: Stepper
// A1: Servo (test)
// 13: LED
// A0: Pot
// A4/A5(SDA/SCL): I2C comms
// 9,10,11,12,A2,A3: Spare
//
// Ideas:
// Shift-out: 10,11,12,13 (in place of LED)
// Leaving A2,A3 spare
// Pin for a status LED
const byte LEDPIN = 13;
// LCD: 70x25mm
//LiquidCrystal_I2C lcd(0x27,2,1,0,4,5,6,7); // Set the LCD I2C address and define the connections
//LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);
LiquidCrystal_I2C lcd(0x27, 16, 2);
// Turntable stepper motor, half steps, in1, 3, 2, 4
AccelStepper ttstepper(8,6,8,7,9);
// I2C boards for Servos
Adafruit_PWMServoDriver pwmboard1 = Adafruit_PWMServoDriver(0x40);
Adafruit_PWMServoDriver pwmboard2 = Adafruit_PWMServoDriver(0x41);
Adafruit_PWMServoDriver pwmboard3 = Adafruit_PWMServoDriver(0x42);
Adafruit_PWMServoDriver pwmboard4 = Adafruit_PWMServoDriver(0x43);
/*
Calibrating your Servos
Servo pulse timing varies between different brands and models. Since it is an analog control circuit, there is
often some variation between samples of the same brand and model. For precise position control, you will want to
calibrate the minumum and maximum pulse-widths in your code to match known positions of the servo.
Find the Minimum:
Using the example code, edit SERVOMIN until the low-point of the sweep reaches the minimum range of travel. It
is best to approach this gradually and stop before the physical limit of travel is reached.
Find the Maximum:
Again using the example code, edit SERVOMAX until the high-point of the sweep reaches the maximum range of travel.
Again, is best to approach this gradually and stop before the physical limit of travel is reached.
Use caution when adjusting SERVOMIN and SERVOMAX. Hitting the physical limits of travel can strip the gears and
permanently damage your servo.
*/
const short int SERVOMIN = 150; // This is the 'minimum' pulse length count (out of 4096)
const short int SERVOMAX = 600; // This is the 'maximum' pulse length count (out of 4096)
//const int USMIN = 600; // This is the rounded 'minimum' microsecond length based on the minimum pulse of 150
//const int USMAX = 2400; // This is the rounded 'maximum' microsecond length based on the maximum pulse of 600
//const int SERVO_FREQ = 50; // Analog servos run at ~50 Hz updates
// Define Pin Connections to 74HC165
const byte latchPin = 3; // to latch the inputs into the registers
const byte clockPin = 4;
const byte dataPin = 2;
const byte pulseWidth = 10; // pulse width in microseconds
const byte REGCOUNT = 5; // How many Shift Registers do we have to read
// What action does the Button initiate
// Each Register has 8 buttons
// Each Button as an ON action and an OFF action
// Each Wife has 7 Sacks, each Sack has 7 Cats, each Cat has 7 Kittens
// omit, or use 0 for no action
// Toggle Switches use both ON and OFF
// Push Switches (or studs) use just ON
// Store this array in PROGMEM
const int buttonactions[REGCOUNT][8][2] PROGMEM = {
{ // Register 0
{0,0}, //Button 0: ON action, OFF action
{0,0},
{0,0},
{0,0},
{0,0},
{0,0},
{0,0},
{0,0} // Button 7
},
{ // Register 1
{0,0}, //Button 0: ON action, OFF action
{0,0},
{0,0},
{0,0},
{0,0},
{0,0},
{0,0},
{0,0} // Button 7
},
{ // Register 2
{0,0}, //Button 0: ON action, OFF action
{0,0},
{0,0},
{0,0},
{0,0},
{0,0},
{0,0},
{0,0} // Button 7
},
{ // Register 3
{1,0}, //Button 0: ON action, OFF action
{0,0},
{0,0},
{0,0},
{0,0},
{0,0},
{0,0},
{0,0} // Button 7
},
{ // Register 4
{2,0}, //Button 0: ON action, OFF action
{3,0},
{4,0},
{11,0},
{12,0},
{16,0},
{17,0},
{18,0} // Button 7
}
};
// Set some constants to make describing the Actions easier and more readable
const byte NONE = -1; // This action does nothing
const byte RESET = 1; // Sets all points/signals to Through / Danger
const byte SERVO_TEST = 2; // Perform actions on the test servo
const byte POINT = 3; // For Point actions
const byte SIGNAL = 4; // For Signal actions
const byte ROUTE = 5; // A Route action can contain multiple sub actions with a short delay between actions
const byte ROUTE_FAST = 6; // A Route action with no delays
const byte TT_TO = 7; // Move the Turntable to a specified place, relative to the home position
const byte TT_BY = 8; // Move the Turntable by a specifed ammount
const byte TT_ZERO = 9; // Set the Turntable home position to the current location
const byte RELAY = 10; // Do a Relay Action
// For Points, choose the road
const boolean THROUGH = 0;
const boolean CURVE = 1;
// For Y points if needed
// const boolean LEFT = 0;
// const boolean RIGHT = 1;
// For Signals:
const boolean DANGER = 0;
const boolean CLEAR = 1;
// For Relays:
const boolean OFF = 0;
const boolean ON = 1;
// How many actions are there
const byte actioncount = 100;
// How many subactions can an action call
const byte subactions = 10;
// A custom structure to hold the actions
// Note the LCD message has a limit of 16 chars per row, split by | (plus the null char, hence 34)
// Also note that some actions may append text to the message
// The ROUTE action can have subactions so list their refs in data - don't nest ROUTE actions inside ROUTE actions!
struct Action {
byte type;
byte item;
int data[subactions];
char message[34];
};
// The Action performed by the Buttons
// {type_id, item_id, data, message}
// Store this array in PROGMEM
const Action actionlist[actioncount] PROGMEM = {
{NONE}, // Action #0, placeholder! Does nothing!
{RESET, NONE, {NONE},"Reset all items"}, // Action #1, run Reset command
// Servo test actions
{SERVO_TEST,0,{90}, "Test Servo|Position:"}, // Action #2: Test Servo 0, set to 90
{SERVO_TEST,0,{-10}, "Test Servo|Position:"}, // Action #3: Move Test servo -10 degrees
{SERVO_TEST,0,{+10}, "Test Servo|Position:"}, // Action #4: Move Test servo +10 degrees
// Point actions
{POINT,0,{THROUGH}, "Loop 1a"}, // Action #5 sets POINT servo 1 to THROUGH
{POINT,0,{CURVE}, "Loop 1a"}, // Action #6 sets POINT servo 1 to CURVE
{POINT,1,{THROUGH}, "Loop 1b"}, // Action #7 sets POINT servo 1 to THROUGH
{POINT,1,{CURVE}, "Loop 1b"}, // Action #8 sets POINT servo 1 to CURVE
// Signal actions
{SIGNAL,2,{CLEAR}, "Home Signal"}, // Action #9 sets servo 3 to CLEAR
{SIGNAL,2,{DANGER}, "Home Singal"}, // Action #10 sets servo 3 to DANGER
// Fiddleyard actions
{ROUTE,0,{5,7,9}, "Yard road 2"}, // Action #11: set yard to road 2 - call subactions 5,7,9
// Crossover actions
{ROUTE,0,{6,7}, "End Crossover|set to Through"}, // Action #12: set a crossover - call subactions 6 & 7
{ROUTE,0,{6,7}, "Mid Crossover|set to Through"}, // Action #13: set a crossover - call subactions 6 & 7
{ROUTE,0,{6,7}, "Cattle Crossover|set to Through"}, // Action #14: set a crossover - call subactions 6 & 7
{ROUTE,0,{6,7}, "Shed Crossover|set to Through"}, // Action #15: set a crossover - call subactions 6 & 7
// Turntable actions
{TT_TO,0,{180},"Half turn"}, //#16 Move TO 180 degrees
{TT_TO,0,{0},"Move Coal-Main"}, //#17 Move TO 0 degrees
{TT_TO,0,{-180},"Turn Coal-Main"}, //#18 Move TO 180 degrees - anti-clockwise
{TT_TO,0,{-30},"Move Main-Coal"}, //#19 Move TO 30 degrees anti-clockwise (equivalent of 330 degrees clockwise)
{TT_BY,0,{1},"Nudge TT +1"}, //#20 Move BY 1 degree clockwise
{TT_BY,0,{-1},"Nudge TT -1"}, //#21 Move BY 1 degree anti-clockwise
{TT_ZERO,0,{0},"Set TT Home pos"}, //#22 Set the current position to 0 - absolute positions are relative to this
{NONE}, //23
{NONE}, //24
{NONE}, //25
{NONE}, //26
{NONE}, //27
{NONE}, //28
{NONE}, //29
{NONE}, //30
{NONE}, //31
{NONE}, //32
{NONE}, //33
{NONE}, //34
{NONE}, //35
{NONE}, //36
{NONE}, //37
{NONE}, //38
{NONE}, //39
{NONE}, //40
{NONE}, //41
{NONE}, //42
{NONE}, //43
{NONE}, //44
{NONE}, //45
{NONE}, //46
{NONE}, //47
{NONE}, //48
{NONE}, //49
{NONE}, //50
{NONE}, //51
{NONE}, //52
{NONE}, //53
{NONE}, //54
{NONE}, //55
{NONE}, //56
{NONE}, //57
{NONE}, //58
{NONE}, //59
{NONE}, //60
{NONE}, //61
{NONE}, //62
{NONE}, //63
{NONE}, //64
{NONE}, //65
{NONE}, //66
{NONE}, //67
{NONE}, //68
{NONE}, //69
{NONE}, //70
{NONE}, //71
{NONE}, //72
{NONE}, //73
{NONE}, //74
{NONE}, //75
{NONE}, //76
{NONE}, //77
{NONE}, //78
{NONE}, //79
{NONE}, //80
{NONE}, //81
{NONE}, //82
{NONE}, //83
{NONE}, //84
{NONE}, //85
{NONE}, //86
{NONE}, //87
{NONE}, //88
{NONE}, //89
{NONE}, //90
{NONE}, //91
{NONE}, //92
{NONE}, //93
{NONE}, //94
{NONE}, //95
{NONE}, //96
{NONE}, //97
{NONE}, //98
{NONE} //99
};
// 46 servos
const short int servocount = 50;
// { board_id, pin_number, low_position, high_position}
// board_id = I2C ref of board, 0 = Arduino
const byte servolist[servocount][4] = {
{0, A1, 70, 110}, // Servo #0 - this is the Test servo
{1, 0, 70, 110}, //1
{1, 0, 70, 110}, //2
{1, 0, 70, 110}, //3
{1, 0, 70, 110}, //4
{1, 0, 70, 110}, //5
{1, 0, 70, 110}, //6
{1, 0, 70, 110}, //7
{1, 0, 70, 110}, //8
{1, 0, 70, 110}, //9
{1, 0, 70, 110}, //10
{1, 0, 70, 110}, //11
{1, 0, 70, 110}, //12
{1, 0, 70, 110}, //13
{1, 0, 70, 110}, //14
{1, 0, 70, 110}, //15
{1, 0, 70, 110}, //16
{1, 0, 70, 110}, //17
{1, 0, 70, 110}, //18
{1, 0, 70, 110}, //19
{1, 0, 70, 110}, //20
{1, 0, 70, 110}, //21
{1, 0, 70, 110}, //22
{1, 0, 70, 110}, //23
{1, 0, 70, 110}, //24
{1, 0, 70, 110}, //25
{1, 0, 70, 110}, //26
{1, 0, 70, 110}, //27
{1, 0, 70, 110}, //28
{1, 0, 70, 110}, //29
{1, 0, 70, 110}, //30
{1, 0, 70, 110}, //31
{1, 0, 70, 110}, //32
{1, 0, 70, 110}, //33
{1, 0, 70, 110}, //34
{1, 0, 70, 110}, //35
{1, 0, 70, 110}, //36
{1, 0, 70, 110}, //37
{1, 0, 70, 110}, //38
{1, 0, 70, 110}, //39
{1, 0, 70, 110}, //40
{1, 0, 70, 110}, //41
{1, 0, 70, 110}, //42
{1, 0, 70, 110}, //43
{1, 0, 70, 110}, //44
{1, 0, 70, 110}, //45
{1, 0, 70, 110}, //46
{1, 0, 70, 110}, //47
{1, 0, 70, 110}, //48
{1, 0, 70, 110} //49
};
// Delay used in the arm bounce sequence in milliseconds
const short int BOUNCE = 100;
// Delay used in the arm pull down sequence in milliseconds
const short int PULL = 300;
// Number of degrees of movement to use for the arm bounce and pull down sequence
const byte BOUNCE_ANGLE = 10;
byte servotestpos = 90; // What position is the test servo in?
void run_action(byte action_id, boolean quiet) {
if (!quiet) digitalWrite(LEDPIN, HIGH); // Turn on the LED whilst the action is taking place
// get the action details
Action thisaction;
memcpy_P (&thisaction, &actionlist[action_id], sizeof (Action));
switch(thisaction.type) {
case RESET:
show_msg(String(thisaction.message) + "|Please wait", quiet);
// Reset all Servos to THROUGH/DANGER (skip Servo 0, the test servo)
for (int i = 1; i < servocount - 1; i++) {
move_servo (i, servolist[i][2]);
// Wait a second before doing the next one
//delay (1000);
}
show_msg(String(thisaction.message) + "|Complete", quiet);
break;
case SERVO_TEST:
// Servo Test
if (thisaction.data[0] == 90) {
servotestpos = 90;
} else {
servotestpos = servotestpos + thisaction.data[0];
if (servotestpos < 0) servotestpos = 0;
if (servotestpos > 180) servotestpos = 180;
}
show_msg(String(thisaction.message) + " " + String(servotestpos), quiet);
move_servo (thisaction.item, servotestpos);
break;
case POINT:
// Do a Point Servo Action
// Set the servo to the THROUGH or CURVE position
if (thisaction.data[0] == THROUGH) {
show_msg(String(thisaction.message) + "|Set to Through", quiet);
move_servo (thisaction.item, servolist[thisaction.item][2]);
} else if (thisaction.data[0] == CURVE){
show_msg(String(thisaction.message) + "|Set to Curve", quiet);
move_servo (thisaction.item, servolist[thisaction.item][3]);
}
break;
case SIGNAL:
// Do a Signal Servo Action
// Set the servo to the DANGER or CLEAR position
// Switching a signal to clear will cause the servo to move slightly towards the clear position, pause for a
// fraction of a second then move fully to the clear position. This is intended to emulate the signalman taking
// up the slack in the operating wires then pulling the lever fully out of the frame. When the switch is returned
// to danger the servo will provide a slight bounce in the arm movement. The degree of bounce, pull-up and the
// timings are all defined by constants.
if (thisaction.data[0] == DANGER) {
// Set to danger...
show_msg(String(thisaction.message) + "|set to Danger", quiet);
move_servo (thisaction.item, servolist[thisaction.item][2]);
delay(BOUNCE);
move_servo (thisaction.item, servolist[thisaction.item][2] - BOUNCE_ANGLE);
delay(BOUNCE);
move_servo (thisaction.item, servolist[thisaction.item][2]);
} else if (thisaction.data[0] == CLEAR){
// Clearing...
show_msg(String(thisaction.message) + "|set to Clear", quiet);
move_servo (thisaction.item, servolist[thisaction.item][2] - BOUNCE_ANGLE);
delay(PULL);
move_servo (thisaction.item, servolist[thisaction.item][3]);
}
break;
case ROUTE:
case ROUTE_FAST:
// Set a Route (e.g. Crossover, Yard - can contain Points and Signals, but not other ROUTE commands)
// Get the subactions array and run through it, calling run_action on each action with the quiet flag set
// to true so no message is displayed for the subaction
show_msg("Setting Route...|" + String(thisaction.message), quiet);
for (int i = 0; i < subactions; i++) {
if (thisaction.data[i] != 0) run_action(thisaction.data[i], true);
// if ROUTE is selected, put a delay in between actions
if (thisaction.type == ROUTE) delay(1000);
}
show_msg(String(thisaction.message) + "|Route set", quiet);
break;
case TT_TO:
case TT_BY:
// Move the Turntable TO or BY a specific number of degrees
show_msg("Turntable|" + String(thisaction.message), quiet);
// is this a relative or absolute move?
//move_tt(thisaction.data[0], (thisaction.type == TT_TO));
// If current postion is 180 degrees then set home position to be 0
//if (ttstepper.currentPosition() == 2048 || ttstepper.currentPosition() == -2048) ttstepper.setCurrentPosition(0);
show_msg(String(thisaction.message) + "|Complete", quiet);
break;
case TT_ZERO:
show_msg((thisaction.message), quiet);
ttstepper.setCurrentPosition(0);
break;
case RELAY:
show_msg((thisaction.message), quiet);
// Set Relay to OFF/ON
break;
default:
break;
};
if (!quiet) digitalWrite(LEDPIN, LOW); // Turn off the LED now the action is complete
}
// Set a servo to a particular angle: 0-180 degrees
void move_servo (byte servo_id, byte pos) {
// First field in Servolist is board, if 0 then direct on Arduino
if (servolist[servo_id][0] == 0) {
Servo this_servo;
this_servo.attach(servolist[servo_id][1]);
this_servo.write(pos);
// Allow time for the servo to move
delay (500);
this_servo.detach();
} else {
// convert the degrees to a pluse length
int pulselength = map(pos, 0, 180, SERVOMIN, SERVOMAX);
// Move the right servo on the relevent I2C board
switch(servolist[servo_id][0]) {
case 1:
pwmboard1.setPWM(servolist[servo_id][1], 0, pulselength);
break;
case 2:
pwmboard2.setPWM(servolist[servo_id][1], 0, pulselength);
break;
case 3:
pwmboard3.setPWM(servolist[servo_id][1], 0, pulselength);
break;
case 4:
pwmboard4.setPWM(servolist[servo_id][1], 0, pulselength);
break;
default:
break;
}
// Allow time for the servo to move
delay (500);
}
}
void move_tt(short int pos, boolean absolute) {
// turn the degrees input to motor position
int ttpos = map(pos, -360, 360, -4096, 4096);
// read the Turntable speed preset sensor value and map it to a range from 0 to 250:
int motorSpeed = map(analogRead(A0), 0, 1023, 0, 250);
// set the motor speed:
if (motorSpeed > 0) ttstepper.setMaxSpeed(motorSpeed);
if (absolute) {
// if the boolean absolute is tue then move to that exact angle
ttstepper.moveTo(ttpos);
while (ttstepper.distanceToGo() != 0) ttstepper.runToPosition();
} else {
// otherwise, move BY the angle specified
ttstepper.move(ttpos);
while (ttstepper.distanceToGo() != 0) ttstepper.run();
}
}
// Display a messsage on the LCD:
void show_msg(String msg, boolean quiet) {
// If the quiet option is selected, just return
if (quiet) return;
lcd.clear();
lcd.home();
// split long messages over two lines, deliminated by the pipe symbol...
int matchPos = msg.indexOf('|');
if (matchPos < 0) {
// No match, so show on one line
} else {
lcd.print(msg.substring(0, matchPos));
msg.remove(0,matchPos + 1);
lcd.setCursor(0,1);
}
lcd.print(msg);
}
// Store the old switch positions
uint32_t oldOptionSwitch[REGCOUNT] = {};
void setup ()
{
Serial.begin( 115200);
Serial.println( "Turn on and off the switches");
pinMode( clockPin, OUTPUT); // clock signal, idle LOW
pinMode( latchPin, OUTPUT); // latch (copy input into registers), idle HIGH
digitalWrite( latchPin, HIGH);
// Set display type as 16 char, 2 rows
lcd.begin(16,2);
//lcd.setBacklightPin(3,POSITIVE); // Turn on the Backlight of the LCD
//lcd.setBacklight(HIGH);
// LED
pinMode(LEDPIN, OUTPUT);
pinMode(A0, OUTPUT);
for (int i = 0; i < servocount; i++) {
// If the servo is on Arduino direct, set the output pin
if (servolist[i][0] == 0) {
pinMode(servolist[i][1], OUTPUT);
}
}
pwmboard1.begin();
pwmboard1.setOscillatorFrequency(27000000);
pwmboard1.setPWMFreq(1600); // This is the maximum PWM frequency
pwmboard2.begin();
pwmboard2.setOscillatorFrequency(27000000);
pwmboard2.setPWMFreq(1600);
pwmboard3.begin();
pwmboard3.setOscillatorFrequency(27000000);
pwmboard3.setPWMFreq(1600);
pwmboard4.begin();
pwmboard4.setOscillatorFrequency(27000000);
pwmboard4.setPWMFreq(1600);
ttstepper.setAcceleration(50.0);
lcd.home (); // go home
show_msg(" Welcome to|Barnstaple D&SR", false);
}
void loop ()
{
// Give a pulse to the parallel load latch of all 74HC165
digitalWrite( latchPin, LOW);
delayMicroseconds( pulseWidth);
digitalWrite( latchPin, HIGH);
// Reading one 74HC165 at a time and checking each switch value
for( int i=0; i < REGCOUNT ; i++)
{
uint32_t optionSwitch = 0;
optionSwitch = ((uint32_t) ReadOne165());
for (int j = 0; j < 8; j++)
{
if( bitRead( optionSwitch, j) != bitRead( oldOptionSwitch[i],j))
{
Serial.print("Register ");
Serial.print(i);
Serial.print( " Switch ");
Serial.print( j);
Serial.print( " is now ");
Serial.println( bitRead( optionSwitch, j) == 0 ? "down ↓" : "up ↑");
Serial.print( "Taking Action ");
byte action = 0;
if (bitRead( optionSwitch, j) == 0) {
action = pgm_read_byte(&(buttonactions[i][j][1]));
} else{
action = pgm_read_byte(&(buttonactions[i][j][0]));
}
Serial.println(action);
// Ignore Actions where the ID is 0
if (action != 0) run_action(action, false);
}
}
oldOptionSwitch[i] = optionSwitch;
}
delay(100); // slow down the sketch to avoid switch bounce
}
// The ReadOne165() function reads only 8 bits,
// because of the similar functions shiftIn() and SPI.transfer()
// which both use 8 bits.
//
// The shiftIn() can not be used here, because the clock is set idle low
// and the shiftIn() makes the clock high to read a bit.
// The 74HC165 require to read the bit first and then give a clock pulse.
//
byte ReadOne165()
{
byte ret = 0x00;
// The first one that is read is the highest bit (input D7 of the 74HC165).
for( int i=7; i>=0; i--)
{
if( digitalRead( dataPin) == HIGH)
bitSet( ret, i);
digitalWrite( clockPin, HIGH);
delayMicroseconds( pulseWidth);
digitalWrite( clockPin, LOW);
}
return( ret);
}