Last Updated: 07/01/2024

#56 PCA9685 Level Crossing Barriers and Lights

Projects >> #56 PCA9685 Level Crossing Barriers and Lights

PCA9685 Level Crossing Servo Barriers and LED Lights

In this project we will build a a Level Crossing using servos and LED's controlled by an Arduino and using PCA9695 PWM Servo Driver Boards

I used MG90 servos, SG90's are the same but with plastic gears and the system should work with other servos as well.

The goal is to immitate the lights and motion of the barriers.

I used this crossing in Pirton just south of Worcester to get the light and barrier timings.

Pirton Crossing

The Arduino BreadBoard Version

Arduino Crossing

Pinout and wiring diagrams

In the diagram below each LED state is available on 4 pins to create the 4 lighting posts usually associated with UK crossings.

Although 2 servos are shown there are 2 clockwise and anti clockwise sockets to allow for the building of 4 barrier or 2 barrier systems.

You may notice there are not buttons or sensors triggering the project. For this project the system is triggered off a timer, however in the code buttons, block sensors etc can easily be used instead.

Wiring Diagram

Pins

A5 to PCA9685 SCL
A4 to PCA9685 SDA

Make sure you have 5v going to the PCA9685 board as well as the VCC and GND. One powers the circuitry, the other supply powers the servo.


Wiring:for a few common boards

SDA/SCL connections

Arduino UNO: A4 (SDA), A5 (SCL) :

Arduino Mega 2560: 20 (SDA), 21 (SCL) :

ESP32: 21(SDA), 22 (SCL)

For other Arduino boards see:https://www.arduino.cc/en/reference/wire

Connection Test 1 ...IMPORTANT

Bad wiring is one of the biggest problems with Arduino projects so run this test to make sure your board is being found before doing anythine else.

Once you have connected your board up as shown above check that it can be found.

If you are using an Arduino board go to File>Examples> Wire>I2c_scanner and upload the skecth to your board.

if all goes well your serial Monitor will display something like

Scanning...
I2C device found at address 0x40 !
done

Once your device has been found it's time to add the LED's

 

Connection Test 2 ...Board and Servo Test

First make sure you have the Adafruit PWM Servo Library installed.

Tools>Manage Libraries> Type Adafruit PWM Servo Library in the serach bar.

If it is not installed install it.

Adafruit PM Servo Library

Connect the board according to the circuit diagram.

Now download a test file from the Adafruit PWM servo Library examples.

File>Examples>Adafruit PWM servo Driver Library>servo.

Load the sketch and in the Serial Monitor you should see something like:

8 channel Servo test!
0
1
2
3
4
5
6
7

If you have a servo in any of the first 8 connections (nearest board connection end) you servo should move back and forth a couple of times.

At this point although we may not understand the sketch we know the board and servos work.

 

Example 1: PCA9685RailCrossingV1.ino


Click to Download code: PCA9685RailCrossingV1.ino

Version 1 of the crossing code works but in my opinion the barriers move too fast

 

 
/* PCA9685RailCrossingV1
   03/01/2024

   Railway Crossing Sketch
   PCA9685 board for servos...2 gate or 4 gate
   PCA9685 board for LED's.... orange static, red flashing

   gates triggered by timer for demo version



*/

#include "Wire.h"

#include "Adafruit_PWMServoDriver.h"
//PCA 9685 settings

//Using the address as this sketch uses multiple PCA9685 boards
Adafruit_PWMServoDriver servoBoard = Adafruit_PWMServoDriver(0x40);//Address found from I2C scanner
Adafruit_PWMServoDriver LEDBoard = Adafruit_PWMServoDriver(0x46);//Address found from I2C scanner
#define USMIN  600 // This is the rounded 'minimum' microsecond length based on the minimum pulse of 150
#define USMAX  2400 // This is the rounded 'maximum' microsecond length based on the maximum pulse of 600
#define SERVO_FREQ 50 // Analog servos run at ~50 Hz updates
//end pca9685 settings

unsigned long currentMillis;

unsigned long triggerMillis;//keeps track of trigger timing

unsigned long ledTimer;//stores time in millis for tracking events
byte ledState = 0;//
byte currentledState = 0;// what is the current state
byte flashState;

