Six-servo robotic arm on a PCA9685: power budget, pulse limits and smooth motion
- Build Logs
- 29 Jul, 2026
A six-servo arm is where hobby robotics stops being about code and starts being about power. The sketch is easy. Getting six servos to move at once without the board browning out, and getting them to move like a machine rather than like something being electrocuted, is the actual project.
This is the complex build in the series. It assumes you have driven a servo before — if you have not, start with our PCA9685 wiring and pulse-range write-up, which covers the board itself in detail.
What you will need
| Part | Qty | Approx. |
|---|---|---|
| PCA9685 + 5× SG90 Servo Combo | 1 | ₹699 |
| Arduino Uno R3 (compatible) | 1 | ₹418 |
| Spare PCA9685 (if chaining a second arm) | — | ₹239 |
Roughly ₹1,120 plus a 5V supply — see the next section, because that supply is not optional and it is not your USB port.
Budget the power first, not last
An SG90 idles at around 10 mA, draws 100–250 mA while moving, and spikes to roughly 700 mA stalled. Stall is not a fault condition here — a servo holding an arm against gravity is partially stalled continuously.
Six of them, moving together, will ask for somewhere between 1.5A and 4A depending on load. Now compare that against what you might have been planning to power them from:
| Source | Realistic budget | Verdict |
|---|---|---|
| Arduino 5V pin (USB powered) | ~400 mA total | not even one servo under load |
| Arduino 5V pin (barrel jack) | ~800 mA, regulator gets hot | no |
| Dedicated 5V 3A supply | 3000 mA | fine for six SG90 |
| 5V 5A supply | 5000 mA | comfortable, room to grow |
Power the servos from a separate 5V supply into the PCA9685's screw terminal — never through the Arduino. The board is designed for exactly this: V+ on the terminal block feeds the servo rail, VCC on the header feeds only the chip's logic.
Then tie the grounds together. The supply's ground and the Arduino's ground must be common, or the PWM signal has no reference and the servos jitter unpredictably. This is the single most common cause of "my servos twitch randomly" on this board.
Add a 1000µF electrolytic capacitor across V+ and GND at the terminal block. When six servos start moving simultaneously the current step is nearly instantaneous, and the wiring between supply and board has enough inductance to let the rail sag for a few milliseconds. That sag resets microcontrollers. The capacitor is a local reservoir that rides it out. Mind the polarity — the stripe is the negative leg.
Pulse ranges: stop copying 150 and 600
Every tutorial uses SERVOMIN 150 and SERVOMAX 600. Those are counts out of the PCA9685's 4096-step cycle, and they are specific to whoever wrote that tutorial's servos.
At the default 50 Hz, one cycle is 20 ms, so one count is 20000/4096 ≈ 4.88 µs. A count of 150 is about 732 µs and 600 is about 2930 µs. An SG90's honest range is roughly 500–2400 µs, so those defaults are asking the servo to travel past both of its mechanical stops.
A servo driven past its stop does not error. It pushes, stalls, draws its full 700 mA, gets hot, and strips its plastic gears over an afternoon. If your arm buzzes at the extremes of travel, it is doing this right now.
Find your own limits per servo, once:
#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x40);
void setup() {
Serial.begin(9600);
pwm.begin();
pwm.setOscillatorFrequency(27000000);
pwm.setPWMFreq(50);
Serial.println(F("Type a count 100-650 to test channel 0"));
}
void loop() {
if (!Serial.available()) return;
int count = Serial.parseInt();
if (count < 80 || count > 700) return;
Serial.print(F("count ")); Serial.print(count);
Serial.print(F(" = ")); Serial.print(count * 4.88);
Serial.println(F(" us"));
pwm.setPWM(0, 0, count);
}
Walk the value up from 150 until the horn stops moving, then back off 10 counts. Do the same downwards. Write the pair down for each joint. They will differ — SG90s vary noticeably between units, and the whole point is that you calibrate rather than assume.
Moving smoothly
setPWM() is a step command: the servo goes as fast as it physically can, which on a loaded arm means it lurches, overshoots and shakes the whole structure. Interpolating between positions is what makes an arm look like a machine.
struct Joint {
uint8_t ch;
int minCount, maxCount; // YOUR measured limits
int current;
int target;
};
Joint joints[6] = {
{0, 160, 590, 375, 375}, // base
{1, 170, 560, 375, 375}, // shoulder
{2, 155, 600, 375, 375}, // elbow
{3, 160, 580, 375, 375}, // wrist pitch
{4, 150, 610, 375, 375}, // wrist roll
{5, 200, 480, 340, 340} // gripper
};
const int STEP = 3; // counts per tick: lower = slower, smoother
const int TICK_MS = 15; // ~66 updates/sec
void setTarget(uint8_t j, int count) {
joints[j].target = constrain(count, joints[j].minCount, joints[j].maxCount);
}
// Move every joint one step toward its target. Called steadily,
// this gives coordinated motion rather than one joint at a time.
bool tick() {
bool moving = false;
for (uint8_t i = 0; i < 6; i++) {
Joint &j = joints[i];
if (j.current == j.target) continue;
int delta = j.target - j.current;
j.current += constrain(delta, -STEP, STEP);
pwm.setPWM(j.ch, 0, j.current);
moving = true;
}
return moving;
}
void loop() {
static unsigned long last = 0;
if (millis() - last >= TICK_MS) {
last = millis();
tick();
}
}
Because every joint advances by at most STEP per tick, they all arrive at different times but move together, which reads as coordinated rather than sequential. If you want them to finish together, scale each joint's step by its share of the largest distance — that is the next refinement, and it is what turns this into real trajectory control.
Sequencing a pick and place
void moveTo(int b, int s, int e, int wp, int wr, int g) {
setTarget(0, b); setTarget(1, s); setTarget(2, e);
setTarget(3, wp); setTarget(4, wr); setTarget(5, g);
while (tick()) delay(TICK_MS); // block until the pose is reached
}
void pickAndPlace() {
moveTo(375, 300, 400, 375, 375, 480); // approach, gripper open
moveTo(375, 250, 450, 375, 375, 480); // descend
moveTo(375, 250, 450, 375, 375, 340); // close gripper
delay(300); // let it settle before lifting
moveTo(375, 350, 380, 375, 375, 340); // lift
moveTo(500, 350, 380, 375, 375, 340); // rotate base
moveTo(500, 250, 450, 375, 375, 340); // descend
moveTo(500, 250, 450, 375, 375, 480); // release
moveTo(500, 350, 380, 375, 375, 480); // retreat
}
The delay(300) after closing the gripper is not padding. A servo reports nothing about whether it reached its position, so if you lift immediately you sometimes lift before the grip has actually closed and the object drops. Waiting is the cheapest possible substitute for feedback.
Things that go wrong
Arduino resets whenever several servos move. Servos are being fed from the Arduino, or there is no bulk capacitor. This is the defining failure of this build.
Servos jitter constantly even when idle. Grounds not tied together, or the supply cannot hold 5V. Check the rail with a meter while the arm is holding a pose, not while it is idle.
One joint buzzes at the end of its travel. You are commanding past its mechanical stop. Re-measure that joint's limits and shrink them.
Nothing responds at all. Wrong I2C address. The PCA9685 is 0x40 with no address jumpers bridged; if you soldered any, it has moved. An I2C scanner settles it in a minute.
Arm sags under its own weight when powered off. Normal — SG90s have no holding torque unpowered. If that matters, design the mechanics so gravity closes the joint rather than opening it, or step up to metal-gear servos.
Gets hot after ten minutes. Something is partially stalled and holding. Check your limits, and check the arm is not fighting itself at rest.
Be realistic about SG90s
Five SG90s and a PCA9685 for ₹699 is an outstanding way to learn multi-axis motion control, sequencing and power budgeting. It is not going to give you repeatable positioning: plastic gears have backlash, there is no positional feedback, and the same commanded count lands in a slightly different place depending on which direction you approached from.
That is a limit of the hardware class, not your build. When you outgrow it, the step up is metal-gear servos with a proper power rail — and the code above carries over unchanged.
Where to take it next
The obvious extension is inverse kinematics: give the arm an x, y, z and let it compute the joint angles. For a 3-DOF planar arm that is a page of trigonometry and it is a genuinely satisfying thing to get working. After that, driving it from the web-server pattern in our room monitor build gets you a browser-controlled arm on your own network.
Running this as a college project? Our on-site training covers exactly this build with a group, and our engineers will help you debug the power rail before it eats a board.