Roboticz Lab ES
‹ Guides

Robotic arm with ESP32 and servos | Intermediate guide

Build a 4-axis arm with ESP32, a PCA9685 driver and MG996R/SG90 servos. Torque math, current budget and wiring explained, with the code the AI writes for you.

Level
Intermediate
Time
One weekend (6-8 h)
Published
Generated image: macro detail of workshop material, accompanying the guide Robotic arm with ESP32 and servos
Bill of materials
Part Qty Approx. price
ESP32 DevKitC (ESP32-WROOM-32, 30-pin) 1 6-12 €
PCA9685 PWM/servo driver (16-channel, I2C, generic clone) 1 3-5 €
MG996R servo (metal gears, high torque) 3 5-7 €/unit
SG90 micro servo (for the gripper) 1 2-3 €
4-DOF arm mechanical kit (acrylic or aluminum, brackets and hardware included, no electronics) 1 20-35 €
5V 5A switching power supply with barrel jack 1 8-12 €
DC barrel jack to screw terminal adapter 1 1-2 €
1000 µF 16V electrolytic capacitor 1 0.50 €
Toggle switch (cuts the V+ line, doubles as an emergency stop) 1 1-2 €
Male-male and male-female dupont wires, plus zip ties 1 3-4 €

How it works

The ESP32 has no trouble generating each servo's PWM signal, but it can't supply the current they pull under load: 3 MG996R servos holding the arm extended can each demand up to 2.5A if they stall, way more than the board's regulator can handle. That's why a PCA9685 sits in between: it takes the command over I2C from the ESP32 at 3.3V (without touching boot or input-only pins) and outputs the PWM using its own servo supply, V+ up to 6V, fully separate from the logic side. The strongest servo goes at the base and shoulder, where the arm leverages the most against gravity. The weakest goes at the gripper, which only has to squeeze a small object.

The build

Why a robotic arm and not another mobile robot? Because here the challenge isn’t moving across the floor, it’s moving weight in the air without anything overheating, stalling dead, or resetting itself. If you’ve already wired an Arduino and know what an external supply is for, this is the logical next step: several motors, one shared supply, and a driver in between acting as the go-between. In my case, the 4-axis arm is the project that teaches the most about current budgeting, because the numbers fail fast if you don’t get them right.

What you’re building and what you already need to know

A 4 degree-of-freedom arm: base (rotates the whole arm), shoulder, elbow and gripper. Four servos, a PCA9685 driver talking I2C to the ESP32, and a separate supply that powers only the servos.

This guide assumes you can already solder the basics, wire dupont cables without tangling them, and upload a sketch from the Arduino IDE (or PlatformIO) to an ESP32. You’ll also need a multimeter to check voltage and continuity before powering anything, a small screwdriver for the servo screws and the terminal block, and patience for calibration: every cheap servo responds a little differently to the same pulse.

Why the ESP32 never touches servo power

Here’s the mistake that fries the most boards: wiring a servo’s red wire to the ESP32’s 5V or 3V3 pin. Don’t. Two reasons, one about voltage and one about current.

The ESP32 runs at 3.3V and its pins aren’t 5V tolerant: Espressif’s own documentation caps the tolerance at 3.6V, so anything above that needs a voltage divider before it touches a GPIO. The servo’s PWM signal itself isn’t the problem (a 3.3V pulse already reads as HIGH on most servos), the problem is current: the regulator on the ESP32 board that produces 3.3V from USB gives out at most a few hundred mA, sized for the chip itself plus a small sensor or two. An MG996R under real load pulls between 500 and 900 mA, and if it stalls against a mechanical stop it climbs to 2.5A. Three of those servos fed by a regulator meant for the microcontroller alone just don’t work, and in the best case the ESP32 resets itself the moment the servos start moving because you’re stealing the voltage it needs to stay powered on.

That’s why the PCA9685 has two separate power rails, and it’s the key part of this build:

  • VCC (logic): wire it to the ESP32’s 3.3V. It only powers the driver chip itself, which draws a few mA, so it puts no real load on the regulator.
  • V+ (servo power): wire it to the external 5V 5A supply, through its own screw terminal. This is the rail that actually moves the motors.

