Build a 4-DOF Robot Arm: PCA9685, Servos, and Kinematics
A desktop robot arm that moves to the poses you command—driven by a PCA9685 and tuned with inverse kinematics you understood in simulation before wiring a single servo.
A robot arm is the first project where a robot stops moving around and starts doing something to the world. A line follower follows; an obstacle-avoider dodges; an arm reaches, positions, and grips. That shift—from driving to manipulation—brings in the one idea every manipulator is built on: kinematics, the geometry that connects the angles you can command to the point in space you actually want.
This build path treats the arm as exactly that: a lesson in kinematics you can hold. You start by understanding how a two-link arm turns joint angles into a tool position and back again, gather the parts a servo arm needs and learn what each one does, then—before spending anything or stripping a gear—explore the reach, the two elbow solutions, and the unreachable poses in the browser simulator. Only then do you wire the real arm, calibrate its servos by hand, and run the inverse kinematics on hardware.
Follow the tech tree below top to bottom. Each node opens once its prerequisites are done, and your progress is saved on this device, so you can build the arm over a weekend without losing your place.
Bill of materials
| Part | Qty | Approx. cost | Notes |
|---|---|---|---|
| Arduino Uno | 1 | $5–8 | Any board works — it only speaks I²C here |
| PCA9685 16-channel driver | 1 | $3 | Not strictly required for four servos, but it is the right tool |
| SG90 servo | 4 | $8 | Buy an MG90S for the shoulder — metal gears, and it is the joint that fails |
| 4-DOF arm kit | 1 | $12–25 | Acrylic is fine to learn on; aluminium flexes far less |
| 5–6 V supply, ≥3 A | 1 | $8 | Four servos stalling together is ~2.6 A |
| 1000 µF capacitor | 1 | $1 | Across V+, close to the terminal |
| Spare servo horns | 1 set | $2 | You will fit one wrong. Everybody does |
Total: roughly $40–60.
Two purchases are worth thinking about rather than defaulting on. A metal-gear MG90S at the shoulder costs a dollar more and is the joint carrying every other joint’s weight — nylon gears there strip under sustained load, not under impact. And a supply sized for stall, because an arm’s distinctive failure is that when it collides with its own frame or grips something solid, every servo stalls at once.
Measure your arm before you write any code
Inverse kinematics is arithmetic on link lengths. These four numbers are the entire input, and getting them from a product listing rather than a ruler is the most common reason an arm reaches confidently to the wrong place.
| Symbol | Measure from | To | Typical |
|---|---|---|---|
L1 |
Shoulder joint axis | Elbow joint axis | 90–105 mm |
L2 |
Elbow joint axis | The gripper’s actual grip point | 80–95 mm |
That last row is doing more work than it looks. “The gripper’s actual grip point” moves as the jaws close, and sizing the gripper itself is a separate problem from positioning it — with its own walls, its own arithmetic and its own failure modes. If your arm reaches the part and then drops it or crushes it, the answer is not in this page: start at the gripper simulator.
| h | Table surface | Shoulder joint axis | 50–70 mm |
| d | Base rotation axis | Shoulder axis, horizontally | 0–15 mm |
Measure between joint axes, not bracket edges. The axis is the centre of the servo’s output spline, often 5–10 mm inside the visible bracket. On a 95 mm link a 10 mm error is over 10%, and it produces an arm that is systematically short in a way no gain corrects.
Those two lengths give you the workspace immediately:
maximum reach = L1 + L2 e.g. 100 + 90 = 190 mm
minimum reach = |L1 - L2| e.g. |100 - 90| = 10 mm
The workspace is an annulus, not a disc. There is a hole in the middle the arm physically cannot reach, and a target near the base coming back “unreachable” is correct behaviour rather than a bug.
Build it in milestones
The assembly order here contains one step that is nearly impossible to undo, and it is step 2.
| # | Milestone | The test | A pass looks like |
|---|---|---|---|
| 1 | Driver responds | Scan the I²C bus | 0x40 appears; V+ has its own supply |
| 2 | Servos homed before assembly | Command every servo to 90° before pressing on a horn | Each horn is fitted at your intended zero pose |
| 3 | Per-servo end stops | Step outward from centre until the servo buzzes | You have min/max counts per servo, not library defaults |
| 4 | Joint map recorded | Command each joint through its range | Offset, sign and mechanical limits written down for all four |
| 5 | Collision check | Sweep every joint through its full range slowly | Nothing hits the frame; limits reflect what you observed |
| 6 | Forward kinematics | Set known angles, compute the tool position, measure it | Computed and measured agree within a few millimetres |
| 7 | Round trip | Feed the computed position back through IK | The angles out match the angles in |
| 8 | All four quadrants | Request targets in front, behind, left, right | All four work — if two mirror, you used atan not atan2 |
| 9 | Reachability | Request a target well beyond L1 + L2 |
A clean “unreachable” — not a NaN, and not a lunge |
| 10 | Smooth motion | Command a large move | Interpolated over steps, all joints together, no lurch |
| 11 | Repeatability | Return to the same pose ten times from different directions | Landing spread measured — that number is your backlash |
Milestone 2 cannot be recovered from later. A servo horn is splined — around 20 teeth — so it fits only in discrete positions roughly 18° apart. Assemble first and calibrate second, and you may find a joint whose usable range is offset by 40° with no mechanical way to fix it. Power each servo, command 90°, then press the horn on.
Move it gently, always
A servo commanded to a new angle goes there at full speed. On an arm that means a violent movement, a current spike across all four servos, and a mechanical shock through nylon gears with the arm’s own leverage multiplying it.
// Interpolate every joint together, so the tool takes a sensible path and
// no single servo carries the whole transition.
void moveTo(float base, float shoulder, float elbow, int steps, int stepMs) {
static float cur[3] = {0, 0, 0};
const float target[3] = {base, shoulder, elbow};
for (int s = 1; s <= steps; s++) {
const float t = (float)s / steps;
for (int j = 0; j < 3; j++) {
const float a = cur[j] + (target[j] - cur[j]) * t;
pwm.setPWM(JOINT[j].channel, 0, countsFor(JOINT[j], a));
}
delay(stepMs);
}
memcpy(cur, target, sizeof(cur));
}
Interpolating all joints together matters as much as the smoothing itself. Moving joints one at a time sends the tool on a strange arc through the workspace — slower, and far more likely to collide with something on the way.
What good looks like
| Measurement | Acrylic kit, SG90s | Aluminium, MG90S |
|---|---|---|
| Repeatability, same approach direction | ±3–5 mm | ±1–2 mm |
| Repeatability, mixed approach directions | ±8–15 mm | ±3–5 mm |
| Usable payload at full extension | 20–30 g | 40–60 g |
| Sag at full extension, unloaded | 3–8 mm | 1–3 mm |
The gap between the two repeatability rows is the backlash, and it is the single largest error source on a hobby arm. Four degrees of play at the shoulder over a 190 mm reach is more than 13 mm at the gripper. The practical answer is not better maths — it is to always approach a target from the same direction, which removes the sign change entirely and turns a ±13 mm error into a repeatable offset you can calibrate out.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Arm reaches the wrong point entirely | Horn fitted at an arbitrary angle | Home every servo to 90° before fitting horns |
| One joint’s range is offset ~18° | Horn one spline tooth out | Remove and refit — software cannot fix this |
| Reaches correctly in front, mirrors behind | atan instead of atan2 |
atan2 resolves all four quadrants |
| IK returns NaN and the arm lunges | acos argument outside −1..1 |
Test reachability before the trigonometry |
| Targets near the base fail | Inside the minimum-reach hole | Correct behaviour — the workspace is an annulus |
| Board resets when the arm moves | Servo current through the Arduino | Separate V+ supply, common ground, 1000 µF cap |
| Arm sags at full extension | Shoulder past its comfortable torque | MG90S at the shoulder; counterbalance with a rubber band |
| Buzzing while holding a pose | Servo fighting a load at its limit | Reduce the load, or detach when holding is unnecessary |
| Joint velocity spikes near full extension | Driving through a singularity | Keep planned paths inside a margin of the boundary |
| Lands differently from each direction | Backlash | Always approach from the same direction |
| All angles consistently short | PCA9685 oscillator not exactly 25 MHz | Measure the real output frequency and call setOscillatorFrequency |
Where to take it next
Add a straight-line move. Interpolating in joint space, as the code above does, makes the tool follow a curve. Interpolating in Cartesian space — computing IK at every step along a straight line — makes it travel straight, which is what any real pick-and-place needs. It is a small change and it is where singularities stop being theoretical: a straight path through near-full extension demands enormous joint velocities.
Add a camera and close the loop. Everything here is open loop: the arm goes where it is told and never checks. A camera reading an ArUco marker on the object lets the arm correct for the error backlash and sag leave behind — which is how a cheap arm becomes accurate.
Step up the hardware once you hit the ceiling. Aluminium links remove most of the flex, MG90S servos remove most of the backlash, and a 6-DOF arm adds a wrist so the tool’s approach angle becomes yours to choose. None of it changes the maths: the two-link solver you wrote here is the same one a better arm uses, which is exactly why the cheap frame was worth building on first.
Project roadmap
The build path
Follow the tech tree from parts to a robot that follows a taped line. Each node unlocks when its prerequisites are done, and your progress saves on this device.
0 / 13 done
Components
- ControllerArduino UnoThe forgiving 8-bit board most people meet robotics through.
- DriverPCA9685 Servo DriverDrive 16 servos over two I²C pins—the board behind most robot arms and hexapods.
- ActuatorSG90 Servo MotorThe nine-gram servo behind most robot arms, grippers, and steering.
- Chassis4-DOF Robot Arm KitThe acrylic or metal frame that turns four servos into a base-shoulder-elbow-gripper manipulator.
- PowerRobot Battery & Power PackThe difference between a robot that runs and one that keeps resetting.
Tutorials in this path
- Advanced · 22 min readInverse Kinematics of a Two-Link Robot ArmWhy a two-link arm has two solutions, where it can and cannot reach, and what a singularity really is.
- Intermediate · 18 min readDrive Servos with the PCA9685 (I²C PWM Driver)Set the frequency, turn angles into pulse widths, and move sixteen servos from two I²C pins — safely powered and calibrated.
Practise before you wire
Tune it in the live simulator
The build path routes through a browser lab. Find gains that follow the track cleanly here, then transfer them to the real robot.
Frequently asked questions
How do you make a robot arm with Arduino?
Mount four servos in a 4-DOF frame—base, shoulder, elbow, and gripper—and drive them with a PCA9685 over I²C so the Arduino doesn't run out of PWM pins. Calibrate each servo's angle-to-pulse range, then feed target points to a two-link inverse-kinematics solver that returns the shoulder and elbow angles. This build path walks that whole chain, and you tune the geometry in a simulator before wiring anything.
How many servos does a 4-DOF robot arm need?
Four—one per degree of freedom: a rotating base, a shoulder, an elbow, and a gripper. Each servo needs its own PWM channel, which is exactly why a multi-joint arm uses a PCA9685 driver instead of the Arduino's handful of hardware PWM pins.
Do I need a PCA9685 for a robot arm?
It is the right tool, though not strictly required. Four servos will run from an Arduino if you power them separately, but the PCA9685 frees the microcontroller's timers, drives every joint from two I²C pins, and routes the servo current through its own V+ supply—which keeps the high current away from your logic and the board from browning out.
Do I have to buy parts before I start this project?
No. The kinematics and the pose-solving run in the browser simulator, so you can understand and explore the arm's reach, its two solutions, and its unreachable poses before buying anything. Only the final steps—wiring, calibration, and running the IK—need the physical Arduino, PCA9685, servos, arm frame, and power supply.
How is inverse kinematics used in a robot arm?
Forward kinematics tells you where the tool ends up given the joint angles; inverse kinematics does the useful inverse—given a target point, it returns the joint angles that reach it. For this arm the shoulder and elbow form a two-link planar solver, the base aims that plane, and the gripper does the picking. The simulator lets you see the solver work, including where a target is out of reach.