unsigned long gateServoTimer;
byte gateState;

void setServoPos(int servo, int pos) {
  //This first bit of code makes sure we are not trying to set the servo outside of limits
  int sendPos;
  if (pos > 179) {
    pos = 179;
  }
  if (pos < 0) {
    pos = 0;
  }
  sendPos = USMIN + ((USMAX - USMIN) / 180 * pos);
  if (servo > -1 && servo < 16) { //only try to move valid servo addresses
    servoBoard.writeMicroseconds(servo, sendPos);
  }
}

//this function should read buttons or sensors to trigger the gates
//Using a timer just for demo purposes.
void gateTrigger() {

  //gates down...train coming
  if (currentledState < 1) { //don't trigger if already triggered
    if (currentMillis - triggerMillis >= 10000) { //triggers after 5 seconds if nothing happening
      triggerMillis = currentMillis;//reset timer
      
      //Gate sequence trigger
      ledState = 1;
      
      Serial.println("Trigger Close");
    }
  }

  //gates up...train passed
  if (currentMillis - triggerMillis >= 6000) { //triggers after 5 seconds...simulate train passing
    if (gateState < 2 && ledState == 5) { //
      Serial.println("Trigger Open");
      triggerMillis = currentMillis + 10000;//reset timer
      //gates up trigger
      gateState = 2;
      
    }
  }
}

//controls movement and LEDs depending on gate state
void ledControl() {
  int q;
  switch (ledState) {
    case 1://system starting...set lights to orange for 3 seconds
      if (currentledState < 1) {
        currentledState = 1;//set the current state
        Serial.println("Orange On");
        for (int q = 0; q < 4; q++) {
          LEDBoard.setPWM(q, 0, 4095);//turn Orange on full brightness
        }
        for (int q = 12; q < 16; q++) {
          LEDBoard.setPWM(q, 0, 4095);//turn red barrier lights on
        }
        ledTimer = currentMillis;
        ledState = 2;
        Serial.println(ledState);
      }

      break;
    case 2://Reds on orange off
      if (currentMillis - ledTimer >= 3000) {
        Serial.println("Reds on orange off 2");
        for (q = 4; q < 12; q++) {
          LEDBoard.setPWM(q, 0, 4095);//turn Reds on full brightness
        }
        for (q = 0; q < 4; q++) {
          LEDBoard.setPWM(q, 0, 4096);//turn Orange off
        }
        ledTimer = currentMillis;
        ledState = 3;
        Serial.println(ledState);
      }
      break;
    case 3://Reds off

      if (currentMillis - ledTimer >= 500) { //0.5 second on before turning off
        Serial.println("Reds off 3");
        for (q = 4; q < 12; q++) {
          LEDBoard.setPWM(q, 0, 4096);//turn Reds off
        }
        ledTimer = currentMillis;
        ledState = 4;

      }

      break;
    case 4://Reds on
      if (currentMillis - ledTimer >= 500) { //0.5 second on before turning on
        Serial.println("Reds on 4");
        for (q = 4; q < 12; q++) {
          LEDBoard.setPWM(q, 0, 4095);//turn Reds on full brightness
        }
        ledState = 5;
        gateState = 1;
        gateServoTimer = currentMillis;
        triggerMillis = currentMillis;
      }
      break;
    case 5://Flashing
      if (currentMillis - ledTimer >= 500) { //0.5 secs on before turning on
        ledTimer = currentMillis;
        if (flashState > 0) {
          for (q = 4; q < 8; q++) {
            LEDBoard.setPWM(q, 0, 4095);//turn Reds on full brightness

          }
          for (q = 8; q < 12; q++) {
            LEDBoard.setPWM(q, 0, 4096);//turn Reds off
          }
          flashState = 0;
        } else {
          for (q = 4; q < 8; q++) {
            LEDBoard.setPWM(q, 0, 4096);//turn Reds off
          }
          for (q = 8; q < 12; q++) {
            LEDBoard.setPWM(q, 0, 4095);//turn Reds on full brightness
          }
          flashState = 1;
        }
      }
      break;
    case 6://everything off
      for (q = 0; q < 16; q++) {
        LEDBoard.setPWM(q, 0, 4096);//everything off;
      }
      currentledState = 0;
      ledState = 0;
      break;
    default:
      // Serial.println(ledState);
      break;
  }
}

