Last Updated: 01/01/2024

PCA9685 Servo Button Control

Projects >> PCA9685 Servo Button Control

#54 PCA9685 Servo Button control aimed at Model railway point/turnout control

In this project we are going to control a number of servos using buttons to select the direction of movement.

This project is the second in a series on using an Arduino with servos. This project is aimed at controlling model railway points/turnouts but the code could be used for any project that needs servos controlled with buttons.

In the previous tutorial we built a PCA9685 Servo Setter Project for working out the movement that the servo needs to make the point switch.
This project will build on the lesons learnt ni that project. The Additional resouces link at the bottom of this page has other useful links to help with this project.

PCA9685 PWM Servo Driver

Project Overview and issues to overcome

The goal of this project will be to move a servo between 2 set angles when a button is pressed. So for each servo we will require 2 buttons.

As well as moving the point/turnout there is often a requirement to switch the power to the "V" of the point. To accomplish this we will use a relay module that has connections for "Normally Open" and "Normally Closed". The difference is that in one state the connection is made when the relay id powered while in the other state the cnotact is made when unpowered. So one is "Switch to turn on" while the other is "Switch to Turn Off".

These relays come in various formats, single, 2, 3,4,6,8 and 16 per board.

Make sure you have the correct version with 3 terminals per relay as shown below. also check the voltage, some are powered by 5v, others by 12v and even 24v.
In my project I have set this up for 5V.

Relays

What type of board should I use?

So far we are using 2 buttons and a relay for each servo, so 3 buttons per servo.
So if you want to use 16 servos you will need 48 pins so will require a Mega2560.

There are methods such as resistor ladders to reduce the number of button pins required as well as various omponents we can use to add extra pins but I have highlighted thhis issue as part of the things to consider when building a project.

In later projects we will look at sending commands via DCC or wireless communication so the buttons will not be needed but it is something to think about before you start.

Power Consumption

This is probably the area that is ignored the most in projects like this but it is very important and can even effect the way our code is written.

A small MG90/SG90 servo draws 70mA at rest but around 500mA at stall current. stall current is the power required at maximum torque so when the servo is pushing at it's hardest. If we have built our mechanism correctly we should only be drawing about 250-300mA at most per servo.

The relays also have a current draw. About 70mA on standby (just the circuitry power) buut 560mA when powered/Switched. As the relay needs to be powered all the time when the point is in one direction that is a constant 560mA draw.

This means that each point at peak power will be drawing 300mA + 560mA so it will max out at around 860mA.

This means if we use a full 16 servos on the PCA9685 board and moved them all at the same time we would draw 13.76Amps...that's a lot of Amps!

Now there are ways to get round this but it just shows how in a project it is very easy for little details to get missed that will have a huge impact later on.

13Amps will require a wire of a minimum of 1.5mm of copper diameter so with insulation around 3mm.

As can be seen from these figures this method of switching polarity will work on smaller layouts but for very large layouts other methods may need to be used.

Pinout and wiring diagrams

This is the basic wiring diagram for the system.

For this example I will only be using 2 servos, to add more servos just repeat the same wiring to different pins and the same with the switches and relays.

If you are using the point blades to switch the power to the V you can ignore the relays.

WARNING The 5V to the PCA9685 board and relays MUST NOT come from the Arduino 5V pin. You will be drawing too many Amps for the Arduino and may cause damage to your Arduino.

Circuit diagram

Pins

8 - 11 are connected to push button (momentary switches) with a 4.7k OHM pull down resistor

12-13 to relays

A5 to PCA9685 SCL
A4 to PCA9685 SDA

Servos are on pins 0 and 12 of the Pca9685 board

Make sure you have 5v going to the PCA9685 and relay boards 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: PCA9685ServoButtonsV1.ino


Click to Download code: PCA9685ServoButtonsV1.ino

This version uses nested if statements to read the buttons and although it works would be very complicated if it was controlling 16 servos.
Thios version has NO relay control.

 

 
/* PCA9685ServoButtonsV1
    31/12/23
    NON relay

    This example shows how to control the servos using buttons.
    This code DOES NOT have any relay code so for swithcing points
    you will need to use the point blades

    This example uses 2 servos, one on Pin 0, the other on pin 12

    adding more buttons/servos is just a matter of extending the code.

   Code is written as a state machine without delay() so that other code (without delay()
   can be added if needed.

   PCA9685 board
   2 servos
   4 buttons
   4 x 4.5k OHM resistors for Pull Down on buttons

   SDA/SCL connections for PCA9685

  Arduino UNO: A4 (SDA), A5 (SCL)
  Arduino Mega 2560: 20 (SDA), 21 (SCL)
  ESP32: 21(SDA), 22 (SCL)

  pins 8 - 11 for buttons with 4.7K pull down resistors
  
*/