Both rails share ground (GND) across the ESP32, the PCA9685 and the external supply. This is the step most people skip, and the one that explains most failures: without a common ground, the PWM signal has no shared 0V reference and the servo can’t interpret the pulse, it jitters or just doesn’t respond at all.

A 1000 µF capacitor on the V+ terminal (between positive and GND, as close to the block as possible) isn’t optional here: when a servo starts moving or suddenly changes direction it pulls a current spike the supply doesn’t always deliver instantly, and the capacitor acts as a buffer for that spike. Without it, you’ll see the ESP32 reset or the PCA9685 drop its I2C connection right when a servo moves.

Torque and leverage: which servo goes where

Why MG996R at the base and shoulder, and SG90 only at the gripper? Lever physics, not preference.

The shoulder has to hold up everything downstream of it: the elbow servo (about 55g), the forearm structure, the gripper servo (9g) and whatever you’re holding. With the arm extended horizontally, the equivalent weight sits around 170-180g at roughly 15cm from the shoulder axis. Multiply weight by distance and you get the torque the servo needs: 0.18kg x 15cm ≈ 2.6 kgf.cm of torque just to hold still, before it even moves. The MG996R rates between 9.4 kgf.cm (at 4.8V) and 11 kgf.cm (at 6V) of stall torque per its datasheet, so you keep a 3.5 to 4x margin: needed because accelerating and braking the arm demands more torque than just holding it steady.

The elbow holds less (only the gripper and whatever it grabs, about 90g at 12cm from the axis, ≈1.1 kgf.cm), so in theory a smaller servo would be enough there. It keeps the MG996R anyway for two practical reasons: it simplifies the build (one model, one calibration routine) and the price difference against a weaker servo is a couple of euros, not worth complicating the BOM over.

The gripper (SG90) only has to close its fingers on a small object, not hold up the arm. Its stall torque is 1.8 kgf.cm (at 4.8V), well below the 2.6 kgf.cm needed at the shoulder. If you try an SG90 there, the servo stalls, keeps pulling its full stall current continuously trying to hold a weight it can’t, heats up, and over time strips the plastic gear teeth. That’s the real reason the strong servo sits near the base and the weak one at the tip, not a convention copied without thinking.

Wiring, pin by pin

SignalESP32Goes to
Logic (VCC)3V3PCA9685 VCC
GroundGNDPCA9685 GND + 5V supply GND
I2C dataGPIO21 (SDA)PCA9685 SDA
I2C clockGPIO22 (SCL)PCA9685 SCL
PCA9685Goes to
V+ (terminal)+ of the 5V/5A supply, through the switch
GND (terminal)- of the 5V/5A supply
OUT0Base servo (MG996R)
OUT1Shoulder servo (MG996R)
OUT2Elbow servo (MG996R)
OUT3Gripper servo (SG90)

GPIO21 and GPIO22 are the ESP32’s default I2C pins and they aren’t strapping pins: those are GPIO0, GPIO2, GPIO5, GPIO12 and GPIO15, and it’s best to keep them free so they don’t interfere with the board’s boot sequence. If you later add limit switches or manual-control potentiometers, use GPIO32-39: they sit in the ADC1 range, though keep in mind GPIO34-39 are input-only, they can’t drive an output.

Mechanical assembly: order matters

Before you screw anything down, center each servo through software first (send it the 90-degree command from a minimal sketch) and only then fit the plastic arm (the horn) onto the shaft, aligned with what you want to be the mechanical center of its travel. If you screw the horn on before centering the servo, the shaft can end up offset and the arm hits a mechanical stop on one side much sooner than it should, which in practice means forcing the servo against an obstacle every time it approaches that end. It’s the most common mechanical failure in these kits, and it looks like a software bug when it’s really a misplaced screw.

Build from the base toward the gripper and leave plenty of slack in the wiring at each joint: the servo cables need to follow the full rotation of the shaft without pulling taut or catching on the frame. Use zip ties to bundle the cables along the arm, but don’t cinch them tight in the areas where things actually move.

Calibration and current control

Cheap servos vary unit to unit even on the same model: the same pulse value won’t put two MG996R at exactly the same angle. That’s why the software doesn’t send a raw angle, it sends a PCA9685 tick range (12-bit, 0 to 4095 at 50Hz) that you have to calibrate per servo, moving it to its extremes and noting where it stops turning without forcing the mechanical stop.