void servoControl() {
  if (gateState == 1) {
    Serial.println("gateState 1");
  }
  switch (gateState) {
    case 1://lower barriers
      if (currentMillis - gateServoTimer >= 2000) {
        for (int q = 0; q < 2; q++) {//clockwise barriers/gates
          setServoPos(q, 89);
        }
        for (int q = 2; q < 4; q++) {//anti clockwise barriers gates
          setServoPos(q, 89);
        }
        gateState = 0;
      }

      break;
    case 2://raise barriers
      //no timer, will trigger from external...train
      for (int q = 0; q < 2; q++) {//clockwise barriers/gates
        setServoPos(q, 0);
      }
      for (int q = 2; q < 4; q++) {//anti clockwise barriers gates
        setServoPos(q, 179);
      }
      gateServoTimer = currentMillis;
      gateState = 3;
      break;
    case 3:
      Serial.println(gateState);
      if (currentMillis - gateServoTimer >= 1500) { //wait 1 secs while gate raises
        gateState = 0;
        ledState = 6;//turn leds off
        triggerMillis = currentMillis;//reset trigger timer
      }
      break;
    default:
      break;
  }
}

void setup() {
  Serial.begin(9600);
  Serial.println("PCA9685RailCrossingV1");

  servoBoard.begin();
  servoBoard.setOscillatorFrequency(27000000);
  servoBoard.setPWMFreq(SERVO_FREQ);  // Analog servos run at ~50 Hz updates

  LEDBoard.begin();//start the PCA9685 for the station building
  LEDBoard.setPWMFreq(1600);  // This is the maximum PWM frequency and suited to LED's

  delay(10);//let the above values take effect

  //make sure everything is off
  for (int q = 0; q < 16; q++) {
    LEDBoard.setPWM(q , 0, 4096);//All Led's off
  }
  //servos in start position
  for (int q = 0; q < 2; q++) {//clockwise barriers/gates
    setServoPos(q, 0);
  }
  for (int q = 2; q < 4; q++) {//anti clockwise barriers gates
    setServoPos(q, 179);
  }
}



void loop() {
  currentMillis = millis();//get current time since board started in milliseconds

  gateTrigger();//function that checks if the gate should be moving...this would usually be reading sensors/buttons
  ledControl();//controls the leds
  servoControl();//servo movement/timing control


}

 

Example 2: PCA9685RailCrossingV2.ino


Click to Download code: PCA9685RailCrossingV2.ino

Version 2 introduces an array that holds the target position and current position for the servos, this allows the barrier to be moved in a slower and more realistic movement.

 

 
/* PCA9685RailCrossingV2
   03/01/2024

   MOds made to slow the servo speeds

   Railway Crossing Sketch
   PCA9685 board for servos...2 gate or 4 gate
   PCA9685 board for LED's.... orange static, red flashing

   gates triggered by timer for demo version



*/

#include "Wire.h"

#include "Adafruit_PWMServoDriver.h"
//PCA 9685 settings

//Using the address as this sketch uses multiple PCA9685 boards
Adafruit_PWMServoDriver servoBoard = Adafruit_PWMServoDriver(0x40);//Address found from I2C scanner
Adafruit_PWMServoDriver LEDBoard = Adafruit_PWMServoDriver(0x46);//Address found from I2C scanner
#define USMIN  600 // This is the rounded 'minimum' microsecond length based on the minimum pulse of 150
#define USMAX  2400 // This is the rounded 'maximum' microsecond length based on the maximum pulse of 600
#define SERVO_FREQ 50 // Analog servos run at ~50 Hz updates
//end pca9685 settings

unsigned long currentMillis;//Keeps track of time in milliseconds for all timing

unsigned long triggerMillis;//keeps track of trigger timing

unsigned long ledTimer;//stores time in millis for tracking events
byte ledState = 0;//Stores the current state of the system
byte currentledState = 0;// what is the current state
byte flashState;//is flash on or off

unsigned long gateServoTimer;
byte gateState;

