Programming for Realistic Semaphore Control using PCA 9685

Hi Folks

1st post here so apologies if I've not picked the correct location or done something otherwise incorrect.

I'm looking for a little help / guidance on what I have done so far with my project and what to do next - I feel like the multiple sources of examples and learning materials have got me to the point I am at but my needs are getting more specific and I'm drifting away from the useful examples on here and elsewhere.

As you can guess from the title the ultimate aim is to control model railway semaphore signals in a realistic way, ideally with a range of potential movements chosen at random each time the operation of a signal is triggered. I am using an Elegoo Uno R3 and have chosen to use PCA9685 boards with the thought that the Uno can go in the control panel and I can daisy chain the PCA9685's round the layout as required. For the moment I am testing with 1 PCA9685 board. The eventual setup will have levers to control the operation which could be on/off, on/on or momentary but at the moment I am using push buttons on a breadboard to simulate this.

I've included a diagram of my setup (hopefully :joy:):

image

I couldn't find a way to represent the PCA9685 but its 6 pins are represented on the RHS.

If that didn't work the setup is as follows:
2 servos connected to pins 0 & 1 of the PCA9685
PCA9685 to Uno connections: SCL->A5, SDA->A4
On the Uno Push Buttons connected to Digital Pins 3 & 4 and GND

The PCA9685 has a separate 5v power supply into the terminals and is connected to Uno power on the Pin connections.

I am trying to code in such a way that I can repeat the setup out over multiple PCA9685s and easily add more switches when required by just duplicating code. The eventual aim is to also get a more realistic look to the signal movement but that is a level beyond me at the min.

Ultimately in the Loop, currently, 'if' statements test the status of a switch then perform a pmw.set to move the servo arm. I have predefined pwm settings for each servo at the beginning of the program which I hope will allow me to fine tune positions easily.

So as it stands my setup allows me to power up the system and all of the signals will be set to Danger, when I press and hold a button they move to clear, release the button and back to danger. Holding the button down isn't an issue as I intend the button to be replaced with an on/off switch of some kind.

My queries are really this:

  1. Have I done anything stupid? :joy:
  2. Are there alternative ways to code this I've missed?
  3. Is the INPUT_PULLUP appropriate to minimise wiring to switches or does this compromise something else?
  4. Am I likely to be able to replace my pwm.Set lines with more complex moves?
  5. Am I likely to run into any issues as the system grows to include more servos?
  6. Anyone fancy giving me a steer in relation to the realistic movements??? :grin: [So far I have studied a couple of youtube videos by folks that have achieved this but I dont know my way round the code well enough to understand what is relevant to my setup]

Here is my code:

#include <Adafruit_PWMServoDriver.h>

Adafruit_PWMServoDriver signalBoard01 = Adafruit_PWMServoDriver(0x40);

#define SERVOMIN  100                                                 // this is the 'minimum' pulse length count (out of 4096)

#define SERVOMAX  710                                                 // this is the 'maximum' pulse length count (out of 4096)

int lever01Pin = 3;
int signal01Clear = 200;
int signal01Danger = 120;

int lever02Pin = 4;
int signal02Clear = 190;
int signal02Danger = 104;

void setup() {

  Serial.begin(9600);
  pinMode(lever01Pin, INPUT_PULLUP);
  pinMode(lever02Pin, INPUT_PULLUP);
  Serial.println("GingerAngles Signal Control!");

  signalBoard01.begin();

  signalBoard01.setPWMFreq(50);                  // Analog servos run at ~60 Hz updates

}


void loop() 
{
int lever01State = digitalRead(lever01Pin);

if(lever01State == LOW)
{
  signalBoard01.setPWM(0, 0, signal01Clear);
  delay(10);
}
  else
  signalBoard01.setPWM(0, 0, signal01Danger);
  delay(10);

  int lever02State = digitalRead(lever02Pin);

if(lever02State == LOW)
{
  signalBoard01.setPWM(1, 0, signal02Clear);
  delay(10);
}
  else
  signalBoard01.setPWM(1, 0, signal02Danger);
  delay(10);
}

Many TIA!

Welcome to the forum

I think that you should consider defining a struct containing the switch pin number, clear angle and danger angle. Create an array of such structs and populate it with the requisite data and you can then iterate through the array, check the switch state and move the servo appropriately.

This will avoid repeating the same lines of code over and over, once for each servo