#include "Wire.h"
#include "Adafruit_PWMServoDriver.h"
//PCA 9685 settings
// called this way, it uses the default address 0x40
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();
// you can also call it with a different address you want
//Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x41);
// you can also call it with a different address and I2C interface
//Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x40, Wire);

#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

//buttons...will all need pull down resistors
const int servo0Left = 8;
const int servo0Right = 9;
const int servo12Left = 10;
const int servo12Right = 11;

// our servos
int servo0 = 0;//port on PCA9685
int servo12 = 12;//port on PCA9685

//these are the values you would get from the PCA9685 Servo setter project
//see https://www.digitaltown.co.uk/projectPCA9685ServoSetter.php
int servo0LPos = 45;//servo left angle
int servo0RPos = 179;//servo right angle
int servo12LPos = 33;
int servo12RPos = 168;

//System variables
unsigned long currentMillis; //will stire the current time in from millis()
unsigned long buttonTimer;//stores the next value in millis for debounce
int buttonDebounce = 200;//smaller vale makes buttons more sensitive, easier to get double press



//Servo movement function
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
    pwm.writeMicroseconds(servo, sendPos);
  }
}

//deals with button presses
//This works but would become very complicated and hard to read with 16 servos
void buttonState() {
  if (currentMillis - buttonTimer >= buttonDebounce) { //are buttons debounced
    buttonTimer = currentMillis;//reset debounce timer
    //the system will only read one button at a time so checks through the buttons in order and if any are pressed
    //that button will be dealt with and any other presses at the same time ignored
    //This means only 1 debounce timer is needed.
    if (digitalRead(servo0Left) > 0) {
      setServoPos(servo0, servo0LPos);
      Serial.println("Servo 0: Left");
    } else {
      if (digitalRead(servo0Right) > 0) {
        setServoPos(servo0, servo0RPos);
        Serial.println("Servo 0: Right");
      } else {
        if (digitalRead(servo12Left) > 0) {
          setServoPos(servo12, servo12LPos);
          Serial.println("Servo 12: Left");
        } else {
          if (digitalRead(servo12Right) > 0) {
            setServoPos(servo12, servo12RPos);
            Serial.println("Servo 12: Right");
          }
        }
      }
    }
  }
}



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

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

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

  pinMode(servo0Left, INPUT);
  pinMode(servo0Right, INPUT);
  pinMode(servo12Left, INPUT);
  pinMode(servo12Right, INPUT);

}

void loop() {
  currentMillis = millis();//get the current millis since board started for timing
  buttonState();//deals with button presses

}

 

Example 2: PCA9685ServoButtonsV2.ino


Click to Download code: PCA9685ServoButtonsV2.ino

This version places all the pin and position variables into an array.
This means it is much easier to add extra servos/buttons into the code with less risk of bugs.
Thios version has NO relay control.

 

 
/* PCA9685ServoButtonsV2
  31/12/23
  Built on the foundations of PCA9685ServoButtonsV1 but with all servo pin and settings data stored in an array.
  This is to make it easier to add more servos/buttons

    NON relay

    This example shows how to control the servos using buttons.
    This code DOES NOT have any relay code so for swithcing points
    you will need to use the point blades

    This example uses 2 servos, one on Pin 0, the other on pin 12

    adding more buttons/servos is just a matter of extending the code.

   Code is written as a state machine without delay() so that other code (without delay()
   can be added if needed.

   PCA9685 board
   2 servos
   4 buttons
   4 x 4.5k OHM resistors for Pull Down on buttons

   SDA/SCL connections for PCA9685

  Arduino UNO: A4 (SDA), A5 (SCL)
  Arduino Mega 2560: 20 (SDA), 21 (SCL)
  ESP32: 21(SDA), 22 (SCL)

  pins 8 - 11 for buttons with 4.7K pull down resistors
  
*/

#include "Wire.h"
#include "Adafruit_PWMServoDriver.h"
//PCA 9685 settings
// called this way, it uses the default address 0x40
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();
// you can also call it with a different address you want
//Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x41);
// you can also call it with a different address and I2C interface
//Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x40, Wire);

#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

//position in the following array is the values you would get from the PCA9685 Servo setter project
//see https://www.digitaltown.co.uk/projectPCA9685ServoSetter.php
# define numButtons 4 //number of buttons controlling servos
int servoVariables[numButtons][3] = { //2 lines per servo (2 buttons)
  {8,0,45}, //pin 8, servo 0, position
  {9,0,179}, //pin 9, servo 0, position
  {10,12,33}, //pin 10,servo 12, position
  {11,12,168} //pin 11, servo 12, position
};
//System variables
unsigned long currentMillis; //will stire the current time in from millis()
unsigned long buttonTimer;//stores the next value in millis for debounce
int buttonDebounce = 200;//smaller vale makes buttons more sensitive, easier to get double press