unsigned long servoMovementTimer;
int servoPos[4][2];//[4 servos 0-3 on servo board][target pos, actual pos]

//sets the position of the servo in degrees.
void setServoPos(int servo, int pos) {
  //This first bit of code makes sure we are not trying to set the servo outside of limits
  int sendPos;
  if (pos > 179) {
    pos = 179;
  }
  if (pos < 0) {
    pos = 0;
  }
  sendPos = USMIN + ((USMAX - USMIN) / 180 * pos);
  if (servo > -1 && servo < 16) { //only try to move valid servo addresses
    servoBoard.writeMicroseconds(servo, sendPos);
  }
}

//This would normally be triggered by sensors/buttons/dcc control
//Using a timer just for demo purposes.
void gateTrigger() {

  //gate closing trigger
  
  if (currentledState < 1) { //don't trigger if already triggered
    if (currentMillis - triggerMillis >= 10000) { //triggers after 5 seconds if nothing happening
      triggerMillis = currentMillis;//reset timer

      //this is the line that trigers the close sequence
      ledState = 1;
      Serial.println("Trigger Close");
    }
  }

//gate opening trigger

  if (currentMillis - triggerMillis >= 10000) { //triggers after 5 seconds...simulate train passing

    //This "if" atatement causes the gates to close, could be triggered by a sensor or button.
    if (gateState < 2 && ledState == 5) { //
      Serial.println("Trigger Open");
      triggerMillis = currentMillis + 10000;//reset timer
      gateState = 2;
      //ledState = 6;
    }
    
  }

}

//controls movement and LEDs depending on led state
void ledControl() {
  int q;
  switch (ledState) {
    case 1://system starting...set lights to orange for 3 seconds
      if (currentledState < 1) {
        currentledState = 1;//set the current state
        Serial.println("Orange On");
        for (int q = 0; q < 4; q++) {
          LEDBoard.setPWM(q, 0, 4095);//turn Orange on full brightness
        }
        for (int q = 12; q < 16; q++) {
          LEDBoard.setPWM(q, 0, 4095);//turn red barrier lights on
        }
        ledTimer = currentMillis;
        ledState = 2;
        Serial.println(ledState);
      }

      break;
    case 2://Reds on orange off 
      if (currentMillis - ledTimer >= 3000) {
        Serial.println("Reds on orange off 2");
        for (q = 4; q < 12; q++) {
          LEDBoard.setPWM(q, 0, 4095);//turn Reds on full brightness
        }
        for (q = 0; q < 4; q++) {
          LEDBoard.setPWM(q, 0, 4096);//turn Orange off
        }
        ledTimer = currentMillis;
        ledState = 3;
        Serial.println(ledState);
      }
      break;
    case 3://Reds off

      if (currentMillis - ledTimer >= 1500) { //0.5 second on before turning off
        Serial.println("Reds off 3");
        for (q = 4; q < 12; q++) {
          LEDBoard.setPWM(q, 0, 4096);//turn Reds off
        }
        ledTimer = currentMillis;
        ledState = 4;

      }

      break;
    case 4://Reds on
      if (currentMillis - ledTimer >= 500) { //0.5 second on before turning on
        Serial.println("Reds on 4");
        for (q = 4; q < 12; q++) {
          LEDBoard.setPWM(q, 0, 4095);//turn Reds on full brightness
        }
        ledState = 5;
        gateState = 1;
        gateServoTimer = currentMillis;
        triggerMillis = currentMillis;
      }
      break;
    case 5://Reds Flashing
      if (currentMillis - ledTimer >= 500) { //0.5 secs on before turning on
        ledTimer = currentMillis;
        if (flashState > 0) {
          for (q = 4; q < 8; q++) {
            LEDBoard.setPWM(q, 0, 4095);//turn Reds on full brightness

          }
          for (q = 8; q < 12; q++) {
            LEDBoard.setPWM(q, 0, 4096);//turn Reds off
          }
          flashState = 0;
        } else {
          for (q = 4; q < 8; q++) {
            LEDBoard.setPWM(q, 0, 4096);//turn Reds off
          }
          for (q = 8; q < 12; q++) {
            LEDBoard.setPWM(q, 0, 4095);//turn Reds on full brightness
          }
          flashState = 1;
        }
      }
      break;
    case 6://turn all LED's off... before barrier is fully down
      for (q = 0; q < 16; q++) {
        LEDBoard.setPWM(q, 0, 4096);//everything off;
      }
      currentledState = 0;
      ledState = 0;
      break;
    default:
      break;
  }
}