If/when the time comes to add complex movements you can add functions to the array of structs that operate on more than one servo, if that is what is involved

1 Like

The problem with that is that the I2C bus that connects the PCA9685 to the Uno was not designed for long distances. You will probably have to stick some repeaters in between.

Personally I would consider multiple small microcontrollers each controlling one or two semaphores and communicate with them from the central control panel using RS232/RS422/RS485.

:grinning: sterretje makes a good point. How large is your layout ?

Below you will find a version of your code using an array of structs as I suggested. Note how much shorter the code is and it would not get any longer even if you used 16 servos

#include <Adafruit_PWMServoDriver.h>

Adafruit_PWMServoDriver signalBoard01 = Adafruit_PWMServoDriver(0x40);

#define SERVOMIN 100  // this is the 'minimum' pulse length count (out of 4096)

#define SERVOMAX 710  // this is the 'maximum' pulse length count (out of 4096)

struct signalData
{
    byte pinNumber;
    byte clear;
    byte danger;
};

signalData signals[] = {
    { 3, 200, 120 },
    { 4, 190, 104 },
};

byte signalCount = sizeof(signals) / sizeof(signals[0]);

void setup()
{
    Serial.begin(9600);

//EDIT :
//    pinMode(lever01Pin, INPUT_PULLUP);
//    pinMode(lever02Pin, INPUT_PULLUP);
    pinMode(signals[0].pinNumber, INPUT_PULLUP);
    pinMode(signals[1].pinNumber, INPUT_PULLUP);

    Serial.println("GingerAngles Signal Control!");

    signalBoard01.begin();
    signalBoard01.setPWMFreq(50);  // Analog servos run at ~60 Hz updates
}

void loop()
{
    for (int s = 0; s < signalCount; s++)
    {
        if (digitalRead(signals[s].pinNumber) == LOW)
        {
            signalBoard01.setPWM(0, 0, signals[s].clear);
            delay(10);
        }
        else
        {
            signalBoard01.setPWM(0, 0, signals[s].danger);
            delay(10);
        }
    }
}

NOTE corrected code

2 Likes
  • We need a layout description - how big, how complex, how many signals
  • I2C is best considered to be usable for "no more than a meter", though some implementations are said to have extended far beyond that.
  • Far better to implement a localized solution. For example, I strongly recommend you review this product:

MRCS Semaphore

Note that you can purchase the components elsewhere, and just buy their boards, quite cheaply. Their software includes bounce, etc. and is quite 'comprehensible' for beginners.

I have a similar device on the drawing board, but it's not ready for prime time, so we won't discuss that.
If you really want to go down the PCA9685 route, I'd still assess their software for ideas about how to manage some of the details.

Thanks for all the replies folks.

@UKHeliBob the Struct looks like a great code saver - I've tried the code you provided but looks like I'll need to do a bit of tweaking elsewhere - can I simply add more 'bytes' to the list? eg

struct signalData
{
    byte leverNumber
    byte pinNumber;
    byte clear;
    byte danger;
};

signalData signals[] = {
    { lever01, 3, 200, 120 },
    { lever02, 4, 190, 104 },
};

byte signalCount = sizeof(signals) / sizeof(signals[0]);

That's a disappointing shortcoming of the I2C bus @sterretje, the reason for wanting to do this was to cut down cabling from panel to individual signals. Hopefully I can make use of one of the RS's yo0u mentioned or perhaps approach it in a different manner.

Sounds like a bit context of the broader layout is required @camsysca ...

This is the CAD plan of the layout with dimensions, the bit I am signalling is the top half. The yellow boxes and associated dimensions are individual boards.