//Servo movement function
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
    pwm.writeMicroseconds(servo, sendPos);
  }
}

//deals with button presses
//This works but would become very complicated and hard to read with 16 servos
void buttonState() {
  int q;
  if (currentMillis - buttonTimer >= buttonDebounce) { //are buttons debounced
    buttonTimer = currentMillis;//reset debounce timer
    for(q=0;q < numButtons;q++){
      if(digitalRead(servoVariables[q][0]) > 0){
        setServoPos(servoVariables[q][1], servoVariables[q][2]);
        q=numButtons;//causes system to exit for loop and wait for debounce again    
      }
    }
  }
}



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

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

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

  for(int q=0;q < numButtons;q++){
    pinMode(servoVariables[q][0], INPUT);//set the pins used for buttons in the servoVariables array
  }

}

void loop() {
  currentMillis = millis();//get the current millis since board started for timing
  buttonState();//deals with button presses
}

 

Example 3: PCA9685ServoButtonsV3.ino


Click to Download code: PCA9685ServoButtonsV3.ino

This version places all the pin and position variables into an array and also adds the relays

The code can be seen to work even without a relay as the built in LED will be on when the servo connected to pin 13 is powered.

Although the code works very well for large layouts there will still be issues with power consumption with lots of relays.

 

 
/* PCA9685ServoButtonsV3
  31/12/23
  Built on the foundations of PCA9685ServoButtonsV2
  This time the relay code has been added.

    

    This example uses 2 servos, one on Pin 0, the other on pin 12
    Relays are on pins 12 and 13

    adding more buttons/servos is just a matter of extending the code.

   Code is written as a state machine without delay() so that other code (without delay()
   can be added if needed.

   PCA9685 board
   2 servos
   4 buttons
   4 x 4.5k OHM resistors for Pull Down on buttons
   2 relay modules

   SDA/SCL connections for PCA9685

  Arduino UNO: A4 (SDA), A5 (SCL)
  Arduino Mega 2560: 20 (SDA), 21 (SCL)
  ESP32: 21(SDA), 22 (SCL)

  pins 8 - 11 for buttons with 4.7K pull down resistors
  pins 12-13 relays

*/

#include "Wire.h"
#include "Adafruit_PWMServoDriver.h"
//PCA 9685 settings
// called this way, it uses the default address 0x40
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();
// you can also call it with a different address you want
//Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x41);
// you can also call it with a different address and I2C interface
//Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x40, Wire);

#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

//position in the following array is the values you would get from the PCA9685 Servo setter project
//see https://www.digitaltown.co.uk/projectPCA9685ServoSetter.php
# define numButtons 4 //number of buttons controlling servos
int servoVariables[numButtons][5] = { //2 lines per servo (2 buttons)
  {8,0,45,12,0}, //pin 8, servo 0, position,relay,off
  {9,0,179,12,1}, //pin 9, servo 0, position,relay,on
  {10,12,33,13,0}, //pin 10,servo 12, position,relay,off
  {11,12,168,13,1} //pin 11, servo 12, position,relay,on
};
//System variables
unsigned long currentMillis; //will stire the current time in from millis()
unsigned long buttonTimer;//stores the next value in millis for debounce
int buttonDebounce = 200;//smaller vale makes buttons more sensitive, easier to get double press

//Servo movement function
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
    pwm.writeMicroseconds(servo, sendPos);
  }
}

//deals with button presses
//This works but would become very complicated and hard to read with 16 servos
void buttonState() {
  int q;
  if (currentMillis - buttonTimer >= buttonDebounce) { //are buttons debounced
    buttonTimer = currentMillis;//reset debounce timer
    for(q=0;q < numButtons;q++){
      if(digitalRead(servoVariables[q][0]) > 0){
        setServoPos(servoVariables[q][1], servoVariables[q][2]);//move the servo
        digitalWrite(servoVariables[q][3],servoVariables[q][4]);//set the relay to appropriate value
        q=numButtons;//causes system to exit for loop and wait for debounce again    
      }
    }
  }
}



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

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

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

  for(int q=0;q < numButtons;q++){
    pinMode(servoVariables[q][0], INPUT);//set the pins used for buttons in the servoVariables array
    pinMode(servoVariables[q][3], OUTPUT);//set the pins used for relays servoVariables array
  }

}

void loop() {
  currentMillis = millis();//get the current millis since board started for timing
  buttonState();//deals with button presses
}

Additional Resource Links

PCA9685 PWM Servo Board 27/12/2023

PCA9685 Servo Setter Project 29/12/23

How To 1 : Debouncing buttons 15/09/2021

Lesson 11: Arrays 28/08/2021

Adafruit PWM servo library

Comments


This site has been designed to be child friendly, this means that comments cannot be added to videos or directly to the site.
To add a comment or ask a question please email the address in this image: and use PCA9685 Servo Button Control as a reference.