Getting the right Arduino robotic arm code is usually the difference between an arm that twitches uselessly and one that moves where you tell it. This guide gives you working, copy-paste sketches for every stage: attaching servos, testing them, driving them by hand or by serial command, recording motions, smoothing them out, and scaling past the Arduino’s pin limit with a PCA9685.
All examples assume hobby servos (SG90, MG996R, or similar) and pair with the full Arduino robotic arm build guide. If you are still picking actuators, read up on servo motors for robotic arms first.
What this guide covers
Work through the sketches in order, or jump to the one you need. Each builds on the last:
- Servo library basics — attach servos to PWM pins and set a safe home pose.
- Sweep / test sketch — confirm every joint moves and find its mechanical limits.
- Potentiometer control — drive each joint by hand with an analog knob.
- Serial-command control — send angles from a PC, script, or Raspberry Pi.
- Record & playback — store poses in an array for repeatable pick-and-place.
- Smooth / eased motion — move joints gradually instead of snapping.
- PCA9685 driver — scale past the Arduino’s pin limit to 16+ servos.
A quick note on conventions before the code: every sketch uses the four-joint layout (base, shoulder, elbow, gripper) from our main build, and angles are clamped to safe per-joint ranges rather than the full 0–180°. Adjust the pin numbers and limits to match your own arm.
Servo library basics: attaching servos to PWM pins
The standard Servo library ships with the Arduino IDE. Each servo gets a Servo object, attached to a PWM-capable pin. On an Uno that is pins 3, 5, 6, 9, 10, and 11, so you can drive up to six servos directly.
#include <Servo.h>
Servo base;
Servo shoulder;
Servo elbow;
Servo gripper;
void setup() {
base.attach(3); // signal wire to D3
shoulder.attach(5);
elbow.attach(6);
gripper.attach(9);
// Send each joint to a safe starting pose
base.write(90);
shoulder.write(90);
elbow.write(90);
gripper.write(20); // gripper open
}
void loop() {
// Holding position; nothing to do here yet
}
Tip: Always power servos from a dedicated 5-6V supply, never the Arduino’s 5V pin. Connect the supply ground to the Arduino ground so the PWM signal has a common reference.
write() takes an angle from 0 to 180. If a joint does not reach its full range, use writeMicroseconds() (typically 500-2500 us) for finer control.
A sweep / test sketch
Before building any logic, confirm every servo moves freely and is mounted at the right angle. This sketch sweeps one joint back and forth so you can watch its travel and set mechanical limits.
#include <Servo.h>
Servo joint;
const int MIN_ANGLE = 10; // avoid slamming the end stops
const int MAX_ANGLE = 170;
void setup() {
joint.attach(9);
}
void loop() {
for (int a = MIN_ANGLE; a <= MAX_ANGLE; a++) {
joint.write(a);
delay(15); // 15 ms per degree = slow, safe sweep
}
for (int a = MAX_ANGLE; a >= MIN_ANGLE; a--) {
joint.write(a);
delay(15);
}
}
Note the MIN_ANGLE and MAX_ANGLE constants. Every joint on a real arm has a smaller usable range than 0-180 once it is bolted into a bracket. Find those limits now and reuse them everywhere.
Potentiometer control of multiple joints
Wiring one potentiometer per joint gives you an intuitive manual controller and is the fastest way to find good poses to record later. Read each pot on an analog pin and map it to the joint’s safe range.
#include <Servo.h>
Servo joints[4];
const int servoPins[4] = {3, 5, 6, 9};
const int potPins[4] = {A0, A1, A2, A3};
const int minA[4] = {10, 20, 15, 10};
const int maxA[4] = {170, 160, 165, 80};
void setup() {
for (int i = 0; i < 4; i++) joints[i].attach(servoPins[i]);
}
void loop() {
for (int i = 0; i < 4; i++) {
int raw = analogRead(potPins[i]); // 0-1023
int angle = map(raw, 0, 1023, minA[i], maxA[i]);
joints[i].write(angle);
}
delay(20);
}
The delay(20) sets roughly a 50 Hz update rate, which matches the servo refresh rate and keeps motion fluid without flooding the servos.
Serial-command control (“0:90”)
Driving joints from the Serial Monitor is the cleanest bridge to a computer, a Python script, or a Raspberry Pi robotic arm controller. This parser accepts commands like 0:90 (joint 0 to 90 degrees) or 2:135.
#include <Servo.h>
Servo joints[4];
const int servoPins[4] = {3, 5, 6, 9};
String buffer = "";
void setup() {
Serial.begin(9600);
for (int i = 0; i < 4; i++) joints[i].attach(servoPins[i]);
Serial.println("Send commands like 0:90");
}
void loop() {
while (Serial.available()) {
char c = Serial.read();
if (c == '\n') {
int sep = buffer.indexOf(':');
if (sep > 0) {
int id = buffer.substring(0, sep).toInt();
int angle = buffer.substring(sep + 1).toInt();
if (id >= 0 && id < 4) {
angle = constrain(angle, 0, 180);
joints[id].write(angle);
Serial.print("Joint "); Serial.print(id);
Serial.print(" -> "); Serial.println(angle);
}
}
buffer = "";
} else if (c != '\r') {
buffer += c;
}
}
}
constrain() clamps bad input so a stray value cannot drive a servo past its limit. This same protocol scales nicely: send 0:90,1:120,2:45 and split on commas if you want multi-joint commands.
Recording and playing back a sequence
For repeatable pick-and-place, store a list of poses in a 2D array and step through them. Here each row is a full-arm pose; each column is a joint angle.
#include <Servo.h>
Servo joints[4];
const int servoPins[4] = {3, 5, 6, 9};
const int NUM_POSES = 4;
int sequence[NUM_POSES][4] = {
{ 90, 90, 90, 20 }, // home, gripper open
{ 45, 120, 110, 20 }, // reach to pick
{ 45, 120, 110, 70 }, // close gripper
{ 90, 90, 90, 70 } // lift and return
};
void setup() {
for (int i = 0; i < 4; i++) joints[i].attach(servoPins[i]);
}
void loop() {
for (int p = 0; p < NUM_POSES; p++) {
for (int j = 0; j < 4; j++) joints[j].write(sequence[p][j]);
delay(1000); // dwell so the move completes
}
}
To record poses instead of hard-coding them, combine this with the potentiometer or serial sketch: capture the current angles into the next free row of sequence[] when a button is pressed, then play the array back.
Smooth / eased motion instead of snapping
The sketch above snaps between poses, which strains the gearbox and looks robotic in the bad way. Move toward the target one step at a time instead.
#include <Servo.h>
Servo joint;
int current = 90;
void moveTo(int target, int stepDelay) {
while (current != target) {
current += (target > current) ? 1 : -1;
joint.write(current);
delay(stepDelay); // larger delay = slower, smoother
}
}
void setup() {
joint.attach(9);
joint.write(current);
}
void loop() {
moveTo(150, 12);
delay(500);
moveTo(30, 12);
delay(500);
}
For acceleration and deceleration (ease in/out), scale the delay by distance from the target, or interpolate all joints together so they arrive simultaneously. When you start computing target angles from a desired gripper position rather than setting joints by hand, you have moved into robotic arm inverse kinematics territory.
Driving many servos with a PCA9685
Past five or six servos the Arduino runs out of timers and the Servo library gets unstable. The PCA9685 is a 16-channel PWM driver on I2C; the easiest interface is Adafruit’s ServoKit.
#include <Adafruit_PWMServoDriver.h>
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x40);
const int SERVO_FREQ = 50; // 50 Hz for analog servos
const int SERVO_MIN = 150; // ~0 deg pulse count
const int SERVO_MAX = 600; // ~180 deg pulse count
void writeAngle(uint8_t channel, int angle) {
int pulse = map(angle, 0, 180, SERVO_MIN, SERVO_MAX);
pwm.setPWM(channel, 0, pulse);
}
void setup() {
pwm.begin();
pwm.setOscillatorFrequency(27000000);
pwm.setPWMFreq(SERVO_FREQ);
writeAngle(0, 90); // base on channel 0
writeAngle(1, 90); // shoulder on channel 1
}
void loop() {}
Calibrate SERVO_MIN and SERVO_MAX per servo brand; the values above are a typical starting point. If you prefer angle-based calls, the higher-level Adafruit_ServoKit library wraps the same chip and lets you write kit.servo[0].angle = 90;.
| Approach | Servos | Library | Best for |
|---|---|---|---|
| Direct PWM pins | up to ~5 | Servo | First builds, simple arms |
| Serial commands | up to ~5 | Servo | PC / Pi control |
| PCA9685 | up to 16 per board | Adafruit_PWMServoDriver / ServoKit | Full 6-DOF arms, multiple drivers |
Troubleshooting: jitter is almost always power
If servos buzz, twitch, or reset the Arduino mid-move, the code is rarely the cause. Servos pull large current spikes the Arduino’s onboard regulator cannot supply, which browns out the logic. Run servos from a separate supply sized for their combined stall current and share grounds. The full sizing math is in our guide to powering a robotic arm.
Other quick checks: confirm you call attach() in setup(), keep delay() values at or above 15-20 ms between writes, and make sure your pose arrays never exceed each joint’s mechanical limits. For every other symptom — I2C errors, drift, weak grip — see the full troubleshooting guide. New to the mechanics behind the code? Start with how to build a robotic arm.
Frequently asked questions
Which library do I need for Arduino robotic arm code?
For up to about four or five servos wired directly to the Arduino, the built-in Servo library is all you need. Once you go beyond that, switch to a PCA9685 board with the Adafruit_PWMServoDriver or Adafruit_ServoKit library so you do not run out of timers or pins.
Why does my robotic arm jitter when running my code?
Jitter is almost always a power problem, not a code problem. Servos draw current spikes the Arduino's 5V regulator cannot supply. Use a separate 5-6V supply rated for the stall current of all servos and tie the grounds together. See our powering guide for sizing.
How do I make the servos move smoothly instead of snapping?
Do not write the target angle in one call. Step the current angle toward the target a degree at a time inside loop(), with a small delay, or use an easing function. The smooth motion sketch below shows both approaches.