and here is the signalling diagram... each of the semaphore signals is shown and eventually I would like all of the ground signals to work also (the little inverted T's with the white discs with red line). Altogether there are approx 28 items to be controlled.
The red lines top to bottom are the approximate board joins.

I'd like the control location to be flexible, again I had thought the PCA9685's would allow me to connect the control 'box' to the layout with the SCL & SDA connections but alas it looks like this will not be the case.

Had I found out about the Arduino sooner I'd of probably looks to also control the Points on the layout with it which would have added a further 16 servos with another 30 odd on the opposite side of the layout. Depending upon the outcome I may revisit the control of the layout so will be interested to see how this pans out.

Hopefully that gives some context. :+1:

Right, so 13 semaphores, with some dual and some triple on the same mast. A bit of a challenge with the MRCS product, for sure. Are the 16 ground signals just LEDs, or is there a moving component as well?
Be aware, though the 9685 must be close to the Arduino, no such restriction exists for the servo cable run to the masts. Many meters is quite common.

1 Like

Would a wireless approach be suitable here?
example -

I don't know, just asking.

1 Like

Have you looked at the MRH postings by Geoff Bunza? There may be some grist for you there, as well.

1 Like

You certainly can. In fact you can add data of any type, including C style strings if you like

struct signalData
{
    byte leverNumber
    byte pinNumber;
    byte clear;
    byte danger;
   char * name;
};
signalData signals[] = {
    { lever01, 3, 200, 120, "Freight yard" },
    { lever02, 4, 190, 104, "Main Line West" },
};

Then print them just like any other variable

Serial.println(signals[s],name);

EDIT - should be

Serial.println(signals[s].name);
1 Like

Tut tut, Bob! :wink:

1 Like

If you mean the text in the code tags, thank you, I have fixed it !

I am surprised that nobody noticed that the original code did not actually compile. Fixed that too

2 Likes

Don't give up on the idea until you have tried it.

It's true that i2c was not designed for longer distances, but it will go further than you might think. One way to increase the range is to reduce the data speed. With your project, you don't need high speed, so you could drop the speed significantly to get more range. I have never tested this myself but would not be surprised if you could get 2m+ that way.

A commonly available and inexpensive component is a TCA9548 i2c multiplexer module. But you don't actually have to use it as a multiplexer. You could simply use it as an 8-way i2c extender, doubling the range you can get otherwise, and allowing you to wire multiple PCA9685 in a "star" arrangement.

1 Like

I'm modelling 1930's LNER ex-GCR ex-LDECR so the ground signals are not LED and will move :+1:

Good to know RE the servo cables.... is it possible to share the power feed locally amongst servos - that way I only need to run the signal wires back to the control panel?

TBH I've no need for wireless so trying to avoid having this added complication. But thanks :+1:

I noticed, but didn't know I'd noticed I'd noticed :joy:

When I use this code the servo connected to pin 0 on the PCA board just 'wobbles' and when either button is pressed the wobble just gets bigger?

Yes, with an important caveat. You must run a ground reference from that supply back to the Arduino/ PCA9685, as each signal needs a return path. I would additionally try a worst-case configuration like that on the bench, with typical wiring lengths spread out. You may find that that approach results in significant servo twitching, so best to discover that early.

1 Like

Whoops on my part

Try this version

There is now an extra data item in the struct that holds the pwm pin number on the PCA 9685

#include <Adafruit_PWMServoDriver.h>

Adafruit_PWMServoDriver signalBoard01 = Adafruit_PWMServoDriver(0x40);

#define SERVOMIN 100  // this is the 'minimum' pulse length count (out of 4096)

#define SERVOMAX 710  // this is the 'maximum' pulse length count (out of 4096)

struct signalData
{
    byte pinNumber;
    byte clear;
    byte danger;
    byte pwmChannel;
};

signalData signals[] = {
    { 3, 200, 120, 0 },
    { 4, 190, 104, 1 },
};

byte signalCount = sizeof(signals) / sizeof(signals[0]);

void setup()
{
    Serial.begin(115200);
    pinMode(signals[0].pinNumber, INPUT_PULLUP);
    pinMode(signals[1].pinNumber, INPUT_PULLUP);

    Serial.println("GingerAngles Signal Control!");

    signalBoard01.begin();
    signalBoard01.setPWMFreq(50);  // Analog servos run at ~60 Hz updates
}

void loop()
{
    for (int s = 0; s < signalCount; s++)
    {
        if (digitalRead(signals[s].pinNumber) == LOW)
        {
            signalBoard01.setPWM(0, 0, signals[s].clear);
            Serial.print("writing clear ");
            Serial.print(signals[s].clear);
            Serial.print(" to pwm channel ");
            Serial.println(signals[s].pwmChannel);
        }
        else
        {
            signalBoard01.setPWM(0, 0, signals[s].danger);
            Serial.print("writing danger ");
            Serial.print(signals[s].danger);
            Serial.print(" to pwm channel ");
            Serial.println(signals[s].pwmChannel);
        }
    }
    Serial.println();
    delay(10);
}
1 Like

If this were an array of structs, I'd probably keep it simple, and make the array index plus 1 the pin number. YMMV, but it's how I do it.