Slow PCA9685 Servo project

Hi, I have a project where I want to control 80 SG90 microservos, using 5 pca9685 servo drivers. The goal of the project is for the servos to move simultanously in a sweeping motion, back and forth.

I have been successful with my code in getting all the servos to move. The problem is that the more servos that are added to the code the slower the speed. For example; when I increase from 1 servo to 16 servos the speed is doubled. When the code accounts for all 80 servos they move, but a lot slower than what I would expect them to compared with the speed when I only have 1 servo added. The speed is noticably slower and moves only a tiny bit faster if I decrease my Sweep_Step_Ms.

I have been using the Arduino Uno, but tried to switch to a Teensy 4.0 to see if the increased processing power would have an affect - it did not have a noticable affect.

The speed is not affected by how many servos are physically connected to the drivers. I am also using a 5V, 10A power source individually connected to each driver, so I dont think lack of power/amp is the problem.

I tried making a scemathic the best of my abbility, hopefully it is possible to read/understand.

And here is the code I have been using:


#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
#include <elapsedMillis.h>


#define SERVOS_PER_BOARD 16
#define NUM_BOARDS 5
#define TOTAL_SERVOS (SERVOS_PER_BOARD * NUM_BOARDS)


#define SERVO_MIN 200
#define SERVO_MAX 450
#define SERVO_FREQ 50  // in Hz

#define OSC_FREQ 27000000UL


#define SWEEP_STEP_MS 10   // milliseconds between each pulse step
#define SWEEP_PAUSE_MS 500  // milliseconds to pause between sweep directions

Adafruit_PWMServoDriver boards[NUM_BOARDS] = {
  Adafruit_PWMServoDriver(0x40),
  Adafruit_PWMServoDriver(0x41),
  Adafruit_PWMServoDriver(0x42),
  Adafruit_PWMServoDriver(0x43),
  Adafruit_PWMServoDriver(0x44) 
  };


elapsedMillis sweepTimer;
elapsedMillis pauseTimer;

uint16_t currentPulse = SERVO_MAX;  // Start at max position
bool sweepingDown = true;           // true = MAX→MIN, false = MIN→MAX
bool inPause = false;               // true while waiting between sweeps
const int BAUD_RATE = 9600;
const uint32_t I2C_CLOCK_FREQ = 400000;

bool initBoards();
void setServoPWM(uint8_t servoIndex, uint16_t pulselen);

// ---------------------------------------------------------------------------
// Board initialization
// ---------------------------------------------------------------------------


bool initBoards() {
  for (uint8_t i = 0; i < NUM_BOARDS; i++) {
    boards[i].begin();
    boards[i].setOscillatorFrequency(OSC_FREQ);
    boards[i].setPWMFreq(SERVO_FREQ);
  }
  return true;
}

void setServoPWM(uint8_t servoIndex, uint16_t pulselen) {
  // Bounds guard: silently ignore out of range indices rather than sending garbage to a nonexistent board channel
  if (servoIndex >= TOTAL_SERVOS) return;

  uint8_t boardIndex = servoIndex / SERVOS_PER_BOARD;
  uint8_t channel = servoIndex % SERVOS_PER_BOARD;

  boards[boardIndex].setPWM(channel, 0, pulselen);
}

// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------

void setup() {
  Serial.begin(BAUD_RATE);
  Wire.setClock(I2C_CLOCK_FREQ);

  Serial.println(F("Initializing 80-servo controller..."));

  if (!initBoards()) {
    Serial.println(F("ERROR: Board initialization failed. Check wiring and I2C addresses."));
    while (1)
      ;  // Halt — no point continuing if drivers aren't responding
  }

  Serial.println(F("All boards initialized. Starting sweep."));

  // Move all servos to the starting position before the loop begins, so they don't jump unpredictably
  for (uint8_t s = 0; s < TOTAL_SERVOS; s++) {
    setServoPWM(s, currentPulse);
  }

  //reset timer
  sweepTimer = 0;
}

// ---------------------------------------------------------------------------
// Main loop
// ---------------------------------------------------------------------------

/**
 * Sweeps all 80 servos back and forth between SERVO_MIN and SERVO_MAX, one pulse-width tick per SWEEP_STEP_MS interval, with a pause of SWEEP_PAUSE_MS between direction changes.
 */