The other half of calibration is about current, not angle: if you jump a servo straight from 0 to 180 degrees, its startup current spike stacks with the other servos’ spikes if they move at the same time. The fix isn’t a bigger supply, it’s moving in small steps with a pause between each one, so the draw spreads out over time instead of arriving all at once.

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

Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x40);

void moveSmooth(uint8_t channel, int from, int to) {
  int step = (to > from) ? 1 : -1;
  for (int angle = from; angle != to; angle += step) {
    int ticks = map(angle, 0, 180, 150, 600); // calibrate these limits per servo
    pwm.setPWM(channel, 0, ticks);
    delay(15);
  }
}

void setup() {
  Wire.begin(21, 22);
  pwm.begin();
  pwm.setPWMFreq(50); // standard analog and digital servos run at 50Hz
}

void loop() {
  moveSmooth(1, 90, 45); // channel 1 = shoulder, 90 to 45 degrees
  delay(1000);
}

The idle current budget (all 4 servos holding position, none stalling) sits around 2.4A: about 700 mA per MG996R plus 300-500 mA from the SG90 (SG90 clone specs vary a lot between manufacturers, use the highest figure you find on your unit’s actual datasheet so you don’t come up short). If a servo stalls, that draw jumps to 2.5A on that axis alone. In the theoretical worst case, all 4 stalling at once pull around 8A, something that shouldn’t happen in normal use (it only happens if you send an impossible angle or the arm hits a physical stop), but it’s the reason the 5A supply and the 1000 µF capacitor work together: the supply covers typical draw with margin, and the capacitor absorbs the short spike while the supply catches up.

Safety

  • Real heat, not just a warning label: a servo stalled for minutes (holding more weight than it can, or pushing against a stop) heats up the winding and can burn the motor or strip the gear teeth. Test unloaded first, with the arm empty, and don’t leave the arm powered and stalled unattended.
  • Pinch points: with 9-11 kgf.cm of torque at the shoulder and elbow, the arm clamps down with real force. Keep fingers clear of the joints’ path, especially the first time you test a new range of motion.
  • Shorts at the terminal block: the PCA9685’s V+ terminal and the supply’s screw terminal have exposed screws. Insulate any bare wire ends and check with a multimeter that there’s no continuity between V+ and GND before powering up.
  • The cutoff switch goes on the V+ line: that way you can kill servo power instantly without unplugging the ESP32 (which can stay connected to your computer to watch the serial monitor while you troubleshoot).

If you want to adjust the budget

Cheaper: if the arm is very light (3D printed in PLA, no gripper or real payload), you can swap the MG996R for SG90 on all 4 axes. Before you do, redo the torque math with your arm’s actual weight: if the result goes above 1.8 kgf.cm on any axis, the SG90 won’t hold there.

Nicer: the genuine Adafruit PCA9685 (about 13-15 €, versus 3-5 € for the generic clone) ships with better documentation and silkscreened labels, useful if this is your first I2C driver. For more portability, swap the wall supply for a 2S (7.4V) LiPo battery with a step-down (UBEC) to 5V 5A: if you go that route, never charge the LiPo without a balance charger, never puncture or bend it, and store it in a fireproof bag while charging.

I know seeing “current budget” and “torque by leverage” all at once feels like a lot. Take it easy: build one axis, confirm it moves on its own calibrated servo, and add the next one. Once all 4 axes move the gripper wherever you decide, you’ve got the base to add inverse kinematics or remote control, which is the logical next step toward the advanced version of this project.

The software (the AI writes it)

The firmware does three things: initializes the PCA9685 over I2C, calibrates each servo's pulse range (it varies unit to unit, even on the same model), and moves each joint in small steps instead of jumps, so it doesn't spike the current all at once. Ask the AI with this prompt: 'ESP32 code using the Adafruit_PWMServoDriver library, PCA9685 at I2C address 0x40 (SDA on GPIO21, SCL on GPIO22), 4 servos on channels 0 to 3 (base, shoulder, elbow, gripper), moving each servo in 1-degree steps with a small delay between steps until it reaches the target angle.' Check that the channels and angle limits match your actual build before uploading.

Keep building

See the latest news entry RSS