//Works through 3 different gate "states"
void servoControl() {
  switch (gateState) {
    case 1://lower barriers...train coming
      if (currentMillis - gateServoTimer >= 2000) {
        for (int q = 0; q < 2; q++) {//clockwise barriers/gates
          servoPos[q][0] = 89;
        }
        for (int q = 2; q < 4; q++) {//anti clockwise barriers gates
          servoPos[q][0] = 89;
        }
        gateState = 0;
      }

      break;
    case 2://raise barriers...train passed
      //no timer, will trigger from external...train
      for (int q = 0; q < 2; q++) {//clockwise barriers/gates
        servoPos[q][0] = 0;
      }
      for (int q = 2; q < 4; q++) {//anti clockwise barriers gates
        servoPos[q][0] = 179;
      }
      gateServoTimer = currentMillis;
      gateState = 3;
      break;
    case 3://Turns the LED's off before the gate has fully lowered as per real thing.
      //Serial.println(gateState);
      if (currentMillis - gateServoTimer >= 2500) { //wait 1 secs while gate raises
        gateState = 0;
        ledState = 6;//turn leds off
        triggerMillis = currentMillis;//reset trigger timer
      }
      break;
    default:
      break;
  }
}



//this moves the servos to the value they have been targetted with
//but at a reduced speed to match the real thing
void servoMovement() {
  int q;
  if (currentMillis - servoMovementTimer >= 50) { //0.05 sec timin
    servoMovementTimer = currentMillis;
    for (q = 0; q < 4; q++) { //work through the 4 servos
      if (servoPos[q][0] > servoPos[q][1]) { //if the target is higher angle
        servoPos[q][1]++;//increment by 1
        setServoPos(q, servoPos[q][1]);//send the new value
      }
      if (servoPos[q][0] < servoPos[q][1]) { //if the target is smaller angle
        servoPos[q][1]--;//decrease by 1
        setServoPos(q, servoPos[q][1]);//send the new value
      }
    }
  }

}

void setup() {
  Serial.begin(9600);
  Serial.println("PCA9685RailCrossingV2");

  servoBoard.begin();
  servoBoard.setOscillatorFrequency(27000000);
  servoBoard.setPWMFreq(SERVO_FREQ);  // Analog servos run at ~50 Hz updates

  LEDBoard.begin();//start the PCA9685 for the station building
  LEDBoard.setPWMFreq(1600);  // This is the maximum PWM frequency and suited to LED's

  delay(10);//let the above values take effect

  //make sure everything is off
  for (int q = 0; q < 16; q++) {
    LEDBoard.setPWM(q , 0, 4096);//turn Orange on full brightness
  }
  //servos in start position
  for (int q = 0; q < 2; q++) {//clockwise barriers/gates
    setServoPos(q, 0);
    servoPos[q][0] = 0;//target
    servoPos[q][1] = 0;//actual
  }
  for (int q = 2; q < 4; q++) {//anti clockwise barriers gates
    setServoPos(q, 179);
    servoPos[q][0] = 179;//target
    servoPos[q][1] = 179;//actual
  }
}



void loop() {
  currentMillis = millis();//get current time since board started in milliseconds

  gateTrigger();//function that checks if the gate should be moving...this would usually be reading sensors/buttons
  ledControl();//controls the leds
  servoControl();//servo movement/timing control
  
  servoMovement();//deals with speed of movement...sweep at controlled speed


}

Additional Resource Links

#55 Multiple PCA9685 PWM Servo Boards with servos and LED's 05/01/2024

PCA9685 PWM Servo Board 27/12/2023

Adafruit PWM servo library

Lesson 7: delay() v's millis(), controlling timing of programs 23/07/2021

State Machine Example based around model railway requirements 08/03/2023

Comments


To ask a question please email the address in this image: and use #56 PCA9685 Level Crossing Barriers and Lights as a reference.