void loop() {

  // Pausing
  if (inPause) {
    if (pauseTimer >= SWEEP_PAUSE_MS) {
      inPause = false;
      sweepTimer = 0;  // Reset sweep timer so next step starts cleanly
    }
    return;  // Nothing else to do while pausing
  }

  // Sweeping down/up
  if (sweepTimer >= SWEEP_STEP_MS) {
    sweepTimer = 0;

    // Apply the current PWM to every servo
    for (uint8_t s = 0; s < TOTAL_SERVOS; s++) {
      setServoPWM(s, currentPulse);
    }

    if (sweepingDown) {
      // Moving MAX to MIN
      if (currentPulse > SERVO_MIN) {
        currentPulse--;
      } else {
        // Hit the bottom, flip direction and pause
        Serial.println(F("Reached MIN. Reversing..."));
        sweepingDown = false;
        inPause = true;
        pauseTimer = 0;
      }
    } else {
      // Moving MIN to MAX
      if (currentPulse < SERVO_MAX) {
        currentPulse++;
      } else {
        // Hit the top, flip direction and pause
        Serial.println(F("Reached MAX. Reversing..."));
        sweepingDown = true;
        inPause = true;
        pauseTimer = 0;
      }
    }
  }
}

I have also made and tried another code where I worked more from the Adafruit Pwm Servo driver "servo" example, which I can upload if it makes sense to. But I have met the same issue with that code.

I am a beginner, and this is my first post in this forum, all help is extremely appreciated! Thank you in advance.

(post deleted by author)

  • You are trying to send 16ms worth of data every 10ms.

  • You have 5 boards, what did you do about the I2C pull-ups ?

All servos are given the same PWM value in each iteration. Connect all servo PWM pins to one line and make one call to setting the actual PWM. Likely some kind of buffer/amplifier is needed to feed 80 PWM lines.

The power handling should remain as it is.

Welcome!
It will never work properly, you have a major power problem:

SG90 servo Specification

  • No load speed: 0.12 seconds / 60 degrees (4.8V)
  • Stall Torque: 1.6 kg / cm (4.8V)
  • Operating temperature: -30 ~ +60 degrees Celsius
  • Dead Set: 7 microseconds
  • Operating voltage: 4.8V-6V
  • Working current: less than 500mA
  • Cable length: 180mm
  • Size: 22mmx12.5mmx29.5mm
  • Weight: 9 grams
    Working current is 1/2 amp per servo, that indicates you will need 80 Amps to run them, that does not take into account the inrush when they start. Be careful, Do NOT power them from a single thin power rail, this wil cause voltage drop and jitters

Seriously consider: thick power distribution (bus bars or heavy gauge wire), multiple injection points, Add bulk capacitance
1000–4700 µF per cluster (every 5–10 servos)**

The best approach would be to Split into groups, 4 × 20 servos, each with its own 5 V 15–20 A supply. Be sure there ia a Common ground shared with your controller

I was unfamiliar with this specification.

AI suggested it was supposed to be a dead band spec, that is to say that changing the servo signal by less woukd not make it move.

A degree is ~5.6 microseconds.

a7

@bipbop12

The PCA9685 has an ALLCALL I2C address that sends the same data to all PCA9685 on the bus at the same time. The address = PCA9685_ALLCALLADR. So rather than iterating through all the boards you just send to the ALLCALL address.

There are also ALL_LED_x registers that write to all the registers.

  Adafruit_PWMServoDriver pwmAll = Adafruit_PWMServoDriver(PCA9685_ALLCALLADR);
  pwmAll.setPWM(channel, 0, pulselen);

That way there will be no slow down between using 1 servo or many servos.

It would be easier, if the servos are truly meant to operate simultaneously in lock step going to exzctly the same angle, for @bipbop12 to use @Railroader's solution from #4.

With a signal,that might need amplification, connect all the servo control lines to one output. Drive it with one call to regular old .write().

Lose the PCA9685s. Ensure adequate current supply for the servos.

a7

Try running in parallel instead of series so U = U1 = U2 = U3..

They are parallel. The i2c and power lines are just fed through to the other side.

  • You have 5 boards, what did you do about the I2C pull-ups ?

Those print lines freezes everything for "char * 1.04ms" = 25ms.
Try removing all tthe print statements in loop, or increase baud rate to 115200.

Is this for a kinetic art setup.
Daniel Rozin (Google it) moved from servos to stepper motors, because of several issues with upscaling.
Leo..

I would like to see the expression on @bipbop12 's face when they read the reply from @Railroader :laughing:

You don’t need to update all 80 servos every loop. Update a small batch each cycle and rotate through them, while only advancing the sweep position after all have been refreshed once. This reduces I²C load while still making all servos appear to move together.

Do you still need help?

Hi, thank you all for the advice. I will go through it today and test it out. I will let you know what works and if I have trouble with understanding some of it.

If you decide to use ALLCALL, just try the ALLCALL first. If you can make that work then you can try the ALL_LED_x

Everyone please keep in mind what @bipbop12 wants to achieve and help them to do that in the simplest way.

@bipbop12 please don't let pride/stubbornness/embarrassment force you to continue with this grossly over-complex solution to a simple problem... Unless you want to clarify your requirements?

Are you impling my solution is too complicated?

Hi, I got the ALLCALL to work with my code and this ended up fixing my problem, now all the servos respond at the same time as I want.
And a big plus that I didnt have to do any hardware changes. Thank you!!!